第一种,线程不安全的懒汉式
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (null == instance) {
instance = new Singleton();
}
return instance;
}
}
第二种,线程安全的懒汉式
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (null == instance) {
instance = new Singleton();
}
return instance;
}
}
第三种,饿汉式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
第四种,静态代码块饿汉式
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
第五种,静态内部类
public class Singleton {
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
第六种,枚举
public enum Singleton {
INSTANCE;
}
在于Java保证任何枚举值都是全局唯一的,即在一个JVM内部,枚举的每个值都只有一个实例。此外,使用枚举来实现单例模式还可以避免通过反射或反序列化的方式破解传统单例模式的实例唯一性。
枚举单例的优点有:
- 线程安全:Java虚拟机会保障每个枚举实例的全局唯一性和一次性初始化,保证了线程安全。
- 防止反射攻击:由于枚举类没有可访问的构造方法,因此不能通过反射方法来调用私有构造器。
- 防止反序列化创建新实例:枚举的反序列化不会通过反射来创建新实例,而是直接通过枚举类型的名字来返回相应的枚举值。
第七种,双重校验锁
public class Singleton implements Serializable {
private static volatile Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance() {
if (null == singleton) {
synchronized (Singleton.class) {
if (null == singleton) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
为什么加volatile?这样做的好处是什么?
在Java中,volatile
关键字的主要作用是确保变量的可见性和部分有序性。当一个变量声明为volatile
时,它可以告诉JVM和编译器不要对这个变量的读写进行重排序,这能保证在不同线程中对这个变量的操作都是可见的,即一个线程对这个变量的修改会立即被其他线程知晓。
使用volatile
关键字是为了防止在singleton
实例被初始化时发生的重排序。初始化一个对象可以大致分为以下几个步骤:
- 分配内存空间。
- 初始化对象。
- 将对象引用赋值给
singleton
变量。
如果没有volatile
关键字,JVM的即时编译器在运行时可能会对上述步骤进行重排序(特别是步骤2和步骤3),使得步骤3可能发生在步骤2之前。这样,当一个线程A执行到步骤3且还未执行步骤2时(即singleton
已指向某块内存但对象尚未完全构造),另一个线程B调用getInstance()
方法,判断singleton
不为null
时直接返回了这个未完全初始化的对象,从而可能在使用时导致错误。
为什么使用双重校验锁(double-checked locking)?
-
性能优化:双重校验锁首先检查
singleton
是否已被初始化,只有在它未被初始化的情况下(大多数情况下这个条件都是false
),才进入同步区块。这样可以减少在多线程环境中的同步开销。 -
线程安全:即使多个线程同时通过了第一次
null
检查,由于同步块的存在,只有一个线程能在同一时间内进入同步块内进行初始化,其它线程将会在入口处阻塞等待。第一个进入同步块的线程完成对象初始化后将退出,并释放锁,随后等待的线程进入后再次检查singleton
是否为null
。由于第一个线程已完成初始化,后续线程不会重复创建新的实例,而是被直接返回已创建的实例。
网友评论