今天我们来详细的学习一下你见过的或没见过的Java单例模式,对不同的单例模式写法,尽可能的说明其原理。
单例模式的核心是一个类只能被创建一个实例化对象
单例模式的实现
- 构造函数私有化,避免外部通过new创建
- 确保单例的线程安全
- 确保单例的唯一性,不能被重复创建
1、饿汉式
public class Signleton{
/**
* 对于一个final变量。
* 如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
* 如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
*/
private final static Signleton instance = new Signleton();
private Signleton(){
}
public static Signleton getInstance(){
return instance;
}
}
优点:饿汉式在类加载的过程中就会创建实例化对象,在getInstance()方法中只是直接返回对象引用。天生就是线程安全的。
缺点:无论类是否被使用,从始至终都会占据内存资源。
2、懒汉式
/**
* 普通懒汉式
*/
public class Signleton{
private static Signleton instance = null;
private Signleton(){
}
public static Signleton getInstance(){
if(instance==null){
instance = new Signleton();
}
return instance;
}
}
Singleton的静态属性instance中,只有instance为null的时候才创建一个实例,构造函数私有,确保每次都只创建一个,避免重复创建。但如果线程A进入if判断,并且判断为空,开始创建对象,与此同时,线程B也进入if判断,而线程A还未完成对象的实例化,那么线程B也会创建对象,所以此方式在多线程场景下是线程不安全的。
/**
* 加锁懒汉式
*/
public class Signleton{
private static Signleton instance = null;
private Signleton(){
}
public synchronized static Signleton getInstance(){
if(instance==null){
instance = new Signleton();
}
return instance;
}
}
给方法加锁,若有ABCD线程使用,A线程先进入,BCD线程都需要等待A线程执行完毕释放锁才能获得锁执行该方法,但这样的方式每次通过getInstance()方法去获取对象都会加锁,效率很低,继续改造。
/**
* 双重锁检查懒汉式(DCL模式)
*/
public class Signleton {
//使用volatile关键字修饰,避免jvm指令重排序
private volatile static Signleton instance = null;
private Signleton(){
}
//假设有ABCD个线程同时使用这个方法
public static Signleton getInstance(){
//BCD都进入了这个方法
if(instance==null){
//而A线程已经给第二个的判断加锁了
synchronized (Signleton.class){
//这时A挂起,对象instance还没创建 ,故BCD都进入了第一个判断里面,并排队等待A释放锁
//A唤醒继续执行并创建了instance对象,执行完毕释放锁。
//此时到B线程进入到第二个判断并加锁,但由于B进入第二个判断时instance 不为null了 故需要再判断多一次 不然会再创建一次实例
if(instance==null){
instance = new Signleton();
}
}
}
return instance;
}
}
通过双重锁检测,即实现了多线程场景下的线程安全,又避免了在对象已经创建的情况下调用getInstance()方法对其加锁,提高了效率。但细心的朋友可能已经发现了,对象使用了volatile关键字修饰,原因是JVM在执行对象创建的过程中存在指令重排序的情况,加上volatile关键字可以避免指令重排序,那什么又是指令重排序呢?
instance = new Signleton();
创建对象的过程在JVM中执行分为三个步骤:
- 在堆内存开辟内存空间。
- 调用Signleton的构造函数来初始化成员变量,形成实例。
- 把对象指向堆内存空间。
然而指令在执行的过程中第2步和第3步的顺序是不确定的,有可能按照123的顺序执行,也有可能按照132的顺序执行,如果线程A按照132的顺序执行,执行到3时(执行外3后instance非null)时间片用完,2还未执行,此时切换到线程B上,由于instance已经不为null,所以线程B直接返回,但同时Signleton类还没有实例,然后就顺理成章的报错了。因此,指令重排序会导致DCL失效。
通过不断的改造,懒汉式的单例模式好像已经无懈可击了,但事实并非如此,我们忘了Java还有一个牛逼的存在,那就是Java反射机制,他专治各种花里胡哨,因为他可以直接通过直接调用构造方法来完成对象的创建。所以为了避免反射机制的破坏,我们可以使用一个信号量来控制。
/**
* 避免反射机制破坏的双重锁检查懒汉式(DCL模式)
*/
public class Signleton {
//使用volatile关键字修饰,避免jvm指令重排序
private volatile static Signleton instance = null;
private static boolean flag = false;
private Signleton(){
if (flag){
throw new RuntimeException("不要试图通过反射机制破坏单例模式");
} else {
flag = true;
}
}
//假设有ABCD个线程同时使用这个方法
public static Signleton getInstance(){
//BCD都进入了这个方法
if(instance==null){
//而A线程已经给第二个的判断加锁了
synchronized (Signleton.class){
//这时A挂起,对象instance还没创建 ,故BCD都进入了第一个判断里面,并排队等待A释放锁
//A唤醒继续执行并创建了instance对象,执行完毕释放锁。
//此时到B线程进入到第二个判断并加锁,但由于B进入第二个判断时instance 不为null了 故需要再判断多一次 不然会再创建一次实例
if(instance==null){
instance = new Signleton();
}
}
}
return instance;
}
public static void main(String[] args) throws Exception {
//首先我们获得这个空参构造器
//由于构造器是私有的,所以我们用.setAccessible()解决了私有构造器的问题
Constructor<Signleton> declaredConstructor = Signleton.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
//这样就又创建出了另一个实例
Signleton instance1 = declaredConstructor.newInstance();
Signleton instance2 = declaredConstructor.newInstance();
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
所以,如果想要并避免反射机制破坏单例模式,可以采用上面的方法,但如果类不会反射机制使用到,大可不必。
3、静态内部类模式
public class Signleton{
private Signleton{
}
//静态嵌套类 这里给个链接 区分静态嵌套类和内部类[静态嵌套类和内部类](http://blog.csdn.net/iispring/article/details/46490319)
private static class SignletonHolder{
public static final Signleton instance = new Signleton();
}
public static Signleton getInstance(){
return SignletonHolder.instance;
}
}
外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化instance,故而不占内存。即当Singleton第一次被加载时,并不需要去加载SingletonHolder,只有当getInstance()方法第一次被调用时,才会去初始化instance,第一次调用getInstance()方法会导致虚拟机加载SingletonHolder类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
那么instance在创建的过程中又是如何保证线程安全的呢?
虚拟机会保证一个类的clinit()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,直到活动线程执行clinit()方法完毕。如果在一个类的clinit()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行clinit()方法后,其他线程唤醒之后不会再次进入clinit()方法。同一个加载器下,一个类型只会初始化一次。
静态内部类模式并不是完美的,他无法传递参数,所以实际应用中可以根据实际情况选择静态内部类模式或者DCL模式,或者下面要讲的枚举式
4、枚举式
public class Signleton{
public static Signleton getInstance(){
return SignletonEnum.INSTANCE.getInstance();
}
public enum SignletonEnum{
INSTANCE;
private Signleton instance;
//由于JVM只会初始化一次枚举实例,所以instance无需加static
private SignletonEnum(){
instance = new Signleton();
}
public getInstance(){
return instance;
}
}
}
定义内部的枚举,由于类加载时JVM只会初始化一次枚举实例,所以在构造函数中创建Signgleton对象并保证了这个对象实例唯一。而且枚举实例的创建是线程安全的,并且不会被反射机制破坏,请看以下源码。
单元素的枚举类型已经成为实现Singleton的最佳方法。