写一个安全的Java单例

  单例模式可能是我们平常工作中最常用的一种设计模式了。单例模式解决的问题也很常见,即如何创建一个唯一的对象。但想安全的创建它其实并不容易,还需要一些思考和对JVM的了解。

  1.首先,课本上告诉我,单例这么写

  

 1 public class Singleton {
 2 
 3     private static Singleton instance;
 4 
 5     private Singleton() {
 6     }
 7 
 8     public static Singleton getInstance() {
 9         if (instance == null) {
10             instance = new Singleton();
11         }
12         return instance;
13     }
14 }

  这段代码最大的问题就是它并不是线程安全的。即在多线程情况下可能new 出多个对象。试想有两个线程同时执行到了第9行,由于没有锁机制,那么两个线程都会进入,就会new出多个对象。

 1 public static CountDownLatch latch = new CountDownLatch(2);
 2 
 3     public static void main(String[] args) {
 4         for (int i = 0; i < 2; i++) {
 5             new Thread(new Runnable() {
 6 
 7                 @Override
 8                 public void run() {
 9                     latch.countDown();
10                     try {
11                         latch.await();
12                     } catch (InterruptedException e) {
13                         // TODO Auto-generated catch block
14                         e.printStackTrace();
15                     }
16                     System.out.println(Singleton.getInstance());
17                 }
18             }).start();
19         }
20     }

  我用上面代码来演示 第一种单例写法的结果。最后会调用Object的toString方法来打印Singleton对象的hashcode

  结果如下

  

第一次结果:
com.deng.pp.Singleton@33bbe97
com.deng.pp.Singleton@11989480

第二次结果:
com.deng.pp.Singleton@1c0956b9
com.deng.pp.Singleton@5e70125b

第三次结果:
com.deng.pp.Singleton@5e70125b
com.deng.pp.Singleton@1c0956b9

第四次结果:
com.deng.pp.Singleton@1c0956b9
com.deng.pp.Singleton@1c0956b9

第五次结果:
com.deng.pp.Singleton@1c0956b9
com.deng.pp.Singleton@1c0956b9

  可以看出,单例代码1确实会存在new 出多个对象的情况。

  将单例代码1的getInstance方法 改成如下,对getInstance方法加synchronized 关键字

  

public static synchronized Singleton getInstance() {
  if (instance == null) {
    instance = new Singleton();
  }
  return instance;
}

  下面是5次测试结果

  

第一次:
com.deng.pp.Singleton@33bbe97
com.deng.pp.Singleton@33bbe97
第二次
com.deng.pp.Singleton@12ceba90
com.deng.pp.Singleton@12ceba90
第三次
com.deng.pp.Singleton@5e70125b
com.deng.pp.Singleton@5e70125b
第四次
com.deng.pp.Singleton@1c0956b9
com.deng.pp.Singleton@1c0956b9
第五次
com.deng.pp.Singleton@1c0956b9
com.deng.pp.Singleton@1c0956b9

  可以确定synchronized确实起了作用。这么做是可以work的,执行结果也没有什么错误。但它有一个最大问题是效率问题。每个线程调用getInstance方法是都要去判断是否有其他线程在执行这个方法,即使instance已经存在也需要去判断是否有线程在方法里面。如果有,就要在外边等。而实际上只需要在new 对象之前等就可以了。根据这个就有了下面的方法:双重检查锁

  

 1 public static Singleton getInstance() {
 2         if (instance == null) {
 3             synchronized (Singleton.class) {
 4                 if (instance == null) {
 5                     instance = new Singleton();
 6                 }
 7             }
 8         }
 9         return instance;
10     }

  来分析一下,假设两个线程到达getInstance方法,线程1先获得了锁,进入初始化方法。线程2因未获得锁在外边等待,线程1出去后,线程2进入同步块,instance不是null,return,完美。

  但是结果可能并不是这样,因为 对象的new操作并不是 原子 的。JVM new 对象的过程大致如下

  1.在堆上分配一块内存空间  2.实例化类放入1分配的内存空间 3.把引用赋给instance

  如果按照123的顺序,上面那段代码就没有问题。但JVM中存在指令重排,即编译器对代码进行优化,改变不相互依赖的代码的执行顺序。上述1,2,3中第三步并不依赖于第二步,即可能存在132这样的顺序。

  那么这种顺序下,线程1执行到3。线程2进入方法,此时由于instance已被赋值,所以不为null。直接return,此时return的对象是不正确的,因为线程1还没有将对象完全初始化完。

  (很抱歉,在我的环境下并没有重现这种问题,如果有其他的可以测试出这种问题的方法,望不吝赐教。)

  解决办法是将instance字段改成

    private volatile static Singleton instance;

  volatile 关键字会禁止指令重排序,从而保证了单例正确性。

  下面的方法也可以实现单例,因为SINGLETON为static的所以在类加载时就会初始化,final保证了只会赋一遍值。项目较小时可以用,很方便,类很多的时候如果都上来就加载可能就很浪费资源了。

public static final Singleton SINGLETON = new Singleton();

  Effective Java作者推荐了一种更好更安全的写法。

public class Singleton {

    private Singleton() {
    }

    public enum Instance{
        INSTANCE;
        
        private Singleton singleton;
        
        Instance() {
            singleton = new Singleton();
        }
        
        public Singleton getInstance(){
            return singleton;
        }
    }
}

  最近才开始写博客,才疏学浅,如文中有任何错误请留言交流。谢谢~

原文地址:https://www.cnblogs.com/xushy/p/8446736.html