目录
前言
在上次讲线程状态的文章里提到了BLOCKED状态--->http://t.csdnimg.cn/T17sQ
因为线程 对于 锁 的竞争引起的堵塞。接着详细展开讲解~
什么是线程安全性问题?
话不多说,先看一段代码:
public class demo17 {
public static void main(String[] args) throws InterruptedException {
int[] count = {0};
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count[0]++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count[0]++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count[0]);
}
}
分析代码,发现使用两个线程t1 t2 来累加 数组 count[0] 的值,在不运行的情况下, 是不是觉得结果是20000.
但实际运行结果却是这样:
等等。
每次都是随机值,很明显这就是一个bug。
原因
1.线程在操作系统中,随机调度,抢占式执行。
这是根本原因。
2.多个线程,同时修改同一个变量
3.修改操作不是原子的。
首先了解一下什么是原子性?
它指的是一个操作是不可分割的,要么完全执行,要么完全不执行。
在多线程环境中,如果一个操作是原子的,那么多个线程同时执行这个操作时,不会出现竞争条件或数据不一致的问题。
但是!!!count[0]++ 这个操作在Java中不是原子操作。它实际上包含以下三个步骤:
1.读取 count[0] 的当前值。
2.将读取的值加1。
3.将新值写回 count[0]。
如果两个线程同时执行这些步骤,可能会发生以下情况:
线程t1读取 count[0] 的值(假设已经到100)。
线程t2也读取 count[0] 的值(仍然是100)。
线程t1将读取的值加1(得到101)并写回。
线程t2也将读取的值加1(得到101)并写回。
结果是 count[0] 的值只增加了1,而不是2,因为两个线程都基于相同的初始值进行了递增操作。
这还是只是一种情况,而且因为原因1中的随机调度和抢占式执行。两个线程的执行顺序几乎是随机的 。也就导致了每次执行的结果都是不一样的。
这也就导致了线程安全问题的出现。
4.内存可见性问题
在多线程编程中,一个线程对某个共享变量做了修改,但其他线程可能看不到这个修改,还以为是原来的值~
举个例子
想象你和你的家人共享一个家庭冰箱(这是你们的共享资源)。冰箱门上有一个便签,用来记录冰箱里有哪些食物(这个便签就是你们的共享变量)。
你添加食物:你往冰箱里添加了一些食物,并在便签上更新了食物清单(这相当于一个线程修改了共享变量的值)。
你的家人看到便签:你的家人在准备晚餐时,查看便签上的食物清单,并根据清单上的内容决定做什么菜(这相当于其他线程看到了共享变量的最新值)。
但是内存可见性问题就是便签上的信息没有及时被其他人看到:
于是他们决定去买这些食物,但实际上冰箱里已经有了(这相当于其他线程没有看到共享变量的最新值,而是看到了原来的值)
5.指令重排序问题
指令重排序是指编译器和处理器为了优化程序的执行效率,可能会对程序中的指令顺序进行调整。
在多线程环境下,指令重排序可能会导致不可预测的行为,因为不同线程之间的操作顺序可能会被打乱。
举个例子:
假设正在准备一顿晚餐,需要完成以下步骤:洗菜->切菜->煮饭->炒菜.
现在,假设你和你的朋友同时在厨房准备晚餐,你们各自负责不同的步骤,但有些步骤需要共享资源(比如同一个锅)。如果你们没有协调好,可能会出现以下情况:你开始洗菜,然后切菜。
你的朋友开始煮饭,然后试图炒菜。
由于你们没有协调好,你的朋友在煮饭完成后试图炒菜,但此时你还没有切好菜,导致炒菜无法进行。这就相当于指令重排序在多线程环境下导致的问题。
解决办法
简单来讲就是,通过加锁的这种方式,使一个线程在执行 count[0]++ 的操作时,其他线程的 count[0]++ 不能插队进来。如图~
看着是不是很像等待 join(),但效率是快多了~
synchronized 关键字(针对原因2和3)
这个关键字就代表上锁,基本用法是:
synchronized(//需要一个锁对象进行后续的判定){
//需要打包到一起的代码
}
重新加上这个关键字:
public class demo17 {
private static final Object lock = new Object(); //定义 lock 对象
public static void main(String[] args) throws InterruptedException {
int[] count = {0};
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (lock) {
count[0]++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (lock) {
count[0]++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count[0]);
}
}
加上后,就能正常执行了~
volatile 关键字
针对可见性问题
比如先看这段代码
public class demo23 {
private static boolean flag = false;
public static void main(String[] args) {
Thread writer = new Thread(() -> {
try {
Thread.sleep(1000); //休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true; //修改共享变量的值
System.out.println("flag是true");
});
Thread reader = new Thread(() -> {
while (!flag) {
//空循环,等待 flag 变为 true
}
System.out.println("flag是false");
});
writer.start();
reader.start();
}
}
一般分析的话,reader 线程看到flag 变为true,两个打印语句都可以正常执行。
但结果是这样:
writer线程 对 flag 的修改可能只反映在它所在 CPU 核心的缓存中,而没有立即同步到主内存中。因此,reader在读取 flag 时,可能还是从自己的缓存中读取,从而看到的是旧值。
所以reader就一直等,就阻塞了~
忘了计算机是怎么工作的?--->http://t.csdnimg.cn/PH0l2
加上volatile关键字
private static volatile boolean flag = false;
结果就可以正常输出了。
针对重排序问题
比如这段代码~
public class demo24 {
private static int a = 0;
private static int b = 0;
private static int x = 0;
private static int y = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
b = 1;
y = a;
});
t1.start();
t2.start();
System.out.println("x = " + x + ", y = " + y);
}
}
结果是这样。
我们期望 x 和 y 的值分别是 1 和 1,因为 a 和 b 都被设置为 1。然而,由于指令重排序,导致 x 和 y 的值分别是 0 和 0。
加上volatile关键字
private static volatile int a = 0;
private static volatile int b = 0;
private static volatile int x = 0;
private static volatile int y = 0;
结果
注意
1.锁对象的作用:上面写的后续的判定就是指 是否是对 “同一个对象” 加锁.
记住,锁对象是什么不重要,重要的是多个线程的锁对象是否是同一个。
因为不是同一对象,各自加锁后也互不影响。也就不会出现阻塞或锁竞争。
比如下图~
public class demo17 {
private static final Object lock1 = new Object(); //定义 lock1 对象
private static final Object lock2 = new Object(); //定义 lock2 对象
public static void main(String[] args) throws InterruptedException {
int[] count1 = {0};
int[] count2 = {0};
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (lock1) {
count1[0]++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (lock2) {
count2[0]++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count1[0]);
System.out.println(count2[0]);
}
}
2.锁对象的类型不能是int double 这种内置的类型,只要是Object类的子类就可以~
看下图~
3.当 synchronized 关键字修饰方法时,这意味着当一个线程正在执行该方法时,其他线程必须等待,直到当前线程执行完毕。
这也好理解,毕竟上了锁嘛~
死锁
死锁的定义
死锁是指两个或多个线程在执行过程中,每个线程都需要其他线程的资源,但每个线程都上锁了,就这样一直阻塞。
死锁的必要条件
互斥:资源不能同时被多个线程使用。例如,一个文件不能同时被两个线程写入。
请求和保持:线程已经占有了至少一个资源,但又申请新的资源,而该资源被其他线程占有,所以它必须等待。
不可抢占:已经分配给某个线程的资源不能被强制拿走,只能由占有它的线程主动释放。
循环等待:线程之间形成了一个等待资源的环形链,每个线程都在等待链中下一个线程占有的资源。
死锁的场景
1.重复加锁
Java中的 synchronized 关键字是可重入的,所以这段代码可以正常运行。
public class demo25 {
private static final Object lock = new Object();
public static void main(String[] args) {
Thread t = new Thread(()->{
synchronized (lock){
System.out.println("hello-1");
synchronized (lock){
System.out.println("hello-2");
}
}
});
t.start();
}
}
然而,如果使用的是不可重入锁或者其他编译语言,重复加锁就会导致问题。
2.嵌套锁
public class demo26{
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("hello");
synchronized (lock2) {
System.out.println("world");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("world");
synchronized (lock1) {
System.out.println("hello");
}
}
});
t1.start();
t2.start();
}
}
t1 首先获取 lock1,然后尝试获取 lock2,但t2 首先获取 lock2,然后尝试获取 lock1。
因为两个线程同时运行,而 t1线程 获取了 lock1,而t2 线程获取了 lock2,那么它们将陷入死锁,因为每个线程都在等待对方释放锁。 结果就是这样了-->一直陷入等待中!
3.哲学家就餐问题
典型模型-->哲学家就餐问题:
假设有五位哲学家围坐在一张圆桌旁,他们要么思考,要么吃饭。每位哲学家之间有一只筷子,这样每位哲学家左右各有一只筷子。哲学家需要两只筷子才能吃饭,但每次只能拿起一只筷子。
对于这种情况,如果不采取适当的同步措施,很容易导致死锁。
例如,所有哲学家同时拿起左边的筷子,然后等待右边的筷子,这样就会形成一个循环等待,导致所有哲学家都无法吃饭。
解决方案:
为筷子进行编号。例如,哲学家必须先拿编号较小的筷子,再拿编号较大的筷子。
这样可以打破循环等待的条件,从而避免死锁的发生。
看到最后,如果觉得文章写得还不错,希望可以给我点个小小的赞,您的支持是我更新的最大动力