【多线程初阶】线程状态 线程安全
文章目录
- 1.线程状态
- 线程的状态及状态转移
- 2.多线程带来的风险 - 线程安全(重点)
- 线程安全问题产生的原因
- 如何解决线程安全问题
1.线程状态
EE的第一篇总览中有提到过
进程的状态
1.就绪
2.阻塞
这都是从操作系统的视角看待的
Java线程也是对操作系统线程的封装,针对状态这里,Java也进行了重新封装,来进行表示
线程的状态及状态转移
-
NEW : 安排了工作,还未开始行动 -->new 了 Thread 对象,还没 start
-
TERMINATED : 工作完成了 -->内核中的线程已经结束了,但是 Thread 对象还在
-
RUNNABLE : 可工作的,又可以分成正在工作中或即将开始工作的
就绪: 1).线程正在CPU上执行 2).线程随时可以去CPU上执行
比如,前面所举的例子,约A开会,A没有出差,随时可以一起开会
- TIMED_WAITING : 这几个都表示排队等着其他事情
①.sleep 状态由RUNNABLE 转变为 TIMED_WAITING
②.另外,join(时间)也会进入到TIMED_WAITING状态
指定时间的阻塞,线程阻塞(不参与CPU调度,不继续执行了),阻塞的时间是有上限的
- WAITING : 这几个都表示排队等着其他事情
与TIMED_WAITING 的区别,WAITING 会死等,没有超时时间的阻塞等待
- BLOCKED : 这几个都表示排队等着其他事情
也是一种阻塞,比较特殊,由于 锁 导致的阻塞
这个线程状态等我们介绍到线程安全问题 涉及到死锁再进行演示
多线程的程序中,理解线程状态,是帮助我们调试程序的关键
比如,发现代码中某个逻辑,好像卡死了(明明调用了,却没有执行/没有执行完)
1.jconsole / 其他工具,查看当前的进程中的所有线程,找到你对应逻辑的线程是谁
2.看线程的状态是啥
看到 TIMED_WAITING / WAITING ,怀疑是不是代码中某个方法产生阻塞,没有被及时唤醒
看到 BLOCKED ,怀疑是不是代码中出现死锁
看到 RUNNABLE ,线程本身没问题,考虑逻辑上某些条件没有预期触发之类的
3.再看看线程具体的调用栈(尤其是 阻塞的状态,线程的代码阻塞在哪一行了…)
2.多线程带来的风险 - 线程安全(重点)
想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的
如果不理解线程安全问题,是很难保证写出正确的多线程代码的
再回到我们观察到的现象,多次执行的结果都不一样并且没有达到预期结果
这样的代码很明显就是有BUG
实际执行效果和预期效果不符合,就叫bug
这样的问题,多线程并发执行引起的问题
如果把两个线程,变成串行执行(一个结束,再执行另一个)
我们将代码写成纯串行执行,观察现象
CPU内部包含寄存器这样的模块,寄存器也能存一些数据
我们使用时间轴(先执行的画上面,后执行的画下面)来模拟几个随机调度的顺序,我们只画几种情况,肯定不止这些,因为调度次序存在无数种可能
第一种情况的具体分析
第三种情况的具体分析
其他的情况就不一一进行分析了
通过上述讨论,不难发现,如果两个线程load时出现的数据都是0,那么意味着一定会少加一次,如果一个load到0另一个load到1,结果就是正确的,也就是说一个线程load需要再另一个线程的save之后才是正确的,所以我们上述的6种随机调度情况只有前两种是正确的
那么上述代码运行出来一定是 >=5w的吗?是否有可能<5w?
是有可能的,只不过概率很小
可以再用随机调度的情况分析一下,是否存在这种情况
是有可能的,只不过概率要小很多
50次和5000次甚至5w次,线程执行的时间长短是不同的
如果是循环50次,很可能在t2.start开始之前,t1 就算完了等后续t2再执行,虽然代码写的是并行,但变成纯串行了
线程安全问题产生的原因
本篇文章先讨论前三种原因
- 1.[根本原因] 操作系统对于线程的调度是随机的,抢占式执行
- .多个线程同时对同一变量进行修改
如果是一个线程修改一个变量 -->没问题
如果是多个线程,不是同时修改同一个变量 -->没问题
如果多个线程修改不同变量 -->没问题
如果多个线程读取同一变量 -->没问题
这些都不会出现中间结果相互覆盖的情况
其中修改操作->写,取值操作->读
- 3.修改操作,不是原子的
原子性,在数据库-事务的学习中,有提到
事务具有1.原子性 2.一致性 3. 持久性 4.隔离性
其中这个原子性,如果是修改操作.只是对应到一个CPU指令,就可以认为是原子的 ,CPU不会出现"一条指令执行一半"的情况,但是count++ 这行代码对应三条CPU指令,就不是原子的
像是++,–,+=,-=都不是原子的,像 = 赋值操作在Java就是原子的
- 4.内存可见性问题,引起的线程不安全
- 5.指令重排序引起的线程不安全
最后两个问题,我们后续再讨论~~
如何解决线程安全问题
- 1.[根本问题]操作系统对于线程的调度是随机的,抢占式执行
这是操作系统的底层设定,我们左右不了
- 2.多个线程同时对同一变量进行修改
这个和代码的结构相关,调整代码结构,规避一些线程不安全的代码,但是这样的方案不够通用,有些情况下,需求上就是需要多线程修改同一变量,比如超买/超卖的问题:某个商品,库存100件,能否创建101个订单?
- 3.修改操作,不是原子的
Java中解决线程安全问题的最主要方案:加锁
计算机中的锁,和生活中的锁,是同样的概念,互斥/排他的
把锁"锁上" 称为 “加锁”
把锁"解开" 称为 “解锁”
一旦把锁加上了,其他人要想加锁,必须要阻塞等待
就可以使用锁,把刚才不是原子操作的 count++ 包裹起来,在count++ 之前,先加锁,然后进行 count++,计算完毕后,在解锁,也就是在执行三条CPU指令过程中,其他线程就没法插队了
加锁操作,不是禁止这个线程被调度走,而是禁止其他线程重新加这个锁,避免其他线程的操作在当前线程执行过程中,插队
加锁/解锁 本身是 操作系统提供的API,很多编程语言都对于这样的API进行了封装,大多数的封装风格,都是采取lock()加锁,unlock()解锁 这两个函数
在Java中使用 synchronized 这样的关键字,搭配代码块,来实现类似的效果
- 进入 synchronized 修饰的代码块,相当于加锁
- 退出 synchronized 修饰的代码块,相当于解锁