Java 多线程进阶:线程安全与单例模式
本文深入讲解 Java 多线程核心知识。首先分析线程安全的五大成因,重点阐述 synchronized 的互斥性与可重入性,以及死锁产生的四大必要条件与场景。接着对比 wait 与 sleep 的区别,详解 volatile 解决内存可见性问题及指令重排序的原理。最后介绍三种单例模式(饿汉、懒汉、双重检查锁),强调 DCL 模式中 volatile 的关键作用,确保线程安全与性能平衡。

本文深入讲解 Java 多线程核心知识。首先分析线程安全的五大成因,重点阐述 synchronized 的互斥性与可重入性,以及死锁产生的四大必要条件与场景。接着对比 wait 与 sleep 的区别,详解 volatile 解决内存可见性问题及指令重排序的原理。最后介绍三种单例模式(饿汉、懒汉、双重检查锁),强调 DCL 模式中 volatile 的关键作用,确保线程安全与性能平衡。

①操作系统的随机调度 (根本原因) ②两个线程同时修改同一个变量 ③修改变量操作不是原子的 ④内存可见性问题 ⑤指令重排序问题
过程如下:线程 A 获得锁---线程 B 阻塞等待---线程 A 释放锁---线程 B 获得锁
package JavaEE;
public class Demo13 {
private static int sum;
// synchronized 的互斥性,线程 1 拿到锁线程 2 再申请就会阻塞
public static void main(String[] args) throws InterruptedException {
// 啥类型的锁都可以
Object lock = new Object();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
// 只对 sum 加了锁,外部的还是可以并发执行
// 本质是引入了锁竞争
synchronized (lock) {
// 看似只有一步操作,在 cpu 上面有三步
// 加锁的本质目的就是确保 sum++ 的原子性,
// 确保同一时刻只有一个线程对它进行操作。
sum++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
synchronized (lock) {
sum++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(sum);
}
}
package JavaEE;
public class Demo15 {
// synchronized 的可重入性,使这里不会死锁
public static void main(String[] args) {
Object lock1 = new Object();
Thread t = new Thread(() -> {
synchronized (lock1) {
synchronized (lock1) {
System.out.println("hh");
}
}
});
t.start();
}
}
同时满足以下四种情况才会产生死锁
注意:1、如果通篇只有一把锁,并且具备可重入性,不会死锁
2、如果多把锁之间不进行嵌套,不会死锁
package JavaEE;
// 最简单的演示死锁方式,重点记忆一下
public class Demo14 {
public static void main(String[] args) throws InterruptedException {
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(() -> {
// 注意,这里要演示死锁的话需要嵌套,两个锁嵌套起来,
// 否则就意味着在不同时间获取锁,而不是同时持有两个锁.
synchronized (lock1) {
try {
// 使用 sleep 休眠确保 t2 也拿到了锁
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock2) {
System.out.println("t1");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock1) {
System.out.println("t2");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
可重入的锁可以解决
上面最近的这个示例
哲学家就餐问题:一个个就餐时正常,同时就餐时就会出现死锁
1、wait 使用后释放锁,sleep 使用后仍持有锁 2、wait 只能在 synchronized 中使用,sleep 任何地方都可以 3、wait 可以被 notify 唤醒,sleep 必须要等休眠时间结束
package JavaEE;
import java.util.Scanner;
public class Demo17 {
public static void main(String[] args) {
Object locker = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker) {
System.out.println("t1wait 之前");
try {
// t1 通过 wait 释放了锁,并进入等待
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1wait 之后");
}
});
Thread t2 = new Thread(() -> {
synchronized (locker) {
System.out.println("t2wait 之前");
try {
// t2 通过 wait 释放了锁,并进入等待
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t2wait 之后");
}
});
Thread t3 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入任意内容结束线程:");
scanner.next();
synchronized (locker) {
// locker.notify();
// locker.notifyAll();
// 一直等到出了这里,t3 释放锁,其他线程继续进行
}
});
t1.start();
t2.start();
t3.start();
}
}
package JavaEE;
import java.util.Scanner;
public class Demo16 {
// 发现变量在线程 1 中读取,线程 2 中修改,加上 volatile 就好
private static boolean flag = true;
// private static volatile boolean flag = true;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flag) {
// Thread.sleep(1);
}
System.out.println("线程 t1 结束");
});
Thread t2 = new Thread(() -> {
System.out.println("输入任意内容结束线程 t1");
Scanner scanner = new Scanner(System.in);
scanner.next();
// 修改的是工作内存中的变量,而不是主存里的
flag = false;
System.out.println("flag = " + flag);
});
t1.start();
t2.start();
}
}
JMM(JAVA 内存模型) 为了提高性能,允许每个线程将主内存的变量拷贝到自己的工作内存中操作。线程直接读写工作内存,不每次都访问主内存。这就导致:一个线程修改了工作内存中的变量,但另一个线程看不到这个修改---因为修改还没同步回主内存,或者说另一个线程还在读自己工作内存中的旧值
volatile 的作用就是,强制要求线程每次读取变量时必须从主内存中读取,每次修改完变量以后必须立刻写回主内存,不能使用工作变量中的缓存副本
package JavaEE;
public class Singleton {
private static Singleton singleton = new Singleton();
// 使用静态类,避免进入调用方法需要创建实例,陷入创建实例方面的死循环
// 直接使用类名就可以调用该方法
public static Singleton getSingleton() {
// (只有读操作,没有写操作)
return singleton;
}
// 保证私有,外部不能创建实例
private Singleton() {};
}
package JavaEE;
public class SingletonLazy {
private static volatile SingletonLazy singletonLazy = null;
public static SingletonLazy getSingletonLazy() {
if (singletonLazy == null) {
// 写
singletonLazy = new SingletonLazy();
}
// 读
return singletonLazy;
}
private SingletonLazy() {};
}
package JavaEE;
public class SingletonLazy {
private static volatile SingletonLazy singletonLazy = null;
private static Object locker = new Object();
public static SingletonLazy getSingletonLazy() {
// 第一次检查:实例是否已经创建?(不用锁,提高性能)
// 作用:规避不必要的加锁操作
if (singletonLazy == null) {
// 加锁,引起锁竞争这样防止同一时间创建多个实例
synchronized (locker) {
// 第二次检查:确保同一时间只有一个实例被创建
// 作用:防止多线程下一个线程创建完,另一个线程又创建
if (singletonLazy == null) {
// 在单例模式下,创建唯一一个 SingletonLazy 类的实例
// volatile 关键字解决指令重排序的位置
// new 操作非原子性:分配内存 - 初始化 - 赋值
singletonLazy = new SingletonLazy();
}
}
}
return singletonLazy;
}
private SingletonLazy() {};
}
指令重排序造成的线程安全问题,DCL(双重检查锁) 单例就可以作为一个典型例子。主要是因为 new 操作不是原子的,它包括分配内存,初始化,赋值三个步骤。在极端情况下,编译器或 CPU 为了优化将初始化和赋值重排序,导致另一个线程拿到一个未初始化的对象
1、线程 A 走到 singletonLazy = new SingletonLazy();,因为重排序,它先赋值了(此时 instance 不再为 null,但指向的是一块还没初始化的内存) 2、线程 B 恰好这时进来检查 if(singletonLazy == null)---发现 singlenLazy 不为 null! 3、线程 B 直接返回并使用这个 singletonLazy,结果访问到的对象里的数据都是默认值(比如 int 是 0,对象引用是 null),程序出错
volatile 的核心作用就是禁止了这种重排序,保证了初始化操作一定在赋值之前产生,从而保证所有线程看到的都是完整初始化的对象


微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online