`
jiahh
  • 浏览: 37233 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

Java多线程编程的常见陷阱

阅读更多
1、在构造函数中启动线程
我在很多代码中都看到这样的问题,在构造函数中启动一个线程,类似这样:
<!--[if !supportLists]-->1.  <!--[endif]-->public class A{ 
<!--[if !supportLists]-->2.  <!--[endif]-->   public A(){ 
<!--[if !supportLists]-->3.  <!--[endif]-->      this.x=1; 
<!--[if !supportLists]-->4.  <!--[endif]-->      this.y=2; 
<!--[if !supportLists]-->5.  <!--[endif]-->      this.thread=new MyThread(); 
<!--[if !supportLists]-->6.  <!--[endif]-->      this.thread.start(); 
<!--[if !supportLists]-->7.  <!--[endif]-->   } 
<!--[if !supportLists]-->8.  <!--[endif]-->    
<!--[if !supportLists]-->9.  <!--[endif]-->}   
这个会引起什么问题呢?如果有个类B继承了类A,依据java类初始化的顺序,A的构造函数一定会在B的构造函数调用前被调用,那么thread线程也将在B被完全初始化之前启动,当thread运行时使用到了类A中的某些变量,那么就可能使用的不是你预期中的值,因为在B的构造函数中你可能赋给这些变量新的值。也就是说此时将有两个线程在使用这些变量,而这些变量却没有同步。
解决这个问题有两个办法:将A设置为final,不可继承;或者提供单独的start方法用来启动线程,而不是放在构造函数中。
2、不完全的同步
都知道对一个变量同步的有效方式是用synchronized保护起来,synchronized可能是对象锁,也可能是类锁,看你是类方法还是实例方法。但是,当你将某个变量在A方法中同步,那么在变量出现的其他地方,你也需要同步,除非你允许弱可见性甚至产生错误值。类似这样的代码:
<!--[if !supportLists]-->1.  <!--[endif]-->class A{ 
<!--[if !supportLists]-->2.  <!--[endif]-->  int x; 
<!--[if !supportLists]-->3.  <!--[endif]-->  public int getX(){ 
<!--[if !supportLists]-->4.  <!--[endif]-->     return x; 
<!--[if !supportLists]-->5.  <!--[endif]-->  } 
<!--[if !supportLists]-->6.  <!--[endif]-->  public synchronized void setX(int x) 
<!--[if !supportLists]-->7.  <!--[endif]-->  { 
<!--[if !supportLists]-->8.  <!--[endif]-->     this.x=x; 
<!--[if !supportLists]-->9.  <!--[endif]-->  } 
<!--[if !supportLists]-->10. <!--[endif]-->}    
x的setter方法有同步,然而getter方法却没有,那么就无法保证其他线程通过getX得到的x是最新的值。事实上,这里的setX的同步是没有必要的,因为对int的写入是原子的,这一点JVM规范已经保证,多个同步没有任何意义;当然,如果这里不是int,而是double或者long,那么getX和setX都将需要同步,因为double和long都是64位,写入和读取都是分成两个32位来进行(这一点取决于jvm的实现,有的jvm实现可能保证对long和double的read、write是原子的),没有保证原子性。类似上面这样的代码,其实都可以通过声明变量为volatile来解决。

3、在使用某个对象当锁时,改变了对象的引用,导致同步失效。
这也是很常见的错误,类似下面的代码:
<!--[if !supportLists]-->1.  <!--[endif]-->synchronized(array[0]) 
<!--[if !supportLists]-->2.  <!--[endif]-->{ 
<!--[if !supportLists]-->3.  <!--[endif]-->   ...... 
<!--[if !supportLists]-->4.  <!--[endif]-->   array[0]=new A(); 
<!--[if !supportLists]-->5.  <!--[endif]-->   ...... 
<!--[if !supportLists]-->6.  <!--[endif]-->}   
同步块使用array[0]作为锁,然而在同步块中却改变了array[0]指向的引用。分析下这个场景,第一个线程获取了array[0]的锁,第二个线程因为无法获取array[0]而等待,在改变了array[0]的引用后,第三个线程获取了新的array[0]的锁,第一和第三两个线程持有的锁是不一样的,同步互斥的目的就完全没有达到了。这样代码的修改,通常是将锁声明为final变量或者引入业务无关的锁对象,保证在同步块内不会被修改引用。
4、没有在循环中调用wait()。
wait和notify用于实现条件变量,你可能知道需要在同步块中调用wait和notify,为了保证条件的改变能做到原子性和可见性。常常看见很多代码做到了同步,却没有在循环中调用wait,而是使用if甚至没有条件判断:
<!--[if !supportLists]-->1.  <!--[endif]-->synchronized(lock) 
<!--[if !supportLists]-->2.  <!--[endif]-->{ 
<!--[if !supportLists]-->3.  <!--[endif]-->   if(isEmpty() 
<!--[if !supportLists]-->4.  <!--[endif]-->     lock.wait(); 
<!--[if !supportLists]-->5.  <!--[endif]-->    
<!--[if !supportLists]-->6.  <!--[endif]-->} 
<!--[if !supportLists]-->7.  <!--[endif]-->    
对条件的判断是使用if,这会造成什么问题呢?在判断条件之前可能调用notify或者notifyAll,那么条件已经满足,不会等待,这没什么问题。在条件没有满足,调用了wait()方法,释放lock锁并进入等待休眠状态。如果线程是在正常情况下,也就是条件被改变之后被唤醒,那么没有任何问题,条件满足继续执行下面的逻辑操作。问题在于线程可能被意外甚至恶意唤醒,由于没有再次进行条件判断,在条件没有被满足的情况下,线程执行了后续的操作。意外唤醒的情况,可能是调用了notifyAll,可能是有人恶意唤醒,也可能是很少情况下的自动苏醒(称为“伪唤醒”)。因此为了防止这种条件没有满足就执行后续操作的情况,需要在被唤醒后再次判断条件,如果条件不满足,继续进入等待状态,条件满足,才进行后续操作。
<!--[if !supportLists]-->1.  <!--[endif]-->synchronized(lock) 
<!--[if !supportLists]-->2.  <!--[endif]-->{ 
<!--[if !supportLists]-->3.  <!--[endif]-->   while(isEmpty() 
<!--[if !supportLists]-->4.  <!--[endif]-->     lock.wait(); 
<!--[if !supportLists]-->5.  <!--[endif]-->    
<!--[if !supportLists]-->6.  <!--[endif]-->}    
没有进行条件判断就调用wait的情况更严重,因为在等待之前可能notify已经被调用,那么在调用了wait之后进入等待休眠状态后就无法保证线程苏醒过来。
5、同步的范围过小或者过大。
同步的范围过小,可能完全没有达到同步的目的;同步的范围过大,可能会影响性能。同步范围过小的一个常见例子是误认为两个同步的方法一起调用也是将同步的,需要记住的是Atomic+Atomic!=Atomic。
<!--[if !supportLists]-->1.  <!--[endif]-->Map map=Collections.synchronizedMap(new HashMap()); 
<!--[if !supportLists]-->2.  <!--[endif]-->if(!map.containsKey("a")){ 
<!--[if !supportLists]-->3.  <!--[endif]-->         map.put("a", value); 
<!--[if !supportLists]-->4.  <!--[endif]-->}   
这是一个很典型的错误,map是线程安全的,containskey和put方法也是线程安全的,然而两个线程安全的方法被组合调用就不一定是线程安全的了。因为在containsKey和put之间,可能有其他线程抢先put进了a,那么就可能覆盖了其他线程设置的值,导致值的丢失。解决这一问题的方法就是扩大同步范围,因为对象锁是可重入的,因此在线程安全方法之上再同步相同的锁对象不会有问题。
<!--[if !supportLists]-->1.  <!--[endif]-->Map map = Collections.synchronizedMap(new HashMap()); 
<!--[if !supportLists]-->2.  <!--[endif]-->synchronized (map) { 
<!--[if !supportLists]-->3.  <!--[endif]-->     if (!map.containsKey("a")) { 
<!--[if !supportLists]-->4.  <!--[endif]-->         map.put("a", value); 
<!--[if !supportLists]-->5.  <!--[endif]-->     } 
<!--[if !supportLists]-->6.  <!--[endif]--> }   
注意,加大锁的范围,也要保证使用的是同一个锁,不然很可能造成死锁。 Collections.synchronizedMap(new HashMap())使用的锁是map本身,因此没有问题。当然,上面的情况现在更推荐使用ConcurrentHashMap,它有putIfAbsent方法来达到同样的目的并且满足线程安全性。
同步范围过大的例子也很多,比如在同步块中new大对象,或者调用费时的IO操作(操作数据库,webservice等)。不得不调用费时操作的时候,一定要指定超时时间,例如通过URLConnection去invoke某个URL时就要设置connect timeout和read timeout,防止锁被独占不释放。同步范围过大的情况下,要在保证线程安全的前提下,将不必要同步的操作从同步块中移出。
6、正确使用volatile
在jdk5修正了volatile的语义后,volatile作为一种轻量级的同步策略就得到了大量的使用。volatile的严格定义参考jvm spec,这里只从volatile能做什么,和不能用来做什么出发做个探讨。
volatile可以用来做什么?
1)状态标志,模拟控制机制。常见用途如控制线程是否停止:
<!--[if !supportLists]-->1.  <!--[endif]-->private volatile boolean stopped; 
<!--[if !supportLists]-->2.  <!--[endif]-->public void close(){ 
<!--[if !supportLists]-->3.  <!--[endif]-->   stopped=true; 
<!--[if !supportLists]-->4.  <!--[endif]-->} 
<!--[if !supportLists]-->5.  <!--[endif]-->
<!--[if !supportLists]-->6.  <!--[endif]-->public void run(){ 
<!--[if !supportLists]-->7.  <!--[endif]-->
<!--[if !supportLists]-->8.  <!--[endif]-->   while(!stopped){ 
<!--[if !supportLists]-->9.  <!--[endif]-->      //do something 
<!--[if !supportLists]-->10. <!--[endif]-->   } 
<!--[if !supportLists]-->11. <!--[endif]-->    
<!--[if !supportLists]-->12. <!--[endif]-->}
前提是do something中不会有阻塞调用之类。volatile保证stopped变量的可见性,run方法中读取stopped变量总是main memory中的最新值。
2)安全发布,如修复DLC问题。
<!--[if !supportLists]-->1.  <!--[endif]-->private volatile IoBufferAllocator instance; 
<!--[if !supportLists]-->2.  <!--[endif]-->public IoBufferAllocator getInsntace(){ 
<!--[if !supportLists]-->3.  <!--[endif]-->    if(instance==null){ 
<!--[if !supportLists]-->4.  <!--[endif]-->        synchronized (IoBufferAllocator.class) { 
<!--[if !supportLists]-->5.  <!--[endif]-->            if(instance==null) 
<!--[if !supportLists]-->6.  <!--[endif]-->                instance=new IoBufferAllocator(); 
<!--[if !supportLists]-->7.  <!--[endif]-->        } 
<!--[if !supportLists]-->8.  <!--[endif]-->    } 
<!--[if !supportLists]-->9.  <!--[endif]-->    return instance; 
<!--[if !supportLists]-->10. <!--[endif]-->}
3)开销较低的读写锁
<!--[if !supportLists]-->1.  <!--[endif]-->public class CheesyCounter { 
<!--[if !supportLists]-->2.  <!--[endif]-->    private volatile int value; 
<!--[if !supportLists]-->3.  <!--[endif]-->
<!--[if !supportLists]-->4.  <!--[endif]-->    public int getValue() { return value; } 
<!--[if !supportLists]-->5.  <!--[endif]-->
<!--[if !supportLists]-->6.  <!--[endif]-->    public synchronized int increment() { 
<!--[if !supportLists]-->7.  <!--[endif]-->        return value++; 
<!--[if !supportLists]-->8.  <!--[endif]-->    } 
<!--[if !supportLists]-->9.  <!--[endif]-->} 
synchronized保证更新的原子性,volatile保证线程间的可见性。
volatile不能用于做什么?
1)不能用于做计数器
<!--[if !supportLists]-->1.  <!--[endif]-->public class CheesyCounter { 
<!--[if !supportLists]-->2.  <!--[endif]-->    private volatile int value; 
<!--[if !supportLists]-->3.  <!--[endif]-->
<!--[if !supportLists]-->4.  <!--[endif]-->    public int getValue() { return value; } 
<!--[if !supportLists]-->5.  <!--[endif]-->
<!--[if !supportLists]-->6.  <!--[endif]-->    public int increment() { 
<!--[if !supportLists]-->7.  <!--[endif]-->        return value++; 
<!--[if !supportLists]-->8.  <!--[endif]-->    } 
<!--[if !supportLists]-->9.  <!--[endif]-->}
因为value++其实是有三个操作组成的:读取、修改、写入,volatile不能保证这个序列是原子的。对value的修改操作依赖于value的最新值。解决这个问题的方法可以将increment方法同步,或者使用AtomicInteger原子类。
2)与其他变量构成不变式
一个典型的例子是定义一个数据范围,需要保证约束lower< upper。
<!--[if !supportLists]-->1.  <!--[endif]-->public class NumberRange { 
<!--[if !supportLists]-->2.  <!--[endif]-->    private volatile int lower, upper; 
<!--[if !supportLists]-->3.  <!--[endif]-->
<!--[if !supportLists]-->4.  <!--[endif]-->    public int getLower() { return lower; } 
<!--[if !supportLists]-->5.  <!--[endif]-->    public int getUpper() { return upper; } 
<!--[if !supportLists]-->6.  <!--[endif]-->
<!--[if !supportLists]-->7.  <!--[endif]-->    public void setLower(int value) {  
<!--[if !supportLists]-->8.  <!--[endif]-->        if (value > upper)  
<!--[if !supportLists]-->9.  <!--[endif]-->            throw new IllegalArgumentException(); 
<!--[if !supportLists]-->10. <!--[endif]-->        lower = value; 
<!--[if !supportLists]-->11. <!--[endif]-->    } 
<!--[if !supportLists]-->12. <!--[endif]-->
<!--[if !supportLists]-->13. <!--[endif]-->    public void setUpper(int value) {  
<!--[if !supportLists]-->14. <!--[endif]-->        if (value < lower)  
<!--[if !supportLists]-->15. <!--[endif]-->            throw new IllegalArgumentException(); 
<!--[if !supportLists]-->16. <!--[endif]-->        upper = value; 
<!--[if !supportLists]-->17. <!--[endif]-->    } 
<!--[if !supportLists]-->18. <!--[endif]-->} 
尽管讲lower和upper声明为volatile,但是setLower和setUpper并不是线程安全方法。假设初始状态为(0,5),同时调用setLower(4)和setUpper(3),两个线程交叉进行,最后结果可能是(4,3),违反了约束条件。修改这个问题的办法就是将setLower和setUpper同步:
<!--[if !supportLists]-->1.  <!--[endif]-->public class NumberRange { 
<!--[if !supportLists]-->2.  <!--[endif]-->    private volatile int lower, upper; 
<!--[if !supportLists]-->3.  <!--[endif]-->
<!--[if !supportLists]-->4.  <!--[endif]-->    public int getLower() { return lower; } 
<!--[if !supportLists]-->5.  <!--[endif]-->    public int getUpper() { return upper; } 
<!--[if !supportLists]-->6.  <!--[endif]-->
<!--[if !supportLists]-->7.  <!--[endif]-->    public synchronized void setLower(int value) {  
<!--[if !supportLists]-->8.  <!--[endif]-->        if (value > upper)  
<!--[if !supportLists]-->9.  <!--[endif]-->            throw new IllegalArgumentException(); 
<!--[if !supportLists]-->10. <!--[endif]-->        lower = value; 
<!--[if !supportLists]-->11. <!--[endif]-->    } 
<!--[if !supportLists]-->12. <!--[endif]-->
<!--[if !supportLists]-->13. <!--[endif]-->    public synchronized void setUpper(int value) {  
<!--[if !supportLists]-->14. <!--[endif]-->        if (value < lower)  
<!--[if !supportLists]-->15. <!--[endif]-->            throw new IllegalArgumentException(); 
<!--[if !supportLists]-->16. <!--[endif]-->        upper = value; 
<!--[if !supportLists]-->17. <!--[endif]-->    } 
<!--[if !supportLists]-->18. <!--[endif]-->}


总结
1、实现Runnable接口比继承Thread类所具有的优势:1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。


2、多线程、多进程的优缺点:多线程的优点:
<!--[if !supportLists]-->·         <!--[endif]-->无需跨进程边界;
<!--[if !supportLists]-->·         <!--[endif]-->程序逻辑和控制方式简单;
<!--[if !supportLists]-->·         <!--[endif]-->所有线程可以直接共享内存和变量等;
<!--[if !supportLists]-->·         <!--[endif]-->线程方式消耗的总资源比进程方式好;
多线程缺点:
<!--[if !supportLists]-->·         <!--[endif]-->每个线程与主程序共用地址空间,受限于2GB地址空间;
<!--[if !supportLists]-->·         <!--[endif]-->线程之间的同步和加锁控制比较麻烦;
<!--[if !supportLists]-->·         <!--[endif]-->一个线程的崩溃可能影响到整个程序的稳定性;
<!--[if !supportLists]-->·         <!--[endif]-->到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server 2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数;
<!--[if !supportLists]-->·         <!--[endif]-->线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU
多进程优点:
<!--[if !supportLists]-->·         <!--[endif]-->每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;
<!--[if !supportLists]-->·         <!--[endif]-->通过增加CPU,就可以容易扩充性能;
<!--[if !supportLists]-->·         <!--[endif]-->可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
<!--[if !supportLists]-->·         <!--[endif]-->每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大
多线程缺点:
<!--[if !supportLists]-->·         <!--[endif]-->逻辑控制复杂,需要和主程序交互;
<!--[if !supportLists]-->·         <!--[endif]-->需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算
<!--[if !supportLists]-->·         <!--[endif]-->多进程调度开销比较大;
最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。当然你也可以利用多线程+多CPU+轮询方式来解决问题……
方法和手段是多样的,关键是自己看起来实现方便有能够满足要求,代价也合适。
分享到:
评论

相关推荐

    汪文君高并发编程实战视频资源下载.txt

    │ 高并发编程第一阶段05讲、采用多线程方式模拟银行排队叫号.mp4 │ 高并发编程第一阶段06讲、用Runnable接口将线程的逻辑执行单元从控制中抽取出来.mp4 │ 高并发编程第一阶段07讲、策略模式在Thread和Runnable...

    汪文君高并发编程实战视频资源全集

    │ 高并发编程第一阶段05讲、采用多线程方式模拟银行排队叫号.mp4 │ 高并发编程第一阶段06讲、用Runnable接口将线程的逻辑执行单元从控制中抽取出来.mp4 │ 高并发编程第一阶段07讲、策略模式在Thread和Runnable...

    Linux多线程服务端编程:使用muduo C++网络库

    《Linux多线程服务端编程:使用muduo C++网络库》主要讲述采用现代C++在x86-64 Linux上编写多线程TCP网络服务程序的主流常规技术,重点讲解一种适应性较强的多线程服务器的编程模型,即one loop per thread。...

    Java并发编程实战

    Java并发编程实战 本书深入浅出地介绍了Java线程和并发,是一本完美的Java并发参考手册。书中从并发性和线程安全性的基本概念出发,介绍了如何使用类库提供的基本并发构建块,用于避免并发危险、构造线程安全的类及...

    各大IT公司面试题集合

    │ 多线程编程之二——MFC中的多线开发.txt │ 多线程编程之四——线程的同步.txt │ 排序.txt │ 最常问问题和最佳答案.txt │ 求职信 .txt │ 深入分析Windows和Linux动态库应用异同 .txt │ 用VC++5.0 定 制 和 ...

    X-INF442-Concurrent_programming:2017-2018-理工学院| INF442-Java并发编程

    同样,他不再是本质上并非并行的硬件:所有现代处理器都是多核的,并允许通过多线程编程在顺序编程中获得更好的性能。 本课程旨在提供有效地掌握并发编程的工具,避免与共享资源的使用相关的各种陷阱。 学生将处理...

    开题报告-基于Java的坦克大战游戏的设计与实现.doc

    Java不但内置多线程功能,而且提供语言级的多线程支持,即定义了一些 用于建立、管理多线程的类和方法,使得开发具有多线程功能的程序变得简单、容易和 有效。 5 简单易学 如前所述,衍生自C++的Java语言,出于安全...

    SCJP+陷阱大全.doc

    掌握如何使用标准Java Development kit V1.5 (JDK)开发应用程序和applets。您将学会Java语言的语法;如何使用Java来创建图形用户接口(GUI),事件处理机制,例外处理,通过开发各种Java程序来...多线程和网络的知识。

    突破程序员基本功的16课.part2

    5.8 多线程的陷阱 5.8.1 不要调用run方法 5.8.2 静态的同步方法 5.8.3 静态初始化块启动新线程执行初始化 5.8.4 注意多线程执行环境 5.9 小结 第6课 流程控制的陷阱 6.1 switch语句陷阱 6.1.1 default分支...

    java8看不到源码-JacpFX:JacpFX项目是一个API,用于使用JavaFX、Spring(或其他DI框架)和类似Actor的组件方

    它使您摆脱了传统多线程编程的陷阱,帮助您将任务执行与客户端应用程序中的 UI 更改分开。 03.12.2015 JacpFX 2.1 final 在 maven.central 可用 JacpFX-2.1 终于发布了。 由于 RC1(见下面的描述)以下问题得到修复...

    Linux C程序设计大全

    1.3.2 Java 1.3.3 Perl 1.3.4 Python 1.3.5 Ruby 1.3.6 PHP 第2章 控制结构 2.1 goto语句 2.1.1 C语言中的无条件跳转 2.1.2 使用goto语句进行出错处理 2.1.3 出错处理的一般模型 2.2 C语言中的分支结构 2.2.1 分支...

    Hands-On-High-Performance-with-Spring-5:Packt发布的Spring 5的动手高性能

    本书涵盖以下激动人心的功能: 掌握Bean Wiring的最佳编程实践和性能改进分析各种AOP实施的性能探索与Spring的数据库交互以优化设计和配置解决Hibernate性能问题和陷阱利用多线程和并发编程来提高应用程序性能如果...

    软件框架设计的艺术

    11.3.2 Java Monitors中的陷阱 193 11.3.3 触发死锁的条件 196 11.3.4 测试死锁 201 11.3.5 对条件竞争进行测试 204 11.3.6 分析随机故障 206 11.3.7 日志的高级用途 208 11.3.8 使用日志记录...

    Node.js 开发指南.pdf

    1.5.2 Java与Javascript 7 1.5.3 微软的加入——JScript 8 1.5.4 标准化——ECMAScript 8 1.5.5 浏览器兼容性问题 9 1.5.6 引擎效率革命和JavaScript的未来 9 1.6 CommonJS 10 1.6.1 服务端JavaScript...

    Node.js+开发指南

    1.5.2 Java与Javascript 7 1.5.3 微软的加入——JScript 8 1.5.4 标准化——ECMAScript 8 1.5.5 浏览器兼容性问题 9 1.5.6 引擎效率革命和JavaScript的未来 9 1.6 CommonJS 10 1.6.1 服务端...

Global site tag (gtag.js) - Google Analytics