1、什么是单例模式
引用自单例模式–维基百科
单例对象的类必须保证只有一个实例存在
单例可以分为两大类:
懒汉式:指全局的单例实例在第一次被使用时构建。
饿汉式:指全局的单例实例在类装载时构建。
–常用的应该是懒汉式,什么时候需要什么时候去创建,才能造成资源的不浪费。
2、懒汉式单例
2.1、教科书版本
/**
* 懒汉式单例--1.0版本
*/
public class SingletonV1 {
private static SingletonV1 singletonV1 = null;
//构造器私有,防止被外部的类调用
private SingletonV1() {
}
private static SingletonV1 getInstance() {
//每次获取单例对象时进行判断,如果为空,则重新new一个出来。
if (singletonV1 == null) {
return new SingletonV1();
}
return singletonV1;
}
}
存在问题
当多线程工作时,两个线程同时运行到 if (singletonV1 == null) ,两个线程都判断为null,便会创建两个对象,就不是单例模式了。
2.2、synchronized(同步锁)版本
多线程的情况下会出现上述情况,那么加个同步锁。
/**
* 懒汉式单例--2.0版本
*/
public class SingletonV2 {
private static SingletonV2 singletonV2 = null;
private SingletonV2() {
}
//加上synchronized 形成同步锁
private synchronized static SingletonV2 getInstance() {
if (singletonV2 == null) {
return new SingletonV2();
}
return singletonV2;
}
}
解决分析
加上synchronized 形成同步锁。当两个线程同时运行到getInstance()
这个方法时,会有一个线程A获得同步锁,继续执行,另一个线程B则需要等待,当线程A完成null的判断,对象的创建,对象的返回之后,线程B才会执行,所以就避免了多线程创建多个实例的情况。
存在问题
虽然给getInstance()
方法加锁,避免了多线程多实例的问题,但是会强制让其他线程进行等待,对程序的运行效率造成负担。
2.3、双重检查(Double-Check)版本
上述
synchronized版本
代码中的代码,其实没必要将整个方法进行同步锁。只需要将new这个操作进行同步处理就行了。创建对象的动作只有一次,后面的动作全是读取那个成员变量,这样做会严重影响后续的性能。
在线程同步前还得加一个(singleton== null)的条件判断,如果对象已经创建了,那么就不需要线程的同步了
/**
* 懒汉式单例--3.0版本
*/
public class SingletonV3 {
private static SingletonV3 singletonV3 = null;
private SingletonV3() {
}
private static SingletonV3 getInstance() {
if (singletonV3 == null) {
synchronized (SingletonV3.class) {
if (singletonV3 == null) {
singletonV3 = new SingletonV3();
}
}
}
return singletonV3;
}
}
这里进行了两次
if (singletonV3 == null)
的判断
- 第一次
if (singletonV3 == null)
,为了解决同步锁的效率问题,只有singletonV3
为空才会进行synchronized
代码块。- 第二次
if (singletonV3 == null)
,为了防止出现多实例的情况。
以上代码还是有些小问题
先弄清楚两个东西原子操作
,指令重排
。
3、原子操作
原子操作
就是原子操作指的是不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch(切换到另一个线程)。
一个简单的原子操作
m = 6;//这是一个原子操作
假设m原先值为0,对于这个操作,要么成功m的值变为6,要么没执行,m的值还是0,而不会出现其他值,而声明并赋值不是一个原子操作
int m = 6;//这不是一个原子操作
对于这个,至少两个操作:
- 声明一个变量m
- 给m赋值为6
这样就会有一个
中间状态
:
变量已经被声明,但是还没有赋值的状态。
在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用m,就可能导致不稳定结果出现。
4、指令重排
指令重排
意思是计算机在不影响结果的情况下,为了优化,会做一些优化,对一些语句的执行顺序进行调整。int a ; // 语句1 a = 8 ; // 语句2 int b = 9 ; // 语句3 int c = a + b ; // 语句4
正常来说,执行顺序是自上而下。由于
指令重排
的原因吗,因为不影响结果,执行顺序可能成为3124或者1324。由于语句3和4没有原子性的问题,语句3和语句4也可能会拆分成原子操作,再重排。–也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。
主要在于singletonV3= new singletonV3()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
1.给 singletonV3分配内存
2.调用 SingletonV3 的构造函数来初始化成员变量,形成实例
3.将singletonV3对象指向分配的内存空间(执行完这步 singletonV3才是非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 singletonV3,已经是非 null 了(但却没有初始化),所以线程二会直接返回 singletonV3,然后使用,然后顺理成章地报错。
5、volatile版本
给instance的声明加上volatile关键字
/**
* 单例模式4.0版本
*/
public class SingletonV4 {
//volatile仅在jdk1.5后使用
private static volatile SingletonV4 singletonV4;
private SingletonV4() {
}
private static SingletonV4 getInstance() {
if (singletonV4 == null) {
synchronized (SingletonV4.class) {
if (singletonV4 == null) {
singletonV4 = new SingletonV4();
}
}
}
return singletonV4;
}
}
volatile
关键字的作用为禁止指令重排,把singletonV4
声明为volatile
,对它的读写就有一层内存屏障,这样在它赋值完成之前,就不会对它执行读的操作。
内存屏障
内存屏障(memory barrier)和volatile什么关系?上面的虚拟机指令里面有提到,如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。这意味着如果你对一个volatile字段进行写操作,你必须知道:1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
volatile
阻止的不是*singletonV4 = new SingletonV4()*这个的指令重排,而是保证了在写操作完成之前,不会调用读的操作。
6、饿汉式单例
饿汉式
单例是指:指全局的单例实例在类装载时构建的实现方式。
由于类装载的过程是由类加载器(ClassLoader)来执行的,这个过程也是由JVM来保证同步的,所以这种方式先天就有一个优势——能够免疫许多由多线程引起的问题。
/**
* 饿汉式单例
*/
public class SingletonV5 {
private static final SingletonV5 INSTANCE = new SingletonV5();
private SingletonV5() {
}
private static SingletonV5 getInstance() {
return INSTANCE;
}
}
缺点
INSTANCE
的初始化是在类加载中进行,而类加载由ClassLoader
执行,对开发者而言很难把握初始化的时机:
1.可能初始化太早,造成资源的浪费;
2.如果初始化本身依赖于一些其他数据,那么很难保证需要的数据,在其初始化完成时是否准备好。
7、枚举实现单例方式
/**
* 枚举单例的实现
*/
public enum SingletonV6 {
INSTANCE;
public void FunTest() {
//...
}
}
作者对这个方法的看法
这种方法在功能上与公有域方法相近,但是比它更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是面对复杂的序列化或者反射攻击的时候。虽然这种方法还没广泛使用,但是单元素的枚举类型已经实现singleton的最佳方法
。