所谓线程不安全实际上就是一段代码在同一时间被两个线程同时执行,导致运行结果与单个线程运行结果不相同

新建一个单例模式类和一个多线程测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class TestSingleTon implements Runnable{
 
    public static void main(String[] args) {
        TestSingleTon t1 = new TestSingleTon();
        TestSingleTon t2 = new TestSingleTon();
         
        Thread thread1 = new Thread(t1);
        Thread thread2 = new Thread(t2);
        thread1.start();
        thread2.start();
    }
 
    @Override
    public void run() {
        System.out.println(SingleTon.getInstance());
         
    }
 
}
 
 
public class SingleTon {
    private static SingleTon singleTon;
     
    public static SingleTon getInstance() {
        if(singleTon==null) {
          singleTon = new SingleTon();
           }
               return singleTon;
    }
 
}

执行后发现控制台打印了两个不同的对象:

1
2
com.wey.demo.SingleTon@22896964
com.wey.demo.SingleTon@1ac5e970

说明有线程并发访问安全问题,获取的不是同一个实例

解决方案(1):使用同步锁机制,最简单的是在getInstance()方法上加synchronized关键字

1
2
3
4
5
6
public synchronized static SingleTon getInstance() {
    if(singleTon==null) {
        singleTon = new SingleTon();
    }
    return singleTon;  
}

对于这种方式,有人觉得在多并发的情况下,每次获取实例都要判断锁,效率比较低下,所以就有人想出了这样的办法,双重判断实例,这种大大减少判断同步锁的次数了。所以实际使用中可以推广。

1
2
3
4
5
6
7
8
9
10
public static SingleTon getInstance() {
    if(singleTon==null) {
        synchronized (SingleTon.class) {//SingleTon的字节码
            if(singleTon==null) {
                singleTon = new SingleTon();
            }
        }
    }
    return singleTon;
}

同时用volatile关键字修饰singleTon即:

1
private volatile static Singleton singleTon = null;

完整的代码应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SingleTon {
    private static SingleTon singleTon;
     
    public static SingleTon getInstance() {
      if(singleTon==null) {
          synchronized(SingleTon.class) {
                        if(singleTon==null) {
                                 singleTon = new SingleTon();
                        }
                  
           }
               return singleTon;
    }
 
}                                                           
为什么要使用volatile 修饰singleTon?

主要在于singleTon = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:

1.给 singleTon 分配内存

2.调用 Singleton 的构造函数来初始化成员变量

3.将singleTon 对象指向分配的内存空间(执行完这步 singleTon  就为非 null 了)。

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 singleTon  已经是非 null 了(但却没有初始化),所以线程二会直接返回 singleTon ,然后使用,然后顺理成章地报错

 

 

解决方案(2):改懒汉式单例为饿汉式单例

1
2
3
4
5
6
7
8
public class SingleTon {
    private static SingleTon singleTon = new SingleTon();
     
    public static SingleTon getInstance() {
        return singleTon;
    }
 
}