什么是单例模式
是指应用程序全局只能创建唯一一个实例的构造模式。一般的做法是:私有化构造方法(只能由自己创建),提供一个只能创建唯一实例的方法给别人调用
单例模式的类型
1、饿汉模式:线程严格安全,但不属于懒加载
public class Singleton {
private static Singleton 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 final Singleton getInstance() {
return SingletonHolder.INSTANCE;
} }
饿汉枚举模式(多此一举,不如直接使用简单饿汉模式):线程严格安全,但不属于懒加载
public enum Singleton {
INSTANCE;
public void whateverMethod() {
} }
2、懒汉线程不安全模式:线程不安全,属于懒加载
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
} }
3、懒汉线程安全模式:线程不严格安全,属于懒加载
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
} }
4、懒汉volatile双重检验锁模式:线程严格安全,属于懒加载
public class Singleton {
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;
} }
注意:懒加载是指,当需要调用实例时再检查是否已有实例,没有实例则创建一个实例;非懒加载就是在应用实例创建之初,就创建并加载到内存里的情况
使用建议:一般在单线程情况下使用第2种方式,如果有多线程需求,一定不允许因为线程原因导致创建两个实例,可以考虑使用第 4 种双检锁方式,或者饿汉模式。
单例模式的原理
这里重点讲懒汉volatile双重检验锁模式,其它的比较简单就不细讲了
1、双重检验锁解决了什么问题
第一次判断是否为空,是在Synchronized同步代码块外,如果已经创建了singleton对象,就不用进入同步代码块,不用竞争锁,直接返回前面创建的实例,提高效率。
第二次判断是否为空,是在Synchronized同步代码块内,假若线程A通过了第一次判断,进入了同步代码块,但是还未执行,线程B就进来了(线程B获得CPU时间片),线程B也通过了第一次判断(线程A并未创建实例,所以B通过了第一次判断),准备进入同步代码块,假若这个时候不判断,就会存在这种情况:线程B创建了实例,此时恰好A也获得执行时间片,如果不加以判断,那么线程A也会创建一个实例,就会造成多实例的情况。
2、volatile解决了什么问题
volatile的作用有两个:
一是,所有线程都能及时获得共享内存的最新状态。
即被volatile修饰的变量,每次读取前必须先从主内存刷新最新的值。每次写入后必须立即同步回主内存当中。
二是,防止指令重排。
new 创建对象,并不是一个原子操作,它可以抽象为一下几条jvm指令
memory = allocate(); //1:分配对象的内存空间
initInstance(memory);//2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序,多线程情况下,导致可能出现误判为空的情况。
而被volatile修饰的变量,会遵循以下原子操作规则:
read、load、use动作必须连续出现。
assign、store、write动作必须连续出现。
因此,volatile修饰该单例能在更细的粒度保证数据在多线程的情况里的一致性,被免误判为空的情况。
ps:Java变量的读写,主要通过以下几种原子操作完成工作内存和主内存的交互
lock:作用于主内存,把变量标识为线程独占状态。
unlock:作用于主内存,解除独占状态。
read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中
单例模式的应用场景
对于一些全局通用的实例,但是创建消耗代价很大,容易被重复创建的场景都可使用,例如:图片加载实例,语音服务实例等。
网友评论