Java中线程状态+线程安全问题+synchronized的用法详解
作者:一枚小比特 发布时间:2023-08-23 08:38:07
java中的线程状态🥇
在操作系统层面,一个线程就两个状态:就绪和阻塞状态.
但是java中为了在线程阻塞时能够更快速的知晓一个线程阻塞的原因,又将阻塞的状态进行了细化.
NEW:线程对象已经创建好了,但是系统层面的线程还没创建好,或者说线程对象还没调用start()
TERMINATED:系统中的线程已经销毁,但是代码中的线程对象还在,也就是run()跑完了,Thread对象还在
RUNNABLE:线程位于就绪队列,随时都有可能被cpu调度执行
TIMED_WAITING:线程执行过程中,线程对象调用了sleep(),进入阻塞,休眠时间到了,就会回到就绪队列
BLOCKED:有一个线程将一个对象上锁(synchronized)之后,另一个线程也想给这个对象上锁,就会陷入BLOCKED状态,只有第一个线程将锁对象解锁了,后一个线程才有可能给这个对象进行上锁.
WAITING:搭配synchronized进行使用wait(),一旦一个线程调用了wait(),会先将所对象解锁,等到另一个线程进行notify(),之后wait中的线程才会被唤醒,当然也可以在wait()中设置一个最长等待时间,防止出现死等.
线程安全问题案例分析👂
多线程对同一变量进行写操作🏎
概念:一串代码什么时候叫作有线程安全问题呢?首先线程安全问题的罪恶之源是,多线程并发执行的时候,会有抢占式执行的现象,这里的抢占式执行,执行的是机器指令!那一串代码什么时候叫作有线程安全问题呢?多线程并发时,不管若干个线程怎么去抢占式执行他们的代码,都不会影响最终结果,就叫作线程安全,但是由于抢占式执行,出现了和预期不一样的结果,就叫作有线程安全问题,出bug了!
典型案例:使用两个线程对同一个数进行自增操作10w次:
public class Demo1 {
private static int count=0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
for(int i=0;i<50000;i++){
count++;
}
});
t1.start();
Thread t2=new Thread(()->{
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
//打印结果:68994
显然预期结果是10w,但算出来就是6w多,这就是出现了线程安全问题.
分析原因:
仅针对每个线程的堆count进行自增的操作:首先要明白,进行一次自增的机器指令有三步:从主内存中把count值拿到cpu寄存器中->把寄存器中的count值进行自增1->把寄存器中的count值刷新到主内存中,我们姑且把这三步叫作:load->add->save
我们假设就是在一个cpu上(画两个cpu好表示)并发执行两组指令(就不会出现同时load这样的情况了):
如出现上图的情况:
观察发现:两个线程都是执行了一次count++,但是两次++的结果却不如意,相当于只进行了一次自增,上述就是出现了线程安全问题了.
并且我们可以预测出上述代码的结果范围:5w-10w之间!,为什么呢?
上面两张图表示的是出现线程安全问题的情况,表现的结果就是两次加加当一次去用了,如果两个线程一直处于这样的状态(也是最坏的状态了),可不就是计算结果就是5w咯,那如果两个线程一直是一个线程完整的执行完load-add-save之后,另一个线程再去执行这样的操作,那就串行式执行了,可不就是10w咯.
3.针对上述案例如何去解决呢?
案例最后也提到了,只要能够实现串行式执行,就能保证结果的正确性,那java确实有这样的功能供我们使用,即synchronized关键字的使用.
也就是说:cpu1执行load之前先给锁对象进行加锁,save之后再进行解锁,cpu2此时才能去给那个对象进行上锁,并进行一系列的操作.此时也就是保证了load-add-save的原子性,使得这三个步骤要么就别执行,执行就一口气执行完.
那你可能会提问,那这样和只用一个main线程去计算自增10w次有什么区别,创建多线程还有什么意义呢?
意义很大,因为我们创建的线程很多时候不仅仅只是一个操作,光针对自增我们可以通过加锁防止出现线程安全问题,但是各线程的其他操作要是不涉及线程安全问题那就可以并发了呀,那此时不就大大提升了执行效率咯.
4.具体如何加锁呢?
此处先只说一种加锁方式,先把上述案例的问题给解决了再说.
使用关键字synchronized,此处使用的是给普通方法加synchronized修饰的方法(除此之外,synchronized还可以修饰代码块和静态方法)
class Counter{
private int count;
synchronized public void increase(){
this.count++;
}
public int getCount(){
return this.count;
}
}
public class Demo2 {
private static int num=50000;
public static void main(String[] args) {
Counter counter=new Counter();//此时对象中的count值默认就是0
Thread t1=new Thread(()->{
for (int i = 0; i < num; i++) {
counter.increase();
}
});
t1.start();
Thread t2=new Thread(()->{
for (int i = 0; i < num; i++) {
counter.increase();
}
});
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter.getCount());
}
}//打印10W
内存可见性问题🍨
首先说明:这是有编译器优化导致的,其次要知道cpu读取变量时:先从主内存将变量的值存至缓存或者寄存器中,cpu计算时再在寄存器中读取这个值.
当某线程频繁的从内存中读取一个不变的变量时,编译器将会把从内存获取变量的值直接优化成从寄存器直接获取.之所以这样优化,是因为,cpu从主内存中读取一个变量比在缓存或者寄存器中读取一个变量的值慢成千上万倍,如果每每在内存中读到的都是同一个值,既然缓存里头已经有这个值了,干嘛还大费周折再去主内存中进行获取呢,直接从缓存中直接读取就可以了,可提升效率.
但是:一旦一个线程被优化成上述的情况,那如果有另一个线程把内存中的值修改了,我被优化的线程还傻乎乎的手里拿着修改之前的值呢,或者内存中的变量值被修改了,被优化的线程此时已经感应不到了.
具体而言:
public class Demo3 {
private static boolean flag=false;
public static void main(String[] args) {
Thread t1=new Thread(()->{
while(!flag){
System.out.println("我是优化完之后直接读取寄存器中的变量值才打印的哦!");
}
});
t1.start();
flag=true;
System.out.println("我已经在主线程中修改了标志位");
}
}
运行上述代码之后,程序并不会终止,而是一直在那打印t1线程中的打印语句.
如何解决上述问题:
引入关键字volatile:防止内存可见性问题,修饰一个变量,那某线程想获取该变量的值的时候,只能去主内存中获取,其次它还可以防止指令重排序,指令重排问题会在线程安全的单例模式(懒汉)进行介绍.具体:
public class Demo3 {
private static volatile boolean flag=false;
public static void main(String[] args) {
Thread t1=new Thread(()->{
while(!flag){
System.out.println("我是优化完之后直接读取寄存器中的变量值才打印的哦!");
}
});
t1.start();
try {
Thread.sleep(1);//主线程给t1留有充足的时间先跑起来
} catch (InterruptedException e) {
e.printStackTrace();
}
flag=true;
System.out.println("我已经在主线程中修改了标志位");
}
}
//打印若干t1中的打印语句之后,主线程main中修改标志位之后,可以终止t1
注意:上述优化现象只会出现在频繁读的情况,如果不是频繁读,就不会出现那样的优化.
指令重排序问题📦
生活案例:买菜
如果是傻乎乎的按照菜单从上到下的去买菜,从路线图可以看出,不必要的路是真的没少走.
如果执行代码时,编译器认为某些个代码调整一下顺序并不会影响结果,那代码的执行顺序就会被调整,就比如可以把上面买菜的顺序调整成:黄瓜->萝卜->青菜->茄子
单线程这样的指令重排一般不会出现问题,但是多线程并发时,还这样优化,就容易出现问题
针对这样的问题,如果是针对一个变量,我们可以使用volatile修饰,如果是针对代码块,我们可以使用synchronized.
synchronized的用法💃
synchronized起作用的本质
修饰普通方法
修饰静态方法
修饰代码块
synchronized起作用的本质🤙
因为我们知道java中所有类都继承了Object,所以所有类都包含了Object的部分,我们可以称这继承的部分是"对象头",使用synchronized进行对象头中的标志位的修改,就可以做到一个对象的锁一个时刻只能被一个线程所持有,其他线程此时不可抢占.这样的设置,就好像把一个对象给锁住了一样.
修饰普通方法📲
如前述两个线程给同一个count进行自增的案例.不再赘述.此时的所对象就是Counter对象
修饰静态方法⚡️
与普通方法类似.只不过这个方法可以类名直接调用.
修饰代码块🍼
首先修饰代码块需要执行锁对象是谁,所以这里可以分为三类,一个是修饰普通方法的方法体这个代码块的写法,其次是修饰静态方法方法体的写法,最后可以单独写一个Object的对象,来对这个Object对象进行上锁.
class Counter{
private int count;
public void increase(){
synchronized(this){
count++;
}
}
public int getCount(){
return this.count;
}
}
class Counter{
private static int count;
public static void increase(){
synchronized(Counter.class){//注意这里锁的是类对象哦
count++;
}
}
public int getCount(){
return this.count;
}
}
class Counter{
private static int count;
private static Object locker=new Object();
public static void increase(){
synchronized(locker){
count++;
}
}
public int getCount(){
return this.count;
}
}
注意:java中这种随手拿一个对象就能上锁的用法,是java中一种很有特色的用法,在别的语言中,都是有专门的锁对象的.
Conclusion💯
java中的线程状态,以及如何区分线程安全问题 罪恶之源是抢占式执行多线程对同一个变量进行修改,多线程只读一个变量是没有线程安全问题的修改操作是非原子性的内存可见性引起的线程安全问题指令重排序引起的线程安全问题 synchronized的本质和用法
1.java中的线程状态,以及如何区分
2.线程安全问题
罪恶之源是抢占式执行
多线程对同一个变量进行修改,多线程只读一个变量是没有线程安全问题的
修改操作是非原子性的
内存可见性引起的线程安全问题
指令重排序引起的线程安全问题
3.synchronized的本质和用法
来源:https://blog.csdn.net/weixin_55667484/article/details/124080608


猜你喜欢
- 对于一个简单的tcp通讯这里我就不再讲述了,今天主要为大家讲解下,如何从::recv中筛选出一个完整包逻辑。就简单的以客户端为例(服务器接收
- JetBrains JVM Debugger Memory View plugin在我最近的研发活动期间寻找新的工具,以提高我的开发经验,使
- git仓库直达List<String> strings = Lists.newArrayList("name=kk&q
- 学习Java 本身是一个挺枯燥的过程,说白了就是每天敲代码而已。但如果换一种思路,可以编写各种各样的程序,不仅加深对代码的理解,同时提高兴趣
- 从Java 5开始,Java语言对方法参数支持一种新写法,叫 可变长度参数列表,其语法就是类型后跟...,表示此处接受的参数为0到多个Obj
- 实现Java多态性的时候,关于方法调用的优先级:我们这样假设下,super(超类)、this(当前类对象)、show(方法)、object(
- 需求场景最近项目中要做一个音乐播放悬浮按钮的功能,最终实现效果如下:问题暴露悬浮窗布局文件就不放了,就是水平LinearLayout里面放几
- Comparable 比较器,内置定义的比较方法,实现比较 较简单Comparator 策略模式,需要定义不同的策略和比较的对象,实现比较
- springBoot项目启动多个实例今天碰到一个需求是,将一个服务提供者启动两个实例,一个实例对外,一个实例对内,对内价格有折扣,两个实例通
- 本文主要介绍了隐式Intent匹配目标组件的规则,若有叙述不清晰或是不准确的地方希望大家指出,谢谢大家: )1. Intent简
- 本文实例讲述了C#正则过滤HTML标签并保留指定标签的方法。分享给大家供大家参考,具体如下:这边主要看到一个过滤的功能:public sta
- 多线程编程多线程编程模式.NET 中,有三种异步编程模式,分别是基于任务的异步模式(TAP)、基于事件的异步模式(EAP)、异步编程模式(A
- 序章简介:bean的加载控制指根据特定情况对bean进行选择性加载以达到适用项目的目标。根据之前对bean加载的八种方式,其中后面四种是可以
- 本文实例讲述了Android编程之蓝牙测试。分享给大家供大家参考。具体分析如下:一、软件平台:win7 + eclipse + sdk二、设
- 本文实例讲述了Java实现爬取百度图片的方法。分享给大家供大家参考,具体如下:在以往用java来处理解析HTML文档或者片段时,我们通常会采
- 背景介绍1,最近有一个大数据量插入的操作入库的业务场景,需要先做一些其他修改操作,然后在执行插入操作,由于插入数据可能会很多,用到多线程去拆
- Java BlockingQueue接口java.util.concurrent.BlockingQueue表示一个可以存取元素,并且线程安
- 本文实例讲述了C#使用有道ip地址查询接口方法。分享给大家供大家参考。具体实现方法如下:#region 读取http://www.yodao
- 记录一下微信第三方实现登录的方法。还是比较简单。一、必要的准备工作1.首先需要注册并被审核通过的微信开放平台帐号,然后创建一个移动应用,也需
- Servlet简介servlet是Server Applet的简称,翻译过来就是服务程序.好吧,这么说你可能还是不太懂,简单的讲,这个ser