《Java多线程编程实战指南(设计模式篇)》答疑总结(陆续更新)


博客分类:

《Java多线程编程实战指南(设计模式篇)》答疑开展以来,不少网友提出的问题既有与本书有关的话题,也有Java多线程编程基础知识的相关话题。由于时间关系,对于重复的问题我不逐一回复。还请各位网友参考本总结。这里我将一些与本书相关以及具有代表性的问题提炼下,并附上的我的简要回复。其实,有些问题的回复如果要再深入或者详细,恐怕得写一篇文章,只是时间关系......

 

活动时间:(11月23日--11月30日)

伦理片 http://www.dotdy.com/


《Java多线程编程实战指南(设计模式篇)》中设计模式是如何总计出来的?为什么不是更多,或更少?

《Java多线程编程实战指南(设计模式篇)》作者黄文海回复:

这些模式是许多人总结出来的。书中收录的都是我使用和在实际项目中接触过的。 

所以,有些我了解但是没有具体用过模式,如Leader/Follower,并没有收录进来。以后我有了一定的这些模式的使用经验可能会把它们加进来。 

 

另外,尽管模式具有一定的语言(平台)中立性。但是,有些模式我认为在Java平台中能够发挥的作用有限,不使用这些模式可能反而使代码更加简洁。例如,POSA中收录的Thread Safe Interface模式,其主要意图在于在某些不用锁的情况下(如同一个线程内的组件调用)可以避免锁的开销,而在需要锁的情况下又能使用锁。在Java中基本上语言本身就支持这样的效果,且应用开发人员不需要做额外的事情:其一,Java中的锁是可重入的,这意味着同一个线程多次获取同一个锁并不会导致死锁;其二,Java自从1.6版开始对synchronized关键字的执行进行优化(包括偏向锁Biased Locking、锁粗化Lock Coarsening和锁去除Lock Elimination等),这使得非竞争条件下,锁的消耗大大降低了。也就是说,非竞争的锁的开销和无锁的开销之间的差距已经缩得很小心。 

 

还有的模式其实是我们已经所熟知,并且也无需在其基础上做一些其它的动作。运用其它都是直截了当的。比如Patterns In Java中收录的Single Threaded Execution,其意图就是使某些代码在任一时刻只有一个线程能够执行。大家可能立马想到synchronized。的确如此。所以,这样的模式我并没有收录。 

 

有没有一个比较通俗易懂的例子来解释多线程的概念?

《Java多线程编程实战指南(设计模式篇)》作者黄文海回复:

 

银行的营业厅开一个柜台的时候,所有客户只能被分配到这个柜台上。如果同时开几个柜台,那么所有客户可以被分配到不同的柜台上。这样,从办理业务的客户角度来看,他们等待的时间短了(响应性更好) 。从银行的角度来看,他们同样的时间能够接待的客户更多了(吞吐率变大了)。 

 

这里,单个柜台可以看做单个线程,它可能导致响应性和吞吐率低;而多个柜台可以看做多个线程,它可能使得响应性和吞吐率增加。 

 

正如我前面的帖子提到的,多线程未必就能提高处理效率,所以我在上面用了“可能”。

 

线程的优先级能否保证线程是按照优先级高低的顺序运行,使用时需要注意什么问题?

《Java多线程编程实战指南(设计模式篇)》作者黄文海回复:

 

线程优先级我感觉最好把它理解成应用代码给JVM线程调度器的一个提示信息。它只是JVM线程调度器进行线程调度时的一个参考信息(而不是全部),它并不能保证线程按照优先级高低的顺序进行运行。举个例子来说,假设有3个线程ThreadA、ThreadB和ThreadC,它们的优先级分别是高、普通(中)和低。假设某一个时刻,ThreadA和ThreadB处于I/O等待状态,而ThreadC处于Runnable状态,那么JVM线程调度器此时会选择ThreadC运行。可见,这里ThreadA的高优先级并没有起到任何作用。

 

滥用线程优先级的可能导致线程饥饿(Thread Starvation),即某些线程永远无法得以运行。这个好比老大同一天给你分配了好几个任务,当你询问这个几个任务的优先级时,他的回答都是”重要“。那么,你就会困惑:我到底该先完成哪个任务呢?可见,此时所谓的优先级,有等于没有。

 

因此,一般不建议设置线程的优先级,使用默认值就可以了。

 

什么是死锁,如何避免死锁?

《Java多线程编程实战指南(设计模式篇)》作者黄文海回复:

 

死锁类似于吝啬鬼落水的故事。一个吝啬鬼掉进河里了,有人准备给他施救,但是施救者的也不会游泳,而他的手使劲往河水处伸还是够不到吝啬鬼。于是他让吝啬鬼把手伸过来(Give me your hand!)。吝啬鬼一听到”给”字(Give)就神经紧张,硬是不肯伸手,觉得伸手给别人是吃亏了。于是吝啬鬼说“不,你伸手给我”(No!Give me your hand!)。于是,一个不能给,一个不肯给,你等我,我等你,救援一事无法进展。

 

死锁避免有两种方法:

1、不使用锁: 例如,Swing和Android都采用这种设计,使得它们的用户界面组件层使用单线程,也就避免了锁,自然也就避免了死锁。详情可参见《Java多线程编程实战指南(设计模式篇)》第14章。

2、对锁的访问顺序进行排序(Lock Ordering)。可参见这篇博文(英文):http://tutorials.jenkov.com/java-concurrency/deadlock-prevention.html

什么情况下使用多线程,什么情况下使用单线程?

《Java多线程编程实战指南(设计模式篇)》作者回复:

 

这实际上是一个收益与成本比的问题。因为多线程也有自身的开销和问题,如上下文切换、锁的开销以及由锁可能导致的死锁等问题,所以使用多线程编程不一定就比使用单线程的处理效率更高。这正如书中所打的一个比方---和尚打水的故事:一个和尚(单线程)挑水喝,两个和尚(多线程)担水喝,三个和尚(多线程)没水喝。可见,多线程的使用可能反而导致处理效率的降低。《Java多线程编程实战指南(设计模式篇)》第1章对这个问题有讲解。如何恰当地使用多线程编程,这点也正是《Java多线程编程实战指南(设计模式篇)》所能起到的一个作用。 

 

另一方面,在任务原始规模比较大(或者说不小)的情况下,恰当地使用多线程可以提高处理效率。例如,《Java多线程编程实战指南(设计模式篇)》第13章提到的一个实战案例:将数据库中的几十万条数据导出到文件中并发送到指定的FTP服务器上。这个实例如果不采用多线程编程,则可能使相应的计算显得非常慢。 

 

特意地使用单线程编程有时反而可能提高处理效率。这里,典型的使用场景是程序的处理过程涉及一些独占资源或者非线程安全的对象。例如,《Java多线程编程实战指南(设计模式篇)》第11章的实战案例:使用非线程安全的FTP客户端组件将一批本地文件FTP上传到指定的多个服务器。这个案例中,我们使用了单线程处理FTP文件上传,以减少多线程相关的开销。而这个线程的实际处理效率也能满足我们实际的需要。 

 

Java平台本身就是个多线程的平台,Java平台中线程无处不在:负责Java程序运行的main线程、垃圾回收GC线程、JIT编译器线程。因此,这里我们所说的单线程编程实际上是在多线程环境中特意使用单线程。 

 

另外,即使是在单CPU的机器上,多线程编程也是有适用场景的。例如,一个线程正在执行I/O操作(如读取文件),此时该线程并不占用CPU(因为它已经被Switch out了),那么其它线程,如执行加密/解密计算的线程此时可以占用CPU执行。这样,便提高了CPU的利用率,有利于提高系统的吞吐率。

 

如何合理设置线程池的大小(线程数)、使用线程时如何合理控制线程数?

《Java多线程编程实战指南(设计模式篇)》作者回复:

 

线程池的大小设置一般要考虑到主机的CPU个数(逻辑CPU个数,NCPU)。线程池大小设置过小会导致CPU资源浪费,而设置过大则可能导致消耗过多的内存以及产生更多的上下文切换(导致CPU额外消耗过多)。粗略地说,对于执行CPU密集型的任务(如加密/解密)的线程池其最大大小可以为2NCPU。对于执行I/O密集型的任务(如写日志文件)的线程池其最大大小可以设置为2NCPU+1。详情参见《Java多线程编程实战指南(设计模式篇)》第9章。 

 

Java中我们可以使用Runtime.getRuntime().availableProcessors()来获取主机的CPU个数。 

 

多个线程同时访问同一个资源(如变量、文件等)时,如何保证线程安全?

《Java多线程编程实战指南(设计模式篇)》作者回复:

我们知道,使用锁(如synchronized和ReetrantLock)是保证线程安全的一个常见方法。这种方法的本质是以互斥的方式保证一个时刻只有一个线程能够访问共享变量(资源)。这好比公路维修的时候,原本四车道路在维修路段被弄成了单车道使得车辆只能一辆一辆通行。所以这种方法的缺点非常明显。《Java多线程编程实战指南(设计模式篇)》第3章、第10章、第11章介绍的3个模式就可以用来保证线程安全。它们背后的思想是要么是共享状态不可变的变量、要么是不共享变量。

影音先锋电影 http://www.iskdy.com/ 

文件共享访问与访问共享变量是类似的。在Java中我们使用文件时并不是直接对文件进行操作,而是通过Stream和Writer进行。而这些接口本身可能已经对并发访问进行处理了,即它们本身保证了共享时的线程安全。但是,这只是其中一个方面(下面会讲到)。例如,java.io.Writer这个抽象类是我们经常使用的类(如PrintWriter和BufferedWrite)的父类。它在写文件的时候是加锁的,即通过锁去保证线程安全。如java.io.Writer中定义的write方法的代码所示:

 

 public void write(String str, int off, int len) throws IOException {

        synchronized (lock) {

           //省略其它代码

        }

  }

  

也就是说上面的write方法是一个原子操作。但是,我们知道“原子操作+原子操作!=原子操作“。所以,如果多个线程使用多个Writer实例写同一个文件,那么这个文件的内容就可能紊乱了。当然,按照上面的分析,多个线程用同一个Writer实例写同一个文件并不会有问题。

 

 

你可能感兴趣的:(《Java多线程编程实战指南(设计模式篇)》答疑总结(陆续更新))