Java单例模式实现被忽视了的问题

单例模式应该是设计模式中应用最广泛的模式,通过单例模式,保证在系统的生命周期中只有一个在运行,在java中,单面经常写成如下格式。

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

看起来很漂亮的代码,但是如果有在多线程的环境下呢?线程1执行完了if判断后被中断, 线程2开始执行并生成了一个实例,线程1接着执行,又生成了一个。最简单的方法是加锁。代码如下:

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

这样写,这个代码执行没有问题,但是想一想,单例模式只生成一个实例,所以实际上只有在第一次的时候if语句条件成立,在后面无论执行多少次,都会直接返回静态实例,但是每次都要锁整段代码,性能和效率上是一种浪费。那么怎么既能保证安全,又能保证性能呢,有好几种方法,这里先说比较常用的双检测(double-check)机制。

public class Singleton {  
   
    private Singleton() {}  
  
     private static Singleton instance;  
  
     public static Singleton getInstance() {  
    
        If (instance == null) {

            synchronized(Singleton.class){
                 if(instance == null){
                    instance = new Singleton();
                 }
             }
         }
        return instance;  
      }  
}  

这样在第一次if条件成立的时候,即使线程被中断,因为有锁保护,在生成实例的过程中被保护,被中断的线程或者新的线程在这个过程中不能执行,而等锁推出后,其他线程恢复执行的时候,又要做一次判断,所以不会出现重复生成。而实例生成后,每次只需要执行第一个if判断,就直接返回,所以也不影响代码效率。看起来很完美,可以还没完,这个代码还有一个bug,就是java的内存模型的问题被疏忽了【第一个问题,java的内存模型】。


Java单例模式实现被忽视了的问题_第1张图片
图片发自App

在java多线程的时候,一般每个线程都会有一个工作内存(对CPU的高速缓存机制的抽象),如上图所示的工作内存和主内存之间的关系及操作顺序。线程运行的时候是使用和修改自己的工作内存中的变量,而并不会立即回写到主内存,所以如果上面的代码,如果线程1执行过程中被打断,而线程2执行完成,并生成一个实例,但是由于线程1还是使用的是自己工作内存的值,那么还会出现多次生成实例的问题。所以我们只考虑了原子性,而疏忽了多线程变成的可见性和有序性。所以可以修改一行代码:

private static volatile Singleton instance;

这个差别就是用volatile修饰instance变量,volatile声明的变量解决可见性和有序性,对于可见行:线程中每次use变量时,都需要连续执行read->load->use几项操作,即所谓的每次使用都要从主内存更新变量值,这样其它线程的修改对该线程就是可见的。并且,线程每次assign变量时,都需要连续执行assign->store->write几项操作,即所谓每次更新完后都会回写到主内存,这样使得其它线程读到的都是最新数据。对于有序性,对于volatile变量前面的代码的修改不会被优化到volatile变量后面,来避免虚拟机的优化造成代码的执行顺序的变化。

// Thread 1

Worker worker = new Worker;
Boolean initialed = true;

//Thread 2

While(! Initialed){
    sleep(100);
}
worker.dosomething();

如果不用volatile生命initialed变量,假如虚拟机对线程1的代码做了重排,线程2执行过程中就会出异常(虽然概率可能会非常低)。

public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }
     
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
         
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

最后再看后面这段代码,运行结果是10000吗?实际上这段程序并不能保证结果是10000,因为volatile只能保证变量的可见性,而不能保证变量的原子性,而在java中,只有赋值和基本类型变量的读取能保证原子性。假如线程1刚从主内存read了变量后被中断,另外一个线程开始执行,读取了主内存的变量,做了自加操作,等线程1开始执行,还是用刚才读取的主内存的值,因为可见性只是保证每次从主内存去读值。

编码是个苦力,每一行代码都得仔细思考,怎么让coder有动力去思考,有时间去思考?

你可能感兴趣的:(Java单例模式实现被忽视了的问题)