我们都知道单例模式的实现最常见的两种实现,一种是恶汉模式,一个懒汉模式,也可以用枚举,静态代码块,静态内部类等实现方式。
1.恶汉模式:
(1)恶汉模式在类被加载的时候就创建好了;优点是多线程情况下百分百只会创建一个;缺点就是如果对象创建了很久不用就浪费空间,如果这个对象还有很多属性值,那就不推荐这个方法
(2)恶汉模式总共有三个步骤:
①私有的,静态的变量。类在加载的时候就创建
②私有构造方法,不允许其他人再来创建对象
③提供一个获取实例的方法
(3)先看恶汉模式代码:
/**
* 恶汉模式,加上final不能被继承
*/
public final class HungrySingleton {
//1.私有,静态的变量。类在加载的时候就创建
private static HungrySingleton singleton = new HungrySingleton();
//2.私有构造方法,不允许其他人再来创建对象
private HungrySingleton(){}
/**
* 3.提供一个获取实例的方法
* @return
*/
public static HungrySingleton getInstance(){
return singleton;
}
}
(4)恶汉模式测试:
这里使用了计数器,计数器有阻塞代码代码运行的功能,直到计数归零后才会继续执行。
public class test2 {
public static void main(String[] args) {
//初始化计数器
int num = 100;
//100个线程公共的计数器
CountDownLatch countDownLatch = new CountDownLatch(num);
//创建100个线程,但是并不是立即run,而是等100个线程创建完毕,此时计数器归零,
//100个线程同时run
for (int i = 0; i < num; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
//阻塞后续代码,直到countDownLatch.countDown()执行了一百次
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印恶汉模式的单例对象
System.out.println(HungrySingleton.getInstance());
}
});
thread.start();
//计数器减1
countDownLatch.countDown();
}
}
}
(5)测试结果
可以发现所有的实例都是一样的,所以多线程下也不存在安全问题。
2.懒汉模式
懒汉模式是在用到这个实例的时候才创建单利对象,不会浪费空间,多线程下编码如果编码不合理会创建多个对象。
(1)懒汉式的同步方法实现。优点:编码简单。缺点:synchronized影响效率.
/**
* 懒汉模式+同步方法
*/
public class LazySingleton {
//私有
private static LazySingleton singleton = null;
//私有构造方法,不允许其他人再来创建对象
private LazySingleton(){}
/**
* 提供一个获取实例的方法
* @return
*/
public static synchronized LazySingleton getInstance(){
if(singleton == null ){
singleton = new LazySingleton();
}
return singleton;
}
}
(2)懒汉式的双重校验锁(DCL)
/**
* 懒汉模式+DCL
*/
public class LazySingleton {
//私有
private static LazySingleton singleton = null;
//私有构造方法,不允许其他人再来创建对象
private LazySingleton(){}
/**
* 提供一个获取实例的方法
* @return
*/
public static LazySingleton getInstance(){
if(singleton == null){ //第一次校验是为了性能
synchronized (LazySingleton.class){
if(singleton == null){ //第二次校验是为了安全
singleton = new LazySingleton();
}
}
}
return singleton;
}
}
其实这种方法也会存在问题,看这段代码:
singleton = new LazySingleton();
这段代码可以分为三个步骤:
①分配对象内存空
②初始化对象
③设置singleton 指向刚刚分配的内存地址
JVM在执行的时候可能会发生指令重排,也就是实际上的顺序可能会出现①③②。当线程1执行完①③步骤时,线程时间片没有了,CPU有上下文的切换,此时来了一个线程2也想拿对象,但是拿到了一个未初始化的对象,用这个对象去调用LazySingleton类中的方法就会出现空指针。
当然这种概率是实在是小,之前有测试过JVM指令重排的概率大概有万分之一,有时候是几十万分之一,外加线程刚好执行完①③就没有时间片的概率也很小。那么出现空指针的情况概率就更小了。
但是为了绝对的安全可以这样更改:
private volatile static LazySingleton singleton = null;
volatile 关键字的作用就是禁止JVM指令重排,就不会初选上述的情况。