java线程实现方式问题

博主最新文章
博主热门文章
您举报文章:
举报原因:
原文地址:
原因补充:
(最多只允许输入30个字)其他回答(1)
org.springframework.core.task.TaskExecutor
你设置的私有变量的类和bean的类不一样,导错包了吗?
清除回答草稿
&&&您需要以后才能回答,未注册用户请先。拯救Java线程死锁问题!最强解决方案看这里! - 产经要闻 - 科技讯
拯救Java线程死锁问题!最强解决方案看这里!
众所周知,在Java 语言中每个Object都有一个隐含的锁,通过该锁,我们可以使用synchronized 关键字来保证代码块的原子性。synchronized 能够使线程在执行到该代码块时,自动获取此内部锁,而一旦离开该代码块,无论...
众所周知,在Java 语言中每个Object都有一个隐含的锁,通过该锁,我们可以使用synchronized 关键字来保证代码块的原子性。synchronized 能够使线程在执行到该代码块时,自动获取此内部锁,而一旦离开该代码块,无论是完成或者中断都会自动释放锁。显然这是一个独占锁,每个锁请求之间是互斥的。相对于众多高级锁 (Lock/ReadWriteLock 等),synchronized 的代价都比后者要高,但是 synchronzied 的语法比较简单,而且也比较容易使用和理解。Lock 一旦调用了 lock() 方法获取到锁而未正确释放的话,便很有可能造成死锁,因此,我们总是在finally代码块中调用unlock(),用以保证锁一定会被释放,而这在代码结构上也是一次调整和冗余。Lock 的实现已经将硬件资源用到了极致,所以未来可优化的空间不大,除非硬件有了更高的性能,但是synchronized不同,它只是一种规范的实现方式,在不同的平台以及不同的硬件,都还有很高的提升空间,也是未来java锁优化的主要方向。既然 synchronzied 都不可能避免死锁产生,那么死锁情况便会是经常出现的错误,本期“安仔课堂”,ISEC实验室的陈老师为大家具体描述死锁发生的原因及解决方法。一、死锁描述死锁是操作系统层面的一个错误,是进程死锁的简称,最早在 1965 年由 Dijkstra 在研究银行家算法时提出的,它是计算机操作系统乃至整个并发程序设计领域最难处理的问题之一。事实上,计算机世界有很多事情需要用多线程方式解决,因为这样才能最大程度上利用资源,体现出计算的高效。但是,实际上来说,计算机系统中有很多一次只能由一个进程使用资源的情况,例如打印机,同时只能有一个进程控制它。在多通道程序设计环境中,若干进程往往要共享这类资源,而且一个进程所需要的资源很可能不止一个。因此就会出现若干进程竞争有限资源,又推进顺序不当,从而构成无限期循环等待的局面,我们称这种状态为死锁。简单一点描述,死锁是指多个进程循环等待他方占有的资源而无限期地僵持下去的局面。很显然,如果没有外力的作用,那么死锁涉及到的各个进程都将永远处于封锁状态。系统发生死锁现象不仅浪费大量的系统资源,还会导致整个系统崩溃,带来灾难性后果。所以,对于死锁问题在理论上和技术上都必须予以高度重视。银行家算法一个银行家如何将一定数目的资金安全地借给若干个客户,既能使客户借到钱完成要干的事,同时又能使自己收回全部资金而不至于破产呢?银行家就像一个操作系统,客户就像运行的进程,银行家的资金就是系统的资源。银行家算法需要确保以下四点:1.当一个顾客对资金的最大需求量不超过银行家现有的资金时就可接纳该顾客;2.顾客可以分期贷款, 但贷款的总数不能超过最大需求量;3.当银行家现有的资金不能满足顾客尚需的贷款数额时,对顾客的贷款可推迟支付,但总能使顾客在有限的时间里得到贷款;4.当顾客得到所需的全部资金后,一定能在有限的时间里归还所有的资金。清单 1. 银行家算法实现死锁示例死锁问题是多线程特有的问题,它可以被认为是线程间切换消耗系统性能的一种极端情况。在死锁时,线程间相互等待资源,而又不释放自身的资源,导致无穷无尽的等待,其结果是系统任务永远无法执行完成。死锁问题是在多线程开发中应该坚决避免和杜绝的问题。一般来说,出现死锁问题需要满足以下条件:1. 互斥条件:一个资源每次只能被一个线程使用。2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。3. 不可剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。只要破坏死锁 4 个必要条件之一中的任何一个,死锁问题就能被解决。我们先来看一个示例,前面说过,死锁是两个甚至多个线程被永久阻塞时的一种运行局面,造成这种局面,至少需要两个线程以及两个或者多个共享资源。如以下清单 2 所示的代码示例,我们编写了一个简单的程序,它将会引起死锁发生,这样我们就会明白如何分析它。清单 2. 死锁示例在上面的程序中同步线程实现了 Runnable 接口,它工作的是两个对象,这两个对象向对方寻求死锁而且都在使用同步阻塞。在主函数中,我使用了三个为同步线程运行的线程,而且在其中每个线程中都有一个可共享的资源。这些线程以向第一个对象获取封锁这种方式运行,但是当它试着向第二个对象获取封锁时,便会进入等待状态,因为它已经被另一个线程封锁住了,这样,在线程引起死锁的过程中,就形成了一个依赖于资源的循环。当我执行上面的程序时,就产生了输出,但是程序却因为死锁无法停止,输出如以下清单 3 所示。清单 3. 清单 2 运行输出在此我们可以清楚地在输出结果中辨认出死锁局面,但是在实际所用的应用中,发现死锁并将它排除是非常难的。二、死锁情况诊断JVM 提供了一些工具可以来帮助诊断死锁的发生,如下面程序清单 4 所示,我们实现了一个死锁,以linux为例,然后尝试通过 jstack 命令追踪、分析死锁发生。清单 4. 死锁示例代码执行代码后,在shell的命令窗口找到当前发生死锁的进程号,如下清单5:根据上面的进程号,通过jstack命令查找对应的堆栈信息,如下清单6:stack 可用于导出 Java 应用程序的线程堆栈,-l 选项用于打印锁的附加信息。我们运行 jstack 命令,输出如上清单6。 从打印出的堆栈信息(清单6)中,可直观的确认出现死锁的位置。三、死锁解决方案死锁是由四个必要条件导致的,所以一般来说,只要破坏这四个必要条件中的一个条件,死锁情况就应该不会发生。1.如果想要打破互斥条件,我们需要允许进程同时访问某些资源,这种方法受制于实际场景,不太容易实现条件。2.打破不可抢占条件,这样需要允许进程强行从占有者那里夺取某些资源,或者简单一点理解,占有资源的进程不能再申请占有其他资源,必须释放手上的资源之后才能发起申请,这个其实也很难找到适用场景。3.进程在运行前申请得到所有的资源,否则该进程不能进入准备执行状态。这个方法看似有点用处,但是它的缺点是可能导致资源利用率和进程并发性降低。4.避免出现资源申请环路,即对资源事先分类编号,按号分配。这种方式可以有效提高资源的利用率和系统吞吐量,但是增加了系统开销,增大了进程对资源的占用时间。如果我们在死锁检查时发现了死锁情况,那么就要努力消除死锁,使系统从死锁状态中恢复过来。以下为消除死锁的几种方式:1.最简单、最常用的方法就是进行系统的重新启动,不过这种方法代价很大,它意味着在这之前所有的进程已经完成的计算工作都将付之东流,包括参与死锁的那些进程,以及未参与死锁的进程。2.撤消进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁,这时又分两种情况:一次性撤消参与死锁的全部进程,剥夺全部资源;或者逐步撤消参与死锁的进程,逐步收回死锁进程占有的资源。一般来说,选择逐步撤消的进程时要按照一定的原则进行,目的是撤消那些代价最小的进程,比如按进程的优先级确定进程的代价;考虑进程运行时的代价和与此进程相关的外部作业的代价等因素。3.进程回退策略,即让参与死锁的进程回退到没有发生死锁前某一点处,并由此点处继续执行,以求再次执行时不再发生死锁。虽然这是个较理想的办法,但是操作起来系统开销极大,要有堆栈这样的机构来记录进程的每一步变化,以便今后的回退,有时这是无法做到的。其实,即便是商业产品,依然会有很多死锁情况发生,例如 MySQL 数据库,它也经常容易出现死锁案例。MySQL死锁情况解决方法假设我们用 Show innodb status 检查引擎状态时发现了死锁情况,如以下清单 7 所示。清单 7. MySQL 死锁我们假设涉事的数据表上面有一个索引,这次的死锁就是由于两条记录同时访问到了相同的索引造成的。我们首先来看看 InnoDB 类型的数据表,只要能够解决索引问题,就可以解决死锁问题。MySQL 的 InnoDB 引擎是行级锁,需要注意的是,这不是对记录进行锁定,而是对索引进行锁定。在 UPDATE、DELETE 操作时,MySQL 不仅锁定 WHERE 条件扫描过的所有索引记录,而且会锁定相邻的键值,即所谓的 next-如语句 UPDATE TSK_TASK SET UPDATE_TIME = NOW() WHERE ID & 10000 会锁定所有主键大于等于 1000 的所有记录,在该语句完成之前,你就不能对主键等于 10000 的记录进行操作;当非簇索引 (non-cluster index) 记录被锁定时,相关的簇索引 (cluster index) 记录也需要被锁定才能完成相应的操作。再分析一下发生问题的两条 SQL 语句:当执行时,MySQL 会使用 KEY_TSKTASK_MONTIME2 索引,因此首先锁定相关的索引记录,因为 KEY_TSKTASK_MONTIME2 是非簇索引,为执行该语句,MySQL 还会锁定簇索引(主键索引)。假设“update TSK_TASK set STATUS_ID=1067,UPDATE_TIME=now () where ID in (9921180)”几乎同时执行时,本语句首先锁定簇索引 (主键),由于需要更新 STATUS_ID 的值,所以还需要锁定 KEY_TSKTASK_MONTIME2 的某些索引记录。这样第一条语句锁定了 KEY_TSKTASK_MONTIME2 的记录,等待主键索引,而第二条语句则锁定了主键索引记录,而等待 KEY_TSKTASK_MONTIME2 的记录,这样死锁就产生了。我们通过拆分第一条语句解决了死锁问题:即先查出符合条件的 ID:select ID from TSK_TASK where STATUS_ID=1061 and MON_TIME & date_sub(now(), INTERVAL 30 minute);然后再更新状态:update TSK_TASK set STATUS_ID=1064 where ID in (….)。四、结束语我们发现,死锁虽然是较早就被发现的问题,但是很多情况下我们设计的程序里还是经常发生死锁情况。我们不能只是分析如何解决死锁这类问题,还需要具体找出预防死锁的方法,这样才能从根本上解决问题。总的来说,还是需要系统架构师、程序员不断积累经验,从业务逻辑设计层面彻底消除死锁发生的可能性。安胜作为国内领先的网络安全检测产品及服务提供商,秉承“创新为安,服务致胜”的经营理念,专注于网络安全类产品的生产与服务;以“研发+服务+销售”的经营模式,“装备+平台+服务”的产品体系,在技术研究、研发创新、产品化等方面已形成一套完整的流程化体系,为广大用户提供量体裁衣的综合解决方案!ISEC实验室作为公司新技术和新产品的预研基地,秉承“我的安全,我做主”的理念,专注于网络安全领域前沿技术研究,提供网络安全培训、应急响应、安全检测等服务。2018年承担全国两会网络安全保障工作。2017年承担全国两会网络安全保障工作;完成金砖“厦门会晤”保障工作;完成北京“一带一路”国际合作高峰论坛网络安全保障;承担中国共产党第十九次全国代表大会网络安全保障。承担第四届世界互联网大会。2016年承担全国两会网络安全保障工作;为贵阳大数据与网络安全攻防演练提供技术支持;承担G20峰会的网络安保工作;承担第三届世界互联网大会。2015年承担第二届世界互联网大会。2014年承担第一届世界互联网大会。不忘初心、砥砺前行;未来,我们将继续坚守、不懈追求,为国家网络安全事业保驾护航!
【免责声明】本文为厂商推广稿件,企业发布本文的目的在于推广其产品或服务,科技讯发布此文仅为传递信息,不代表科技讯赞同其观点,不对内容真实性负责,仅供用户参考之用,不构成任何投资、使用等行为的建议。请读者使用之前核实真实性,以及可能存在的风险,任何后果均由读者自行承担。
换一换
04-24 12:56
04-24 12:55
04-24 12:54
04-24 12:52
04-23 18:11
03-15 12:28
03-13 17:52
03-05 10:51
02-02 16:44
02-02 16:44
04-24 10:25
04-24 10:21
04-23 14:25
04-23 14:21
04-22 20:42
04-22 18:34
04-22 18:29
04-22 18:24
04-21 16:41
04-21 16:02
04-21 10:49
04-20 12:20
04-20 12:14
04-19 10:54
04-19 10:33
04-19 10:28
04-18 17:17
04-18 09:58
04-17 17:05
04-16 17:07
04-16 15:33
04-16 09:57
04-15 22:55
04-15 22:30
04-15 21:05
04-14 19:18
04-14 18:21
04-14 17:52
04-14 12:41
04-14 12:31
& 科技讯版权所有如果我是国王:关于解决 Java 编程语言线程问题的建议
Java 语言的线程模型是此语言的一个最难另人满意的部分。尽管 Java
语言本身就支持线程编程是件好事,但是它对线程的语法和类包的支持太少,只能适用于极小型的应用环境。关于 Java 线程编程的大多数书籍都长篇累牍地指出了 Java
线程模型的缺陷,并提供了解决这些问题的急救包 (Band-Aid/ 邦迪创可贴 ) 类库。我称这些类为急救包,是因为它们所能解决的问题本应是由
语言本身语法所包含的。从长远来看,以语法而不是类库方法,将能产生更高效的代码。这是因为编译器和
Java 虚拟器 (JVM)
能一同优化程序代码,而这些优化对于类库中的代码是很难或无法实现的。在我的《
Taming Java Threads》(请参阅
)书中以及本文中,我进一步建议对 Java
编程语言本身进行一些修改,以使得它能够真正解决这些线程编程的问题。本文和我这本书的主要区别是,我在撰写本文时进行了更多的思考 ,
所以对书中的提议加以了提高。这些建议只是尝试性的 --
只是我个人对这些问题的想法,而且实现这些想法需要进行大量的工作以及同行们的评价。但这是毕竟是一个开端,我有意为解决这些问题成立一个专门的工作组,如果您感兴趣,请发
。一旦我真正着手进行,我就会给您发通知。
这里提出的建议是非常大胆的。有些人建议对 Java 语言规范
(JLS)(请参阅
)进行细微和少量的修改以解决当前模糊的
JVM 行为,但是我却想对其进行更为彻底的改进。
在实际草稿中,我的许多建议包括为此语言引入新的关键字。虽然通常要求不要突破一个语言的现有代码是正确的,但是如果该语言的并不是要保持不变以至于过时的话,它就必须能引入新的关键字。为了使引入的关键字与现有的标识符不产生冲突,经过细心考虑,我将使用一个
($) 字符,而这个字符在现有的标识符中是非法的。( 例如,使用 $task,
task)。此时需要编译器的命令行开关提供支持,能使用这些关键字的变体,而不是忽略这个美元符号。task(任务)的概念
Java 线程模型的根本问题是它完全不是面向对象的。面向对象 (OO)
设计人员根本不按线程角度考虑问题;他们考虑的是
异步信息(同步信息被立即处理
直到信息处理完成才返回消息句柄;异步信息收到后将在后台处理一段时间
-- 而早在信息处理结束前就返回消息句柄)。Java 编程语言中的
Toolkit.getImage()
方法就是异步信息的一个好例子。
getImage()
的消息句柄将被立即返回,而不必等到整个图像被后台线程取回。
这是面向对象 (OO) 的处理方法。但是,如前所述,Java 的线程模型是非面向对象的。一个 Java 编程语言线程实际上只是一个
run()过程,它调用了其它的过程。在这里就根本没有对象、异步或同步信息以及其它概念。
对于此问题,在我的书中深入讨论过的一个解决方法是,使用一个
Active_object。active
对象是可以接收异步请求的对象,它在接收到请求后的一段时间内以后台方式得以处理。在
编程语言中,一个请求可被封装在一个对象中。例如,你可以把一个通过
Runnable接口实现的实例传送给此 active 对象,该接口的
run()方法封装了需要完成的工作。该 runnable 对象被此
active 对象排入到队列中,当轮到它执行时,active
对象使用一个后台线程来执行它。
在一个 active
对象上运行的异步信息实际上是同步的,因为它们被一个单一的服务线程按顺序从队列中取出并执行。因此,使用一个
active 对象以一种更为过程化的模型可以消除大多数的同步问题。在某种意义上,Java 编程语言的整个 Swing/AWT 子系统是一个 active 对象。向一个 Swing 队列传送一条讯息的唯一安全的途径是,调用一个类似
SwingUtilities.invokeLater()的方法,这样就在 Swing 事件队列上发送了一个 runnable 对象,当轮到它执行时, Swing 事件处理线程将会处理它。
那么我的第一个建议是,向 Java 编程语言中加入一个
task(任务)的概念,从而将 active 对象集成到语言中。(
task 的概念是从 Intel 的 RMX 操作系统和 Ada
编程语言借鉴过来的。大多数实时操作系统都支持类似的概念。)
一个任务有一个内置的 active
对象分发程序,并自动管理那些处理异步信息的全部机制。
定义一个任务和定义一个类基本相同,不同的只是需要在任务的方法前加一个
asynchronous修饰符来指示 active
对象的分配程序在后台处理这些方法。请参考我的书中第九章的基于类方法,再看以下的
file_io 类,它使用了在《
Taming Java Threads》中所讨论的
Active_object类来实现异步写操作:
interface Exception_handler{
void handle_exception( Throwable e );}
class File_io_task{
Active_object dispatcher = new Active_object();
final OutputS
final Exception_
File_io_task( String file_name, Exception_handler handler )
throws IOException
file = new FileOutputStream( file_name );
this.handler =
public void write( final byte[] bytes ){
// The following call asks the active-object dispatcher
// to enqueue the Runnable object on its request
// queue. A thread associated with the active object
// dequeues the runnable objects and executes them
// one at a time.
dispatcher.dispatch
new Runnable()
public void run(){
byte[] copy new byte[ bytes.length ];
System.arrayCopy(
bytes.length );
file.write( copy );
catch( Throwable problem )
handler.handle_exception( problem );
}所有的写请求都用一个
dispatch()过程调用被放在
active-object
的输入队列中排队。在后台处理这些异步信息时出现的任何异常
(exception) 都由
Exception_handler对象处理,此
Exception_handler对象被传送到
File_io_task
的构造函数中。您要写内容到文件时,代码如下:
File_io_task io =
new File_io_task
( "foo.txt"
new Exception_handler
public void handle( Throwable e ){
e.printStackTrace();
io.write( some_bytes );这种基于类的处理方法,其主要问题是太复杂了 --
对于一个这样简单的操作,代码太杂了。向 Java 语言引入
$asynchronous
关键字后,就可以按下面这样重写以前的代码:
$task File_io $error{ $.printStackTrace(); }
File_io( String file_name ) throws IOException
file = new FileOutputStream( file_name );
asynchronous public write( byte[] bytes )
file.write( bytes );
注意,异步方法并没有指定返回值,因为其句柄将被立即返回,而不用等到请求的操作处理完成后。所以,此时没有合理的返回值。对于派生出的模型,
$task关键字和
一样同效:
可以实现接口、继承类和继承的其它任务。标有
asynchronous关键字的方法由
在后台处理。其它的方法将同步运行,就像在类中一样。
$task关键字可以用一个可选的
从句修饰 ( 如上所示 ),
它表明对任何无法被异步方法本身捕捉的异常将有一个缺省的处理程序。我使用
$来代表被抛出的异常对象。如果没有指定
$error从句,
就将打印出一个合理的出错信息(很可能是堆栈跟踪信息)。
注意,为确保线程安全,异步方法的参数必须是不变 (immutable)
的。运行时系统应通过相关语义来保证这种不变性(简单的复制通常是不够的)。所有的 task 对象必须支持一些伪信息 (pseudo-message),例如:some_task.close()在此调用后发送的任何异步信息都产生一个
TaskClosedException。但是,在 active
对象队列上等候的消息仍能被提供。
some_task.join()
调用程序被阻断,直到此任务关闭、而且所有未完成的请求都被处理完毕。除了常用的修饰符(
public等 ),
task关键字还应接受一个
$pooled(n)修饰符,它导致
task使用一个线程池,而不是使用单个线程来运行异步请求。
n指定了所需线程池的大小;必要时,此线程池可以增加,但是当不再需要线程时,它应该缩到原来的大小。伪域 (pseudo-field) $pool_size 返回在
$pooled(n)中指定的原始
Taming Java
Threads》的第八章中,我给出了一个服务器端的 socket
处理程序,作为线程池的例子。它是关于使用线程池的任务的一个好例子。其基本思路是产生一个独立对象,它的任务是监控一个服务器端的
socket。每当一个客户机连接到服务器时,服务器端的对象会从池中抓取一个预先创建的睡眠线程,并把此线程设置为服务于客户端连接。socket
服务器会产出一个额外的客户服务线程,但是当连接关闭时,这些额外的线程将被删除。实现
socket 服务器的推荐语法如下:
public $pooled(10) $task Client_handler {
PrintWriter log = new PrintWriter( System.out );
public asynchronous void handle( Socket connection_to_the_client ) {
log.println("writing");
// client-handling code goes here. Every call to
// handle()
is executed on its own thread, but 10
// threads are pre-created for this purpose. Additional
// threads are created on an as-needed basis, but are
// discarded when handle() returns.
$task Socket_server
Client_handler client_handlers = new Client_handler();
public Socket_server( int port_number ){
server = new ServerSocket(port_number);
public $asynchronous listen(Client_handler client) {
// This method is executed on its own thread.
while( true )
client_handlers.handle( server.accept() );
Socket_server = new Socket_server( the_port_number );
server.listen()Socket_server对象使用一个独立的后台线程处理异步的
listen()请求,它封装 socket
的“接受”循环。当每个客户端连接时,
listen()请求一个
Client_handler通过调用
来处理请求。每个
请求在它们自己的线程中执行(因为这是一个
注意,每个传送到
$pooled $task
的异步消息实际上都使用它们自己的线程来处理。典型情况下,由于一个
$pooled $task
用于实现一个自主操作;所以对于解决与访问状态变量有关的潜在的同步问题,最好的解决方法是在
$asynchronous方法中使用
this是指向的对象的一个独有副本。这就是说,当向一个
$pooled $task发送一个异步请求时,将执行一个
clone()操作,并且此方法的
指针会指向此克隆对象。线程之间的通信可通过对
区的同步访问实现。
synchronized
虽然在多数情况下,
消除了同步操作的要求,但是不是所有的多线程系统都用任务来实现。所以,还需要改进现有的线程模块。
synchronized关键字有下列缺点:
无法指定一个超时值。无法中断一个正在等待请求锁的线程。无法安全地请求多个锁 。( 多个锁只能以依次序获得。)解决这些问题的办法是:扩展
synchronized的语法,使它支持多个参数和能接受一个超时说明(在下面的括弧中指定)。下面是我希望的语法:
synchronized(x && y
z对象的锁。
synchronized(x || y ||
对象的锁。
synchronized( (x && y )
|| z)对于前面代码的一些扩展。synchronized(...)[1000]设置 1 秒超时以获得一个锁。synchronized[1000]
f(){...}在进入
f()函数时获得
的锁,但可有 1 秒超时。
TimeoutException是
RuntimeException
派生类,它在等待超时后即被抛出。
超时是需要的,但还不足以使代码强壮。您还需要具备从外部中止请求锁等待的能力。所以,当向一个等待锁的线程传送一个
interrupt()方法后,此方法应抛出一个
SynchronizationException
对象,并中断等待的线程。这个异常应是
RuntimeException
的一个派生类,这样不必特别处理它。
synchronized
语法这些推荐的更改方法的主要问题是,它们需要在二进制代码级上修改。而目前这些代码使用进入监控 (enter-monitor) 和退出监控 (exit-monitor) 指令来实现
synchronized。而这些指令没有参数,所以需要扩展二进制代码的定义以支持多个锁定请求。但是这种修改不会比在
Java 2 中修改 Java 虚拟机的更轻松,但它是向下兼容现存的 Java
另一个可解决的问题是最常见的死锁情况,在这种情况下,两个线程都在等待对方完成某个操作。设想下面的一个例子(假设的): class Broken
Object lock1 = new Object();
Object lock2 = new Object();
synchronized( lock1 )
synchronized( lock2 )
// do something
synchronized( lock2 )
synchronized( lock1 )
// do something
}设想一个线程调用
a(),但在获得 
lock1之后在获得
lock2之前被剥夺运行权。 第二个线程进入运行,调用
b(),获得了
lock2,但是由于第一个线程占用
lock1,所以它无法获得
lock1,所以它随后处于等待状态。此时第一个线程被唤醒,它试图获得
lock2,但是由于被第二个线程占据,所以无法获得。
此时出现死锁。下面的 synchronize-on-multiple-objects
的语法可解决这个问题:
synchronized( lock1 && lock2 )
synchronized( lock2 && lock3 )
}编译器 ( 或虚拟机 ) 会重新排列请求锁的顺序,使
lock1总是被首先获得,这就消除了死锁。
但是,这种方法对多线程不一定总成功,所以得提供一些方法来自动打破死锁。一个简单的办法就是在等待第二个锁时常释放已获得的锁。这就是说,应采取如下的等待方式,而不是永远等待:
while( true )
synchronized( some_lock )[10]
// do the work here.
catch( TimeoutException e )
如果等待锁的每个程序使用不同的超时值,就可打破死锁而其中一个线程就可运行。我建议用以下的语法来取代前面的代码:
synchronized( some_lock )[]
// do the work here.
}synchronized语句将永远等待,但是它时常会放弃已获得的锁以打破潜在的死锁可能。在理想情况下,每个重复等待的超时值比前一个相差一随机值。
notify()wait()/
notify()系统也有一些问题:
wait()是正常返回还是因超时返回。
无法使用传统条件变量来实现处于一个“信号”(signaled) 状态。太容易发生嵌套的监控 (monitor) 锁定。超时检测问题可以通过重新定义
wait()使它返回一个
boolean变量 ( 而不是
void) 来解决。一个
true返回值指示一个正常返回,而
指示因超时返回。
基于状态的条件变量的概念是很重要的。如果此变量被设置成
false状态,那么等待的线程将要被阻断,直到此变量进入
true状态;任何等待
的条件变量的等待线程会被自动释放。
( 在这种情况下,
调用不会发生阻断。)。通过如下扩展
的语法,可以支持这个功能:
释放所有等待的线程,而不改变其下面的条件变量的状态。notify(true);把条件变量的状态设置为 true
并释放任何等待的进程。其后对于
的调用不会发生阻断。
notify(false);把条件变量的状态设置为 false ( 其后对于
wait()的调用会发生阻断 )。
嵌套监控锁定问题非常麻烦,我并没有简单的解决办法。嵌套监控锁定是一种死锁形式,当某个锁的占有线程在挂起其自身之前不释放锁时,会发生这种嵌套监控封锁。
下面是此问题的一个例子(还是假设的),但是实际的例子是非常多的: class Stack
LinkedList list = new LinkedList();
public synchronized void push(Object x)
synchronized(list)
list.addLast( x );
public synchronized Object pop()
synchronized(list)
if( list.size() &= 0 )
return list.removeLast();
}此例中,在
操作中涉及两个锁:一个在
Stack对象上,另一个在
LinkedList
对象上。下面我们考虑当一个线程试图调用一个空栈的 pop()
操作时的情况。此线程获得这两个锁,然后调用
wait()释放
Stack对象上 的锁,但是没有释放在 list
上的锁。如果此时第二个线程试图向堆栈中压入一个对象,它会在
synchronized(list)语句上永远挂起,
而且永远不会被允许压入一个对象。由于第一个线程等待的是一个非空栈,这样就会发生死锁。这就是说,第一个线程永远无法从
返回,因为由于它占据着锁,而导致第二个线程永远无法运行到
notify()语句。
在这个例子中,有很多明显的办法来解决问题:例如,对任何的方法都使用同步。但是在真实世界中,解决方法通常不是这么简单。一个可行的方法是,在
中按照反顺序释放当前线程获取的
所有锁,然后当等待条件满足后,重新按原始获取顺序取得它们。
但是,我能想象出利用这种方式的代码对于人们来说简直无法理解,所以我认为它不是一个真正可行的方法。如果您有好的方法,请给我发
我也希望能等到下述复杂条件被实现的一天。例如: (a && (b || c)).wait();其中
是任意对象。
同时支持抢占式和协作式线程的能力在某些服务器应用程序中是基本要求,尤其是在想使系统达到最高性能的情况下。我认为
Java 编程语言在简化线程模型上走得太远了,并且 Java 编程语言应支持
Posix/Solaris
的“绿色 (green) 线程”和“轻便 (lightweight) 进程”概念(在“(
Java Threads”第一章中讨论)。 这就是说,有些 Java
虚拟机的实现(例如在 NT 上的 Java
虚拟机)应在其内部仿真协作式进程,其它 Java
虚拟机应仿真抢占式线程。而且向 Java
虚拟机加入这些扩展是很容易的。
一个 Java 的
应始终是抢占式的。这就是说,一个 Java 编程语言的线程应像 Solaris
的轻便进程一样工作。
Runnable接口可以用于定义一个
Solaris 式的“绿色线程”,此线程必需能把控制权转给运行
在相同轻便进程中的其它绿色线程。
例如,目前的语法:
class My_thread implements Runnable
public void run(){ /*...*/ }
new Thread( new My_thread );能有效地为
对象产生一个绿色线程,并把它绑定到由
对象代表的轻便进程中。这种实现对于现有代码是透明的,因为它的有效性和现有的完全一样。
对象想成为绿色线程,使用这种方法,只需向
的构造函数传递几个
Runnable对象,就可以扩展 Java
编程语言的现有语法,以支持在一个单一轻便线程有多个绿色线程。(绿色线程之间可以相互协作,但是它们可被运行在其它轻便进程
Thread对象 ) 上的绿色进程 (
Runnable对象 )
抢占。)。例如,下面的代码会为每个 runnable
对象创建一个绿色线程,这些绿色线程会共享由
对象代表的轻便进程。
new Thread( new My_runnable_object(), new My_other_runnable_object() );现有的覆盖 (override)
Thread对象并实现
的习惯继续有效,但是它应映射到一个被绑定到一轻便进程的绿色线程。( 在
Thread()类中的缺省
方法会在内部有效地创建第二个
Runnable对象。)
线程间的协作
应在语言中加入更多的功能以支持线程间的相互通信。目前,
PipedInputStream
PipedOutputStream
类可用于这个目的。但是对于大多数应用程序,它们太弱了。我建议向
Thread类加入下列函数:
wait_for_start()
方法,它通常处于阻塞状态,直到一个线程的
方法启动。( 如果等待的线程在调用 run
之前被释放,这没有什么问题)。用这种方法,一个线程可以创建一个或多个辅助线程,并保证在创建线程继续执行操作之前,这些辅助线程会处于运行状态。
Object类)增加
$send (Object o)
Object=$receive()
方法,它们将使用一个内部阻断队列在线程之间传送对象。阻断队列应作为第一个
$send()调用的副产品被自动创建。
调用会把对象加入队列。
$receive()
调用通常处于阻塞状态,直到有一个对象被加入队列,然后它返回此对象。这种方法中的变量应支持设定入队和出队的操作超时能力:
$send (Object o, long timeout)和
$receive (long
timeout)。对于读写锁的内部支持
读写锁的概念应内置到 Java 编程语言中。读写器锁在“
Taming Java
Threads”(和其它地方)中有详细讨论,概括地说:一个读写锁支持多个线程同时访问一个对象,但是在同一时刻只有一个线程可以修改此对象,并且在访问进行时不能修改。读写锁的语法可以借用
synchronized关键字:
static Object global_
public void a()
$reading( global_resource )
// While in this block, other threads requesting read
// access to global_resource will get it, but threads
// requesting write access will block.
public void b()
$writing( global_resource )
// Blocks until all ongoing read or write operations on
// global_resource are complete. No read or write
// operation or global_resource can be initiated while
// within this block.
public $reading void c()
// just like $reading(this)...
public $writing void d()
// just like $writing(this)...
}对于一个对象,应该只有在
$writing块中没有线程时,才支持多个线程进入
$reading块。在进行读操作时,一个试图进入
$writing块的线程会被阻断,直到读线程退出
$reading块。 当有其它线程处于
块时,试图进入
$reading或
块的线程会被阻断,直到此写线程退出
$writing块。
如果读和写线程都在等待,缺省情况下,读线程会首先进行。但是,可以使用
$writer_priority属性修改类的定义来改变这种缺省方式。如:
$write_priority class IO
$writing write( byte[] data )
$reading byte[] read( )
}访问部分创建的对象应是非法的
当前情况下,JLS
允许访问部分创建的对象。例如,在一个构造函数中创建的线程可以访问正被创建的对象,既使此对象没有完全被创建。下面代码的结果无法确定:
class Broken
new Thread()
public void run()
}.start();
x为 -1 的线程可以和设置
的线程同时进行。所以,此时
x的值无法预测。
对此问题的一个解决方法是,在构造函数没有返回之前,对于在此构造函数中创建的线程,既使它的优先级比调用
new的线程高,也要禁止运行它的
这就是说,在构造函数返回之前,
start()请求必须被推迟。
另外,Java
编程语言应可允许构造函数的同步。换句话说,下面的代码(在当前情况下是非法的)会象预期的那样工作:
class Illegal
synchronizedBroken()
new Thread()
public void run()
synchronized( Illegal.this )
}.start();
}我认为第一种方法比第二种更简洁,但实现起来更为困难。volatile
关键字应象预期的那样工作
JLS 要求保留对于 volatile 操作的请求。大多数 Java
虚拟机都简单地忽略了这部分内容,这是不应该的。在多处理器的情况下,许多主机都出现了这种问题,但是它本应由
JLS 加以解决的。如果您对这方面感兴趣,马里兰大学的 Bill Pugh
正在致力于这项工作(请参阅
访问的问题
如果缺少良好的访问控制,会使线程编程非常困难。大多数情况下,如果能保证线程只从同步子系统中调用,不必考虑线程安全 (threadsafe) 问题。我建议对
Java 编程语言的访问权限概念做如下限制;应精确使用
关键字来限制包访问权。我认为当缺省行为的存在是任何一种计算机语言的一个瑕疵,我对现在存在这种缺省权限感到很迷惑(而且这种缺省是“包 (package)”级别的而不是“私有 (private)”)。
在其它方面,Java 编程语言都不提供等同的缺省关键字。虽然使用显式的
的限定词会破坏现有代码,但是它将使代码的可读性更强,并能消除整个类的潜在错误
( 例如,如果访问权是由于错误被忽略,而不是被故意忽略 )。
private protected,它的功能应和现在的
protected一样,但是不应允许包级别的访问。
private private
语法指定“实现的访问”对于所有外部对象是私有的,甚至是当前对象是的同一个类的。对于“.”左边的唯一引用(隐式或显式)应是
的语法,以授权它可制定特定类的访问。例如,下面的代码应允许
Fred类的对象可调用
some_method(),但是对其它类的对象,这个方法应是私有的。
public(Fred) void some_method()
}这种建议不同于 C++ 的 "friend" 机制。 在 "friend"
机制中,它授权一个类访问另一个类的
所有私有部分。在这里,我建议对有限的方法集合进行严格控制的访问。用这种方法,一个类可以为另一个类定义一个接口,而这个接口对系统的其余类是不可见的。一个明显的变化是:
public(Fred, Wilma) void some_method()
}除非域引用的是真正不变 (immutable) 的对象或
final基本类型,否则所有域的定义应是
对于一个类中域的直接访问违反了 OO
设计的两个基本规则:抽象和封装。从线程的观点来看,允许直接访问域只使对它进行非同步访问更容易一些。
关键字。带有此关键字的对象可被一个“bean
盒”应用程序访问,这个程序使用在
类中定义的反射操作 (introspection) API,否则与 private private
$property属性可用在域和方法,这样现有的
JavaBean getter/setter 方法可以很容易地被定义为属性。
不变性 (immutability)
由于对不变对象的访问不需要同步,所以在多线程条件下,不变的概念(一个对象的值在创建后不可更改)是无价的。Java
编程言语中,对于不变性的实现不够严格,有两个原因:
对于一个不变对象,在其被未完全创建之前,可以对它进行访问。这种访问对于某些域可以产生不正确的值。对于恒定 ( 类的所有域都是
的定义太松散。对于由
final引用指定的对象,虽然引用本身不能改变,但是对象本身可以改变状态。
第一个问题可以解决,不允许线程在构造函数中开始执行
( 或者在构造函数返回之前不能执行开始请求 )。对于第二个问题,通过限定
final修饰符指向恒定对象,可以解决此问题。这就是说,对于一个对象,只有所有的域是
final,并且所有引用的对象的域也都是
final,此对象才真正是恒定的。为了不打破现有代码,这个定义可以使用编译器加强,即只有一个类被显式标为不变时,此类才是不变类。方法如下:
$immutablepublic class Fred
// all fields in this class must be final, and if the
// field is a reference, all fields in the referenced
// class must be final as well (recursively).
static int x constant = 0;
// use of `final` is optional when $immutable
// is present.
$immutable修饰符后,在域定义中的
final修饰符是可选的。
最后,当使用内部类 (inner class) 后,在 Java
编译器中的一个错误使它无法可靠地创建不变对象。当一个类有重要的内部类时 ( 我的代码常有 ),编译器经常不正确地显示下列错误信息:"Blank final variable 'name' may not have been initialized.
It must be assigned a value in an initializer, or in every constructor."既使空的 final
在每个构造函数中都有初始化,还是会出现这个错误信息。自从在 1.1
版本中引入内部类后,编译器中一直有这个错误。在此版本中(三年以后),这个错误依然存在。现在,该是改正这个错误的时候了。对于类级域的实例级访问
除了访问权限外,还有一个问题,即类级(静态)方法和实例(非静态)方法都能直接访问类级(静态)域。这种访问是非常危险的,因为实例方法的同步不会获取类级的锁,所以一个
synchronized static方法和一个
synchronized
方法还是能同时访问类的域。改正此问题的一个明显的方法是,要求在实例方法中只有使用
static访问方法才能访问非不变类的
域。当然,这种要求需要编译器和运行时间检查。在这种规定下,下面的代码是非法的:
class Broken
synchronized static void f()
synchronized void g()
可以并行运行,所以它们能同时改变
的值(产生不定的结果)。请记住,这里有两个锁:
方法要求属于
对象的锁,而非静态方法要求属于此类实例的锁。当从实例方法中访问非不变
域时,编译器应要求满足下面两个结构中的任意一个:
class Broken
synchronized private static accessor( long value )
}synchronized static void f()
synchronized void g()
accessor( -1 );}
}或则,编译器应获得读 / 写锁的使用:
class Broken
synchronized static void f()
$writing(x){ x = 0 };
synchronized void g()
$writing(x){ x = -1 };
}另外一种方法是(这也是一种
理想的方法)--
自动使用一个读 / 写锁来同步访问非不变 static
域,这样,程序员就不必担心这个问题。
后台线程的突然结束
当所有的非后台线程终止后,后台线程都被突然结束。当后台线程创建了一些全局资源(例如一个数据库连接或一个临时文件),而后台线程结束时这些资源没有被关闭或删除就会导致问题。对于这个问题,我建议制定规则,使 Java
虚拟机在下列情况下不关闭应用程序:有任何非后台线程正在运行,或者:有任何后台线程正在执行一个
synchronized方法或
synchronized代码块。
后台线程在它执行完
synchronized块或
synchronized方法后可被立即关闭。
suspend()和
由于实用原因这也许不可行,但是我希望不要废除
ThreadGroup
中 )。但是,我会改变
的语义,使得调用它时不会破坏已有代码。但是,关于
的问题,请记住,当线程终止后,
将释放所有锁,这样可能潜在地使正在此对象上工作的线程进入一种不稳定(局部修改)的状态。由于停止的线程已释放它在此对象上的所有锁,所以这些对象无法再被访问。
对于这个问题,可以重新定义
的行为,使线程只有在不占有任何锁时才立即终止。如果它占据着锁,我建议在此线程释放最后一个锁后才终止它。
可以使用一个和抛出异常相似的机制来实现此行为。被停止线程应设置一个标志,并且当退出所有同步块时立即测试此标志。如果设置了此标志,就抛出一个隐式的异常,
但是此异常应不再能被捕捉并且当线程结束时不会产生任何输出。注意,微软的
NT 操作系统不能很好地处理一个外部指示的突然停止 (abrupt)。(它不把
消息通知动态连接库,所以可能导致系统级的资源漏洞。)这就是我建议使用类似异常的方法简单地导致
run()返回的原因。
与这种和异常类似的处理方法带来的实际问题是,你必需在每个
synchronized
块后都插入代码来测试“stopped”标志。并且这种附加的代码会降低系统性能并增加代码长度。我想到的另外一个办法是使
实现一个“延迟的 (lazy)”停止,在这种情况下,在下次调用
yield()时才终止。我还想向
Thread中加入一个
isStopped()和
stopped()方法
Thread将像
isInterrupted()和
interrupted()一样工作,但是会检测
“stop-requested”的状态 )。这种方法不向第一种那样通用,但是可行并且不会产生过载。
suspend()和
resume()方法放回到
编程语言中,它们是很有用的,我不想被当成是幼儿园的小孩。由于它们可能产生潜在的危险(当被挂起时,一个线程可以占据一个锁)而去掉它们是没有道理的。请让我自己来决定是否使用它们。
如果接收的线程正占据着锁,Sun 公司应该把它们作为调用
suspend()的一个运行时间异常处理 (run-time
exception);或者更好的方法是,延迟实际的挂起过程,直到线程释放所有的锁。
被阻断的 I/O 应正确工作
应该能打断任何被阻断的操作,而不是只让它们
sleep()。我在“
Taming Java
Threads”的第二章中的 socket
部分讨论了此问题。但是现在,对于一个被阻断的 socket 上的 I/O
操作,打断它的唯一办法是关闭这个
socket,而没有办法打断一个被阻断的文件 I/O
操作。例如,一旦开始一个读请求并且进入阻断状态后,除非到它实际读出一些东西,
否则线程一直出于阻断状态。既使关掉文件句柄也不能打断读操作。
还有,程序应支持 I/O
操作的超时。所有可能出现阻断操作的对象(例如 InputStream
对象)也都应支持这种方法:
InputStream s = ...;
s.set_timeout( 1000 );这和 Socket 类的
setSoTimeout(time)方法是等价的。同样地,应该支持把超时作为参数传递到阻断的调用。
ThreadGroup
ThreadGroup应该实现
中能够改变线程状态的所有方法。我特别想让它实现
方法,这样我就可等待组中的所有线程的终止。
以上是我的建议。就像我在标题中所说的那样,如果我是国王 ...(哎)。我希望这些改变(或其它等同的方法)最终能被引入
Java 语言中。我确实认为 Java 语言是一种伟大的编程语言;但是我也认为
Java 的线程模型设计得还不够完善,这是一件很可惜的事情。但是,Java
编程语言正在演变,所以还有可提高的前景。Allen 撰写了八本书籍,最近新出的一本讨论了 Java
线程的陷阱和缺陷《
Taming Java
Threads》。他长期从事设计和编制面向对象软件。从事了 8 年的 C++
编程工作后,Allen 在 1996 年由 C++ 转向 Java。他现在视 C++
为一个噩梦,其可怕的经历正被逐渐淡忘。他从 1982
年起就自己和为加利弗尼亚大学伯克利分校教授计算机编程(首先是
C,然后是 C++ 和 MFC,现在是面向对象设计和 Java)。 Allen 也提供
Java 和面向对象设计方面的公众课程和私授 (in-house)
课程。他还提供面向对象设计的咨询并承包 Java 编程项目。请通过此 Web
站点和 Allen 取得联系并获取信息:
相关主题您可以参阅本文在 developerWorks 全球站点上的
的更新摘编。该书
探讨了在 Java
语言中多线程编程的陷阱和问题,并提供了一个与线程相关的 Java
程序包来解决这些问题。
马里兰大学的 Bill Pugh 正在致力修改 JLS 来提高其线程模型。Bill
的提议并不如本文所推荐的那么广,他主要致力于让现有的线程模型以更为合理方式运行。更多信息可从
可找到全部 Java 语言的规范。
要从一个纯技术角度来审视线程,参阅 Doug Lea 编著的
。这是本很棒的书,但是它的风格是非常学术化的并不一定适合所有的读者。对《
Taming Java Threads》是个很好的补充读物。
由 Scott Oaks 和 Henry Wong 编写的
Taming Java Threads
要轻量些,但是如果您从未编写过线程程序这本书更为适合。Oaks 和 Wong
同样实现了 Holub
提供的帮助类,而且看看对同一问题的不同解决方案总是有益的。
由 Bill Lewis 和 Daniel J. Berg  编写的
是对线程 ( 不限于 Java) 的很好入门介绍。
Java 线程的一些技术信息可在
中 Paul Jakubik
讨论了多线程系统的 SMP 问题。
添加或订阅评论,请先或。
有新评论时提醒我
static.content.url=http://www.ibm.com/developerworks/js/artrating/SITE_ID=10Zone=Java technologyArticleID=52962ArticleTitle=如果我是国王:关于解决 Java 编程语言线程问题的建议publish-date=}

我要回帖

更多关于 java的线程 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信