2024年1月

内存泄漏

内存泄漏指的就是在运行过程中定义的各种各样的变量无法被垃圾回收器正常标记为不可达并触发后续的回收流程,主要原因还是因为对可回收对象引用没有去除,导致垃圾回收器通过GC ROOT可达性分析时认为当前是可达的;这时随着系统的运行时间,累积的不可回收的对象就越多,直到垃圾回收器执行Full GC还是没有空余空间存放新加入的对象,这时虚拟机就会抛出out of memory错误。此种错误可以分类为内存泄漏导致的,原因是应该回收的对象无法被垃圾回收器正常回收从而导致内存不足。说起内存泄漏近十年引起比较大的是便是Android 5.0引起的内存问题,该Bug导致手机在使用一段时间后必须手动重启系统释放内存;不然,无法运行任何应用,包括系统自带APPS桌面等都会引起FC崩溃;这时查看内存占用发现可用内存为负数。安卓5.0导致的内存泄漏:
http://www.lupaworld.com/portal.php?mod=view&aid=249186&continueFlag=6f56029cba978a1a4ac09c91b20c196d

接下来说一个我在实际开发中文件遇到的内存泄漏问题,该问题在测试环境不易发现,主要原因有以下几点:

1、测试环境经常更新重启,相当于GC回收了对象

2、用户量太少了,根本撑不到内存泄露出现的那一刻

但是,生产环境那就不一样了,用户多,运行时间久,如果存在长期没有被回收的对象时,久而久之就会触发内存不足的情况

言归正传,生产环境出现的内存泄露问题是由对Redis订阅使用不当导致,下面我把引起内存泄漏的代码贴上来

1   /**
2 * 因为@ServerEndpoint不支持注入,所以使用SpringUtils获取IOC实例3      */
4     private StringRedisTemplate redisTampate = SpringUtils.getBean(StringRedisTemplate.class);5     private RedisMessageListenerContainer redisMessageListenerContainer = SpringUtils.getBean(RedisMessageListenerContainer.class);6     privateSession session;7 
8 @OnOpen9     public void onOpen(Session session, @PathParam("topic") String topic, @PathParam("username") String username) {
11         this.session =session;12         sessions.add(this);13         SubscribeListener subscribeListener = newSubscribeListener();14 subscribeListener.setSession(session);15 subscribeListener.setStringRedisTemplate(redisTampate);
17         try{18             redisMessageListenerContainer.addMessageListener(subscribeListener, newChannelTopic(topic));19         } catch(Exception e) {20 e.printStackTrace();21 }22     }

这是一个WebSocket的项目,利用即时通信的特性实现了,由后台触发前端页面的刷新

眼尖的人应该已经发现问题所在了吧?这就是一个使用Spring和Redis不当导致的内存泄露问题

接下来我们分析

首先代码的4到7行申明了三个成员变量,主要关注点还是第5行的RedisMessageListenerContainer 变量,从SpringContext中取出了Redis的消息监听容器;在接下来的onOpen方法里定义了获取了WebSocket连接成功后产生而Session会话,这里的Session会话不是WebSession而是WebSocketSession,可以定义正会话帧,每次用户连接成功之后服务器就分发一个WebSocket给当前用户,当断开连接时该会话帧就会断开对象引用,垃圾回收器就可以回收;说到这里其实问题已经非常明显了,那就这个WebSession压根就没有被垃圾回收器回收掉,每次用户连接就产生一个WebSession对象,并通过地14行代买引用给Redis的监听器,然后再由将监听器添加到Redis消息监听容器中,而Redis消息监听容器又是从SpringContext中取出的,那就意味着该对象是一个SpringIOC中的Bean实例,这一个有GC ROOT引用的对象;这样一来后续产生的每一个WebSession会话帧都会被Redis消息监听容器的实例引用,垃圾回收器在进行可达性分析时都认为该对象是可达的,判定无法回收,从而就导致了内存泄漏。

这个问题其实是对垃圾回收器和Spring原理不了解导致的,在日常开发中应该尽可能的避免这些问题

转载至我的博客
https://www.infrastack.cn
,公众号:架构成长指南

当我们使用 Mysql数据库到达一定量级以后,性能就会逐步下降,而解决此类问题,常用的手段就是引入数据库中间件进行分库分表处理,比如使用
Mycat

ShadingShpere

tddl
,但是这种都是过去式了,现在使用分布式数据库可以避免分库分表

为什么不建议分库分表呢?

分库分表以后,会面临以下问题

  • 分页问题,例如:使用传统写法,随着页数过大性能会急剧下降
  • 分布式事务问题
  • 数据迁移问题,例如:需要把现有数据通过分配算法导入到所有的分库中
  • 数据扩容问题,分库分表的数据总有一天也会到达极限,需要增大分片
  • 开发模式变化,比如在请求数据时,需要带分片键,否则就会导致所有节点执行
  • 跨库跨表查询问题
  • 业务需要进行一定取舍,由于分库分表的局限性,有些场景下需要业务进行取舍

以上只是列举了一部分问题,为了避免这些问题,可以使用分布式数据库TiDB来处理

TiDB介绍

TiDB

PingCAP
公司研发的一款开源分布式关系型数据库,从 2015年 9 月开源,至今已经有9 年时间,可以说已经非常成熟,它是一款同时支持OLTP(在线事务处理)和OLAP(在线分析处理)的融合型分布式数据库产品,具备水平扩缩容,金融级高可用、实时 HTAP(Hybrid Transactional and Analytical Processing)、云原生的分布式数据库,兼容 MySQL 5.7 协议和 MySQL 生态等重要特性,它适合高可用、强一致要求较高、数据规模较大等各种应用场景。

核心特性

  • 金融级高可用
  • 在线水平扩容或者缩容,并且存算分离
  • 云原生的分布式数据库,支持部署在公有云,私有云,混合云中
  • 实时HTAP,提供TIKV行存储引擎和TiFlash列存储引擎
  • 兼容MySQL协议和MySQL生态
  • 分布式事务强一致性
  • 从 MySQL 无缝切换到 TiDB,几乎无需修改代码,迁移成本极低
  • PD在分布式理论CAP方面满足CP,是强一致性的

应用场景

  • 对数据一致性及高可靠、系统高可用、可扩展性、容灾要求较高的金融行业属性的场景
  • 对存储容量、可扩展性、并发要求较高的海量数据及高并发的OLTP场景
  • 数据汇聚、二次加工处理的场景

案例

TiDB 有
1500
多家不同行业的企业应用在了生产环境,以下是一些有代表性企业,要想查看更多案例,可以访问TiDB 官网查询

系统架构

TIDB Server

SQL 层,对外暴露 MySQL 协议的连接 endpoint,负责接收SQL请求,处理SQL相关的逻辑,并通过PD找到存储计算所需数据的TiKV地址,与TiKV交互获取数据,最终返回结果。TiDB Server 是无状态的,其本身并不存储数据,只负责计算,可以无限水平扩展,可以通过负载均衡组件(LVS、HAProxy或F5)对外提供统一的接入地址,客户端的连接可以均匀地分摊在多个 TiDB 实例上以达到负载均衡的效果。

PD Server

整个集群的管理模块,其主要工作有三个:

  1. 存储集群的元信息(某个Key存储在那个TiKV节点);
  2. 对TiKV集群进行调度和负载均衡、Leader选举;
  3. 分配全局唯一且递增的事务ID。

PD 是一个集群,需要部署奇数个节点,一般线上推荐至少部署3个节点。PD在选举的过程中无法对外提供服务,这个时间大约是3秒。

TIKV Server

TiDB 现在同时支持OLTP 和 OLAP,而TiKV负责存储OLTP数据,从外部看TiKV是一个分布式的提供事务的Key-Value存储引擎。存储数据的基本单位是Region,每个Region负责存储一个Key Range(从StartKey到EndKey的左闭右开区间)的数据,每个TiKV节点会负责多个Region。

TiKV如何做到数据不丢失的?

简单理解,就是把数据复制到多台机器上,这样一个节点down 机,其他节点上的副本还能继续提供服务;复杂理解,需要这个数据可靠并且高效复制到其他节点,并且能处理副本失效的情况,那怎么做呢,就是使用
Raft
一致性算法

Region 与副本之间通过 Raft 协议来维持数据一致性,任何写请求都只能在 Leader 上写入,并且需要写入多数副本后(默认配置为 3 副本,即所有请求必须至少写入两个副本成功)才会返回客户端写入成功。

分布式事务支持

TiKV 支持分布式事务,我们可以一次性写入多个 key-value 而不必关心这些 key-value 是否处于同一个数据切片 (Region) 上,TiKV 的分布式事务参考了Google 在 BigTable 中使用的事务模型
Percolator
,具体可以访问论文了解

与MySQL的对比

支持的特性

  • 支持分布式事务,原理是基于Google Percolator,Percolator是基于Bigtable的,所以数据结构直接使用了Bigtable的Tablet。详情可参考https://zhuanlan.zhihu.com/p/39896539
  • 支持锁,TIDB是乐观锁 +MVCC ,MySQL是悲观锁+MVCC,要注意TIDB执行Update、Insert、Delete时不会检查冲突,只有在提交时才会检查写写冲突,所以在业务端执行SQL语句后,要注意检查返回值,即使执行没有出错,提交的时候也可能出错。

不支持的功能特性

  • 不支持存储过程、函数、触发器
  • 自增id只支持在单个TIDB Server的自增,不支持多个TIDB Server的自增。
  • 外键约束
  • 临时表
  • Mysql追踪优化器
  • XA
    语法(TiDB 内部使用两阶段提交,但并没有通过 SQL 接口公开)

资源使用情况

以下内容参考:
https://pingcap.medium.com/an-8x-system-performance-boost-why-we-migrated-from-mysql-to-a-newsql-database-a42570ab765a

TiDB 具有很高的数据压缩比,MySQL 中的 10.8 TB 数据在 TiDB 中变成了 3.2 TB,还是三副本的总数据量。因此,
MySQL 与 TiDB 的空间使用比例为 3.4:1。

同等量级,使用2 年以后,资源使用情况

  • MySQL使用32 个节点,而 TiDB 只有 14 个
  • MySql 用了 512 个 CPU 核心,而 TiDB 将仅使用 224 个,不到 MySQL 的一半。
  • MySQL 使用 48 TB 存储空间,而 TiDB 将使用 16 TB,仅为 MySQL 的 1/3。

性能测试

测试报告 1

来源:
https://www.percona.com/blog/a-quick-look-into-tidb-performance-on-a-single-server/

五个 ecs 实例,使用了不同配置,以此测试

  • t2.medium:2 个 CPU 核心
  • x1e.xlarge:4 个 CPU 核心
  • r4.4xlarge:16 个 CPU 核心
  • m4.16xlarge:64 个 CPU 核心
  • m5.24xlarge:96 个 CPU 核心

MySQL 中的数据库大小为 70Gb,TiDB 中的数据库大小为 30Gb(压缩)。该表没有二级索引(主键除外)。

测试用例

  1. 简单计数(*):

     select count(*) from ontime;
    
  2. 简单分组依据

     select count(*), year from ontime group by year order by year;
    
  3. 用于全表扫描的复杂过滤器

    select * from ontime where UniqueCarrier = 'DL' and TailNum = 'N317NB' and FlightNum = '2' and Origin = 'JFK' and Dest = 'FLL' limit 10;
    
  4. 复杂的分组依据和排序依据查询

    select SQL_CALC_FOUND_ROWS 
    FlightDate, UniqueCarrier as carrier,
    FlightNum, 
    Origin, 
    Dest 
    FROM ontime 
    WHERE 
    DestState not in ('AK', 'HI', 'PR', 'VI')
    and OriginState not in ('AK', 'HI', 'PR', 'VI')
    and flightdate > '2015-01-01' 
    and ArrDelay < 15 
    and cancelled = 0 and Diverted = 0  
    and DivAirportLandings = '0'    
    ORDER by DepDelay DESC
    LIMIT 10;
    

    下图表示结果(条形表示查询响应时间,越小越好):

系统基准测试

在 m4.16xlarge 实例上使用 Sysbench 进行点选择(意味着通过主键选择一行,线程范围从 1 到 128)(内存限制:无磁盘读取)。结果在这里。条形代表每秒的交易数量,越多越好:

系统测试报告 2

来源:
https://www.dcits.com/show-269-4103-1.html

硬件配置

测试场景

测试分两阶段进行,第一阶段测试数据为100万单,第二阶段测试数据为1300万单。在此基础上,使用Jmeter压力测试10万单结果如下:

从测试结果来看,在小数据量mysql性能是好于TiDB,因为 TiDB 是分布式架构,如果小数据量,在网络通讯节点分发一致性等方面花的时间就很多,然后各个节点执行完还要汇总返回,所以开销是比较大的,但是数据量一上来TiDB 优势就体现出来了,所以如果数据量比较小,没必要使用 TiDB

总结

以上介绍了 TiDB架构,以及它的一些特性,同时也与 mysql 进行了对比,如果贵司的数据量比较大,正在考虑要分库分表,那么完全可以使用它,来避免分库分表,分库分表是一个过渡方案,使用分布式数据库才是终极方案。同时如果贵司的数据量比较小,那么就没必要引入了

扫描下面的二维码关注我们的微信公众帐号,在微信公众帐号中回复◉加群◉即可加入到我们的技术讨论群里面共同学习。

本文深入探讨了知识图谱的构建全流程,涵盖了基础理论、数据获取与预处理、知识表示方法、知识图谱构建技术等关键环节。

关注TechLead,分享AI全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人

file

一、概述

知识图谱,作为人工智能和语义网技术的重要组成部分,其核心在于将现实世界的对象和概念以及它们之间的多种关系以图形的方式组织起来。它不仅仅是一种数据结构,更是一种知识的表达和存储方式,能够为机器学习提供丰富、结构化的背景知识,从而提升算法的理解和推理能力。

在人工智能领域,知识图谱的重要性不言而喻。它提供了一种机器可读的知识表达方式,使计算机能够更好地理解和处理复杂的人类语言和现实世界的关系。通过构建知识图谱,人工智能系统可以更有效地进行知识的整合、推理和查询,从而在众多应用领域发挥重要作用。

具体到应用场景,知识图谱被广泛应用于搜索引擎优化、智能问答系统、推荐系统、自然语言处理等领域。例如,在搜索引擎中,通过知识图谱可以更精确地理解用户的查询意图和上下文,提供更相关和丰富的搜索结果。在智能问答系统中,知识图谱使得机器能够理解和回答更复杂的问题,实现更准确的信息检索和知识发现。

此外,知识图谱还在医疗健康、金融分析、风险管理等领域展现出巨大潜力。在医疗领域,利用知识图谱可以整合和分析大量的医疗数据,为疾病诊断和药物研发提供支持。在金融领域,则可以通过知识图谱对市场趋势、风险因素进行更深入的分析和预测。

总的来说,知识图谱作为连接数据、知识和智能的桥梁,其在人工智能的各个领域都扮演着至关重要的角色。随着技术的不断进步和应用领域的拓展,知识图谱将在智能化社会中发挥越来越重要的作用。

二、知识图谱的基础理论

file

定义与分类

知识图谱是一种通过图形结构表达知识的方法,它通过节点(实体)和边(关系)来表示和存储现实世界中的各种对象及其相互联系。这些实体和关系构成了一个复杂的网络,使得知识的存储不再是孤立的,而是相互关联和支持的。

知识图谱根据其内容和应用领域可以分为多种类型。例如,通用知识图谱旨在覆盖广泛的领域知识,如Google的Knowledge Graph;而领域知识图谱则专注于特定领域,如医疗、金融等。此外,根据构建方法的不同,知识图谱还可以分为基于规则的、基于统计的和混合型知识图谱。

核心组成

知识图谱的核心组成元素包括实体、关系和属性。实体是知识图谱中的基本单位,代表现实世界中的对象,如人、地点、组织等。关系则描述了实体之间的各种联系,例如“属于”、“位于”等。属性是对实体的具体描述,如年龄、位置等。这些元素共同构成了知识图谱的骨架,使得知识的组织和检索变得更加高效和精确。

历史与发展

知识图谱的概念最早可以追溯到语义网和链接数据的概念。早期的语义网关注于如何使网络上的数据更加机器可读,而链接数据则强调了数据之间的关联。知识图谱的出现是对这些理念的进一步发展和实践应用,它通过更加高效的数据结构和技术,使得知识的表示、存储和检索更加高效和智能。

随着人工智能和大数据技术的发展,知识图谱在自然语言处理、机器学习等领域得到了广泛应用。例如,知识图谱在提升搜索引擎的智能化、优化推荐系统的准确性等方面发挥了重要作用。此外,随着技术的不断进步,知识图谱的构建和应用也在不断地演变和优化,包括利用深度学习技术进行知识提取和图谱构建,以及在更多领域的应用拓展。

三、知识获取与预处理

file

数据源选择

知识图谱构建的首要步骤是确定和获取数据源。数据源的选择直接影响知识图谱的质量和应用范围。通常,数据源可以分为两大类:公开数据集和私有数据。公开数据集,如Wikipedia、Freebase、DBpedia等,提供了丰富的通用知识,适用于构建通用知识图谱。而私有数据,如企业内部数据库、专业期刊等,则更适用于构建特定领域的知识图谱。

选择数据源时,应考虑数据的可靠性、相关性、完整性和更新频率。可靠性保证了数据的准确性,相关性和完整性直接影响知识图谱的应用价值,而更新频率则关系到知识图谱的时效性。在实践中,通常需要结合多个数据源,以获取更全面和深入的知识覆盖。

数据清洗

获取数据后,下一步是数据清洗。这一过程涉及从原始数据中移除错误、重复或不完整的信息。数据清洗的方法包括去噪声、数据规范化、缺失值处理等。去噪声是移除数据集中的错误和无关数据,例如,去除格式错误的记录或非相关领域的信息。数据规范化涉及将数据转换为一致的格式,如统一日期格式、货币单位等。对于缺失值,可以采用插值、预测或删除不完整记录的方法处理。

数据清洗不仅提高了数据的质量,还能增强后续处理的效率和准确性。因此,这一步骤在知识图谱构建中至关重要。

实体识别

实体识别是指从文本中识别出知识图谱中的实体,这是构建知识图谱的核心步骤之一。实体识别通常依赖于自然语言处理(NLP)技术,特别是命名实体识别(NER)。NER技术能够从非结构化的文本中识别出具有特定意义的片段,如人名、地名、机构名等。

实体识别的方法多种多样,包括基于规则的方法、统计模型以及近年来兴起的基于深度学习的方法。基于规则的方法依赖于预定义的规则来识别实体,适用于结构化程度较高的领域。统计模型,如隐马尔可夫模型(HMM)、条件随机场(CRF)等,通过学习样本数据中的统计特征来识别实体。而基于深度学习的方法,如使用长短时记忆网络(LSTM)或BERT等预训练模型,能够更有效地处理语言的复杂性和多样性,提高识别的准确率和鲁棒性。

实体识别不仅需要高准确性,还要考虑到速度和可扩展性,特别是在处理大规模数据集时。因此,选择合适的实体识别技术和优化算法是至关重要的。

四、知识表示方法

file
知识表示是知识图谱构建中的核心环节,它涉及将现实世界的复杂信息和关系转化为计算机可理解和处理的格式。有效的知识表示不仅有助于提高知识图谱的查询效率,还能加强知识的推理能力,是实现知识图谱功能的关键。

知识表示模型

知识表示的首要任务是选择合适的模型。当前主流的知识表示模型包括资源描述框架(RDF)、Web本体语言(OWL)和属性图模型。

RDF

RDF是一种将信息表示为“主体-谓词-宾语”三元组的模型,它使得知识的表示形式既灵活又标准化。在RDF中,每个实体和关系都被赋予一个唯一的URI(统一资源标识符),以确保其全球唯一性和可互操作性。RDF的优势在于其简单性和扩展性,但它在表达复杂关系和属性方面存在局限。

OWL

OWL是基于RDF的一种更为复杂和强大的知识表示语言。它支持更丰富的数据类型和关系,包括类、属性、个体等,并能表达复杂的逻辑关系,如等价类、属性限制等。OWL的优势在于其表达能力和逻辑推理能力,适用于构建复杂的领域知识图谱。

属性图模型

属性图模型通过图结构来表示知识,其中节点代表实体,边代表关系,节点和边都可以附带属性。这种模型直观且易于实现,适用于大规模的图数据处理。它在图数据库中得到了广泛应用,如Neo4j、ArangoDB等。

本体构建

本体是知识图谱中用来描述特定领域知识和概念的一组术语和定义。本体的构建是知识图谱构建的重要部分,它定义了知识图谱中的实体类别、属性和关系类型。

本体构建的关键在于准确地把握和表达领域知识。这通常需要领域专家的参与,以确保本体的准确性和全面性。在实际操作中,可以使用本体编辑工具如Protégé来创建和管理本体,同时结合NLP技术自动化提取和维护本体结构。

关系提取与表示

关系提取是指从原始数据中识别出实体之间的关系,并将其加入到知识图谱中。这一步骤通常依赖于文本分析和数据挖掘技术。关系提取的方法包括基于规则的方法、机器学习方法和深度学习方法。

关系的表示要考虑到其多样性和复杂性。在简单的情况下,关系可以被直接表示为实体之间的连接。但在复杂情况下,关系可能涉及多个实体和属性,甚至是关系的层次和类型。在这种情况下,需要更复杂的数据结构和算法来准确表示关系。

五、知识图谱构建技术

file
构建知识图谱是一个复杂的过程,涉及数据处理、知识提取、存储管理等多个阶段。本节将详细探讨知识图谱构建的关键技术,并提供具体的代码示例。

图数据库选择

选择合适的图数据库是构建知识图谱的首要步骤。图数据库专为处理图形数据而设计,提供高效的节点、边查询和存储能力。常见的图数据库有Neo4j、ArangoDB等。

Neo4j

Neo4j是一个高性能的NoSQL图形数据库,支持Cypher查询语言,适合于处理复杂的关系数据。它的优势在于强大的关系处理能力和良好的社区支持。

ArangoDB

ArangoDB是一个多模型数据库,支持文档、键值及图形数据。它在灵活性和扩展性方面表现出色,适用于多种类型的数据存储需求。

构建流程

构建知识图谱的过程大致可分为数据预处理、实体关系识别、图数据库存储和优化几个阶段。

数据预处理

数据预处理包括数据清洗、实体识别等步骤,目的是将原始数据转换为适合构建知识图谱的格式。

import pandas as pd

# 示例:清洗和准备数据
def clean_data(data):
    # 数据清洗逻辑
    cleaned_data = data.dropna() # 去除空值
    return cleaned_data

# 假设我们有一个原始数据集
raw_data = pd.read_csv('example_dataset.csv')
cleaned_data = clean_data(raw_data)

实体关系识别

实体关系识别是从清洗后的数据中提取实体和关系。这里以Python和PyTorch实现一个简单的命名实体识别模型为例。

import torch
import torch.nn as nn
import torch.optim as optim

# 示例:定义一个简单的命名实体识别模型
class NERModel(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim):
        super(NERModel, self).__init__()
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, batch_first=True)
        self.fc = nn.Linear(hidden_dim, vocab_size)

    def forward(self, x):
        embedded = self.embedding(x)
        lstm_out, _ = self.lstm(embedded)
        out = self.fc(lstm_out)
        return out

# 初始化模型、损失函数和优化器
model = NERModel(vocab_size=1000, embedding_dim=64, hidden_dim=128)
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

图数据库存储

将提取的实体和关系存储到图数据库中。以Neo4j为例,展示如何使用Cypher语言存储数据。

// 示例:使用Cypher语言在Neo4j中创建节点和关系
CREATE (p1:Person {name: 'Alice'})
CREATE (p2:Person {name: 'Bob'})
CREATE (p1)-[:KNOWS]->(p2)

优化和索引

为提高查询效率,可以在图数据库中创建索引。

// 示例:在Neo4j中为Person节点的name属性创建索引
CREATE INDEX ON :Person(name)

深度学习在构建中的应用

深度学习技术在知识图谱构建中主要用于实体识别、关系提取和知识融合。以下展示一个使用深度学习进行关系提取的示例。

# 示例:使用深度学习进行关系提取
class RelationExtractionModel(nn.Module):
    def __init__(self, input_dim, hidden_dim):
        super(RelationExtractionModel, self).__init__()
        self.lstm = nn.LSTM(input_dim, hidden_dim, batch_first=True)
        self.fc = nn.Linear(hidden_dim, 2) # 假设有两种关系类型

    def forward(self, x):
        lstm_out, _ = self.lstm(x)
        out = self.fc(lstm_out[:, -1, :])
        return out

# 初始化模型、损失函数和优化器
relation_model = RelationExtractionModel(input_dim=300, hidden_dim=128)
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(relation_model.parameters(), lr=0.001)

在这个模型中,我们使用LSTM网络从文本数据中提取特征,并通过全连接层预测实体间的关系类型。

关注TechLead,分享AI全维度知识。作者拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,同济本复旦硕,复旦机器人智能实验室成员,阿里云认证的资深架构师,项目管理专业人士,上亿营收AI产品研发负责人
如有帮助,请多关注
TeahLead KrisChang,10+年的互联网和人工智能从业经验,10年+技术和业务团队管理经验,同济软件工程本科,复旦工程管理硕士,阿里云认证云服务资深架构师,上亿营收AI产品业务负责人。

前言

一个看似简单实则有坑的问题:空白网页的背景色是什么?


大家是不是都会认为是白色,但事实并非如此,有时候我们眼睛看到的也不一定是真的



引言


大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第三篇内容:线程与线程组。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!!

在现代软件开发中,多线程编程已成为提升程序性能和并发能力的关键技术之一。Java作为主流的面向对象编程语言,其对多线程的支持尤为强大且灵活。深入理解并掌握Java中的线程组(ThreadGroup)与线程优先级机制是构建高效、稳定并发应用的基础。

线程组在Java多线程体系中扮演着组织者和管理者的角色,它允许开发者以树状结构的形式批量控制一组相关的线程。每个线程必然隶属于一个线程组,这种层级关系不仅有助于资源的有效分配和管理,还能防止内存泄漏问题,确保即使“上级”线程结束时,“下级”线程也能被垃圾回收器正确地识别和处理。

线程优先级则是Java提供的一种影响调度策略的手段,虽然范围从1到10,但实际执行顺序并不严格遵循优先级数值大小,而是由操作系统依据自身的线程调度算法来决定。尽管如此,设置合理的线程优先级对于指导系统合理调配CPU资源仍具有一定的参考价值,如通过调用Thread类的setPriority()方法,可以建议高优先级的线程更有可能先于低优先级线程执行。

为了更好地说明这一点,以下是一个简单的示例代码:

public class PriorityDemo {
    public static void main(String[] args) {
        Thread highPriorityThread = new Thread(() -> {
            System.out.println("High priority thread running");
        });
        highPriorityThread.setPriority(10);

        Thread lowPriorityThread = new Thread(() -> {
            System.out.println("Low priority thread running");
        });

        // 启动两个线程
        highPriorityThread.start();
        lowPriorityThread.start();

        // 注意:这仅演示了设置优先级,并不保证高优先级线程一定先执行
    }
}

然而,值得注意的是,在实际场景中,过度依赖线程优先级来精确控制线程执行顺序并非最佳实践,因为操作系统可能不会严格按照Java中设定的优先级进行调度。此外,Java还提供了守护线程(Daemon Thread)这一特性,它们会在所有非守护线程结束后自动结束,适用于后台服务等辅助功能,可通过调用setDaemon(true)将线程设为守护线程。

综上所述,深入浅出Java多线程之线程组和线程优先级的核心内容包括线程组的构造与管理功能、线程优先级的实际意义及应用场景,以及守护线程的概念与使用,这些知识共同构成了Java多线程编程中不可或缺的一环。接下来,我们将详细探讨各个部分的具体实现及其背后的原理。



线程组(ThreadGroup)




定义与基本概念

Java中的线程组(ThreadGroup)是一个用于管理和组织一组相关线程的容器。每个线程在Java中必须隶属于一个线程组,它不仅提供了一种逻辑上的分组方式,也便于进行批量控制和异常处理等操作。线程组通过树状结构来表示层级关系,从而实现对线程生命周期的集中管理。



数据结构与属性

线程组的数据结构主要体现在其内部成员变量上,包括:

  • private final ThreadGroup parent; :指向父线程组的引用,体现了线程组之间的继承关系。
  • String name; :线程组的名字,用于标识和区分不同线程组。
  • int maxPriority; :定义了该线程组内所有线程允许的最大优先级,当线程试图设置高于此值的优先级时,系统会将其自动调整为组内的最大优先级。
  • boolean daemon; :指示线程组是否为守护线程组,子线程将继承这一属性,若为true,则当所有非守护线程终止后,守护线程也将结束。
  • 以及记录线程和子线程组数量、具体实例的数组如 Thread threads[]; ThreadGroup groups[]; 等。



权限控制与安全管理

Java中的线程组涉及到权限控制,例如在创建或修改线程组时需要检查调用线程是否有足够的权限。这通过
checkAccess()
方法来实现,它会委托给系统的SecurityManager对象执行相应的安全检查。例如,在创建线程组时,系统会调用
checkParentAccess()
方法确保当前线程具有添加子线程组到父线程组的权限。

下面是一个简化的示例代码,演示如何创建线程组并检查访问权限:

public class ThreadGroupDemo {
    public static void main(String[] args) {
        // 获取当前线程及其所属的线程组
        Thread currentThread = Thread.currentThread();
        ThreadGroup currentGroup = currentThread.getThreadGroup();

        // 检查当前线程是否有权限在其所在线程组下创建新的线程组
        currentGroup.checkAccess();

        // 创建新的线程组,父线程组为当前线程组
        ThreadGroup newGroup = new ThreadGroup(currentGroup, "NewGroup");

        // 若SecurityManager存在,这里将会触发相应的权限检查
    }
}

综上所述,线程组在Java多线程编程中提供了层次化的线程组织模型,并通过数据结构属性、创建与继承关系以及权限控制机制,实现了对线程集合的有效管理和安全性保障。



线程组的管理和控制



批量控制与统一异常处理

在Java中,线程组可以实现对一组线程的批量操作和统一管理。例如,通过重写
ThreadGroup
类的
uncaughtException(Thread t, Throwable e)
方法,可以在一个线程组中的任意线程抛出未捕获异常时,由该线程组统一进行异常处理。

public class ThreadGroupExceptionHandlerDemo {
    public static void main(String[] args) {
        ThreadGroup threadGroup = new ThreadGroup("MyGroup") {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println(t.getName() + " threw an exception: " + e.getMessage());
            }
        };

        Thread thread1 = new Thread(threadGroup, () -> {
            throw new RuntimeException("An unchecked exception from Thread 1");
        });
        Thread thread2 = new Thread(threadGroup, () -> {
            // Some other code that might throw exceptions as well
        });

        thread1.start();
        thread2.start();

        // Let's wait for threads to finish and handle any exceptions thrown
        while (threadGroup.activeCount() > 0) {
            threadGroup.wait();
        }
    }
}

在这个例子中,所有属于"MyGroup"线程组的线程在其run()方法内抛出未被捕获的异常时,都会触发自定义的
uncaughtException()
方法,从而实现了对整个线程组内异常的集中处理。



线程优先级限制

Java线程组还提供了设置其下所有线程最大优先级的功能,这意味着即使某个线程尝试将优先级设置得高于线程组的最大允许值,最终也会被限制在最大优先级以下。如:

public class ThreadGroupPriorityLimitDemo {
    public static void main(String[] args) {
        ThreadGroup threadGroup = new ThreadGroup("LimitedPriorityGroup");
        threadGroup.setMaxPriority(6);

        Thread highPriorityThread = new Thread(threadGroup, () -> {
            Thread.currentThread().setPriority(9); // This will be capped at 6
            System.out.println("Actual priority of this thread: " + Thread.currentThread().getPriority());
        });

        highPriorityThread.start();
    }
}

运行上述代码后,尽管高优先级线程试图将其优先级设为9,但受限于线程组的限制,实际执行时其优先级仍会被调整为6。



创建与继承关系

创建线程组有多种构造函数,例如:

// 默认构造函数,创建名为"system"的线程组
ThreadGroup defaultGroup = new ThreadGroup();

// 使用字符串名称创建新的线程组,默认父线程组是当前运行线程所在的组
ThreadGroup myGroup = new ThreadGroup("MyGroupName");

// 显式指定父线程组和名称创建新线程组
ThreadGroup parentGroup = ...;
ThreadGroup childGroup = new ThreadGroup(parentGroup, "ChildGroupName");

新建的线程默认会继承父线程所在线程组,但也可以通过构造函数显式指定线程组。

创建新线程时,默认情况下会继承父线程(即当前创建线程的线程)所在的线程组。如果需要创建新的线程组,并指定它作为新线程的归属,则可以通过构造函数明确指定。

public class ThreadGroupInheritanceDemo {
    public static void main(String[] args) {
        ThreadGroup parentGroup = new ThreadGroup("ParentGroup");
        Thread childThread = new Thread(parentGroup, () -> {
            System.out.println("Child thread belongs to group: " + Thread.currentThread().getThreadGroup().getName());
        });

        childThread.start();
    }
}

在此示例中,新建的子线程会显示其所属的线程组是"ParentGroup"。



线程生命周期管理

线程组对成员线程的生命周期具有一定的管理作用,尤其是在垃圾回收方面。由于线程组采用树状结构组织,上级线程组包含下级线程组或线程,当上级线程组被销毁时,其下的所有线程和线程组都将被终止,有助于防止“上级”线程被“下级”线程引用而导致无法被垃圾回收器有效回收。此外,守护线程的特性也在线程组的生命周期管理中发挥作用,当所有非守护线程结束时,守护线程也将自动结束。



线程优先级




优先级范围与操作系统映射

在Java中,线程的优先级是一个介于1到10之间的整数值,其中1代表最低优先级,10代表最高优先级。然而,尽管Java提供了这种精细的优先级划分,但并非所有操作系统都能精确地支持这10个级别的优先级区分。实际操作时,Java会将程序员设置的优先级作为一个参考值传递给底层操作系统,最终线程在操作系统层面的实际执行优先级是由操作系统本身的调度策略决定的。例如,在某些系统上,可能只会将Java线程优先级映射为低、中、高三档。



设置优先级的方法

在Java程序中,可以通过调用
Thread
类的
setPriority(int priority)
方法来设置线程的优先级。以下是一个简单的代码示例:

public class ThreadPriorityDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            // 线程执行的任务
            System.out.println("Thread running with priority: " + Thread.currentThread().getPriority());
        });

        // 设置线程优先级为9(假设这是最高优先级)
        thread.setPriority(9);

        thread.start();
    }
}

在上述代码中,我们创建了一个新线程并将其优先级设置为9,然后启动它。通过输出当前线程的优先级,可以验证设置是否成功。



优先级的实际效果

尽管可以设定线程优先级,但在多线程环境下,线程的执行顺序并不完全受优先级控制。高优先级的线程理论上会有更大的概率先于低优先级的线程执行,但这并不是绝对的保证。Java线程调度器采用抢占式调度策略,这意味着优先级较高的线程更有可能获得CPU资源,但在同一优先级下,线程的执行遵循“先到先服务”原则。

为了直观展示优先级对线程执行的影响,考虑以下例子:

public class PriorityExecutionDemo {
    public static void main(String[] args) {
        IntStream.rangeClosed(110).forEach(priority -> {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    Thread currentThread = Thread.currentThread();
                    System.out.format("Priority %d - Starting thread: %s%n",
                                      currentThread.getPriority(),
                                      currentThread.getName());
                    try {
                        Thread.sleep(100); // 延迟以模拟其他任务
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.format("Priority %d - Ended thread: %s%n",
                                      currentThread.getPriority(),
                                      currentThread.getName());
                }
            });
            t.setPriority(priority);
            t.start();
        });
    }
}

在这段代码中,我们创建了10个线程,每个线程的优先级从1递增到10,并启动它们。虽然理论上优先级高的线程应该更快执行完毕,但实际上由于线程调度的不确定性以及短时间内的延迟,可能会观察到不同的执行顺序。

总之,虽然Java提供了线程优先级机制,但它并不能确保严格按照优先级顺序执行线程,而是作为影响调度决策的一个因素存在。开发者应当谨慎使用优先级,特别是在需要严格确定性执行顺序的情况下,应依赖同步机制而非单纯依赖线程优先级。



守护线程(Daemon Thread)




守护线程特性

在Java多线程编程中,守护线程(Daemon Thread)是一种特殊类型的线程。它们的特点在于其生命周期与应用程序的主进程或者非守护线程紧密关联。当所有非守护线程结束运行后,即使守护线程还在执行,JVM也会停止运行并退出程序,这意味着守护线程不会阻止JVM的关闭。

守护线程通常用于执行那些不直接影响应用程序主要任务、且不需要等待其完成的任务,比如后台清理工作、监控服务或资源回收等辅助性功能。一旦主程序逻辑完成,守护线程会被系统自动忽略,不会阻塞JVM的正常终止。



应用场景与设置

守护线程的一个典型应用场景是在服务器程序中处理日志记录、监控和定时任务。例如,一个长时间运行的服务可能会启动一个守护线程来定期清理过期的数据或者检查系统的健康状态,而这些操作并不是主线程必须等待完成的任务。

设置线程为守护线程的方法非常简单,只需要调用Thread类的setDaemon()方法,并传入一个布尔值true即可。下面是一个实例代码:

public class DaemonThreadDemo {
    public static void main(String[] args) {
        // 创建一个非守护线程(主线程)
        Thread mainThread = Thread.currentThread();

        // 创建一个守护线程
        Thread daemonThread = new Thread(() -> {
            while (true) {
                System.out.println("Daemon thread is running...");
                try {
                    Thread.sleep(1000); // 模拟一些后台任务
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 设置daemonThread为守护线程
        daemonThread.setDaemon(true);
        daemonThread.start();

        // 主线程执行完毕后会立即退出,此时守护线程也会随之结束
        System.out.println("Main thread has finished.");
    }
}

在上述示例中,
daemonThread
被设置为守护线程,当main方法中的主线程执行完毕并打印出"Main thread has finished."时,由于没有其他非守护线程在运行,JVM将结束运行,即使守护线程仍在执行循环任务。

总之,守护线程是Java多线程编程中一种重要而又特殊的线程类型,它主要用于处理那些无关紧要、不必等待其完成的任务,在确保应用程序高效利用资源的同时,也能够灵活控制程序的退出时机。



实践案例与注意事项




示例代码解析

在Java多线程编程中,理解和运用线程组以及线程优先级是十分重要的。以下是一个实践案例,通过设置线程组的优先级限制并观察实际效果:

public class ThreadGroupPriorityDemo {
    public static void main(String[] args) {
        // 创建一个线程组,并设置其最大优先级为6
        ThreadGroup threadGroup = new ThreadGroup("LimitedPriorityGroup");
        threadGroup.setMaxPriority(6);

        // 创建一个新的线程,并将其优先级设为9(高于线程组的最大优先级)
        Thread highPriorityThread = new Thread(threadGroup, () -> {
            System.out.println("Test thread's name: " + Thread.currentThread().getName());
            System.out.println("Actual priority of this thread: " + Thread.currentThread().getPriority());
        });
        highPriorityThread.setPriority(9);  // 这个优先级将会被线程组限制

        // 启动该线程
        highPriorityThread.start();

        // 输出结果会显示线程的实际优先级已经被线程组调整为6
    }
}

此例中,当创建了一个具有优先级上限的线程组后,尝试将线程的优先级设置得高于线程组允许的最大值时,系统会自动将其优先级降低到线程组设定的阈值内。



注意事项


  1. 合理使用线程组
    :线程组对于批量控制和管理一组相关线程非常有用,比如可以统一设置异常处理器、控制线程生命周期等。但应当注意避免过度划分线程组导致管理复杂度增加。

  2. 谨慎设置线程优先级
    :虽然Java提供了线程优先级设置机制,但操作系统对线程优先级的处理存在差异,且高优先级线程并不意味着绝对先于低优先级线程执行。因此,在大多数情况下,应遵循默认优先级或仅适度调整以适应特定需求,而非依赖优先级来精确控制线程间的执行顺序。

  3. 守护线程的正确使用
    :确保理解守护线程的特点和应用场景,只在合适的地方使用它们,如后台监控、清理工作等非关键任务。同时要注意,不要让守护线程持有阻止JVM退出的重要资源。

  4. 安全性和权限控制
    :在涉及线程组及线程操作时,尤其是在安全性要求较高的环境中,要充分考虑SecurityManager的作用,确保调用线程有正确的权限进行相应操作。

  5. 异常处理策略
    :为了提高程序健壮性,可利用线程组的 uncaughtException() 方法实现统一的异常处理策略,这样即使子线程发生未捕获异常,也可以按照预定逻辑进行处理。

总结来说,在实际编程实践中,开发者应当根据业务场景灵活应用线程组和线程优先级的功能,并始终关注其可能带来的并发问题和系统稳定性影响,以期达到最优的并发性能和良好的编程习惯。



结论


在深入浅出Java多线程的过程中,我们详细探讨了线程组和线程优先级的概念与实践应用。线程组作为一个管理容器,不仅提供了组织和批量控制线程的机制,还允许通过重写
uncaughtException()
方法对整个线程组内异常进行统一处理,增强了程序健壮性。其树状结构有助于维护线程间的层次关系,同时通过对最大优先级的设定,可以限制子线程的优先级上限,确保资源的有效管理和调度。

线程优先级虽可在1到10之间设置,但实际执行时需注意操作系统对其支持程度的差异,并且高优先级并不意味着一定能先于低优先级线程执行。实践中应谨慎使用优先级调整,更多依赖于Java的抢占式调度策略以及“先来先服务”原则。通过实例代码,我们展示了即使设置了高于线程组最大优先级的值,线程的实际优先级也会被限定在线程组的最大优先级范围内。

守护线程(Daemon Thread)作为特殊的线程类型,主要用于执行非关键任务,当所有非守护线程终止时,守护线程也随之结束,不会阻止JVM的退出。这在实现后台服务、监控或清理工作等场景中具有重要意义。

综上所述,在开发Java多线程应用程序时,理解并合理运用线程组和线程优先级能够优化系统性能和稳定性,但同时也需要注意它们并非决定线程执行顺序的绝对手段。最佳实践是结合具体业务需求和系统环境特性,适度调整线程优先级,充分利用线程组进行资源和异常管理,并根据需要正确配置守护线程,以保证应用程序高效运行的同时,避免不必要的内存泄漏和阻塞问题。通过案例演示和理论知识相结合的方式,开发者能够更好地理解和运用这些概念,从而设计出更加高效、稳定且易于维护的并发程序。

本文使用
markdown.com.cn
排版