单例及double check lock

来源:互联网 发布:mac itunes资料库在哪 编辑:程序博客网 时间:2024/05/22 15:13

ex1:

public class Singleton    {        private static final Singleton singleton = null;        private Singleton()        {        }        public static Singleton getInstance()        {            if (singleton== null)            {                singleton= new Singleton();            }            return singleton;        }    }   

这个不多说了,肯定是错误的,如果多个线程访问的时候都死==null,那么接下来就是产生多个实例。不算单例模式。

ex2:

public class Singleton    {        private static final Singleton singleton = null;        private Singleton()        {        }        public static Singleton getInstance()        {            if (singleton== null)            {                synchronized (Singleton.class) {                    singleton= new Singleton();                }            }            return singleton;        }    }  

这个虽然加了同步锁,但是如果多个线程同步访问==null,那么还是会产生多个实例,只是产生实例的时候同步而已。

ex3:

public class Singleton    {        private static final Singleton singleton = null;        private Singleton()        {        }        public static Singleton getInstance()        {            synchronized (Singleton.class)            {                if (singleton== null)                {                    singleton= new Singleton();                }            }            return singleton;        }    }  

这个就可以了,但是这个地方对于每个访问的这个方法都是同步,而最需要的同步只是在new的地方,所以这样做虽然保证了单例,但是效率有点低哦。

ex4:

public class Singleton    {        private static final Singleton singleton = null;        private Singleton()        {        }        public static Singleton getInstance()        {            if (singleton== null)            {                synchronized (Singleton.class)                {                    if (singleton== null)                    {                        singleton= new Singleton();                    }                }            }            return singleton;        }    }  

这个不错,保证了单例。这个叫做double-check 双重检查。

还有一个比较简单的写法。

ex5:

public class Singleton    {        private static final Singleton singleton = new Singleton();    private Singleton()        {        }        public static Singleton getInstance()        {            return singleton;        }    }  

这样也可以,具体2种差别就不多说了。下面这个看起来要简单好多

double-check双重锁
对于多线程编程来说,同步问题是我们需要考虑的最多的问题,同步的锁什么时候加,加在哪里都需要考虑,当然在不影响功能的情况下,同步越少越好,锁加的越迟越优是我们都必须认同的。DCL(Double Check Lock)就是为了达到这个目的。
DCL简单来说就是check-lock-check-act,先检查再锁,锁之后再检查一次,最后才执行操作。这样做的目的是尽可能的推迟锁的时间。网上普遍举的一个例子是延迟加载的例子。

public   class  LazySingleton {      private   static   volatile  LazySingleton instance;      public   static  LazySingleton getInstantce() {          if  (instance ==  null ) {              synchronized  (LazySingleton. class ) {                  if  (instance ==  null ) {                      instance = new  LazySingleton();                  }              }          }          return  instance;      }  } 

对上面的例子来说,我们当然也可以把锁加载方法上,那样的话每次获取实例都需 要获取锁,但其实对这个instance来说,只有在第一次创建实例的时候才需要同步,所以为了减少同步,我们先check了一下,看看这个 instance是否为空,如果为空,表示是第一使用这个instance,那就锁住它,new一个LazySingleton的实例,下次另一个线程来 getInstance的时候,看到这个instance不为空,就表示已经创建过一个实例了,那就可以直接得到这个实例,避免再次锁。这是第一个 check的作用。

第二个check是解决锁竞争情况下的问题,假设现在两个线程来请求getInstance,A、B线程同时发现instance为空,因为我们 在方法上没有加锁,然后A线程率先获得锁,进入同步代码块,new了一个instance,之后释放锁,接着B线程获得了这个锁,发现instance已 经被创建了,就直接释放锁,退出同步代码块。所以这就是check-lock-then check。

网上有很多文章讨论DCL的失效问题,我就不赘述了,Java5之后可以通过将字段声明为volatile来避免这个问题。
我推荐一篇很好的文章《用happen-before规则重新审视DCL》,里面讲的非常好。

上面这个是最简单的例子,网上随处可见,双重检查的使用可不只限于单例的初始化,下面我举个实际使用中的例子。
缓存用户信息,我们用一个hashmap做用户信息的缓存,key是userId。

public   class  UserCacheDBService {      private   volatile  Map<Long, UserDO> map =  new  ConcurrentHashMap<Long, UserDO>();      private  Object mutex =  new  Object();      /**       * 取用户数据,先从缓存中取,缓存中没有再从DB取       * @param userId       * @return       */       public  UserDO getUserDO(Long userId) {          UserDO userDO = map.get(userId);          if (userDO ==  null ) {                            ① check              synchronized (mutex) {                       ② lock                  if  (!map.containsKey(userId)) {        ③ check                      userDO = getUserFromDB(userId);    ④ act                      map.put(userId, userDO);                  }              }          }          if (userDO ==  null ) {                             ⑤              userDO = map.get(userId);          }          return  userDO;      }      private  UserDO getUserFromDB(Long userId) {          // TODO Auto-generated method stub           return   null ;      }  }  
public class UserCacheDBService {      private volatile Map<Long, UserDO> map = new ConcurrentHashMap<Long, UserDO>();      private Object mutex = new Object();      /**      * 取用户数据,先从缓存中取,缓存中没有再从DB取      * @param userId      * @return      */      public UserDO getUserDO(Long userId) {          UserDO userDO = map.get(userId);          if(userDO == null) {                            ① check              synchronized(mutex) {                       ② lock                  if (!map.containsKey(userId)) {        ③ check                      userDO = getUserFromDB(userId);    ④ act                      map.put(userId, userDO);                  }              }          }          if(userDO == null) {                             ⑤              userDO = map.get(userId);          }          return userDO;      }      private UserDO getUserFromDB(Long userId) {          // TODO Auto-generated method stub          return null;      }  }  

三种做法:
1、 没有锁,即没有②和③,当在代码①处判断userDO为空之后,直接从DB取数据,这种情况下有可能会造成数据的错误。举个例子,A和B两个线程,A线程 需要取用户信息,B线程更新这个user,同时把更新后的数据放入map。在没有任何锁的情况下,A线程在时间上先于B线程,A首先从DB取出这个 user,随后线程调度,B线程更新了user,并把新的user放入map,最后A再把自己之前得到的老的user放入map,就覆盖了B的操作。B以 为自己已经更新了缓存,其实并没有。

2、 没有第二次check,即没有③的情况,在加锁之后立即从DB取数据,这种情况可能会多几次DB操作。同样A和B两个线程,都需要取用户信息,A和B在进 入代码①处时都发现map中没有自己需要的user,随后A线程率先获得锁,把新user放入map,释放锁,紧接着B线程获得锁,又从DB取了一次数据 放入map。

3、 双重检查,取用户数据的时候,我们首先从map中根据userId获取UserDO,然后check是否取到了user(即user是否为空),如果没有 取到,那就开始lock,然后再check一次map中是否有这个user信息(避免其他线程先获得锁,已经往map中放了这个user),没有的话,从 DB中得到user,放入map。

4、 在⑤处又判断了一次userDO为空的话就从map中取一次,这是由于此线程有可能在代码③处发现map中已经存在这个userDO,就没有进行④操作。

所以DCL只要记住:check-lock-check-act!

这种看起来很完美的优化技巧就是double-checked locking,但遗憾地告诉你,根据JLS规范,上面的代码不可靠!线程有可能得到一个不为null,但是构造不完全的对象。
Why?
造成不可靠的原因是编译器为了提高执行效率的指令重排。只要认为在单线程下是没问题的,它就可以进行乱序写入!以保证不要让cpu指令流水线中断。
这里写图片描述
Java5以后的版本,可以利用volatile关键字。
Why?
在java5以前,volatile原语不怎么强大,只能保证对象的可见性
但在java5之后,volatile语义加强了,被volatile修饰的对象,将禁止该对象上的读写指令重排序
这样,就保证了线程B读对象时,已经初始化完全了