导航
首先通过懒汉式的单例模式简单代码实现作为开头,发现有线程安全问题,并且在此懒汉模式代码上进行改进,衍生出同步懒汉设计模式,双重检查懒汉设计模式。另外还有静态内部类方式实现单例,它是一种基于类初始化的延迟加载解决方案。
与懒汉式相对应的是饿汉式单例模式,其在类加载时就进行初始化实例,所以并不存在懒汉式单例模式存在的线程同步安全问题。
除以上探讨的单例模式实现外,还列举了三种实现的单例模式的方法:枚举类,容器,ThreadLocal。
单例模式介绍——摘要
定义: 保证一个类仅有一个实例,并提供一个全局访问点
类型:创建型
适用场景:想确保任何情况下只有一个实例
优点:
- 在内存中只有一个实例,减少内存开销
- 避免资源的多重占用
- 设置全局访问点,严格控制访问
缺点:没有接口,扩展困难
重点关注几点:
- 私有构造器
- 线程安全
- 延迟加载
后期更新:
- 序列化,反射相关安全性
懒汉式
上代码:
/**
* 懒汉。顾名思义,等我们用到的时候再实例化
*/
public class LazySingleton {
private static LazySingleton instance = null; //初始化,为null
//私有构造器:外部不允许直接通过new运算获取对象实例
private LazySingleton(){
}
//静态方法,外面方面直接通过静态方法来获取实例,不需先实例化类
public static LazySingleton getInstance(){
if(singleton == null){
singleton = new LazySingleton();
}
return singleton;
}
}
上面代码很容易理解,外部只能通过唯一的访问点LazySingleton.getInstance()
来获取对象实例。
但是对于单线程来说,这样写没有问题。到了多线程我们再分析getInstance
代码块,很容易发现问题。我们假设现在有两个线程同时来获取实例:
/**
* 测试用例
* 同时开两个线程,对最简单的懒汉单例模式进行测试
*/
public class LazyTest {
//主线程
public static void main(String[] args) {
//第一个线程
new Thread( () ->{
LazySingleton singleton = LazySingleton.getInstance();
System.out.println( "Current Singleton :" + singleton );
} ).start();
//第二个线程
new Thread( () -> {
LazySingleton singleton = LazySingleton.getInstance();
System.out.println( "Current Singleton :" + singleton );
} ).start();
}
}
现在分析会出现什么情况,最简单的当然是第一个线程开始运行,知道它运行结束,再轮到第二个线程开始运行。然而我们查看两个线程的分别调用getInstance
的执行图:
线程1执行①代码之后,正要执行singleton = new LazySingleton() 的时候,由于CPU线程调度,使线程2开始执行。线程2一路执行下去。并且返回Singleton实例对象。而后,线程1继续执行,直到结束。
很清晰的知道,LazySingleton已经被实例化两次,违反了Singleton的原则。
有一系列的对最基本的懒汉式改进的方法。
懒汉式——同步(synchronized)
最容易改进的方法,只需添加synchronized关键字即可。
// 添加关键字同步synchronized,加锁
public synchronized static LazySingleton getInstance(){
if(singleton == null){
singleton = new LazySingleton();
}
return singleton;
}
当添加此关键字的时候,我们再看到上面两个线程的getInstance
执行图 。 当①执行之后,开始试图进入运行线程2的代码。但是此时该代码是加锁的,线程2会被 阻塞 ,待线程1运行结束之后,线程2方可执行。如此就 避免了if判断的线程错误 。最后只能获得一个唯一的实例。
但是:
- 同步锁的加锁解锁较为消耗资源。
- synchronized 关键字修饰static函数的时候,其实相当于synchronized整个类,范围较大,不利于控制。
懒汉式——双重检查(Double Check)
上代码:
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance = null; //初始化,为null
//私有构造器:外部不允许直接通过new运算获取对象实例
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
if(instance == null){
synchronized (LazyDoubleCheckSingleton.class){
if(instance == null) {
/****2, 3可以重排序。
*****使用volatile关键字禁止重排序
*****/
// 1. 为对象分配内存
// 2. 初始化对象
// 3. 设置instance指向刚分配的内存地址
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
}
synchronized关键字
从上面的代码看到,所谓double check就是进行双重判断。相比于直接在static方法上用synchronized,在局部代码块用synchronized修饰,配合double check使用在性能上更胜一筹。然而如此一来却有一个坑。
而volatile关键字修饰就是为了填补这个坑的。
volatile关键字
注意到用于修饰instance 的volatile
关键字。这里集中看到这行代码:
instance = new LazyDoubleCheckSingleton();
。
这行代码并不是原子操作。所谓原子操作,可以简单理解为此操作一步执行,不可拆分。 具体到这个例子中,
instance = new LazyDoubleCheckSingleton();
的执行步骤如下:
- 为对象分配内存
- 初始化对象
- 设置instance指向刚分配的内存地址
- 外部可以开始对instance进行访问以及其它操作
而java编译器在编译时有个指令重排序的概念。在这里的意思就是2,3执行步骤可能会交换(但并不影响4,也就是说并不影响最终的结果),这样做的好处是根据具体情况来调整执行顺序,提高执行效率。
那么这就会导致隐藏的程序bug。现在假定线程1的执行顺序如下
- 为对象分配内存
- 设置instance指向刚分配的内存地址
- 初始化对象
- 外部可以开始对instance进行访问以及其它操作
现在假定当上面的步骤2执行完毕。CPU线程调度,轮到线程2开始执行
getInstance()
代码。执行第一步:if(instance == null){/*...*/}
,很显然,instace已经指向了分配的内存地址,所以instance != null
。所以线程2的执行结果是直接返回instace对象。线程2中的应用层代码可以直接获取到instace实例并且开始使用,但是值得注意的是我们的instance并没有执行过初始化对象这一步骤,而我们知道,一个对象没有完成对象初始化就开始使用,在某些情况下是非常严重的错误,程序bug。
而我们的代码private volatile static LazyDoubleCheckSingleton instance = null;
中的volatile就是为了禁止指令重排序的。
当然我们 还有一种解决方案: 就是上面的2,3指令执行步骤在别的线程看来是不可见的,也就是说,别的线程是把2,3这两个步骤看成一个整体,外部不能介入其中执行的。
这种解决方案就是下面要介绍的基于***静态内部类***的解决方案。
静态内部类方式(基于类初始化的延迟加载解决方案)
直接上代码,代码很容易。重要的是理解内在JVM机制。
public class OuterSingleton {
private OuterSingleton(){
}
public static OuterSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private static class InnerClass{
private static OuterSingleton staticInnerClassSingleton = new OuterSingleton();
}
}
我们要获取的单例,为OuterSingleton类的单例。而静态内部类作为创建这个实现单例的内在机制。
上面代码很容易看懂,值得注意的是两个private:一个是构造器的private(之前的几个单例模式也说过,外部只能通过getInstance
获取单例对象,防止new一个对象);一个是静态内部类的private,静态内部类只是实现单例的内在机制,不应暴露给外部。
为什么静态内部类能够代替volatile关键字
,解决指令重排序的问题?
JVM在类的初始化阶段( Class被加载之后~~线程使用之前 )执行类的初始化(也就是我们前面所说的 1,2,3 或者 1,3,2阶段)。类的初始化期间,类会去获取一个锁,此锁用于同步多个线程对于一个类的初始化。
其中类被初始化会发生在以下几种情况:什么时候类会被初始化
饿汉式
从名字可以看出,饿汉式与懒汉式相对应。
饿汉式单例模式实现较为简单,在类加载时就完成了初始化操作,如此避免了线程同步问题。当然缺点也是因为在类加载时就完成了初始化,没有了懒汉式的延迟加载效果。
public class HungerySingleton {
public static final HungerySingleton instance;
static{
instance = new HungerySingleton();
}
private HungerySingleton(){
}
public static HungerySingleton getInstance(){
return instance;
}
}
当然,也能使用如下方法:
public class HungerySingleton {
public static final HungerySingleton instance = new HungerySingleton();
private HungerySingleton(){
}
public static HungerySingleton getInstance(){
return instance;
}
}
枚举类(Enum)
public enum EnumSingleton {
INSTANCE{
protected void methodTest(){
System.out.println("Method test.");
}
};
protected abstract void methodTest();
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}
容器式
- 通过容器存储 <key,Object>,也就是一个key对应于一个类的单个实例。
- 优点:通过key-value容器存储,当单例过多时,方便统一管理,节省资源。
- 缺点:线程不安全(下面的代码实现)
import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
import java.util.Map;
public class ContainerSingleton {
private ContainerSingleton(){
}
//or `new HashMap<String, Object>();`
private static Map<String,Object> singletonMap = new HashMap<>();
public static void putInstance(String key,Object instance){
if(StringUtils.isNotBlank(key) && instance != null){
if(!singletonMap.containsKey(key)){
singletonMap.put(key,instance);
}
}
}
public static Object getInstance(String key){
return singletonMap.get(key);
}
}
ThreadLocal方式
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> instance =
new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton(){
}
public static ThreadLocalSingleton getInstance(){
return instance.get();
}
}
这里所说的单例是相对于线程来说的。也就是说在 同一个线程内,都共享一个单例的内存空间;而对不同线程来说,获取到的是各自线程的单例。 如图: