Java多线程编程的常见陷阱

2009-06-16 13:48 killme2008 blogjava 字号:T | T

本文介绍了Java多线程编程中的常见陷阱,如在构造函数中启动线程,不完全的同步,在使用某个对象当锁时,改变了对象的引用,导致同步失效等等。

1、在构造函数中启动线程

我在很多代码中都看到这样的问题,在构造函数中启动一个线程,类似这样:

  1. public class A{
  2. public A(){
  3. this.x=1;
  4. this.y=2;
  5. this.thread=new MyThread();
  6. this.thread.start();
  7. }
  8. }

这个会引起什么问题呢?如果有个类B继承了类A,依据java类初始化的顺序,A的构造函数一定会在B的构造函数调用前被调用,那么thread线程也将在B被完全初始化之前启动,当thread运行时使用到了类A中的某些变量,那么就可能使用的不是你预期中的值,因为在B的构造函数中你可能赋给这些变量新的值。也就是说此时将有两个线程在使用这些变量,而这些变量却没有同步。

解决这个问题有两个办法:将A设置为final,不可继承;或者提供单独的start方法用来启动线程,而不是放在构造函数中。

2、不完全的同步

都知道对一个变量同步的有效方式是用synchronized保护起来,synchronized可能是对象锁,也可能是类锁,看你是类方法还是实例方法。但是,当你将某个变量在A方法中同步,那么在变量出现的其他地方,你也需要同步,除非你允许弱可见性甚至产生错误值。类似这样的代码:

  1. class A{
  2. int x;
  3. public int getX(){
  4. return x;
  5. }
  6. public synchronized void setX(int x)
  7. {
  8. this.x=x;
  9. }
  10. }

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、在使用某个对象当锁时,改变了对象的引用,导致同步失效。

这也是很常见的错误,类似下面的代码:

  1. ])
  2. {
  3. ......
  4. array[0]=new A();
  5. ......
  6. }

同步块使用array[0]作为锁,然而在同步块中却改变了array[0]指向的引用。分析下这个场景,第一个线程获取了array[0]的锁,第二个线程因为无法获取array[0]而等待,在改变了array[0]的引用后,第三个线程获取了新的array[0]的锁,第一和第三两个线程持有的锁是不一样的,同步互斥的目的就完全没有达到了。这样代码的修改,通常是将锁声明为final变量或者引入业务无关的锁对象,保证在同步块内不会被修改引用。

4、没有在循环中调用wait()。

wait和notify用于实现条件变量,你可能知道需要在同步块中调用wait和notify,为了保证条件的改变能做到原子性和可见性。常常看见很多代码做到了同步,却没有在循环中调用wait,而是使用if甚至没有条件判断:

  1. synchronized(lock)
  2. {
  3. if(isEmpty()
  4. lock.wait();
  5. }

对条件的判断是使用if,这会造成什么问题呢?在判断条件之前可能调用notify或者notifyAll,那么条件已经满足,不会等待,这没什么问题。在条件没有满足,调用了wait()方法,释放lock锁并进入等待休眠状态。如果线程是在正常情况下,也就是条件被改变之后被唤醒,那么没有任何问题,条件满足继续执行下面的逻辑操作。问题在于线程可能被意外甚至恶意唤醒,由于没有再次进行条件判断,在条件没有被满足的情况下,线程执行了后续的操作。意外唤醒的情况,可能是调用了notifyAll,可能是有人恶意唤醒,也可能是很少情况下的自动苏醒(称为“伪唤醒”)。因此为了防止这种条件没有满足就执行后续操作的情况,需要在被唤醒后再次判断条件,如果条件不满足,继续进入等待状态,条件满足,才进行后续操作。

  1. synchronized(lock)
  2. {
  3. while(isEmpty()
  4. lock.wait();
  5. }

没有进行条件判断就调用wait的情况更严重,因为在等待之前可能notify已经被调用,那么在调用了wait之后进入等待休眠状态后就无法保证线程苏醒过来。

5、同步的范围过小或者过大。

同步的范围过小,可能完全没有达到同步的目的;同步的范围过大,可能会影响性能。同步范围过小的一个常见例子是误认为两个同步的方法一起调用也是将同步的,需要记住的是Atomic+Atomic!=Atomic。

  1. Map map=Collections.synchronizedMap(new HashMap());
  2. if(!map.containsKey("a")){
  3. map.put("a", value);
  4. }

这是一个很典型的错误,map是线程安全的,containskey和put方法也是线程安全的,然而两个线程安全的方法被组合调用就不一定是线程安全的了。因为在containsKey和put之间,可能有其他线程抢先put进了a,那么就可能覆盖了其他线程设置的值,导致值的丢失。解决这一问题的方法就是扩大同步范围,因为对象锁是可重入的,因此在线程安全方法之上再同步相同的锁对象不会有问题。

  1. Map map = Collections.synchronizedMap(new HashMap());
  2. synchronized (map) {
  3. if (!map.containsKey("a")) {
  4. map.put("a", value);
  5. }
  6. }

注意,加大锁的范围,也要保证使用的是同一个锁,不然很可能造成死锁。 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)状态标志,模拟控制机制。常见用途如控制线程是否停止:

  1. private volatile boolean stopped;
  2. public void close(){
  3. stopped=true;
  4. }
  5. public void run(){
  6. while(!stopped){
  7. //do something
  8. }
  9. }

前提是do something中不会有阻塞调用之类。volatile保证stopped变量的可见性,run方法中读取stopped变量总是main memory中的最新值。

2)安全发布,如修复DLC问题。

  1. private volatile IoBufferAllocator instance;
  2. public IoBufferAllocator getInsntace(){
  3. if(instance==null){
  4. synchronized (IoBufferAllocator.class) {
  5. if(instance==null)
  6. instance=new IoBufferAllocator();
  7. }
  8. }
  9. return instance;
  10. }

3)开销较低的读写锁

  1. public class CheesyCounter {
  2. private volatile int value;
  3. public int getValue() { return value; }
  4. public synchronized int increment() {
  5. return value++;
  6. }
  7. }

synchronized保证更新的原子性,volatile保证线程间的可见性。

volatile不能用于做什么?

1)不能用于做计数器

  1. public class CheesyCounter {
  2. private volatile int value;
  3. public int getValue() { return value; }
  4. public int increment() {
  5. return value++;
  6. }
  7. }

因为value++其实是有三个操作组成的:读取、修改、写入,volatile不能保证这个序列是原子的。对value的修改操作依赖于value的最新值。解决这个问题的方法可以将increment方法同步,或者使用AtomicInteger原子类。

2)与其他变量构成不变式

一个典型的例子是定义一个数据范围,需要保证约束lower< upper。

  1. public class NumberRange {
  2. private volatile int lower, upper;
  3. public int getLower() { return lower; }
  4. public int getUpper() { return upper; }
  5. public void setLower(int value) {
  6. if (value > upper)
  7. throw new IllegalArgumentException();
  8. lower = value;
  9. }
  10. public void setUpper(int value) {
  11. if (value < lower)
  12. throw new IllegalArgumentException();
  13. upper = value;
  14. }
  15. }

尽管讲lower和upper声明为volatile,但是setLower和setUpper并不是线程安全方法。假设初始状态为(0,5),同时调用setLower(4)和setUpper(3),两个线程交叉进行,最后结果可能是(4,3),违反了约束条件。修改这个问题的办法就是将setLower和setUpper同步:

  1. public class NumberRange {
  2. private volatile int lower, upper;
  3. public int getLower() { return lower; }
  4. public int getUpper() { return upper; }
  5. public synchronized void setLower(int value) {
  6. if (value > upper)
  7. throw new IllegalArgumentException();
  8. lower = value;
  9. }
  10. public synchronized void setUpper(int value) {
  11. if (value < lower)
  12. throw new IllegalArgumentException();
  13. upper = value;
  14. }
  15. }

Java多线程编程的常见陷阱(转)的更多相关文章

  1. [转]Java多线程编程的常见陷阱

    1.在构造函数中启动线程 我在很多代码中都看到这样的问题,在构造函数中启动一个线程,类似这样: public class A{ public A(){ this.x=1; this.y=2; this ...

  2. Java多线程编程的常见陷阱

    .在构造函数中启动线程 我在很多代码中都看到这样的问题,在构造函数中启动一个线程,类似这样: public class A{ public A(){ ; ; this.thread=new MyThr ...

  3. Java多线程编程总结(精华)

    Java多线程编程总结 2007-05-17 11:21:59 标签:多线程 java 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http ...

  4. Java多线程编程核心技术

    Java多线程编程核心技术 这本书有利于对Java多线程API的理解,但不容易从中总结规律. JDK文档 1. Thread类 部分源码: public class Thread implements ...

  5. java多线程编程模式

    前言 区别于java设计模式,下面介绍的是在多线程场景下,如何设计出合理的思路. 不可变对象模式 场景 1. 对象的变化频率不高 每一次变化就是一次深拷贝,会影响cpu以及gc,如果频繁操作会影响性能 ...

  6. Java多线程编程(4)--线程同步机制

    一.锁 1.锁的概念   线程安全问题的产生是因为多个线程并发访问共享数据造成的,如果能将多个线程对共享数据的并发访问改为串行访问,即一个共享数据同一时刻只能被一个线程访问,就可以避免线程安全问题.锁 ...

  7. 《Java多线程编程实战指南(核心篇)》阅读笔记

    <Java多线程编程实战指南(核心篇)>阅读笔记 */--> <Java多线程编程实战指南(核心篇)>阅读笔记 Table of Contents 1. 线程概念 1.1 ...

  8. 《Java多线程编程核心技术》知识梳理

    <Java多线程编程核心技术> @author ergwang https://www.cnblogs.com/ergwang/ 文章末尾附pdf和png下载链接 第1章 Java多线程技 ...

  9. Java多线程编程实战02:多线程编程模型

    多线程编程模型 线程安全名词 串行.并发和并行 串行:一个人,将任务一个一个完成 并发:一个人,有策略地同时做多件事情 并行:多个人,每人做一个事情 竞态 名词 竞态:计算结果的正确性与时间有关的现象 ...

随机推荐

  1. ubuntu apt-get常用命令的使用

             packagename指代为软件包的名称 apt-get install packagename     安装一个新软件包(参见下文的aptitude) apt-get remove ...

  2. 分享关于学习new BufferedWriter()方法时常遇到的一个无厘头的问题

    今天在学习IO的过程中,关于处理流BufferedWriter的使用时,遇到了一个很犯二但是又会让初学者经常没有避免的问题,百度后才发现有人和我一样二,这还是对java基础掌握得不牢固的原因啊. 首先 ...

  3. IP工具类——IpAddress.java

    根据IP地址获取详细的地域信息,也可通过 http://whois.pconline.com.cn/ 获取地址信息. 源码如下:(点击下载  IpAddress.java) import java.i ...

  4. PS字体工具字体显示不出来

    显示一个小点:搜索了各种答案,扩大字号(最大72)更改前景色和背景色,最正确的解释就是分辨率太低,我发现分辨率是1,一般的设置成300,1分辨率情况下可以在图层那选择文字图层,然后按ctrl+t,拉大 ...

  5. Spring factorybean

    自定义FactoryBean 需要实现FactoryBean接口 通过FacotryBean来配置bean的实例. class: 指向FactoryBean的全类名 property:配置Factor ...

  6. 难搞的EXCHANGE重新安装错误

    Sample Exchange Setup Log III: [8/12/2010 2:59:38 AM] [1] [ERROR] Unable to remove product with code ...

  7. Android软件开发之发送短信与系统短信库解析

    今天我和同学们讨论一下Android平台下如何调用系统方法发送短信.接收短信.系统的短信库相关的问题.进入正题,我们先使用Eclipse工具模拟给自己的模拟器发送一条短信.在Eclipse下打开DDM ...

  8. 改善C#程序的50种方法

    为什么程序已经可以正常工作了,我们还要改变它们呢?答案就是我们可以让它们变得更好.我们常常会改变所使用的工具或者语言,因为新的工具或者语言更富生产力.如果固守旧有的习惯,我们将得不到期望的结果.对于C ...

  9. org.hibernate.LazyInitializationException: failed to lazily initialize

    今天搞了一上午,都在解决这个问题:org.hibernate.LazyInitializationException: failed to lazily initialize 原因很简单,是在非法的s ...

  10. android学习之BUG——The connection to adb is down, and a severe error has occured.

    开始--运行--cmd 进入命令提示符 输入netstat -ano 即可看到所有连接的PID 之后在任务管理器中找到这个PID所对应的程序如果任务管理器中没有PID这一项,可以在任务管理器中选&qu ...