zoukankan      html  css  js  c++  java
  • Java并发——DCL问题

    转自: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操作之前所做的修改。 

    例子六:不同的锁 

    class B {
    	private Object lock1 = new Object();
    	private Object lock2 = new Object();
    
    	private int some;
    
    	public int getSome() {
    		synchronized(lock1) { return some; }
    	}
    
    	public void setSome(int v) {
    		synchronized(lock2) { some = v; }
    	}
    }
    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

      

  • 相关阅读:
    hdu 5446 Unknown Treasure lucas和CRT
    Hdu 5444 Elven Postman dfs
    hdu 5443 The Water Problem 线段树
    hdu 5442 Favorite Donut 后缀数组
    hdu 5441 Travel 离线带权并查集
    hdu 5438 Ponds 拓扑排序
    hdu 5437 Alisha’s Party 优先队列
    HDU 5433 Xiao Ming climbing dp
    hdu 5432 Pyramid Split 二分
    Codeforces Round #319 (Div. 1) B. Invariance of Tree 构造
  • 原文地址:https://www.cnblogs.com/timlearn/p/4125297.html
Copyright © 2011-2022 走看看