单例模式的目标:
1.在调用getInstance()方法时返回一个且唯一的Singleton对象。
2.能够在多线程使用时也能保证获取的Singleton对象唯一
3.getInstance()方法的性能要保证
4.能在需要的时候才初始化,否则不用初始化
单例模式的几种写法,由浅到深,根据平常的需求选择适当的实现方式。
写法一 饿汉式 :
/**
* 饿汉式
* 基于ClassLoader的机制,在同一classLoader下,该方式可以解决多线程同步的问题,
* 但是该种单例模式没有办法实现懒加载
*/
public class SingletonHungry {
/**
* 在ClassLoader加载该类时,就会初始化mInstance
*/
private static SingletonHungry mInstance = new SingletonHungry();
private SingletonHungry() {
}
public static SingletonHungry getInstance() {
return mInstance;
}
}
根据java虚拟机和ClassLoader的特性,一个类在一个ClassLoader中只会被加载一次,当我们实现SingletonHungry类时,mInstance就已经被实例化了,保证了在多线程并发情况下获取到的对象是唯一,但不适用例如某类实例需求依赖在运行时的参数来生成对象。
写法二 懒加载(非线程安全):
/**
* 懒汉式
* 只有在getInstance()时才会初始化mInstance
* Created by chuck on 17/1/18.
*/
public class SingletonLazy {
private static SingletonLazy mInstance;
private SingletonLazy() {
}
public static SingletonLazy getInstanceUnLocked() {
if (mInstance == null) {//line1
mInstance = new SingletonLazy();//line2
}
return mInstance;//line3
}
}
这种方式无法满足多线程的情况,线程A执行了line1但line2还未完成对象的初始化,线程B执行到line1就直接跳过line2执行line3了,就会出问题,这里可以了解下new SingletonLazy()这个操作不是原子操作,该操作分为:
1.分配内存空间
2.初始化对象
3.将对象指向分配好的地址空间(执行完之后就不再是null了)
其中第2,3步在一些编译器中为了优化单线程中的执行性能是可以重排的。重排之后就是这样的:
1.分配内存空间
3.将对象指向分配好的地址空间(执行完之后就不再是null了)
2.初始化对象
所以,当重排的时候就会出问题,此方法保证不了线程安全,所以我们加上同步。
写法三 懒加载(线程安全):
/**
* 懒汉式
* 只有在getInstance()时才会初始化mInstance
* Created by chuck on 17/1/18.
*/
public class SingletonLazy {
private static SingletonLazy mInstance;
private SingletonLazy() {
}
/**
* 方法名多了Locked表示是线程安全的,没有其他意义
*/
public synchronized static SingletonLazy getInstanceLocked() {
if (mInstance == null) {
mInstance = new SingletonLazy();
}
return mInstance;
}
}
该方法加上synchronized关键字保证了线程安全,同一时间两个线程不能同时访问getInstanceLocked()方法,但当有多个线程会频繁调用getInstanceLocked()方法的话会造成很大的性能损失,该方法就比较适合那些没有多线程频繁调用时的场景。为了解决这个问题出现了双重检查锁定(简称DCL)。
写法四 双重检查锁定(DCL):
/**
* 双重检查锁定DCL
* Created by chuck on 17/1/18.
*/
public class SingletonLazy {
private static SingletonLazy mInstance;
private SingletonLazy() {
}
public static SingletonLazy getInstance() {
if (mInstance == null) {//第一次检查
synchronized (SingletonLazy.class) {//加锁
if (mInstance == null) {//第二次次检查
mInstance = new SingletonLazy();//new 一个对象
}
}
}
return mInstance;
}
}
这个方法并发和性能的问题得以解决,但也存在之前说的重排情况,即多线程会获取到一个未初始化的对象。为了解决这个情况,我们引入了volatile关键字:
/**
* 双重检查锁定DCL
* Created by chuck on 17/1/18.
*/
public class SingletonLazy {
private volatile static SingletonLazy mInstance;
private SingletonLazy() {
}
public static SingletonLazy getInstance() {
if (mInstance == null) {//第一次检查
synchronized (SingletonLazy.class) {//加锁
if (mInstance == null) {//第二次次检查
mInstance = new SingletonLazy();//new 一个对象
}
}
}
return mInstance;
}
}
volatile关键字会禁止重排,在JDK1.5以上,volatile关键字修饰的变量保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。 禁止进行指令重排序。
写法五 静态内部类:
/**
* 静态内部类方式实际上是结合了饿汉式和懒汉式的优点的一种方式
* Created by chuck on 17/1/18.
*/
public class SingletonInner {
private SingletonInner() {
}
/**
* 在调用getInstance()方法时才会去初始化mInstance
* 实现了懒加载
*
* @return
*/
public static SingletonInner getInstance() {
return SingletonInnerHolder.mInstance;
}
/**
* 静态内部类
* 因为一个ClassLoader下同一个类只会加载一次,保证了并发时不会得到不同的对象
*/
public static class SingletonInnerHolder {
public static SingletonInner mInstance = new SingletonInner();
}
}
这种方法很简便,我们在单例类SingletonInner类中,实现了一个static的内部类SingletonInnerHolder,在内部类SingletonInnerHolder中去实例mInstance,保证我们在需要的时候再去实例化mInstance对象,又由于classLoader对同一个类,只加载一次,我们在加载SingletonInnerHolder类时保证了线程间安全,不管多少线程,得到的也是同一个类,保证了并发下是该方式是可用的,不会出现重排情况,因为下一个线程永远都只能等SingletonInner()实例化完成才能执行,不会拿到未实例化的mInstance,因为classLoader机制。
以上主要参考https://blog.csdn.net/chenkai19920410/article/details/54612505后自己做的一些总结,方便理解单例模式的各种写法,加深印象,更多详细内容请参考原作者。