"风不来见我 我自去见风"

前面说到了一些关于多线程的话题,那么下面的这几篇文章围绕着多线程来说一些案例,那么第一篇便是多线程案例之单例模式。
什么是单例模式:
单例模式是一种设计模式,确保在整个应用程序中只有一个类的实例,并且提供一个全局访问点。
单例模式的作用:
- 控制实例数目:确保类只有一个实例,防止创建多个对象,节省资源。
- 全局访问:提供一个全局访问点,可以通过该点访问唯一的实例。
- 延迟初始化:单例对象可以在第一次使用时创建,有助于提高性能。
- 节约资源:对于需要频繁访问的对象或服务,使用单例模式可以减少内存消耗。
单例常见用例:
- 数据库连接池:通常我们只需一个连接池来管理多个数据库连接。
- 日志管理:只有一个日志管理实例,统一处理日志信息。
- 配置管理:配置类可以在整个应用中共享,同步配置状态。
饿汉版单例:
java
public class ThreadHungerSingle {
private static ThreadHungerSingle instance = new ThreadHungerSingle();
public static ThreadHungerSingle getInstance() {
return instance;
}
//单例模式最关键部分
private ThreadHungerSingle() {};
}
饿汉 的意思就是 "迫切",在类的加载的时候,就会被创建出这个单例的实例。
懒汉版单例:
java
public class ThreadLazySingle {
private static ThreadLazySingle instance = null;
public static ThreadLazySingle getInstance() {
if(null == instance) {
instance = new ThreadLazySingle();
}
return instance;
}
private ThreadLazySingle() {};
}
懒汉 的特点就是 "懒",在计算机中,"懒"是一个褒义词呢。只有在被用到时,才会真正创建出实例。
那么对于上述的两个版本,如果是在多线程环境下,调用getInstance时,谁会存在线程安全问题呢?
显然地是我们的懒汉版单例在多线程环境下会存在问题,饿汉版只是单纯的"读"操作,而懒汉版会有"写"操作,下面我们一起来看看这个图便知道了:

存在线程安全问题的话,此时我们就需要进行 加锁 操作来解决了,那么代码如下:
java
class ThreadLazySingle2 {
private static ThreadLazySingle2 instance = null;
private static final Object locker = new Object();
public static ThreadLazySingle2 getInstance() {
synchronized (locker) {
if(null == instance) {
instance = new ThreadLazySingle2();
}
}
return instance;
}
private ThreadLazySingle2() {};
}
加了锁之后确实解决了线程安全问题,但是加锁同样也可能带来阻塞。如果上述代码,已经new完对象了,那么后续的代码都是单纯的"读"操作,此时调用getInstance也是线程安全的,那么就没有必要加锁了。所以我们可以在前面加上同样的判断:
java
class ThreadLazySingle2 {
private static ThreadLazySingle2 instance = null;
private static final Object locker = new Object();
public static ThreadLazySingle2 getInstance() {
if(null == instance) {
synchronized (locker) {
if(null == instance) {
instance = new ThreadLazySingle2();
}
}
}
return instance;
}
private ThreadLazySingle2() {};
}
上述两个相同的判断条件看起来好像是有问题的,但是它们各有各的用处,第一个条件判断,判断的是是否需要进行加锁,第二个条件判断,判断的是是否需要进行实例对象。对于锁之间的阻塞,我们无法预测它们会阻塞多久,可能会是"沧海桑田"。
指令重排序:
上述代码中,还存在一个问题,可能会因为编译器的优化带来指令重排序的线程安全问题。什么是指令重排序呢:是计算机体系结构中的一种优化技术,主要指处理器或编译器在不改变程序最终结果的前提下,重新排列指令的执行顺序,以提高性能(如减少流水线停顿、提高缓存命中率等)。
instance = new ThreadLazySingle2();这里我分为三步进行举例说明:1.分配地址空间 2.执行构造方法 3.内存空间的地址,赋值给引用变量。我们一起来看一看,在多线程环境下,他会存在什么样的问题呢:

怎样解决指令重排序的问题呢,也是使用我们的关键字 volatile ,加上 volatile 关键字之后,编译器就会发现 instance 是 "易失" 的,便不会进行上述的优化,所以我们的最终版本是这样的:
java
class ThreadLazySingle1 {
private static volatile ThreadLazySingle1 instance = null;
private static final Object locker = new Object();
public static ThreadLazySingle1 getInstance() {
//此处的if判定是否需要加锁
if(null == instance) {
//由于只需要第一次创建出来实例就行,后续的代码,都是单纯的 读 操作,此时 getInstance不加锁也是 线程安全的,没有必要加锁
//当前写法,只要调用getInstance,都会触发加锁操作,此时虽然没有加锁操作了,但是也会因为加锁,产生阻塞,影响到性能
synchronized (locker) {
//此处的if判断是否要创建对象
if(null == instance) {
//这个代码可能会因为指令重排序,引起线程安全问题 所以上面应该加上 volatile 关键字
instance = new ThreadLazySingle1();
}
}
}
return instance;
}
private ThreadLazySingle1() {};
}
因此 Java 的volatile 有两个功能:1.保证内存可见性 2.禁止指令重排序
好了,我们本期的内容就讲到这里了,我们下期再见!!!