对比
线程安全 | 效率 | 延迟加载 | |
---|---|---|---|
懒汉模式 | 安全(方法加锁) | 低(方法加锁) | 能(对象空才实例化) |
饿汉模式 | 安全(初始化加载) | 高(无锁) | 不能(类初始化就已经加载) |
DCL模式 | 安全(代码块加锁) | 高(仅第一次加锁) | 能(对象空才实例化) |
静态内部类模式 | 安全(初始化加载) | 高(无锁) | 能(内部类加载才实例化) |
枚举模式 | 安全(jvm保证初始化加载) | 高(无锁) | 不能(枚举类加载就完成实例化) |
懒汉模式
写法1
public class Singleton01 {
//懒汉模式:线程安全,效率低,能实现延迟加载
//构建单例对象,初始化放在静态方法中
private static Singleton01 singleton = null;
//构造器私有化
private Singleton01(){};
//静态方法初始化获取实例
public static synchronized Singleton01 getInstance(){
if(singleton == null){
singleton = new Singleton01();
}
return singleton;
}
}
写法2(此方法等同于下面的DCL)
public class Singleton01 {
//懒汉模式:线程安全,效率低,能实现延迟加载
//构建单例对象,初始化放在静态方法中
private static Singleton01 singleton = null;
//构造器私有化
private Singleton01(){};
//静态方法初始化获取实例
public static synchronized Singleton01 getInstance(){
if(singleton == null){
singleton = new Singleton01();
}
return singleton;
}
private static synchronized void singletonInit(){
if(singleton == null){
singleton = new Singleton01();
}
}
public static Singleton01 getInstance02(){
if(singleton == null){
singletonInit();
}
return singleton;
}
}
特点
线程安全(方法有synchronized),调用效率不高(每次进方法都要获取锁),但是能实现延迟加载(getInstance中判空才会初始化)
饿汉模式
public class Singleton02 {
//饿汉模式:线程安全,效率高,不能实现延迟加载
//构建单例对象,直接类初始化一次
private static Singleton02 singleton02 = new Singleton02();
//构造器私有化
private Singleton02(){};
//静态方式直接获取
public static Singleton02 getInstance(){
return singleton02;
}
}
特点
线程安全(类初始化实例对象就会创建),效率高(无锁),没有延迟加载(类初始化就创建了)
DCL(Double Check Lock)双重校验锁
public class Singleton03 {
//双重校验锁
private static volatile Singleton03 singleton03 = null;
//构造器私有化
private Singleton03(){};
//静态方法获取实例
public static Singleton03 getInstance(){
//先判断实例是否为空
if(singleton03 == null){
//为空加锁
synchronized(Singleton03.class){
//再判断一次,有可能前面一次判断实例为空到加锁之前,
//过程中有其他线程创建了对象,重新判空确保安全
if(singleton03 == null){
/*实例依旧为空则创建对象
* 但是由于JVM命令重排,new 一个对象有三步
* 1. 在堆空间分配内存
* 2. 将对象初始化(调用对象的构造器)
* 3. 将内存空间的地址赋值给对象的引用
* 由于JVM会将没有依赖的2,3两步重排执行(指的是步骤1分配内存必须在步骤3将内存地址赋值到引用之前,因此1,3两个步骤有依赖,但是步骤2跟步骤3则是没有依赖,JVM可能会重排执行),那么有可能出现先执行1,再执行3,再执行2;
* 这样就可能A线程执行1,在执行3的时候,B线程执行到了锁外面判空的地方,然后发现对象不为空(因为singleton03是static修饰的共享变量,B线程有可能看到对象被修改),直接返回对象
* 但是实际上对象还没有执行2步骤初始化,这样后面使用直接报错;
* 解决方法:将singleton03加上Volatile禁止命令重排(并非可见性)
*/
singleton03 = new Singleton03();
}
}
}
return singleton03;
}
}
特点
- 实例对象如果不加volatile会出现不安全,原因:
- jvm自动命令重排
- 线程B拿到还未初始化但是有引用的实例对象,导致运行报错;
- 加了volatile之后,由于volatile禁止命令重排,上图中肯定是1,2,3顺序执行,则能保证线程安全,效率高(只有第一次初始化加锁),能实现延迟加载(需要的时候才初始化)
静态内部类
public class Singleton04 {
//构造器私有化
private Singleton04(){};
//静态内部类(主类加载时不会加载静态内部类)
private static class Instance{
private static Singleton04 singleton04 = new Singleton04();
}
//静态方法获取实例
public static Singleton04 getInstance(){
return Instance.singleton04;
}
}
特点
- 为什么要使用静态内部类?因为静态内部类可以直接外部类打点调用初始化,如果是用普通内部类则需要创建外部类实例,但是这个外部类实例又要由内部类创建,因此使用静态内部类,那这样实例对象也得是静态的;
- 线程安全(只在内部类初始化一次),效率高(无锁),能实现延迟加载(外部类加载,内部类不会加载,只当使用到getInstance的时候才会加载)
- 但是其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错
枚举单例
public class Singleton05 {
//构造器私有化
private Singleton05(){};
//构建静态枚举类
private static enum Instance{
//枚举对象
INSTANCE;
//实例对象
private Singleton05 singleton05 = null;
//枚举对象构造器有jvm保证只执行一遍
private Instance(){
singleton05 = new Singleton05();
}
}
//静态方法获取实例
public static Singleton05 getInstance(){
return Instance.INSTANCE.singleton05;
}
}
特点
线程安全(jvm保证实例只被初始化一次),效率高(无锁),不能实现延迟加载(枚举在加载的时候,实例对象就已经被初始化)
选择方式
- 单例对象小,不需要延时加载,枚举好于饿汉
- 单例对象大,需要延时加载,静态内部类好于懒汉
- DCL加volatile在jdk5之前可能仍然有问题,故不建议使用;