多线程安全单例模式
什么是单例模式?
在文章开始之前我们还是有必要介绍一下什么是单例模式。单例模式是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种模式方法。
从概念中体现出了单例的一些特点:
(1)、在任何情况下,单例类永远只有一个实例存在
(2)、单例需要有能力为整个系统提供这一唯一实例
1. 全局变量的缺点:
必须在程序一开始就创建好对象,如果程序在这次的执行过程中又一直没用到它,就非常耗费资源。
2. 经典的单例模式实现:
public class Singleton {
//用一个静态变量来记录Singleton类的唯一实例
//1、提供私有的属性 --> 存储对象的地址
private static Singleton uniqueInstance;
//2、私有地构造方法 --> 只能本类可以创建对象
private Singleton() {}
//注意这个方法也是静态的 -->获取属性
public static Singleton getInstance() {
if(uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
单例常被用来管理共享的资源,例如 数据库连接、线程池、缓存、注册表, Spring中大量用到单例模式。
单例模式确保一个类只有一个实例,并提供一个全局访问点。
这个模式的问题:在多线程时,并不能保证这个类只被实例化一次。
3. 处理多线程:
public class Singleton {
//用一个静态变量来记录Singleton类的唯一实例
private static Singleton uniqueInstance;
private Singleton() {}
//注意这个方法也是静态的 使用synchronized线程同步
public static synchronized Singleton getInstance() {
if(uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
通过增加synchronized关键字到**getInstance()**方法中,迫使每个线程在进入方法之前,要先等别的线程离开该方法。也就是说,不会有两个线程可以同时进入这个方法。
这种方法存在的问题:只有第一次执行此方法时,才真正需要同步。换句话说,一旦设置好uniqueInstance变量,就不再需要同步这个方法了。之后每次调用这个方法,同步都是一种浪费。
4.改善多线程
4.1 如果getInstance()的性能对应用程序不是很关键,就不用优化了
4.2 使用急切创建实例,而不用延迟实例化的做法
public class Singleton {
//实例化时就创建对象 单例模式中的饿汉式
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return uniqueInstance;
}
}
private static Singleton uniqueInstance = new Singleton();在静态初始化器(static initializer)中创建单例,这保证了线程安全。
利用这个做法,JVM在加载这个类时马上创建此唯一的单件实例。JVM保证任何线程访问uniqueInstance静态变量之前,一定先创建些实例。
4.3 用“双重检查加锁”,在getInstance()中减少使用同步
首先检查实例是否已经创建,如果尚未创建,才进行同步。这样一来,只有第一次会同步,这正是我们想要的。
public class Singleton {
//volatile其他线程可能访问一个没有初始化的对象
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if(uniqueInstance == null) { //(1)
//只有第一次才彻底执行这里的代码
synchronized() {
//再检查一次
if(uniqueInstance == null)
uniqueInstance = new Singleton();
}
}
return uniqueInstance;
}
}
在最开始如果有1、2、3个线程走到了(1)处,假设1进入了同步块,2、3等待。1实例化后,2进入同步块,发现uniqueInstance已经不为空,跳出同步块。接着3进入,又跳出同步块。
volatile关键字确保:当uniqueInstance变量被初始化成Singleton实例时,多个线程正确地uniqueInstance变量。如果性能是你关心的重点,那么这个做法可以帮你大大地减少getInstance()的时间耗费。
最后给一个完整的demo:
package com.gqz.thread;
/*
* 单例模式:懒汉式套路 在多线程环境下 对外存在一个对象
* 1、构造器私有化 --> 避免外部 new构造器
* 2、提供私有地属性 --> 存储对象的地址
* 3、提供公共的静态方法--->获取属性
*/
public class DoubleCheckedLocking {
//2、提供私有地属性
//直接new对象了是饿汉式 没有new的事懒汉式
//volatile其他线程可能访问一个没有初始化的对象
private static volatile DoubleCheckedLocking instance;
//1、构造器私有化
private DoubleCheckedLocking() {
}
//3、提供公共的静态方法-->获取属性
public static DoubleCheckedLocking getInstance() {
//再次检测
if (null != instance) { //避免不必要的同步,如果已经存在对象
return instance;
}
//防止多个线程 需要加上锁 synchronized
synchronized (DoubleCheckedLocking.class) {
if (null == instance) {
instance = new DoubleCheckedLocking();
/** //创建对象 可能存在指令重排
* 1、开辟空间 2、初始化对象信息 3、返回对象的地址给引用
*/
}
}
return instance;
}
public static void main(String[] args) {
Thread thread = new Thread(()->{
System.out.println(DoubleCheckedLocking.getInstance());
});
thread.start();
System.out.println(DoubleCheckedLocking.getInstance());
}
}
com.gqz.thread.DoubleCheckedLocking@816f27d
com.gqz.thread.DoubleCheckedLocking@816f27d
表明在多线程下,还是得到同一个单例对象。
![](https://pic.cnblogs.com/face/1351763/20200703092346.png)