面试题剖析:单例设计模式线程安全问题

本文作者:黄海燕,叩丁狼高级讲师。原创文章,转载请注明出处。

1. volatile 关键字

1.1 volatile 关键字作用:

在百度百科截取的描述如下:

叩丁狼教育.png

叩丁狼教育.png

说明volatile 关键字作用作用有两点:

  1. 防止指令重排:规定了volatile 变量不能指令重排,必须先写再读。

  2. 内存可见:线程从内存中读取volatile修饰的变量的数据,直接从主内存中获取数据,不需要经过CPU缓存,这样使得多线程获取的数据都是一致的。如图所示:


    叩丁狼教育.png

    叩丁狼教育.png

1.2 volatile和synchronized的区别

volatile不能够替代synchronized,原因有两点:

1.对于多线程,不是一种互斥关系
2.不能保证变量状态的“原子性操作”,所以volatile不能保证原子性问题

1.3解决单例设计模式线程安全问题

实现单例设计模式两种

  1. 饿汉式(不存在原子性,是线程安全的)

实现1:

//饿汉式:很饿需要立马创建对象
public class Singleton1 {
    //1.定义一个对象
    private static final Singleton1 instance = new Singleton1();
    //2.私有化构造器,避免外部类创建对象
    private Singleton1(){}
    //3.获取对象的静态方法
    public static Singleton1 getInstance(){
        return instance;
    }
}

实现2:枚举方式(最安全)

//饿汉式(枚举)
public enum EnumSingleton {
   INSTANCE;
}
  1. 懒汉式(懒加载):存在原子性问题,线程不安全
//懒汉式:很懒,使用对象的时候才创建对象,但是省资源
public class Singleton2 {
    private static Singleton2 instance;

    private Singleton2() {
    }

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

①由于 instance = new Singleton2();存在原子性问题,所以我们应该用synchronized代码块将其同步。这里由于synchronized很耗资源,所以粒度越小越好,最好不要使用同步方法。

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

②在多个线程的情况,可能存在线程1和线程2都已经执行了instance == null的判断,可能线程1抢到了锁线程2就阻塞在了同步代码块入口,当线程1执行完毕释放锁,线程2拿到锁的时候因为之前判断instance == null为true就会创建对象,那么此时就无法保证单例了,所以我们应该继续在同步代码块中再判断一次instance == null。这样的做法我们有个专业名词,称之为双重检查锁定。

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

③instance = new Singleton2();这句代码存在指令重排问题,什么意思?

一般的执行顺序为:

1)给对象分配内存空间
2)初始化对象
3)变量instance 指向内存空间

在单线程中,由于步骤2)和步骤3)即使交换顺序也不会影响最终效果,所以可能发生指令重排,顺序为:

1)给对象分配内存空间
3)变量instance 指向内存空间
2)初始化对象

如果出现指令重排就会发生以下问题,如图所示:

叩丁狼教育.png

叩丁狼教育.png

注意:由于线程2在外面的判断就为false,没有去运行需要竞争锁的代码,所以没有进入阻塞状态,和线程1是并行状态,导致访问对象出现问题,所以为了避免这个问题,我们应该不让指令重排发生,那么使用volatile修饰对象,让对象先写再读,固定对象的指令,避免指令重排。

最终线程安全的单例懒汉式代码如下:

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

想获取更多技术视频,请前往叩丁狼官网:http://www.wolfcode.cn/all_article.html

猜你喜欢

转载自blog.csdn.net/wolfcode_cn/article/details/84874632
今日推荐