浅谈java线程状态与线程安全解析
作者:invictusQAQ 发布时间:2023-05-11 19:20:25
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,但是却发现是一个无效对象。
来源:https://blog.csdn.net/weixin_60778429/article/details/125959468


猜你喜欢
- 这篇文章主要介绍了springboot redis分布式锁代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价
- 表单代码<!DOCTYPE html><html lang="en" xmlns="http
- 为什么使用JUnit5JUnit4被广泛使用,但是许多场景下使用起来语法较为繁琐,JUnit5中支持lambda表达式,语法简单且代码不冗余
- 1. mapper.xml设置resultTyperesultType="com.alibaba.fastjson.JSONObj
- Gesture在 ViewGroup中使用GestureDetector类可以让我们快速的处理手势事件,如点击,滑动等。使用GestureD
- 简介 相信很多人都接触spring框架很长时间了,每次搭建spring框架的时候都需要配置好多的jar、xml,做很多繁琐重复的配置,稍微不
- 这种结果的原因在于,Random()函数的默认种子是时间,但在循环中产生随机数时,由于运算速度太快,用做种子的时间是相同的(毫秒级),因此产
- 串口通讯是一种计算机常用的数据传输方式。程序运行如下:首先,检查计算机的串口,并获取所有串口信息。private void CheckPor
- 在Servlet2.5中,我们要实现文件上传功能时,一般都需要借助第三方开源组件,例如Apache的commons-fileupload组件
- 前言我们平时在开发的时候,发起网络请求前,会需要显示一个Loading,一般的做法都是在xml布局上添加好Loading,然后在Activi
- 本文实例中的自定义类PictureBox继承于UserControl,最终实现简单的分屏功能。分享给大家供大家参考之用。具体实现代码如下:p
- 就网络和应用程序而言,键盘快捷键很重要,今天我们要谈的便是让这类快捷键得以在Flutter运作的小部件:Focus、Shortcuts和Ac
- 消息都是存放在一个消息队列中去,而消息循环线程就是围绕这个消息队列进入一个无限循环的,直到线程退出。如果队列中有消息,消息循环线程就会把它取
- swagger简介Swagger是一款RESTful接口的文档在线自动生成、功能测试功能框架。一个规范和完整的框架,用于生成、描述、调用和可
- 从SpringMVC源码解析所用的例子,一个http://localhost:9090/web/hi?name=yang请求调用到下面的地方
- 在编程中经常需要使用到表格(报表)的处理主要以Excel表格为主。下面给出用java写入数据到excel表格方法:1.添加jar文件java
- 有时候我们做Android开发,需要弹一个用户提示,但是有时候设计的提示弹窗是带有图片的,我们每次写一个特别麻烦。所以我特地封装了一个工具类
- Lua是目前国内使用最多的热更语言,基于Lua的热更框架也非常多,最近学习了一下ToLua的热更框架,主要使用的问题在于C#和Lua之间的互
- 把做工程过程常用的一些代码段做个记录,下面代码是关于Java实现图片倒影的的代码,应该对大家有较大用处。 public class
- 对Android的SD卡进行读取权限设置时: <uses-permission android:name="android.