有一些类,在内存中没有必要存在多个对象。这时候就出现了单例模式。
1. 饿汉式
使用
static
保证了线程安全,在类加载到内存的时候,进行实例化。
/**
* 饿汉式
* 类加载到内存后,就实例化一个单例,JVM保证线程安全
* 简单实用,推荐使用!
* 唯一缺点:不管用到与否,类装载时就完成实例化
* Class.forName("")
* (话说你不用的,你装载它干啥)
*/
public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {};
public static Mgr01 getInstance() {
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2);
}
}
2. 懒汉式
有人说上面的饿汉式,我都还没有用,你就给我创建了一个对象,能不能在我用的时候再创建对象?于是又有了懒汉式。在调用
getInstance
方法的时候,才去创建对象,而且创建之前先判断是不是为空。
单线程环境下,这段代码确实没有问题。但是多线程情况下,就会有问题。看代码中的注释(很好理解的)。
/**
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
*/
public class Mgr03 {
private static Mgr03 INSTANCE;
private Mgr03() {
}
public static Mgr03 getInstance() {
if (INSTANCE == null) { // 一个线程过来了,判断了,INSTANCE 是null。这时候又有一个线程过来了,
// 也判断了INSTANCE 是null。然后第一个线程继续执行,创建了一个对象。接着第二个线程继续开始执行,
//也会创建一个新的对象(第二个线程已经执行过判断INSTANCE 是不是null的操作)。这时候就不能保证单例了。
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr03();
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->
System.out.println(Mgr03.getInstance().hashCode())
).start();
}
}
}
3. 如何解决懒汉式中存在的线程安全问题?
-
getInstance
方法上加个锁不就行了
确实能够达到目的,但是又有人说了,整个函数加锁,有效率问题。能不能将锁细化?于是又有了第
种方案。2
/**
* lazy loading
* 也称懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
* 可以通过synchronized解决,但也带来效率下降
*/
public class Mgr04 {
private static Mgr04 INSTANCE;
private Mgr04() {
}
public static synchronized Mgr04 getInstance() {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr04();
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr04.getInstance().hashCode());
}).start();
}
}
}
- 加锁之前,先判断
是不是instance
,是null
然后才加锁。null
乍一看,好像挺好的,没啥问题。但是这种写法也是有线程安全问题的(参考方法中的注释)。
/**
* lazy loading
*/
public class Mgr05 {
private static Mgr05 INSTANCE;
private Mgr05() {
}
public static Mgr05 getInstance() {
if (INSTANCE == null) { // 一个线程是来了,判断INSTANCE 是null,这时候第二个线程来了,
//也判断了INSTANCE 是null。线程二获得了锁,然后创建了对象,执行完释放了锁;第二个线程获得锁,
//继续执行,也会创建一个新的对象(因为它没有再次判断INSTANCE 是不是null)。这不就有两个对象了吗?
//妄图通过减小同步代码块的方式提高效率,然后不可行
synchronized (Mgr05.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr05();
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr05.getInstance().hashCode());
}).start();
}
}
}
- 加锁之后,再判断
是不是INSTANCE
上面的代码是由于加锁之后,没有判断null
是不是INSTANCE
导致的。那简单,加锁之后再判断一下null
是不是INSTANCE
null
不就解决了吗?
这就引出了单例模式
的写法。也就是Double Check Lock
方法里面,加锁之前和之后分别检查下getInstance
是不是INSTANCE
null
。
没问题了吗?注意代码中的
volatile
是注释掉的。
这就是一个面试题了。单例模式中的
写法,实例变量是否需要加DCL
以及为什么?volatile
/**
* lazy loading
*/
public class Mgr06 {
private static /*volatile*/ Mgr06 INSTANCE; //JIT
private Mgr06() {
}
public static Mgr06 getInstance() {
if (INSTANCE == null) {
//双重检查
synchronized (Mgr06.class) {
if(INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr06.getInstance().hashCode());
}).start();
}
}
}
4. new
对象的操作中的指令重排序问题
new
要回答单例模式中的
DCL
写法,实例变量是否需要加
volatile
以及为什么?这个问题,首先得要清楚
new
一个对象的操作其实是分为三条指令的。
分别是申请内存赋默认值、初始化、将实例变量指向对象。
这三条指令是可能发生指令重排序的,初始化操作和将将实例变量指向对象的操作的顺序会互换。这时候就会出现线程安全的问题。
第一个线程来了,判断
INSTANCE
是
null
,然后加锁,进入了
new
对象的过程,如果发生指令重排序(先对实例变量进行了赋值操作),在
new
到一半的时候,
INSTANCE
已经被赋值,这时候,第二个线程来了,判断
INSTANCE
不是
null
,会直接返回还没有初始化完成的
INSTANCE
对象,就会出现问题。
加
volatile
禁止指令重排序。就可以解决上述问题。
5. 推荐使用饿汉式
其实工作中使用饿汉式就够了,没必要搞得这么复杂。所谓面试造火箭,工作拧螺丝。。。。内卷的厉害。