对于OOP语言而言,每new() 一个对象,就会有一个对象实例生成。但是很多时候需要在程序运行时全局使用同一个实例,避免生成多余的实例或者资源浪费。这类设计模式就是单例模式。
单例模式有如下要求:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
其中第二和第三点都是为了给第三点打补丁,如果不是由单例类本身创建和提供实例,那么就没办法在全局中确保使用同一个实例。单例模式通过设置私有的构造函数以防止外部创建对象,为此,需要设置一个对外接口getInstance()
方法提供对象。
饿汉式
public class SingleObject { // 1.饿汉模式,类加载时通过静态关键字就创建好对象private static SingleObject instance = new SingleObject(); private SingleObject() { } public static SingleObject getInstance() { return instance; } }
饿汉式单例模式认为对象总会使用,因此提前创建好对象。这种方式线程安全,但是在类加载时就初始化,浪费内存,容易产生垃圾对象。
测试可以发现得到的是同一对象:
public class Main { public static void main(String[] args) { for (int i = 0; i < 50; i++) { SingleObject instance = SingleObject.getInstance(); System.out.println(instance); } }
}
输出:
SingleObject.SingleObject@1b6d3586
SingleObject.SingleObject@1b6d3586
SingleObject.SingleObject@1b6d3586
SingleObject.SingleObject@1b6d3586
SingleObject.SingleObject@1b6d3586
……
懒汉式
public class SingleObject { //2. 懒汉模式 private static SingleObject instance; private SingleObject() { } public static SingleObject getInstance() { if (instance == null) { instance = new SingleObject(); } return instance; } }
和饿汉式单例模式不同,懒汉式单例模式只在外部第一次需要对象的时候才创建。这种方式也被称为懒加载(Lazyload),spring中的@Lazy
注解通过类似的方式实现类的懒加载。避免了饿汉式浪费内存的问题。但是在多线程并发的情况下,由于多个线程可能同时进入判断if (instance == null)
,可能会导致多个实例的创建。
并发情况下测试代码:
public class Main { public static void main(String[] args) throws InterruptedException { int threadCount = 10; CountDownLatch countDownLatch = new CountDownLatch(threadCount); Runnable runnable = new Runnable() { @Override public void run() { SingleObject instance = SingleObject.getInstance(); System.out.println(instance + ",线程" + Thread.currentThread().getName()); countDownLatch.countDown(); } }; for (int i = 0; i < threadCount; i++) { new Thread(runnable).start(); } }
}
运行结果:
SingleObject.SingleObject@65a9a79e,线程Thread-4
SingleObject.SingleObject@65a9a79e,线程Thread-5
SingleObject.SingleObject@65a9a79e,线程Thread-2
SingleObject.SingleObject@6517209b,线程Thread-9 //新实例
SingleObject.SingleObject@65a9a79e,线程Thread-7
SingleObject.SingleObject@65a9a79e,线程Thread-3
SingleObject.SingleObject@65a9a79e,线程Thread-8
SingleObject.SingleObject@65a9a79e,线程Thread-1
SingleObject.SingleObject@2f992f4f,线程Thread-0 //新实例
SingleObject.SingleObject@65a9a79e,线程Thread-6
Process finished with exit code 0
懒汉式+双重检查
线程安全的加锁方式
避免线程不安全的方式最常用的就是加锁,对于懒汉式的改进很简单,就是直接加锁。
public class SingleObject { //2. 懒汉模式 private static SingleObject instance; private SingleObject() { } public static SingleObject getInstance() { synchronized (SingleObject.class){ if (instance == null) { instance = new SingleObject(); } } return instance; }
}
双重检查
加锁很重要的一点就是性能差,会影响效率。为此给出双重检查版本。
public class SingleObject { //2. 懒汉模式 private static SingleObject instance; private SingleObject() { } public static SingleObject getInstance() { if (instance == null){ // 第一次检查,如果不为null,直接返回synchronized (SingleObject.class){ if (instance == null) { // 第二次检查instance = new SingleObject(); } } } return instance; }
}
结果
SingleObject.SingleObject@7a5f6ff8,线程Thread-5
SingleObject.SingleObject@7a5f6ff8,线程Thread-9
SingleObject.SingleObject@7a5f6ff8,线程Thread-7
SingleObject.SingleObject@7a5f6ff8,线程Thread-2
SingleObject.SingleObject@7a5f6ff8,线程Thread-3
SingleObject.SingleObject@7a5f6ff8,线程Thread-4
SingleObject.SingleObject@7a5f6ff8,线程Thread-8
SingleObject.SingleObject@7a5f6ff8,线程Thread-0
SingleObject.SingleObject@7a5f6ff8,线程Thread-1
SingleObject.SingleObject@7a5f6ff8,线程Thread-6Process finished with exit code 0
volatile关键字
private volatile static SingleObject instance;
然而以上的双重检查并不能完全保障完美实现单例模式,因为instance = new Singleton()
并非原子操作,包含给对象实例分配内存空间,调用构造方法初始化对象,将对象指向分配的内存空间三步。这三步可能由于提高执行效率而改变执行顺序。
a. memory = allocate() //分配内存 写操作
b. ctorInstanc(memory) //初始化对象 写操作
c. instance = memory //设置instance指向刚分配的地址 写操作
被volatile关键字修饰的变量被称为volatile变量。通过插入内存屏障禁止指令重排序,保证写操作前的代码不会重排到写操作之后,读操作后的代码不会重排到读操作之前。此时会按照a-b-c的顺序执行。
如果没有加上volatile关键字,上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行instance = new Singleton()
时,B线程进来执行到第一次if (instance == null)
。假设此时A执行的过程中发生了指令重排序,即先执行了a和c,没有执行b。那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接返回一个未初始化的对象。
参考
Java设计模式—单例模式的实现方式和使用场景-阿里云开发者社区
Java中Volatile关键字详解 - 郑斌blog - 博客园
Java volatile关键字最全总结:原理剖析与实例讲解(简单易懂)-CSDN博客