浅谈java线程状态与线程安全解析
作者:invictusQAQ
1.线程的几种状态
1.1 线程的状态
以下就是我们线程所有的状态和意义:
NEW | 已经创建Thread但未创建线程 |
RUNNABLE | 可工作的. 又可以分成正在工作中和即将开始工作 |
BLOCKED | 等待锁(阻塞状态) |
WAITING | 调用wati方法(阻塞状态) |
TIMED_WAITING | 调用sleep方法(阻塞状态) |
TERMINATED | 系统线程执行完毕已销毁,但Thread还存在 |
注意:
BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知.
TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒
1.2 线程状态的转移
各线程之间的转移关系可以简化成下图:
关于yield方法:
在多线程中我们存在一个yield方法可以让线程在就绪队列中重新”排队“,不改变线程状态。相当于你去帮别人排队,但是轮到你了那个人还没回来,你就就让原本排在你后面的人换到你的位置上,但你仍然处于排队状态。这种”大公无私“的行为可以类比到我们的yield方法帮助我们理解。
public class Demo{ public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Runnable() { @Override public void run() { while (true) { System.out.println("张三"); // 先注释掉, 再放开 //Thread.yield(); } } }, "t1"); t1.start(); Thread t2 = new Thread(new Runnable() { @Override public void run() { while (true) { System.out.println("李四"); } } }, "t2"); t2.start(); } }
可以看到:
1. 不使用 yield 的时候, 张三李四大概五五开
2. 使用 yield 时, 张三的数量远远少于李四
结论: yield 不改变线程的状态, 但是会重新去排队.
2.有关线程安全问题
2.1 一个简单的例子
// 创建两个线程, 让这俩线程同时并发的对一个变量, 自增 5w 次. 最终预期能够一共自增 10w 次. class Counter { // 用来保存计数的变量 public int count; public void increase() { count++; } } public class Demo { // 这个实例用来进行累加. // public static Counter counter = new Counter(); public static void main(String[] args) { 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(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("count: " + counter.count); } }
大家先看到以上的代码,意思很简单,用两个线程对同一个变量进行自增操作,运行的结果如下
看起来不太对,我们再试一次
结果有了变化,但仍然不是我们想要的结果,是什么导致了5w+5w<10w呢?其中一个原因就是线程的随机调度和改操作不具有原子性。 这些概念我们下面会详细讲,这里我们先简单了解一下。
首先我们的自增操作在cpu内其实分为三步:
1.LOAD:cpu从内存中读取数据到寄存器
2.ADD:在寄存器内实现自增
3.SAVE:将寄存器的数据写回内存中
而我们已经知道cpu对于线程调度我们可以理解为是随机的,所以会有很多种可能,比如下图
其中纵轴代表运行时间,这里我们可以看到两个线程相当于互不影响,线程1完成自增操作后又将数据写回内存由线程2再去操作,这种情况下是没有问题的。但是也可能是下面的一种情况
此时线程1还没有将自增后的数据写回内存而线程2就已经将要修改的数据读入了寄存器,此时相当于线程2读到了那个还未自增的数据,相当于两个线程对同一个数进行了自增,所以此时相当于只自增了一次。其实情况还有很多,这里我们仅举例比较经典的例子。所以这也能够解释为什么结果大于5w而小于10w了。
2.2 造成线程不安全的原因
2.2.1 操作系统的随机调度/抢占式运行
这种是操作系统内核就已经决定的,我们无能为力。类似于我们上一个例子,就是因为线程的随机调度和操作不具有原子性造成的。
2.2.2 操作不具有原子性
什么是原子性
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入 房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性 的。转换成代码我们可以理解成只具有一条指令的操作。
当然这个问题我们可以通过加锁操作解决(以后会提到)。
一条 java 语句不一定是原子的,也不一定只是一条指令,比如我们上面提到的自增操作。
不保证原子性会给多线程带来什么问题
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。 这点也和线程的抢占式调度密切相关. 如果线程不是 "抢占" 的, 就算没有原子性, 也问题不大.
2.2.3 多个线程修改同一个变量
1.一个线程修改变量没事
2.多个线程同时读一个变量也没事
3.多个线程同时修改不同变量也没有问题
唯独需要注意多个线程修改同一个变量,如果不加以处理可能会造成我们之前讲到的例子的问题
2.2.4 内存可见性问题
jvm中规定了java的内存模型
线程之间的共享变量存在 主内存 (Main Memory).
每一个线程都有自己的 "工作内存" (Working Memory) .
当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.
正是因为这种机制,所以可能会出现下面的问题:
由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 "副本". 此时修改线程 1 的工作内存中的值, 线程2 的工作内存不一定会及时变化.通俗的讲就是 线程1针对工作内容修改了数据,而线程2此时并不一定能够及时同步修改的数据,所以可能会引发各种问题。
2.2.5 指令重排序
所谓指令重排序是指jvm针对我们的代码,可能会在保证逻辑不变的情况下去调整指令执行的顺序以达到运行效率更高的效果。这种情况在单线程的情况下可以很好实现,而在多线程的情况下就可能会出现bug,导致程序逻辑改变。比如对于下面这行代码:
Test t=new Test();
它其实总共有三个步骤:
1.创建内存空间
2.往这个内存空间构造一个对象
3.将这个内存引用赋给t
在单线程的情况下2,3互换并不会有上面影响,但假如在多线程情况下我们按1,3,2来执行,当执行到3时t为非null,此时线程2读取t,但是却发现是一个无效对象。
到此这篇关于浅谈java线程状态与线程安全解析的文章就介绍到这了,更多相关java线程状态与线程安全内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!