【并发编程】单例模式与线程安全

单例模式大概有以下几种写法:

1.饿汉模式

2.懒汉模式

3.double-check

4.枚举

5.静态内部类

分别从这几种方式来看线程安全问题。

1.饿汉模式(2种写法)

根据初始化时机的不同分为两种写法

1.1 声明+初始化(线程安全)

在声明的时候直接初始化,可以保证它是线程安全的,因为instance被声明为static,属于类,而类加载时就会创建instance实例,不论线程如何访问,都只会获取到该实例。

缺点:有句话叫“是盔甲也是软肋”,在类加载时就创建实例虽然能保证线程安全,但如果初始化过程十分复杂会导致类加载的时间变长,并且如果这个实例不会被用到,就浪费了资源。

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

1.2 静态块初始化(线程安全)

我们也可以在静态块中对实例初始化,不过要注意的是static instance的声明应当在static块之前,否则会出现instance对象为null。

public class Singleton{
    private Singleton(){}
    //单例对象
    private static Singleton instance = null;
    static{
        instance = new Singleton();
    }
    //静态工厂方法
    public static Singleton getInstance(){
        return instance;
    }
}

2.懒汉模式(2种)

2.1线程不安全的写法

懒汉模式是在第一次调用静态工厂方法时创建实例,为什么说它是线程不安全的呢?
首先在单线程的情况下是没有任何问题的,现在假如有两个线程A和B同时访问getInstance方法,都判定instance为空,这时候就会创建出不同的实例。

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

2.2线程安全的写法

使用synchronized修饰静态方法,可以确保同一时刻只有一个线程可以访问方法,做到了线程安全,但是会带来性能开销。

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

3.double-check(2种)

3.1线程不安全的double-check(因为指令重排)

双重检查,根据懒汉模式稍加改动

双重检查是指,在静态工厂方法getInstance中,先执行一次instance的检查,如果instance为空,则使用synchronized关键字锁住这个类,可以保证synchronized作用范围内只有一个实例被创建。

public class Singleton{
    private Singleton(){}
    private static Singleton instance = null;
    public static Singleton getInstance(){
        if(instance == null){ //double - check   // B
            synchronized(Singleton.class){
                if(instance == null){ //A -> 3
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

但为什么说他是线程不安全的呢?标题已经剧透啦,是因为指令重排。具体来看一下是怎样的情况。

首先对于这条语句

instance = new Singleton();

会被CPU转换为下面的指令 :

1.memory = allocate() 分配内存空间

2.ctorInstance() 初始化对象

3.instance = memory 设置instance指向刚刚分配的内存

对于2和3,因为没有什么必要的先后顺序(根据happens-before原则无法推导出它的顺序),因此可能会被重排序,也许会排成下面的样子:

1.memory = allocate() 分配内存空间

3.instance = memory 设置instance指向刚刚分配的内存

2.ctorInstance() 初始化对象

对照上面的程序,如果此时有两个线程A和B,当A已经执行到标识的位置,并且已经执行了上面第3条将instance指向分配好的内存,此时B执行判断,发现instance不为空,那么直接return instance了,可是这时候instance还没初始化呢,就被B线程发布了,我们说这里发生了对象逸出,因此它是不安全的。

如何能保证安全呢?看下边一个写法。

 3.2 线程安全的double-check(禁止指令重排)

既然上面说,是因为指令重排导致了不安全,那就不要指令重排了,也许你就会想到,是不是用volatile关键字来禁止指令重排。

看下实现,非常简单就是给instance对象添加volatile关键字。

volatile阻止的不是上面说的3个指令的重排,而是保证了在一个写操作(instance = new Singletone() // A)完成之前,不会调用读操作(if (instance == null) //B)。

public class Singleton{
    private Singleton(){}
    //使用volatile禁止重排序
    private volatile static Singleton instance = null;
    public static Singleton getInstance(){
        if(instance == null){ //double - check   // B
            synchronized(Singleton.class){
                if(instance == null){ //A
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

4.枚举类(线程安全)

JVM保证枚举类的构造函数只被调用一次

相比懒汉,在安全方面更容易保证

相比饿汉,在调用时才会初始化

public class Singleton{
    private Singleton(){}
    public static Singleton getInstance(){
        return Inner.INSTANCE.getInstance();
    }
    private enum Inner{
        INSTANCE;
        private Singleton singleton;
        Inner(){
            singleton = new Singleton();
        }
        public Singleton getInstance(){
            return singleton;
        }
    }
}

5.静态内部类(线程安全)

public class Singleton{
    private Singleton(){}
    public static Singleton getInstance(){
        return StaticSingleton.instance;
    }
    private static class StaticSingleton{
        private static final Singleton instance = new Singleton();
    }
}

写的话一般写5或者4就可以了。

(这是一个学习笔记总结贴,课程《Java并发编程入门与高并发面试》,来自慕课网的jimin老师,讲的超好推荐大家去看)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值