单例模式是指整个程序的运行中只有一个实例,并提供一个全局访问点。
单例模式模式的三个基本要点:
程序启动之后就会创建,但是创建完了之后可能不会使用,从而浪费了系统资源
优点:没有任何锁,执行效率高
适用于单例模式较少的场景:
如果我们在程序启动后,一定会加载到类,那么用饿汉模式实现的单例简单又实用;
如果我们是写一些工具类,则优先考虑使用懒汉模式,可以避免提前被加载到内存中,占用系统资源。
public class Singleton {
// 1.创建私有的构造函数,为了防止其他类直接创建
private Singleton(){}
// 2.定义私有类变量(线程安全)
private static Singleton singleton = new Singleton();
// 3.提供公共的获取实例的方法
public static Singleton getInstance() {
return singleton;
}
}
针对饿汉方式浪费资源的问题,将第二步
private static Singleton singleton = new Singleton()
修改为
private static Singleton singleton = null;
再在公共的获取实例的方法内进行判断,是否第一次访问,这样当程序启动之后并不会实例化,等什么时候调用什么时候才会初始化,防止资源的浪费
最后代码为:
public class Singleton {
private Singleton(){}
private static Singleton singleton = null;
private static Singleton getInstance() {
if(singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
这样解决了:
因为类加载时就初始化,浪费内存的问题,但是同样也带来的新的问题: 线程不安全
线程不安全的代码演示:
public class ThreadDemo {
/**
* 1.懒汉方式 V1(线程不安全)演示:
*/
static class Singleton {
// 1.创建一个私有的构造函数
private Singleton(){}
// 2. 创建一个私有的类变量
private static Singleton singleton = null;
// 3. 提供统一的访问方法
public static Singleton getInstance() throws InterruptedException {
if (singleton == null) {
Thread.sleep(1000);
// 第一次访问
singleton = new Singleton();
}
return singleton;
}
}
private static Singleton s1 = null;
private static Singleton s2 = null;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
s1 = Singleton.getInstance();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
s2 = Singleton.getInstance();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();
t1.join();
t2.join();
System.out.println(s1 == s2);
}
}
运行结果可能为 false
线程 | t1 | t2 |
---|---|---|
第一次访问 | if (singleton == null) 括号内判断为 true | |
t2 获得时间片 | 还未实例化对象,t2 进入 if 判断也为 true |
线程 t1 和 t2实例化了两个对象,线程不安全
给 getInstance() 方法加锁:
public class Singleton {
private Singleton(){}
private static Singleton singleton = null;
// 给方法加锁
private static synchronized Singleton getInstance() {
if(singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
是解决了 v1 版本的第一次访问会出现线程不安全的问题
但是因为简单粗暴的给getInstance() 方法加锁,导致无论什么时候访问都要排队执行,大大降低了性能
使用双重校验:
在第一次判断后加锁,然后再进行一次判断。这样只有第一次访问的时候才会排队执行,减小了加锁对性能的影响
public class Singleton {
private Singleton(){}
private static Singleton singleton = null;
private static Singleton getInstance() {
// 双重校验锁
if(singleton == null) {
synchronized(Singleton.class) {
if(singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
看似已经解决了线程不安全的问题,细看还是可能发生线程不安全的问题
让我们看看:
singleton = new Singleton();
发现这里是非原子的,执行步骤:
若发生指令重排序后的执行顺序为: 1 -> 3 -> 2 ,在多线程运行时:
线程 | 线程1 | 线程2 |
---|---|---|
线程1 第一次访问 | if (singleton == null) 括号内判断为 true | |
开辟了内存空间 | ||
将变量指向了内存空间 | ||
线程2 获得时间片 | 暂停执行 | 线程2 获得了时间片 |
if (singleton == null) false | ||
return singleton 返回了空对象 |
线程1 singleton 已经指向了之前开辟的内存,但是因为指令重排序的原因实例没有进行初始化,线程2就获得了时间片,并且直接进行了返回
在上述代码的基础上使用 volatile 修饰私有类变量
volatile关键字作用:
// 使用 volatile 修饰
private static volatile Singleton singleton = null;
最终代码:
static class Singleton {
// 1.创建一个私有的构造函数
private Singleton(){}
// 2. 创建一个私有的类变量
private static volatile Singleton singleton = null;
// 3. 提供统一的访问方法
public static Singleton getInstance() throws InterruptedException {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
// 第一次访问
singleton = new Singleton();
}
}
}
return singleton;
}
}