目录
<3> 双检锁/双重校验锁(DCL: double-checked locking)
一、单例模式
单例模式通常会涉及到单一的一个类,这个类负责创建自己的对象,并同时确定这个类仅仅只有一个对象被创建。显然,单例模式也属于创建型模式,同样也提供了创建对象的最佳方式。单例模式是 Java 最简单的设计模式其中之一,这种模式下的类,会提供一种访问这个唯一对象的方式,可以直接访问,并不需要再次实例化这个类。
我们必须保证在使用单例模式时,一个类有且仅有一个实例,并提供一个全局访问点来访问这个类。
二、测试代码
代码架构图
SingletonObj类
public class SingletonObj {
/**
* SingletonObj类
* 1. 在该类内部直接创建对象变量
* 2. 将构造方法私有化(关键代码),防止外部再次实例化这个类实例化,导致这个类产生多个实例
* 3. getSingletonObj() 方法提供了唯一的方式来获取 SingletonObj 类的唯一的对象
*/
private static final SingletonObj SINGLETON_OBJ = new SingletonObj();
private SingletonObj() {}
public static SingletonObj getSingletonObj() {
System.out.println("SingletonObj类中的SINGLETON_OBJ: " + SINGLETON_OBJ.hashCode());
return SINGLETON_OBJ;
}
public void sayHello() {
System.out.println("这里是SingletonObj的单例模式~");
}
}
Test类
public class Test {
public static void main(String[] args) {
SingletonObj singletonObj = SingletonObj.getSingletonObj();
System.out.println("Test类中的singletonObj: " + singletonObj.hashCode());
singletonObj.sayHello();
}
}
测试结果
SingletonObj类中的SINGLETON_OBJ: 1163157884
Test类中的singletonObj: 1163157884
这里是SingletonObj的单例模式~
从结果中,我们可以看到,单例模式下类的对象变量的 hasCode 的值是相同的,也就是说明,这两个对象变量指向的是同一个对象,也就是说只存在唯一一个对象。
我们了解完单例模式的的基本思想后,接下来,我们来看看单例模式有哪些实现方式?
三、单例模式的实现方式
<1> 懒汉模式
1) 线程不安全
① 是否实现 Lazy loading:是 ② 多线程是否安全:否
这种创建单例的方式不支持多线程,因为没有加锁 synchronized,所以严格来说不算是单例模式。
测试代码
SingletonObj类
public class SingletonObj {
private static SingletonObj SINGLETON_OBJ;
private SingletonObj(){}
public static SingletonObj getSingletonObj(){
if(SINGLETON_OBJ == null){
SINGLETON_OBJ = new SingletonObj();
}
return SINGLETON_OBJ;
}
}
Test类
public class Test {
public static void main(String[] args) {
Runnable r1 = () -> System.out.println("这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
Runnable r2 = () -> System.out.println("这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
new Thread(r1).start();
new Thread(r2).start();
}
}
测试结果
这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: 1037172763
这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: 224049094
我们可以看到,在不同的线程下,SingletonObj 类的对象并不是同一个,在两个线程中分别创建了两个对象吗,所以这种单例模式的实现,在多线程的情况下是不安全的。
2) 线程安全
① 是否实现 Lazy loading:是 ② 多线程是否安全:是
这种方式必须加锁,才可以在多线程中使用,但是效率低下,因为加锁 synchronized 会影响效率,这种方式只有在调用时才会被实例化,避免了内存的浪费。
测试代码
SingletonObj类
public class SingletonObj {
private static SingletonObj SINGLETON_OBJ;
private SingletonObj() {}
public static synchronized SingletonObj getSingletonObj() {
if(SINGLETON_OBJ == null) {
SINGLETON_OBJ = new SingletonObj();
}
return SINGLETON_OBJ;
}
}
Test类
public class Test {
public static void main(String[] args) {
Runnable r1 = () -> System.out.println("这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
Runnable r2 = () -> System.out.println("这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
new Thread(r1).start();
new Thread(r2).start();
}
}
测试结果
这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: 1037172763
这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: 1037172763
我们可以看到,在进行加锁后,不同线程中的 SingletonObj 的对象是中唯一,并不会发生改变,所以这种方式在多线程下是安全的。
<2> 饿汉模式
① 是否实现 Lazy loading:否 ② 多线程是否安全:是
这种方式来实现单例比较常用,但是很容产生垃圾对象,基于 classloader 机制避免了多线程的同步问题,相对于懒汉模式的线程安全相比,没有了枷锁,所以在效率上会提高。这种创建方式,只要 SingletonObj 类被装载,那么就会被实例化,没有 Lazy loading,会浪费内存。
测试代码
SingletonObj类
public class SingletonObj {
private static final SingletonObj SINGLETON_OBJ = new SingletonObj();
private SingletonObj() {}
public static SingletonObj getSingletonObj() {
return SINGLETON_OBJ;
}
}
Test类
public class Test {
public static void main(String[] args) {
Runnable r1 = () -> System.out.println("这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
Runnable r2 = () -> System.out.println("这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
new Thread(r1).start();
new Thread(r2).start();
}
}
测试结果
这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: 1037172763
这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: 1037172763
<3> 双检锁/双重校验锁(DCL: double-checked locking)
① 是否实现 Lazy loading:是 ② 多线程是否安全:是
这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
测试代码
SingletonObj类
public class SingletonObj {
private volatile static SingletonObj SINGLETON_OBJ;
private SingletonObj() {}
public static SingletonObj getSingletonObj() {
if(SINGLETON_OBJ == null) {
synchronized (SingletonObj.class) {
if(SINGLETON_OBJ == null) {
SINGLETON_OBJ = new SingletonObj();
}
}
}
return SINGLETON_OBJ;
}
}
Test类
public class Test {
public static void main(String[] args) throws Exception {
Runnable r1 = () -> System.out.println("这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
Runnable r2 = () -> System.out.println("这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
new Thread(r1).start();
new Thread(r2).start();
}
}
测试结果
这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: 478974226
这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: 478974226
<4> 登记式/静态内部类
① 是否实现 Lazy loading:是 ② 多线程是否安全:是
这种方式可以达到双重校验锁一样的效果,但实现更加简单,对静态域使用的是延迟初始化,而对于双重检验锁来说,可以在实例域使用延迟初始化。
与饿汉式相同,这种方式同样利用了 classloader 机制来保证初始化实例只有一个线程,与饿汉式不同的是,饿汉式只要 SingletonObj 类被装载,那么就会被实例化,而这种方式 SingletonObj 类被装载后,不一定会被实例化。因为 InnerClass 类并没有被主动的调用,只有调用 getSingletonObj( ) 方法时,才会显式的装载 SingletonObj 类,从而实例化 SingletonObj 类。
测试代码
SingletonObj类
public class SingletonObj {
private static class InnerClass {
private static final SingletonObj SINGLETON_OBJ = new SingletonObj();
}
private SingletonObj() {}
public static SingletonObj getSingletonObj() {
return InnerClass.SINGLETON_OBJ;
}
}
Test类
public class Test {
public static void main(String[] args) {
Runnable r1 = () -> System.out.println("这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
Runnable r2 = () -> System.out.println("这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: " + SingletonObj.getSingletonObj().hashCode());
new Thread(r1).start();
new Thread(r2).start();
}
}
测试结果
这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: 478974226
这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: 478974226
<5> 枚举
① 是否实现 Lazy loading:是 ② 多线程是否安全:是
当前利用枚举来实现单例模式还没有被广泛接纳使用,但是这是实现单例模式的最佳方法,它更简洁,自动支持序列化机制,绝对防止多次实例化。
测试代码
SingletonObj类
public enum SingletonObj {
SINGLETON_OBJ;
public void sayHello() {
System.out.println("这里是枚举类类, SINGLETON_OBJ: " + SINGLETON_OBJ.hashCode());
}
}
Test类
public class Test {
public static void main(String[] args) {
Runnable r1 = () -> System.out.println("这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: " + SingletonObj.SINGLETON_OBJ.hashCode());
Runnable r2 = () -> System.out.println("这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: " + SingletonObj.SINGLETON_OBJ.hashCode());
new Thread(r1).start();
new Thread(r2).start();
SingletonObj.SINGLETON_OBJ.sayHello();
}
}
测试结果
这里是枚举类类, SINGLETON_OBJ: 478974226
这里是 r1 线程, 线程 r1 中的 SingletonObj 类的对象是: 478974226
这里是 r2 线程, 线程 r2 中的 SingletonObj 类的对象是: 478974226
总结
1. 在使用单例模式时,声明的对象,构造函数都需要私有化,防止外界的访问。
2. 懒汉模式与饿汉模式的本质区别在于,懒汉模式在方法外只是声明对象,只有在调用方法时才会进行 new 对象,而饿汉模式则是直接声明创建对象。
3. 不建议使用懒汉模式,建议使用饿汉模式来实现单例模式。
4. 需要明确做到 Lazy loading时,可以使用登记式/静态内部类。
5. 如果涉及反序列化创建对象时,可以使用枚举。
6. 以上情况都不符合,可以考虑双重校验锁。