目录
1.什么是可见性问题
在多线程环境下,读和写发生在不同的线程中,可能会出现:读线程不能及时的读取到其他线程写入的最新的值,这就是所谓的可见性问题。
一个count变量,线程1判断count!=0退出线程,此时线程2改变count值,由于内存可见性,线程1不能及时获取,所以线程1不能及时退出。
public class Demo_501 {
private static class Counter {
public static int count = 0;
}
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 线程启动");
while (Counter.count == 0) {
// 一直循环
}
System.out.println(Thread.currentThread().getName() + " 线程退出");
}, "t1");
// 启动线程
t1.start();
// 确保t1先启动
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 线程启动");
// 获取用户输入
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个非零的值:");
Counter.count = scanner.nextInt();
System.out.println(Thread.currentThread().getName() + " 线程退出");
}, "t2");
// 启动线程
t2.start();
}
}
2.解决内存可见性
1.在CPU层面——缓存一致性协议(MESI)
MESI可以理解为一种通知机制。
如果缓存中的值发生变换,就会及时通知CPU更新,以保证一致性。
2.volatile关键字
volatile可以保证内存可见性,在编译的过程中,volatile修饰的变量见后加入了相关的内存屏障。
1.保证内存可见性
代码在写入volatile修饰变量时:
- 改变线程工作内存中volatile变量副本的值
- 将改变后的副本值从工作内存刷新到主内存
代码在读取volatile修饰变量时:
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本
2.不保证原子性
这个是最初的演示线程安全的代码--->线程不安全重现
- 给 increase 方法去掉 synchronized
- 给 count 加上 volatile 关键字
class Counter {
volatile public int count = 0;
void increase() {
count++;
}
}
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
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);
}
}
在多线程环境中,一般synchronized和volatile搭配使用,否则会报错。
3.禁止指令重排序
由于使用volatile,在程序编译好之后,会在volatile修饰的指令之间加入相关的内存屏障,从而限制CPU对执行顺序的优化。
3.wait notify notifyAll
由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.
完成这个协调工作, 主要涉及到三个方法:
- wait() / wait(long timeout): 让当前线程进入等待状态.
- notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的方法.
1.wait()
wait 做的事情:
- 使当前执行代码的线程进行等待. (把线程放到等待队列中)
- 释放当前的锁
- 满足一定条件时被唤醒, 重新尝试获取这个锁.
注意:wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常。
wait结束条件:
- 其他线程调用该对象的notify方法
- wait等待时间超过指定时间
- 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
此时由于没有notify唤醒,所以一直阻塞中。
2.notify()
notify 方法是唤醒等待的线程:
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
import java.util.concurrent.TimeUnit;
/**
* 演示 wait() 和 notify()
* 创建两个线程,一个调用wait()一个调用notify()
* @Author 比特就业课
* @Date 2023-01-08
*/
public class Demo_502 {
public static void main(String[] args) {
// 定义一个锁对象
Object locker = new Object();
// 创建调用wait()的线程
Thread t1 = new Thread(() -> {
while (true) {
System.out.println("wait 之前");
try {
// 加入锁
synchronized (locker) {
locker.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait 之后");
System.out.println("===============================");
}
}, "wait");
// 启动t1
t1.start();
Thread t2 = new Thread(() -> {
while (true) {
System.out.println("notify 之前");
// 加入锁对象
synchronized (locker) {
locker.notify();
}
System.out.println("notify 之后");
try {
// 休眠一会
// 用并发包中的工具类
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "notify");
// 启动t2
t2.start();
}
}
3.notifyAll()
notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.
notify与notifyAll
notify 只唤醒等待队列中的一个线程. 其他线程还是乖乖等着
notifyAll 一下全都唤醒, 需要这些线程重新竞争锁
4.wait与sleep区别
wait用于线程之间的通信,sleep让线程阻塞一段时间
wait要搭配synchronized使用,sleep不需要
wait是Obiect类的方法,sleep是Thread类的静态方法