1.多线程带来的的⻛险-线程安全
1.1 观察线性不安全
// 此处定义⼀个 int 类型的变量
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// 对 count 变量进⾏⾃增 5w 次
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
// 对 count 变量进⾏⾃增 5w 次
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
// 如果没有这俩 join, 肯定不⾏的. 线程还没⾃增完, 就开始打印了.
t1.join();
t2.join();
// 预期结果应该是 10w
System.out.println("count: " + count);
}
1.2 线程安全的概念
1.3线程不安全的原因
多个线程修改同⼀个变量


我们把⼀段代码想象成⼀个房间,每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证,A进⼊房间之后,还没有出来;B 是不是也可以进⼊房间,打断 A 在房间⾥的隐私。这个就是不具备原⼦性的。
这点也和线程的抢占式调度密切相关. 如果线程不是 "抢占" 的, 就算没有原⼦性, 也问题不⼤.
可⻅性



⽐如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果只是 第⼀次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问内存了. 效率就⼤⼤提⾼了.
值的⼀提的是, 快和慢都是相对的. CPU 访问寄存器速度远远快于内存, 但是内存的访问速度⼜远远快于硬盘.
对应的, CPU 的价格最贵, 内存次之, 硬盘最便宜.
编译器对于指令重排序的前提是 "保持逻辑不发⽣变化". 这⼀点在单线程环境下⽐较容易判断, 但是 在多线程环境下就没那么容易了, 多线程的代码执⾏复杂程度更⾼, 编译器很难在编译阶段对代码的执⾏效果进⾏预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.
1.4解决之前的线程不安全问题
先观察代码,在之后进行解释
// 此处定义⼀个 int 类型的变量
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(() -> {
// 对 count 变量进⾏⾃增 5w 次
for (int i = 0; i < 50000; i++) {
synchronized (locker) {
count++;
}
}
});
Thread t2 = new Thread(() -> {
// 对 count 变量进⾏⾃增 5w 次
for (int i = 0; i < 50000; i++) {
synchronized (locker) {
count++;
}
}
});
t1.start();
t2.start();
// 如果没有这俩 join, 肯定不⾏的. 线程还没⾃增完, 就开始打印了.
t1.join();
t2.join();
// 预期结果应该是 10w
System.out.println("count: " + count);
}
2.synchronized 关键字 - 监视器锁 monitor lock
2.1 synchronized的特性
1)互斥

synchronized⽤的锁是存在Java对象头⾥的。
可以粗略理解成, 每个对象在内存中存储的时候, 都存有⼀块内存表⽰当前的 "锁定" 状态(类似于厕所的 "有⼈/⽆⼈").如果当前是 "⽆⼈" 状态, 那么就可以使⽤, 使⽤时需要设为 "有⼈" 状态.如果当前是 "有⼈" 状态, 那么其他⼈⽆法使⽤, 只能排队
理解 "阻塞等待".针对每⼀把锁, 操作系统内部都维护了⼀个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进⾏加锁, 就加不上了, 就会阻塞等待, ⼀直等到之前的线程解锁之后, 由操作系统唤醒⼀个新的线程, 再来获取到这个锁.注意:• 上⼀个线程解锁之后, 下⼀个线程并不是⽴即就能获取到锁. ⽽是要靠操作系统来 "唤醒". 这也就是操作系统线程调度的⼀部分⼯作.• 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B ⽐ C 先来的, 但是 B 不⼀定就能获取到锁,⽽是和 C 重新竞争, 并不遵守先来后到的规则.
synchronized的底层是使⽤操作系统的mutex lock实现的。
2)可重入
synchronized 同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题;
理解 "把⾃⼰锁死"⼀个线程没有释放锁, 然后⼜尝试再次加锁.// 第⼀次加锁, 加锁成功lock();// 第⼆次加锁, 锁已经被占⽤, 阻塞等待.lock();按照之前对于锁的设定, 第⼆次加锁的时候, 就会阻塞等待. 直到第⼀次的锁被释放, 才能获取到第⼆个锁. 但是释放第⼀个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想⼲了, 也就⽆法进⾏解锁操作. 这时候就会 死锁
Java 中的 synchronized 是 可重⼊锁, 因此没有上⾯的问题.
for (int i = 0; i < 50000; i++) {
synchronized (locker) {
synchronized (locker) {
count++;
}
}
}
2.2 synchronized 使⽤⽰例
1) 修饰代码块: 明确指定锁哪个对象.
public class SynchronizedDemo {
private Object locker = new Object();
public void method() {
synchronized (locker) {
}
}
}
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
2) 直接修饰普通⽅法: 锁的 SynchronizedDemo 对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
3) 修饰静态⽅法: 锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
2.3 Java 标准库中的线程安全类
3. volatile 关键字
volatile 能保证内存可⻅性

前⾯我们讨论内存可⻅性时说了, 直接访问⼯作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度⾮常快, 但是可能出现数据不⼀致的情况.加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.flag == 0) {
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输⼊⼀个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
// 执⾏效果
// 当⽤⼾输⼊⾮0值时, t1 线程循环不会结束. (这显然是⼀个 bug)
t1 读的是⾃⼰⼯作内存中的内容.当 t2 对 flag 变量进⾏修改, 此时 t1 感知不到 flag 的变化.
如果给 flag 加上 volatile
static class Counter {
public volatile int flag = 0;
}
// 执⾏效果
// 当⽤⼾输⼊⾮0值时, t1 线程循环能够⽴即结束.
volatile 不保证原⼦性
static class Counter {
volatile public int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
4.wait和notify
球场上的每个运动员都是独⽴的 "执⾏流" , 可以认为是⼀个 "线程".⽽完成⼀个具体的进攻得分动作, 则需要多个运动员相互配合, 按照⼀定的顺序执⾏⼀定的动作, 线程1 先 "传球" , 线程2 才能 "扣篮"
注意: wait, notify, notifyAll 都是 Object 类的⽅法.
4.1 wait()⽅法
wait 要搭配 synchronized 来使⽤. 脱离 synchronized 使⽤ wait 会直接抛出异常.
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
4.2notify()方法
static class WaitTask implements Runnable {
private Object locker;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
while (true) {
try {
System.out.println("wait 开始");
locker.wait();
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
static class NotifyTask implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
Thread.sleep(1000);
t2.start();
}
4.3notifyAll()方法
static class WaitTask implements Runnable {
// 代码不变
}
static class NotifyTask implements Runnable {
// 代码不变
}
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t3 = new Thread(new WaitTask(locker));
Thread t4 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
t3.start();
t4.start();
Thread.sleep(1000);
t2.start();
}
此时可以看到, 调⽤ notify 只能唤醒⼀个线程。
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notifyAll();
System.out.println("notify 结束");
}
}
此时可以看到, 调⽤ notifyAll 能同时唤醒 3 个wait 中的线程
4.4 wait 和 sleep 的对⽐
wait
和 sleep
是在编程和操作系统中常用的两种控制执行流的机制,尽管它们的名称相似,但它们的功能和用途有很大的不同。以下是它们的主要区别:
1. 功能
-
sleep:
sleep
函数使当前线程暂停执行一段指定的时间。无论是 CPU 资源还是其他资源,线程在此期间不会执行任何操作。使用sleep
通常用于延迟操作或等待某些条件的实现。
-
wait:
wait
通常用于线程或进程间的同步。它使一个线程等待另一个线程或进程的状态改变(例如,等待子进程结束)。在此期间,调用wait
的线程会被阻塞,直到它所等待的条件成立。
2. 用法
-
sleep:
- 通常用于控制时间间隔,例如在执行某些操作之前给用户提供时间,或在重复操作之间引入延迟。
-
wait:
- 多用于线程同步和进程间通信中,例如使用条件变量、信号量等机制。它使线程在某些条件不满足时处于阻塞状态,直到其他线程调用
notify
或notifyAll
等方法来唤醒它。
- 多用于线程同步和进程间通信中,例如使用条件变量、信号量等机制。它使线程在某些条件不满足时处于阻塞状态,直到其他线程调用
3. 影响
-
sleep:
- 使用
sleep
后,线程会在指定时间内完全不执行,不会消耗 CPU 资源,但会保持占用系统资源。
- 使用
-
wait:
wait
使线程在条件不满足时进入等待状态,释放持有的锁或资源,直到被唤醒,允许其他线程继续执行。这有助于提高资源利用率和避免死锁。
4. 示例
-
sleep 示例(伪代码):
import time print("Start sleeping...") time.sleep(5) # 暂停 5 秒 print("Wake up!")
-
wait 示例(伪代码):
condition = threading.Condition() def thread_function(): with condition: condition.wait() # 等待其他线程通知 print("Thread resumed!") def notify_function(): time.sleep(5) # 假设等待某些条件 with condition: condition.notify() # 通知等待的线程
总结
wait
和 sleep
都是控制程序执行流的工具,但它们在功能、用途和实现机制上有明显的区别。在实际编程中,根据具体需求选择使用 sleep
或 wait
是很重要的。