线程安全问题
线程安全问题
1. 什么是线程安全
线程是cpu随机调度, 抢占式执行的, 这就导致程序的结果和预期不同, 我们把这样的问题叫做线程安全问题
例子:
class Demo19 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count);
}
}
我们期望程序并发执行,且最后打印 10 0000
但是, 上述代码3次程序结果随机, 这样期望和结果不同的问题, 就是线程安全问题
2. 导致线程安全问题的原因
原因1 -> 线程是抢占式执行
为什么, 上述程序3次结果随机 ?
首先, count++, 这段代码会编译成3个指令
1. 把count在内存的值读取到寄存器
2. 寄存器中值 + 1
3. 把寄存器中值, 写回内存
有可能是一个cpu核心并发执行, 这一刻执行线程1, 下一刻执行线程2
也有可能是二个cpu核心并行执行
但是无论哪种情况都会出问题, 因为线程是抢占式执行的
看下面的例子, 进行理解
正确的执行顺序
错误的执行顺序, 这只是一种可能性
总结: 由于线程是抢占式执行, 这就导致指令执行的顺序随机 -> 结果也就随机了
原因2 -> 两个线程针对同一个变量进行修改
查看代码
class Demo19 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count);
}
}
原因3 -> 针对同一个变量进行修改的操作不是 "原子的" ( 一条指令 )
count++ -> 3条指令
原因4 -> 内存可见性导致的线程安全问题
查看代码
import java.util.Scanner;
class Demo23 {
private static int count = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (count == 0) {
; // 循环体中, 啥都没有.
// 无其他操作 -> 输入1 -> 线程不终止
}
System.out.println("t1 执行结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数: ");
count = scanner.nextInt();
});
t1.start();
t2.start();
}
}
t1线程不终止, 原因 ?
这是内存可见性 ( 编译器优化导致的问题 )
count == 0, 这段代码分为2个指令, load, cmp
由于load操作比cmp慢很多, 所以为了提升速度, 编译器做了优化 -> 只执行一次load指令, 之后就不执行了。把count的值放到寄存器中, 每次cmp指令就拿寄存器中的值比较
所以, 当t2线程改变count的值, t1线程不执行load指令, t1线程也就不会终止
为什么写个打印就能终止线程了
查看代码
class Demo22 {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (count == 0) {
System.out.println("t1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t1线程执行结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数: ");
count = scanner.nextInt();
System.out.println("t2线程执行结束");
});
t1.start();
t2.start();
t1.join();
t2.join();
;
}
}
现在, t1线程涉及3个指令 load, cmp, I/O, 由于I/O比load慢很多, I/O不能优化
所以, 编译器就不会优化load指令了
编译器是否做优化, 这个不好说
如何彻底解决内存可见性问题 ?
用关键字volatile,提示编译器不做优化
查看代码
import java.util.Scanner;
class Demo23 {
private static volatile int count = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
// count == 0, 不会进行优化
while (count == 0) {
; // 循环体中, 啥都没有.
}
System.out.println("t1 执行结束");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数: ");
count = scanner.nextInt();
});
t1.start();
t2.start();
}
}
原因5 -> 指令重排序
3. 如何解决线程安全问题
针对原因1 -> 没办法解决, 线程抢占式执行是底层写死的
针对原因2 -> 可以解决线程安全问题, 但是不好
针对原因3 -> 最好的解决方法, 把针对变量的修改的操作, 搞成 "原子的"
就是把count++的3条指令 load -> add -> save "打包成一条指令 ", 这样线程之间就算是抢占式执行, 也能得到正确的结果
"把多条指令打包成一条指令", 这样的操作是通过锁实现的
1. 在 java中, 锁就是任意一个对象
2. 锁有两个核心操作 -> 加锁、解锁
3. 一个线程拿到了锁对象加锁之后, 另一个线程如果也尝试拿这同一把锁进行加锁, 那么这个线程就会阻塞等待 -> 锁竞争 / 锁冲突
代码例子:
查看代码
class Demo19 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
// 先创建出一个对象, 使用这个对象作为锁
Object locker1 = new Object();
// Object locker2 = new Object();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
// t1线程对 locker1锁对象 进行加锁
synchronized (locker1) {
count++;
}
// 对 locker1锁对象 解锁
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
// t2 线程也是对 locker1这个锁对象进行加锁
// 如果t1线程已经拿到了锁对象, 那么t2线程会阻塞等待
// t1线程对locker1 解锁 -> t2线程再对锁尝试加锁
synchronized (locker1) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count);
}
}
"把多条指令打包成一条指令", 并不是把多条指令变成一条指令,而是通过锁引入阻塞等待, 让一部分代码串行执行, 一部分代码并发执行
比如上述代码中, synchnized关键字中的代码就是串行执行, 其他for循环代码都是并发执行
写法2:
查看代码
class Counter {
private int count = 0;
public void add() {
count++;
/*synchronized (this) {
count++;
}*/
}
public int get() {
return count;
}
}
class Demo20 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Counter counter2 = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
synchronized (counter) {
counter.add();
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
synchronized (counter) {
counter.add();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + counter.get());
}
}
写法3:
synchronized 修饰普通方法, 针对this对象加锁
查看代码
package demo2;
class Counter {
private int count = 0;
// 简化写法: 一进入add()函数, 就尝试对this对象 进行加锁
synchronized public void add() {
count++;
}
public int get() {
return count;
}
}
class Demo20 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Counter counter2 = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + counter.get());
}
}
写法4:
synchronized 修饰静态方法, 针对 该类的类对象加锁
查看代码
package demo2;
class Counter {
private static int count = 0;
public static void func() {
// Counter.class 表示当前类对象, 一个类只能有一个类对象
synchronized (Counter.class) {
count++;
}
}
public int get() {
return count;
}
}
class Demo20 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Counter counter2 = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.func();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.func();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + counter.get());
}
}
4. 可重入锁
每个锁对象会记录当前是哪个线程, 正在持有锁 (哪个线程已经对锁对象加锁了)
在针对锁对象, 加锁之前会先进行判断, 如果是同一个线程, 放行 —— 可重入锁
如果是不同线程, 则正常阻塞
查看代码
class Counter2 {
private int count = 0;
void add() {
// 按道理, counter2这个锁已经加锁了, 这里会阻塞等待
// 但是java jvm 对synchronized关键字这里做了特殊处理
// 如果当前线程已经对该锁对象加锁了, 就直接放行
synchronized (this) {
count++;
}
}
int get() {
return count;
}
}
class Demo21 {
public static void main(String[] args) throws InterruptedException {
Counter2 counter2 = new Counter2();
//Counter2 counter1 = new Counter2();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter2.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
synchronized (counter2) {
counter2.add();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + counter2.get());
}
}
5. 死锁
一个死锁的例子:
查看代码
package demo2;
class Demo22 {
public static void main(String[] args) throws InterruptedException {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker1) {
try {
// 为了更好的控制线程的执行顺序, 引入 sleep, 否则死锁可能重现不出来.
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// ... t1线程阻塞
synchronized (locker2) {
System.out.println("t1 获取了两把锁");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker2) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// ... t2线程 阻塞
synchronized (locker1) {
System.out.println("t2 获取到两把锁");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
如何解决死锁 ?
产生的死锁4个必要条件, 少了一个就解决死锁了
针对条件3, 解决死锁
查看代码
class Demo22 {
public static void main(String[] args) throws InterruptedException {
Object locker1 = new Object();
Object locker2 = new Object();
// 约定加锁顺序 t1: locker1 加锁 -> locker2 加锁 -> locker2 解锁 -> locker1 解锁
// 约定加锁顺序 t2: locker1 加锁 -> locker2 加锁 -> locker2 解锁 -> locker1 解锁
Thread t1 = new Thread(() -> {
synchronized (locker1) {
try {
// 为了更好的控制线程的执行顺序, 引入 sleep, 否则死锁可能重现不出来.
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("t1 获取了两把锁");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("t2 获取到两把锁");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
针对条件4, 解决死锁
查看代码
package demo2;
class Demo22 {
public static void main(String[] args) throws InterruptedException {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker1) {
try {
// 为了更好的控制线程的执行顺序, 引入 sleep, 否则死锁可能重现不出来.
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("t1 获取了两把锁");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker1) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("t2 获取到两把锁");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
6. wait、notify
什么是线程饿死 ? 这里有问题
假设这样一种情况, 某个线程频繁的加解/锁, 但是该线程又能很快获取锁 , 这就导致其他线程拿不到锁, 进而阻塞等待
这种情况, 就可以用wait让该线程主动阻塞, 让其他线程继续执行
wait() -> 解锁 + 阻塞等待 ,sleep() -> 阻塞等待
notify() -> 随机唤醒一个, 由于调用 锁对象.wait() 陷入阻塞的线程
例子1 :
重要 ! -> 里面有很多重要的细节
查看代码
class Demo26 {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker) {
System.out.println("t1 等待之前");
try {
// 解锁 -> t1 线程阻塞等待
locker.wait();
// notify把这里唤醒阻塞之后, 不会立即执行
// 只有t1线程重新加上锁了之后, 再继续执行
// Runnable -> Waiting (wait) -> Runnable(notify) -> Blocked (由于锁导致的阻塞, 得再加上锁, 才能继续执行)
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1 等待之后");
}
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
synchronized (locker) {
System.out.println("输入: ");
scanner.next();
// 唤醒 -> 由于locker这个锁对象, 导致阻塞的线程t1
System.out.println("唤醒t1线程");
locker.notify();
}
});
t1.start();
t2.start();
}
}
例子2:
查看代码
class Demo26 {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker) {
System.out.println("t1 等待之前");
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1 等待之后");
}
});
Thread t2 = new Thread(() -> {
synchronized (locker) {
System.out.println("t2 等待之前");
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t2 等待之后");
}
});
Thread t3 = new Thread(() -> {
synchronized (locker) {
System.out.println("t3 通知之前");
// notify() 随机唤醒等待的线程
// 只能唤醒一个等待线程
locker.notify();
// 唤醒全部等待线程
locker.notifyAll();
System.out.println("t3 通知之后");
}
});
t1.start();
t2.start();
Thread.sleep(100);
t3.start();
}
}