单例模式

409 阅读6分钟

保证一个类仅有一个实例,并提供一个该实例的全局访问点。——《设计模式》

一、单例模式种类

1.1、懒汉式V1

public class Singleton {  

    private static Singleton instance;  

    private Singleton (){}  // 构造器私有

    public static Singleton getInstance() {  
        if (instance == null) {  // 1
            instance = new Singleton();  // 2
        }  
        return instance;  
    }  
}

缺点:线程不安全。

原因是在上面1处的代码如果在多线程下,一个线程进入了 if (singleton == null) 判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式

1.2、懒汉式V2(线程安全)

public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

这种方式线程安全,getInstance() 方法进行了线程同步,但是这种方式不推荐使用。

缺点:效率太低

每个线程在想获得类的实例时候,执行 getInstance() 方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。方法进行同步效率太低要改进。

1.3、双重校验锁

public class Singleton {

    private static volatile Singleton singleton; // 1

    private Singleton() {}  //私有构造函数

    //静态工厂方法
    public static Singleton getInstance() {
        if (singleton == null) { //双重检测机制
            synchronized (Singleton.class) { //同步锁
                if (singleton == null) { //双重检测机制
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

双重校验锁线程安全推荐使用。

疑问:

1.这种方式为什么要检查两次?检查一次行不行?检查一次不行。

  • 第一次判断singleton == null作用是:如果单例对象已经创建,就直接返回对象,不需要进入同步锁,提高效率。

  • 第二次判断singleton == null作用是:假如没有这个判断,多个线程同时通过第一次singleton == null之后,只有一个线程进入临界区,其他线程排队,没有第二次检查的话,会创建多个实例。

2:在代码1处使用了关键字volatile,为什么需要?

因为 JVM 指令重排序,也就是并不限制处理器的指令顺序,说白了就是在不影响结果的情况下,顺序可能会被打乱。

在执行sInstance = new Singleton();这条命令语句时,JMM并不是一下就执行完毕的,即不是原子性,实质上这句命令分为三大部分:

  1. 为对象分配内存
  2. 执行构造方法语句,初始化实例对象
  3. 把sInstance的引用指向分配的内存空间

在JMM中这三个步骤中的2和3不一定是顺序执行的,如果线程A执行的顺序为1、3、2,在第2步执行完毕的时候,恰好线程B执行第一次判空语句,则会直接返回sInstance,那么此时获取到的sInstance仅仅只是不为null,实质上没有初始化,这样的对象肯定是有问题的!

而volatile关键字的存在意义就是保证了执行命令不会被重排序,也就避免了这种异常情况的发生,所以这种获取单例的方法才是真正的安全可靠!

1.4、饿汉式

public class Singleton {  

    private static Singleton instance = new Singleton();  

    private Singleton (){}  

    public static Singleton getInstance() {  
        return instance;  
    }  
}

1.5、饿汉式其他变种1-静态常量

public class Singleton {  

    private static final Singleton INSTANCE = new Singleton();  

    private Singleton (){}  

    public static Singleton getInstance() {  
        return INSTANCE;  
    }  

}

1.6、饿汉式其他变种2-静态代码块

public class Singleton {  

    private static Singleton instance;
    
    static {
       instance = new Singleton();  
    }

    private Singleton (){}  

    public static Singleton getInstance() {  
        return instance;  
    }  
}

上面几种本质一样

优点:无线程安全问题。

缺点:不能延迟加载,如果这个对象没有使用到,浪费内存。

1.7、静态内部类

public class Singleton {

    private Singleton() {}

    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

这种方式 避免了线程不安全,延迟加载,效率高。

1.8、枚举

public enum EnumSingleton {
    INSTANCE;
    private Object object;
    EnumSingleton() {
        object = new Object();
    }
    public Object getInstance() {
        return object;
    }
}

优点:线程安全,能防止序列化破坏。

疑问:

饿汉式/静态内部类/枚举是通用什么方式保证线程安全?

以上的静态内部类、饿汉等模式均是通过定义静态的成员变量,以保证单例对象可以在类初始化的过程中被实例化。

这其实是利用了ClassLoader的线程安全机制。ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。

所以, 除非被重写,这个方法默认在整个装载过程中都是线程安全的。所以在类加载过程中对象的创建也是线程安全的。

那么这些方式都是和synchronized有关系的,有没有哪种方式可以完全不使用synchronized的吗?

1.9、CAS(AtomicReference)实现单例模式

public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();

    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是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。

缺点:

  • CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。

  • 如果N个线程同时执行到 singleton = new Singleton();的时候,会有大量对象被创建,可能导致内存溢出。

二、破坏单例模式的方式和解决办法

2.1、反射破坏单例

可以通过setAccessible(true)来绕过 private 限制,从而调用到类的私有构造函数创建对象。

Singleton1 singleton1 = Singleton1.getInstance();
Constructor<Singleton1> cons = Singleton1.class.getDeclaredConstructor();
cons.setAccessible(true);
Singleton1 singleton2 =  cons.newInstance(); // 注意 需要通过 Constructor newInstance
System.out.println(singleton1);
System.out.println(singleton2);

结果

com.pandacase.single.Singleton1@71bc1ae4
com.pandacase.single.Singleton1@6ed3ef1

解决办法:

在私有构造器中,判断是否已经存在单例对象了,如果存在,则抛出异常

例如:

public class Singleton {
    
    private static int count = 0;
    private static volatile Singleton singleton; // 1

    //私有构造函数
    private Singleton() {
         synchronized (Singleton.class) {
            if (count > 0){
                throw new RuntimeException("反射破坏单例对象.....");
            }
            count++;
        }
    } 

    //静态工厂方法
    public static Singleton getInstance() {
        if (singleton == null) { //双重检测机制
            synchronized (Singleton.class) { //同步锁
                if (singleton == null) { //双重检测机制
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

2.2、序列化破坏单例(前提是单例类实现了Serializable接口)

当单例对象有必要实现 Serializable 接口时,即使将其构造函数设为私有,在它反序列化时依然会通过特殊的途径再创建类的一个新的实例,相当于调用了该类的构造函数有效地获得了一个新实例。

解决办法:

 /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */  
 public Object readResolve() {  
        return instance;  
}

2.3、对象克隆破坏单例

clone()是 Object 的方法,每一个对象都是 Object 的子类,都有clone()方法。clone()方法并不是调用构造函数来创建对象,而是直接拷贝内存区域。因此当我们的单例对象实现了 Cloneable 接口时,尽管其构造函数是私有的,仍可以通过克隆来创建一个新对象,单例模式也相应失效了。

解决办法:

重写clone()方法,调clone()时直接返回已经实例的对象

public class Singleton implements Cloneable{
    
    private static int count = 0;
    private static volatile Singleton singleton; // 1

    //私有构造函数
    private Singleton() {
         synchronized (Singleton.class) {
            if (count > 0){
                throw new RuntimeException("反射破坏单例对象.....");
            }
            count++;
        }
    } 

    //静态工厂方法
    public static Singleton getInstance() {
        if (singleton == null) { //双重检测机制
            synchronized (Singleton.class) { //同步锁
                if (singleton == null) { //双重检测机制
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
  // 重写 clone 方法
  @Override
  protected Object clone() throws CloneNotSupportedException {
    return singleton;
  }
}