单例模式与多线程的结合,使用过程中如果考虑的不全面,会造成一些意想不到的后果,本文将介绍如何正确在多线程中使用单例模式。
单例模式:是一种创建型设计模式,保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
优点:
1.单例模式在内存中只有一个实例,减少内存开支。
2.减少系统的性能开销(如读取配置)。
3.避免对资源的多重占用(例如对写入文件操作,避免了对同一个资源文件的同时写操作)。
缺点:
1.违反了单一职责原则。
2.不易扩展。
3.在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
立即加载就是使用类的时候已经将对象创建完毕,也称为
饿汉模式
。饿汉模式是线程安全的。
public class Singleton {
private static Singleton singleton = new Singleton();
public Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
public class Run {
public static void main(String[] args) {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> System.out.println(Singleton.getInstance().hashCode()));
}
for (int i = 0; i < 10; i++) {
threads[i].start();
}
}
}
1492429476
1492429476
1492429476
1492429476
1492429476
1492429476
1492429476
1492429476
1492429476
1492429476
查看控制台打印信息,获取Singleton的hashCode是同一个值,说明对象是同一个。
public class Singleton {
private static Singleton singleton;
public Singleton() {
}
public static Singleton getInstance() {
if (null == singleton) {
// 模拟出现初始化耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
return singleton;
}
}
public class Run {
public static void main(String[] args) {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> System.out.println(Singleton.getInstance().hashCode()));
}
for (int i = 0; i < 10; i++) {
threads[i].start();
}
}
}
1099352210
821275946
810944986
1492429476
1136524994
1562877374
2023620726
1512870095
489548175
1673776013
在多线程环境中,以上的懒汉模式的示例代码就是完全错误的,因为它不能保持单例。那么这个问题如何处理呢 ?
修改我们的Singleton类代码如下:
public class Singleton {
private static Singleton singleton;
public Singleton() {
}
public synchronized static Singleton getInstance() {
if (null == singleton) {
// 模拟出现初始化耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
return singleton;
}
}
885690336
885690336
885690336
885690336
885690336
885690336
885690336
885690336
885690336
885690336
使用synchronized进行同步,问题虽然是解决了,其他线程必须要等待持有该对象锁的线程释放锁后才可以继续执行,效率低下。
使用synchronized修改方法后,线程必须持有对象锁后才能执行getInstance(),效率低下,下面我们使用同步代码块来继续优化。
public class Singleton {
private static Singleton singleton;
public Singleton() {
}
public static Singleton getInstance() {
if (null == singleton) {
// 模拟出现初始化耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}
1512870095
1769823406
489548175
1492429476
1099352210
1562877374
821275946
489548175
810944986
针对重要的代码进行单独同步,其他前置初始化操作单独处理,相比之下效率得到提升,但是在多线程场景下,无法保证单实例。
下面使用DCL(double checked locking)机制,来实现多线程环境中的正确延迟加载单例的写法。
public class Singleton {
private volatile static Singleton singleton;
public Singleton() {
}
public static Singleton getInstance() {
// 第一次检查
if (null == singleton) {
// 模拟出现初始化耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Singleton.class) {
// 第二次检查
if (null == singleton) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
885690336
885690336
885690336
885690336
885690336
885690336
885690336
885690336
885690336
DCL可以解决多线程单例模式(懒汉模式)的非线程安全问题,使用静态内置类实现单例模式也是线程安全的。
public class Singleton {
public static class StaticSingleton {
private static Singleton singleton = new Singleton();
}
public Singleton() {
}
public static Singleton getInstance() {
return StaticSingleton.singleton;
}
}