4. 多线程(2)---线程的状态和多线程带来的风险
文章目录
- 前言
- 1. 线程的状态
- 1.1. 观察线程的所有状态
- 1.2. 通过不同线程的状态,来调试代码,观察现象
- 2. 多线程的带来的风险---线程不安全
- 2.1.观察线程不安全的现象
- 2.2 线程不安全的原因
- 2.3.线程不安全的原因
前言
上一篇博客我们学习了,线程的创建,这次我们讲解 线程的状态 和 线程不安全问题。
1. 线程的状态
1.1. 观察线程的所有状态
从操作系统的视角来看,线程的状态分为:就绪和阻塞。
Java线程也是对操作系统线程的封装,
针对状态这里,Java也进行了重新封装,一共分为下面几种状态
- NEW:安排了工作,还未开始行动
- RUNNABLE:可工作的,又可以分成正在工作中和即将开始工作
- BLOCKED:由于锁而造成的阻塞
- WAITING:死等,没有超时时间的阻塞等待
- TIMED_WAITING:有超时时间的阻塞等待
- TERMINATED:工作完成了
我们有两种方式观察线程的状态
- 使用 getState() 方法
- 使用 jconsole 程序观察
下面我们写代码示例,来分别观察线程的状态
- NEW:安排了工作,还未开始行动
- TERMINATED:工作完成了
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
});
System.out.println(t.getState());
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
}
- TIMED_WAITING:有超时时间或者是指定时间的阻塞等待
public class Demo13 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
System.out.println(t.getState());
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
}
}
也可以使用 join(时间) 也会进入到 TIMED_WAITING
public class Demo14 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
System.out.println(t.getState());
t.start();
t.join(600000*1000);
}
}
使用 jconsole 工具
- RUNNABLE:可工作的,又可以分成正在工作中和即将开始工作
public class Demo13 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true){
/*try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}*/
}
});
System.out.println(t.getState());
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
}
}
while 循环中这段指令虽然什么都没有做,但是一直进行个循环操作,在CPU上执行。
- WAITING:死等,没有超时时间的阻塞等待
public class Demo14 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
System.out.println(t.getState());
t.start();
t.join();
}
}
下图就是一个简易的不同状态下的转换。
1.2. 通过不同线程的状态,来调试代码,观察现象
一个程序员工作的大部分时间不是在写代码,而是在调试代码 (找 bug)。
在多次的程序中,理解线程状态,是帮助我们调试程序的关键。
比如,发现某个代码逻辑,好像卡死了 (明明调用了,没有执行/没有执行完)
检查流程:
- 使用jconsole 或者其他工具,查看当前的进程中所有的线程,找到对应的逻辑线程是什么
- 看线程的状态是什么
看到 Timed_waiting / waiting,换衣是不是代码在某个方法上产生了阻塞,没有被即使唤醒
看到 blocked,怀疑是不是代码中出现了死锁
看到 Runnable,线程本身没有问题,考虑逻辑上某些条件是不是没有触发 - 再看看线程具体的调用栈,尤其是在阻塞的状态,现成代码阻塞在哪一行
2. 多线程的带来的风险—线程不安全
2.1.观察线程不安全的现象
下面我们来讨论一下线程不安全的情况。
举例一个例子:
使用两个线程,分别对 count进行加一操作,分别循环5000次,观察结果,预期是 10000次
public class Demo15 {
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);
}
}
这样写,会不会得到我们想要的结果呢?
每次结果都不一样,很难出现预期结果,每次发现都不是我们想要的结果。
为什么呢?
这个问题等会再说,
但是我们可以根据上面的代码进行修改,然后得到100000,那怎么进行修改呢?
只需要把start和join进行交换
public class Demo15 {
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();
t1.join();
t2.start();
t2.join();
System.out.println("count:"+count);
}
}
结果如下:
这样改就可以了竟然,下面我们来讲解一下线程不安全出现的原因。
2.2 线程不安全的原因
我们先解释一下上面出现的原因。
现在就需要我们之前的知识了,站在CPU的角度来看执行指令
这个操作看起来是一行代码,实际上对应到 3 个CPU 指令
- load: 把内存中的值 (count变量) 读取到 CPU 寄存器
- add: 把指定寄存器中的值,进行+1操作,结果还是在这个寄存器中
- save:把寄存器中的值,写回到内存中
上述这三个指令执行的过程中,CPU随时可能会触发线程的调取切换(因为操作系统的调度是随机的)。
可能会发生下面的情况:
123线程切走
12切走…线程切回来,然后是3
1 线程切走… 线程切回来,然后23一起执行
1线程切走…线程切回来 2线程切走…线程切回来,然后是3线程
下面是图解:
我们拿出两个来看一下,count的变化
上面便是上述代码出现的问题的解释。
2.3.线程不安全的原因
-
[根本] 操作系统对于线程的调度 是随机的。抢占式的
-
多个线程同时修改一个同一个变量
上述代码t1线程和t2线程修改同一个内存空间
如果是一个线程修改一个变量 - - - 没有问题
如果是多个线程,不是同时修改同一个变量 - - - 没有问题
如果是多个线程修改不同变量 - - - 没问题
如果多个线程读取同一个变量 - - - 没问题
取值操作 是 读操作,只有一条执行命令 -
修改的操作,不是原子的
在数据库中我们在讲事务的时候,事务有四大特性:
原子性 - - - 不可再分,一致性,持久性,隔离性。
如果修改操作,只是对应一个CPU指令,就可以认为是原子的,CPU不会出现“一条指令执行一半”的情况。
如果对应到多个CPU指令,那就不是原子的了。 -
内存可见性问题,引发的线程不安全
-
指令重排序,引发的线程不安全
第4条和第5条等到以后再讲。
关于为什么上面第二个代码可以成功,还有没有别的方法来实现线程安全,请听下回分析!
完