转自:http://www.iteye.com/topic/875420

如果你搜索网上分析dcl为什么在java中失效的原因,都会谈到编译器会做优化云云,我相信大家看到这个一定会觉得很沮丧、很无助,对自己写的程序很没信心。我很理解这种感受,因为我也经历过,这或许是为什么网上一直有人喜欢谈dcl的原因。如果放在java5之前,从编译器的角度去解释dcl也无可厚非,在java5的JMM(内存模型)已经得到很大的修正,如果到现在还只能从编译器的角度去解释dcl,那简直就在污辱java,要知道java的最大优势就是只需要考虑一个平台。你可以完全无视网上绝大多数关于dcl的讨论,很多时候他们自己都说不清楚,除Doug Lea等几个大牛,我不相信谁比谁更权威。

很多人不理解dcl,不是dcl有多么复杂,恰恰相反,而是对基础掌握得不够。所以,我会先从基础讲起,然后再分析DCL。

我们都知道,当两个线程同时读写(或同时写)一个共享变量时会发生数据竞争。那我怎么才能知道发生了数据竞争呢?我需要去读取那个变量,发生数据竞争通常有两个表现:一是读取到陈旧数据,即读取到虽是曾经写入的数据,但不是最新的。二是读取到之前根本没有写入的值,也就是说读到垃圾。

数据陈旧性

为了读取到另一个线程写入的最新数据,JMM定义了一系列的规则,最基本的规则就是要利用同步。在Java中,同步的手段有synchronized和volatile两种,这里我只会涉及到syncrhonized。请大家先记住以下规则,接下来我会细讲。

规则一:必须对变量的所有写和所有读同步,才能读取到该最新的数据。

先看下面的代码:

public class A {
private int some;
public int another; public int getSome() { return some; }
public synchronized int getSomeWithSync() { return some; }
public void setSome(int v) { some = v; }
public synchronized void setSomeWithSync(int v) { some = v; }
}

让我们来分析一个线程写,另一个线程读的情形,一共四种情形。初始情况都是a = new A(),暂不考虑其它线程。

情形一:读写都不同步。

Thread1 Thread2
(1) a.setSome(13)  
  (2) a.getSome()

这种情况下,即使thread1先写入some为13,thread2再读取some,它能读到13吗?在没有同步协调下,结果是不确定的。从图上看出,两个线程独立运行,JMM并不保证一个线程能够看到另一个线程写入的值。在这个例子中,就是thread2可能读到0(即some的初始值)而不是13。注意,在理论上,即使thread2在thread1写入some之后再等上一万年也还是可能读到some的初始值0,尽管这在实际几乎不可能发生。

情形二:写同步,读不同步

Thread1 Thread2
(1) a.setSomeWithSync(13)  
  (2) a.getSome()

情形三:读同步,写不同步

Thread1 Thread2
(1) a.setSome(13)  
  (2) a.getSomeWithSync()

在这两种情况下,thread1和thread2只对读或只对写some加了锁,这不起任何作用,和[情形一]一样,thread2仍有可能读到some的初始值0。从图上也可看出,thread1和thread2互相之间并没有任何影响,一方加锁并不影响另一方的继续运行。图中也显示,同步操作相当于在同步开始执行lock操作,在同步结束时执行unlock操作。

情形四:读写都同步

Thread1 Thread2
(1) a.setSomeWithSync(13)  
  (2) a.getSomeWithSync()

在情形四中,thread1写入some时,thread2等待thread1写入完成,并且它能看到thread1对some做的修改,这时thread2保证能读到13。实际上,thread2不仅能看到thread1对some的修改,而且还能看到thread1在修改some之前所做的任何修改。说得更精确一些,就是一个线程的lock操作能看见另一线程对同一个对象unlock操作之前的所有修改,请注意图中的红色箭头。 沿着图中箭头指示方向,箭头结尾处总能看到箭头开始处操作做的修改。这样,a.some[thread2]能看见lock[thread2],lock[thread2]能看见unlock[thread1],unlock[thread1]又能看见a.some=13[thread1],即能看到some的值为13。

再来看一个稍微复杂一点的例子:

例子五

Thread1 Thread2
(1) a.another = 5  
(2) a.setSomeWithSync(13)  
  (3) a.getSomeWithSync()
(4) a.another = 7  
  (5) a.another

thread2最后会读到another的什么值呢?会不会读到another的初始值0呢,毕竟所有对another的访问都没有同步?不会。从图中很清晰地可以看出,thread2的another至少到看到thread1在lock之前写入的5,却并不能保证它能看到thread1在unlock写入的7。因此,thread2可以什么读到another的值可能5或7,但不会是0。你或许已经发现,如果去掉图中thread2读取a.some的操作,这时相当于一个空的同步块,对结论并没有任何影响。这说明空的同步块是起作用的,编译器不能擅自将空的同步块优化掉,但你在使用空的同步块应该特别小心,通常它都不是你想要的结果。另外需要注意,unlock操作和lock操作必须针对同一个对象,才能保证unlock操作能看到lock操作之前所做的修改。

例子六:不同的锁

Thread1 Thread2
(1) b.setSome(13)  
  (2) b.getSome()

在这种情况下,虽然getSome和setSome都加了锁,但由于它们是不同的锁,一个线程运行时并不能阻塞另一个线程运行。因此这里的情形和情形一、二、三一样,thread2不保证读到thread1写入的some最新值。

现在来看DCL:

例子七: DCL

public class LazySingleton {
private int someField; private static LazySingleton instance; private LazySingleton() {
this.someField = 201; // (1)
} public static LazySingleton getInstance() {
if (instance == null) { // (2)
synchronized(LazySingleton.class) { // (3)
if (instance == null) { // (4)
instance = new LazySingleton(); // (5)
}
}
}
return instance; // (6)
} public int getSomeField() {
return this.someField; // (7)
}
}

  假设thread1先调用getInstance(),由于此时还没有任何线程创建LazySingleton实例,它会创建一个实例s并返回。这是thread2再调用getInstance(),当它运行到(2)处,由于这时读instance没有同步,它有可能读到s或者null(参考情形二)。先考虑它读到s的情形,画出流程图就是下面这样的:

由于thread2已经读到s,所以getInstance()会立即返回s,这是没有任何问题,但当它读取s.someFiled时问题就发生了。 从图中可以看thread2没有任何同步,所以它可能看不到thread1写入someField的值20,对thread2来说,它可能读到s.someField为0,这就是DCL的根本问题。从上面的分析也可以看出,为什么试图修正DCL但又希望完全避免同步的方法几乎总是行不通的。

接下来考虑thread2在(2)处读到instance为null的情形,画出流程图:

接下来thread2会在有锁的情况下读取instance的值,这时它保证能读到s,理由参考情形四或者通过图中箭头指示方向来判定。

关于DCL就说这么多,留下两个问题:

    1. 接着考虑thread2在(2)读到instance为null的情形,它接着调用s.someFiled会得到什么?会得到0吗?
    2. DCL为什么要double check,能不能去掉(4)处的check?若不能,为什么?

原子性 
回到情形一,为什么我们说thread2读到some的值只可能为为0或13,而不可能为其它?这是由java对int、引用读写都是原子性所决定的。所谓“原子性”,就是不可分割的最小单元,有数据库事务概念的同学们应该对此容易理解。当调用some=13时,要么就写入成功要么就写入失败,不可能写入一半。但是,java对double, long的读写却不是原子操作,这意味着可能发生某些极端意外的情况。看例子:

public class C {
private /* volatile */ long x; // (1) public void setX(long v) { x = v; }
public long getX() { return x; }
}

  

Thread1 Thread2
(1) c.setX(0x1122334400112233L)  
  (2) c.getX()

thread2读取x的值可能为0,1122334400112233外,还可能为别的完全意想不到的值。一种情况假设jvm对long的写入是先写低4字节,再写高4字节,那么读取到x的值还可能为112233。但是我们不对jvm做如此假设,为了保证对long或double的读写是原子操作,有两种方式,一是使用volatile,二是使用synchronized。对上面的例子,如果取消(1)处的volatile注释,将能保证thread2读取到x的值要么为0,要么为1122334400112233。如果使用同步,则必须像下面这样对getX,setX都同步:

public class C {
private /* volatile */ long x; // (1) public synchronized void setX(long v) { x = v; }
public synchronized long getX() { return x; }
}

因此对原子性也有规则(volatile其实也是一种同步)。

规则二:对double, long变量,只有对所有读写都同步,才能保证它的原子性

有时候我们需要保证一个复合操作的原子性,这时就只能使用synchronized。

public class Canvas {
private int curX, curY; public /* synchronized */ getPos() {
return new int[] { curX, curY }; } public /* synchronized */ void moveTo(int x, int y) {
curX = x;
curY = y;
}
}

  

Thread1 Thread2
(1) c.moveTo(1, 1)  
(2) c.moveTo(2, 2)  
  (3) c.getPos()

当没有同步的情况下,thread2的getPos可能会得到[1, 2], 尽管该点可能从来没有出现过。之所以会出现这样的结果,是因为thread2在调用getPos()时,curX有0,1或2三种可能,同样curY也有0,1或2三种可能,所以getPos()可能返回[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]共九种可能。要避免这种情况,只有将getPos()和moveTo都设为同步方法。

总结 
以上分析了数据竞争的两种症状,陈旧数据和非原子操作,都是由于没有恰当同步引起的。这些其实都是相当基础的知识,同步可有两种效果:一是保证读取最新数据,二是保证操作原子性,但是大多数书籍都对后者过份强调,对前者认识不足,以致对多线程的认识上存在很多误区。如果想要掌握java线程高级知识,我只推荐《Java并发编程设计原则与模式》。其实我已经好久没有写Java了,这些东西都是我两年前的知识,如果存在问题,欢迎大家指出,千万不要客气。

还有一篇讲的也很不错:http://www.iteye.com/topic/260515

  

Java并发——DCL问题的更多相关文章

  1. Java并发(七):双重检验锁定DCL

    双重检查锁定(Double Check Lock,DCL) 1.懒汉式单例模式,无法保证线程安全: public class Singleton { private static Singleton ...

  2. 【Java并发编程】并发编程大合集-值得收藏

    http://blog.csdn.net/ns_code/article/details/17539599这个博主的关于java并发编程系列很不错,值得收藏. 为了方便各位网友学习以及方便自己复习之用 ...

  3. 【死磕Java并发】-----深入分析volatile的实现原理

      通过前面一章我们了解了synchronized是一个重量级的锁,虽然JVM对它做了很多优化,而下面介绍的volatile则是轻量级的synchronized.如果一个变量使用volatile,则它 ...

  4. Java 并发编程:volatile的使用及其原理

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  5. Java并发编程面试题 Top 50 整理版

    本文在 Java线程面试题 Top 50的基础上,对部分答案进行进行了整理和补充,问题答案主要来自<Java编程思想(第四版)>,<Java并发编程实战>和一些优秀的博客,当然 ...

  6. 【死磕Java并发】----- 死磕 Java 并发精品合集

    [死磕 Java 并发]系列是 LZ 在 2017 年写的第一个死磕系列,一直没有做一个合集,这篇博客则是将整个系列做一个概览. 先来一个总览图: [高清图,请关注"Java技术驿站&quo ...

  7. [Java] [Singleton] [DCL][happens-before]

    Singleton 只能有一个实例:必须自己创建自己的实例:必须给其他所有对象提供这一实例 实现方法 饿汉式singleton 预先加载法 class Single { private Single( ...

  8. Java并发编程基础-线程安全问题及JMM(volatile)

    什么情况下应该使用多线程 : 线程出现的目的是什么?解决进程中多任务的实时性问题?其实简单来说,也就是解决“阻塞”的问题,阻塞的意思就是程序运行到某个函数或过程后等待某些事件发生而暂时停止 CPU 占 ...

  9. 【Java并发编程】11、volatile的使用及其原理

    一.volatile的作用 在<Java并发编程:核心理论>一文中,我们已经提到过可见性.有序性及原子性问题,通常情况下我们可以通过Synchronized关键字来解决这些个问题,不过如果 ...

随机推荐

  1. 在HTML标签元素中,绑定JS函数

    <a onclick="ShowMsg(this)" id="myA" href="#">按钮</a> //JS方法 ...

  2. 【flask】项目集成Sentry收集线上错误日志

    flask集成sentry分为4个步骤: 首先在sentry官网注册1个账号 然后创建1个新的项目,这里我选择的是flask,这会得到一些关于sdk的使用说明 接下来创建一个简单的flask项目使用s ...

  3. 2018 icpc 青岛

    https://zoj.pintia.cn/contests/91827364639/problems C 要把这两个二进制串变为相同,需要先看哪些位置不同,设为数组c,某位为1则两位不同. 分1形成 ...

  4. postman通过引入外部文件实现参数化

    postman可通过引入外部文件进行参数化 目录 1.准备好接口信息 2.设置 1.准备好接口信息 这里的usr和psw是要参数化的对象 2.设置 文件准备 添加文件,并设置好循环次数即可

  5. 【MyBatis】-----【MyBatis】---表级联系【一对多】

    一.核心配置文件 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration ...

  6. 那些堪称神器的 Chrome 插件

    Chrome 的简洁快速以及丰富的插件种类使得它在国内日益盛行,帮助了我们很多 Chrome 用户提升了工作效率,而今天要给大家推荐8款实用甚至堪称神器的 Chrome 插件,希望对提升大家的工作效率 ...

  7. Java ——集合框架 list lambda set map 遍历方法 数据结构

    本节重点思维导图 集合框架 有序无序:元素放入的顺序与取出的顺序是否一致,一致即为有序,不一致即无序. List:允许重复.有序 ArrayList:长度可变的数组,遍历速度快 LinkedList: ...

  8. 磁盘管理|df、du|分区 fdisk |格式化

    3.磁盘管理 3.1命令df ·用于查看已挂载磁盘的总容量,使用容量,剩余容量等. -i:查看inodes的使用情况 -h:使用合适的单位显示 -k:以KB为单位显示 -m:以MB为单位显示 3.1. ...

  9. 扫描 + 注解完成bean的自动配置

    链接:https://pan.baidu.com/s/1W3TINXNnqpxmkIADOcJZCQ 提取码:fmt5 我们知道,我们一般是通过id或name调用getBean方法来从IOC容器中获取 ...

  10. CentOS 7安装Python 2.6(与已有版本共存)

    1. 安装需要用到的包 yum install -y zlib-devel bzip2-devel openssl-devel xz-libs wget 2. 下载 Python 2.6.8 版本 w ...