常用的单例有以下5种写法,如果还有其他的写法,也基本是从以下5种稍微修改而来,由于内容基本一致,并且可能不是很常用,因此在本文中不再赘述。
1.饿汉模式
public class Singleton {
// 1.饿汉
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
饿汉模式,比较常见的一种写法。在类加载的时候就对实例进行初始化,没有线程安全问题;获取实例的静态方法没有使用同步,调用效率高;但是没有使用懒加载,如果该实例从始至终都没被使用过,则会造成内存浪费。
**总结:**线程安全、非懒加载、效率高。
**是否推荐:**可以使用,但不推荐。
2.懒汉模式
public class Singleton {
// 2.懒汉
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
线程安全的懒汉模式,比较常见的一种写法。在第一次使用的时候才进行初始化,达到了懒加载的效果;由于获取实例的静态方法用synchronized修饰,所以也没有线程安全的问题;但是,这种写法每次获取实例都要进行同步(加锁),因此效率较低,并且可能很多同步都是没必要的。
**总结:**线程安全、懒加载、效率低。
**是否推荐:**可以使用,但不推荐。
**注:**该模式还有另一种常见写法,就是把getInstance方法上的synchronized去掉,这种方法有线程安全问题,不能使用。
3.双重检测机制(DCL)
public class Singleton {
// 3.双重检测机制
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
双重检测机制(双重检查加锁),比较常见的一种写法。在第一次使用的时候才进行初始化,达到了懒加载的效果;在进行初始化的时候会进行同步(加锁),因此没有线程安全问题;并且只有第一次进行初始化才进行同步,因此不会有效率方面的问题。
《Java Concurrency in Practice》作者Brian Goetz在书中提到关于DCL的观点:促使DCL模式出现的驱动力(无竞争同步的执行速度很慢,以及JVM启动时很慢)已经不复存在,因而它不是一种高效的优化措施。延迟初始化占位类模式(静态内部类)能带来同样的优势,并且更容易理解。
**总结:**线程安全、懒加载、效率高。
**是否推荐:**可以使用。
**注:**该模式还有另一种常见写法,就是把静态实例singleton的volatile修饰去掉,这种方法有线程安全方面的问题,不能使用。在我的另一篇文章有提到这个:volatile关键字详解,这里直接截取该部分内容,请见下面这个例子。
例子:双重检测机制实现单例(没有volatile修饰)
这段代码是单例的双重检测机制实现,相信很多人都用过,并且觉得这个代码是没问题的。在大多数情况,这段代码确实没问题,但在极端的情况下,有个隐藏的问题。
例子分析:
假设有两个线程同时访问这段代码,此时线程A走到15行开始初始化对象,线程B则刚走到12行进行第一次检测。这时要介绍下15行初始化这行代码,这行代码虽然只有一句话,但是被编译后会变成以下3条指令:
正常情况下,这3条执行时按顺序执行,双重检测机制就没有问题。但是CPU内部会在保证不影响最终结果的前提下对指令进行重新排序(不影响最终结果只是针对单线程,切记),指令重排的主要目的是为了提高效率。在本例中,如果这3条指令被重排成以下顺序:
如果线程A执行完1和3,instance对象还未完成初始化,但是已经不再指向null。此时线程B抢占到CPU资源,执行第12行的检测结果为false,则执行第19行,从而返回一个还未初始化完成的instance对象,从而出导致问题出现。要解决这个问题,只需要使用volatile关键字修饰instance对象即可。
4.静态内部类(延迟初始化占位类)
public class Singleton {
// 4.静态内部类
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static final Singleton getInstance() {
return Holder.INSTANCE;
}
}
静态内部类(延迟初始化占位类),比较常见的一种写法。JVM将推迟SingletonHolder的初始化操作,直到开始使用这个类时才初始化,并且由于通过一个静态初始化来初始化Singleton,因此不需要额外的同步。当任何一个线程第一次调用getInstance时,都会使SingletonHolder被加载和被初始化,此时静态初始化器将执行Singleton的初始化操作。
通过静态初始化来初始化Singleton为什么不需要额外的同步?
在初始器中采用了特殊的方式来处理静态域(或者在静态初始化代码块中初始化的值),并提供了额外的线程安全性保证。静态初始化器是由JVM在类的初始化阶段执行,即在类被加载后并且被线程使用之前。由于JVM将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。因此无论是在被构造期间还是被引用时,静态初始化的对象都不需要显式的同步。然而,这个规则仅适用于在构造时的状态,如果对象是可变的,那么在读线程和写线程之间仍然需要通过同步来确保随后的修改操作是可见的,以及避免数据破坏。
**总结:**线程安全、懒加载、效率高。
**是否推荐:**推荐使用、《Java Concurrency in Practice》作者Brian Goetz推荐使用的方式。
5.枚举
public enum Singleton {
INSTANCE;
}
读者福利
分享一份自己整理好的Java面试手册,还有一些面试题pdf
不要停下自己学习的脚步
:**推荐使用、《Java Concurrency in Practice》作者Brian Goetz推荐使用的方式。
5.枚举
public enum Singleton {
INSTANCE;
}
读者福利
分享一份自己整理好的Java面试手册,还有一些面试题pdf
不要停下自己学习的脚步
[外链图片转存中…(img-XfAoQhCG-1718912929264)]
[外链图片转存中…(img-HYDIeuoT-1718912929265)]