前言:单例模式,在设计模式中是比较常见的一种设计模式了,我们知道单例模式在并发的情况下要保证线程安全。作为Java程序员,我们一般采用加Synchronized锁的方式来保证线程安全的。
问题:现在问题来了---如果我们不使用synchronized和lock这样加锁的方式,如何实现一个线程安全的单例呢?
解决方案:
第一种:可以使用恶汉模式实现单例
public class Singleton{
private static Singleton instance=new Singleton();
private Singleton(){}
private static Singleton getInstance(){
return this.instance;
}
}
还有一种恶汉模式的变种:
public class Singleton{
private Singleton instance=null;
static{
instance=new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){
return this.instance;
}
}
注解:以上两种实现方式是使用static 来定义静态成员变量或者静态代码,借助Class类加载机制来实现线程安全的单例。
第二种方式:采用静态内部类来实现(也可以说是懒汉模式)
public class Singleton{
private static class SingletonHolder{
private static final Singleton INSTANCE=new Singleton();
}
private Singleton(){}
public static final Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
TIP:关于静态内部类:静态内部类和非静态内部类一样,都是在被调用时才会被加载
如果用static来修饰一个内部类,那么就是静态内部类。这个内部类属于外部类本身,但是不属于外部类的任
何对象。因此使用static修饰的内部类称为静态内部类。静态内部类有如下规则:
(1)、静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。
(2)、外部类可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象访问其实例成员。
静态内部类:就是我跟你没关系,自己可以完全独立存在,但是我就借你的壳用一下,来隐藏自己。
静态内部类和非静态内部类一样,都是在被调用时才会被加载
这种方式与前面两种有所优化,就是使用了lazy-loading。Singleton类被加载了,但是instance没有立即初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。
第三种方式:枚举
除了以上方式,还可以使用枚举的方式,如:
public enum Singleton{
INSTANCE;
public void whateverMethod(){
}
}
这种方式是Effective Java作者Josh Bloch提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很强的壁垒。
以上几种答案,其实原理都是利用借助了类加载的时候初始化单例。即是ClassLoader的线程安全机制。
所谓的ClassLoader的线程安全机制,就是ClassLoader的loadClass方法在加载类的时候,使用了synchronized关键字。也正是因为这样,除非被重写,这个方法默认在整个装载过程中都是同步的,也就是保证了线程安全。所以以上方法底层实现原理还是用到了synchronized。
第四种方法:使用CAS,CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。实现单例的方式如下:
public class Singleton{
private static final AutomicReference INSTANCE=new
AutomicReference();
private Singleton(){}
public static Singleton getInstance(){
for(;;){
Singleton singleton=INSTANCE.get();
if(null!=singleton){
return singleton;
}
singleton=new Singleton();
if(INSTANCE.compareAndSet(null,singleton)){
return singleton;
}
}
}
}
使用CAS的好处:在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等的算法,依赖底层硬件的实现,相对于锁它没有线程切换阻塞的额外消耗,可以支持较大的并行度。
这种方式的缺点:如果忙等待一直执行不成功(一直死循环中),会对CPU造成较大的执行开销。
而且,如果N个线程同时执行singleton=new Singleton()的时候,会有大量的对象创建,很可能导致内存溢出。
参考资料:https://www.cnblogs.com/maohuidong/p/7843807.html
https://www.cnblogs.com/GrimMjx/p/10105626.html