多线程之线程安全 - javaee
创始人
2025-06-01 16:40:20
0

前言

本篇通过了解线程不安全产生的原因,解决线程不安全的方式,一般出现线程不安全的情况的问题,如有错误,请在评论区指正,让我们一起交流,共同进步!


文章目录

  • 前言
  • 1. 多线程执行产生的线程不安全问题
  • 2. 线程安全
      • 线程的抢占式执行 <=> 根本原因
      • 多个线程修改同一个变量
      • 线程的修改,不是原子性的
      • 内存可见性问题
      • 指令重排序问题
  • 总结

本文开始

1. 多线程执行产生的线程不安全问题

问题产生:
在多线程情况下,线程的无序调度,会产出bug, 称之为线程不安全问题;

原因产生:
通过下面例子认识一下 !
两个线程通过调用同一个类同一个方法add, 一起计算变量count的值,每个线程调用方法一次,使变量count自增一次,每个线程都调用方法add 10000 次,最后想要得到结果是20000;但是结果确与我们想的不一样,通过代码来看一下吧!
两个线程调用同一个方法代码实现(有线程安全问题):

class Sum{private int count = 0;public void add() {count++;}public int getCount(){return count;}
}
public class ThreadDemo2 {public static void main(String[] args) throws InterruptedException {Sum s = new Sum();Thread t1 = new Thread( () -> {for (int i = 0; i < 10000; i++) {s.add();}});Thread t2 = new Thread( () -> {for (int i = 0; i < 10000; i++) {s.add();}});//启动线程t1.start();t2.start();//线程等待t1.join();t2.join();//获取计算结果System.out.println(s.getCount());}
}

结果如下:

在这里插入图片描述

上述结果只是一次执行的结果,可以多次执行,通过执行结果可以发现,每次执行的结果都会小于20000,这就是线程安全产生的问题;

为什么会产生上述结果的原因?
实际在执行调用add方法时,进行的自增++操作,在寄存器上是分三部执行的load,add,save, 这三种执行的顺序是无法确定的,所以可能产生寄存器增加一次,或者多次,但最后结果只显示增加1了次,产生的计算结果小于20000的这种情况;

【注】寄存器进行++操作本质:
load: 把内存数据读取到cpu寄存器中
add: 把寄存器中的值,进行+1 操作 =》增长1
save: 再把寄存器中的值写会到内存中

只是语言描述可能有点抽象,通过画图来进一步理解一下!!!

寄存器完整的自增操作在这里插入图片描述

上述是严格按照寄存器1先执行三部操作,寄存器2再执行三部操作,得到的结果才是2,如果三部操作前后执行顺序有交叉部分,可能就出现线程安全情况,从下图了解情况;

在这里插入图片描述

由上述图可得,自增两次,结果确只有一次,一次结果被覆盖的情况,这只是其中一种情况,中间可能有两次,甚至可多次自增情况被覆盖,造成虽然自增了很多次,但结果只有少数次. 这是因为多线程的调度是无序的,所以这三步的指令执行顺序也是不确定,这就产生了bug,导致结果会小于20000;

由此,我们再认识一下常见的线程不安全的原因!

2. 线程安全

为什么会出现线程安全?
在多线程情况下,线程的无序调度会造成线程安全 又称 线程抢占式执行

出现线程安全的原因:

线程的抢占式执行 <=> 根本原因

多个线程执行操作,不能确定操作的执行的顺序,这就是现线程的抢占式执行;

多个线程修改同一个变量

计算一个数字,定义一个变量,count计算,使用两个线程或多个线程对这个count变量进行++操作,由于++操作分为三部load, add, save这三部分执行的顺序不能确定,结果就可能产生某一次或两次操作被覆盖的情况,导致最后的结果是小于20000的;这就产生了bug;

完整代码参考最开始代码在这里插入图片描述

线程的修改,不是原子性的

什么是原子性?
原子:不能分割的最小单位,将一些操作看成整体,不能分开;
例如:++操作,它对应的CPU指令可以看作三部分,load,add,save, 如果分开执行就认为不是原子的了;必须将这3部分看作一个整体,再执行就可以看作是一个原子操作;

问题又来了,怎么给线程变成原子性呢? =》加锁
认识锁的两个操作:
① 加锁:当线程加锁后,其他线程必须等待此线程执行结束
② 解锁:线程解锁后,其他线程才能继续竞争这个锁;

加锁操作需要使用关键字:synchronized;
使用关键字修饰代码块,将线程中需要加锁的代码,都放入代码块中,这就实现了原子性;

加锁操作的目的:
加锁,就是让两个线程的部分代码串行化,大部分代码是并发的;
这就要与join区分一下了,join 让两个线程完整的进行串行化,而不是部分;
部分串行化例如:上述代码两个线程,调用一个方法add, add之前会创建循环变量i, 循环条件的判断, 调add之后count会++,给count加锁后,count之前的操作认为是并发的,线程1调用count执行完后,线程2再调用count这是串行化,count后面的执行返回,变量i++等操作也是并发的;

对上述2代码进行修改,对count进行加锁操作:最后得到的count结果就是20000

class Sum{private int count = 0;public void add() {synchronized (this) {count++;}}public int getCount(){return count;}
}

不同的加锁方式:

在这里插入图片描述

加锁操作中()括号:里面是加锁对象,不能是基本数据类型;
静态类加锁()括号中是类对象,如上图;
【注】类对象:表示.class文件的内容(方法,属性等)

在这里插入图片描述

内存可见性问题

通过一段代码,发现内存可见性问题:

    public static int flag = 0;//控制循环条件public static void main(String[] args) {Thread t = new Thread( () -> {while (flag == 0) {}System.out.println("循环结束!");});Thread t2 = new Thread( () -> {Scanner scanner = new Scanner(System.in);System.out.println("输入一个整数:");flag = scanner.nextInt();});t.start();t2.start();}

上述代码本来是,t线程执行循环为死循环,等t2线程执行输入操作更改flag的值,这样flag!=0, 条件为假循环结束;但是循环却没有结束,这个原因其实就是内存可见性问题;

判断while( falg == 0) 这个条件指令有两步
1.load: 从 内存中读取数据到cup寄存器上
2.cmp:比较寄存器中的值是否是0
【注】读取速度:寄存器 > 内存 > 硬盘
内存可见性问题的产生:
根据读取速度,发现load读取内存开销较大,因为是死循环,没有修改load的时候结果都是一样的,编译器就会做出优化操作,优化掉load; 这样只有第一次执行load, 后面操作只执行cmp比较操作;
这样就是t2线程更改了flag的值,但是寄存器不再读取,只用修改前的值,操作就会死循环,发生线程安全问题;

内存可见性: 在多线程环境下,编译器对代码进行优化,产生了误判(像上述代码认为flag的值没改),从而引起了Bug, 导致代码出错;
【注】编译器优化:智能调整代码执行逻辑,在保证程序结果不变的前提下,通过加减语句,语句变换,等一系列操作,让代码执行效率提升;
处理内存可见性问题:
使用volatile关键字:被volatile修饰的变量,编译器会禁止代码优化,从而保证每次都是从内存中重读取数据;
代码修改:

  volatile public static int flag = 0;//控制循环条件//volatile: 保证内存可见性

在这里插入图片描述

【注】volatile : 1.不保证原子性,适用场景一个线程读,一个线程写的情况;synchronized: 多个线程写;
2.volatile 禁止指令重排序

指令重排序问题

问题:由于一些代码操作的执行的顺序不同,可能会产生bug;
指令重排序: 编译器优化,保证整体逻辑不变,调整代码的执行顺序,让程序更高效;

例如new 对象操作,认为分为3步:
1.先申请内存空间
2.调用构造方法(初始化内存数据)
3.对象的引用赋值(内存地址的赋值)
线程1先执行1,3操作,2操作执行顺序并不确定,在此期间其他线程使用对象,调用其方法属性,虽然对象不为空,但是没有初始化,就可能会产生bug;
解决方式:给代码加volatile,创建的就会禁止指令重排序;


总结

✨✨✨各位读友,本篇分享到内容如果对你有帮助给个👍赞鼓励一下吧!!
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!

相关内容

热门资讯

最新或2023(历届)国庆节手...   小朋友们,国庆节假期七天长,应到户外去逛一逛;调整心态快乐无束,大自然中感受清风;吃吃美食来个几...
最新或2023(历届)国庆节手...   “出淤泥而不染,濯清涟而不妖”说的就是荷花~古人喜爱用荷花表达自己不受外界影响依然保持自己的君子...
最新或2023(历届)国庆节手...   在十月的桂花香中迎来了我们的十一国庆节~~我们都是爱祖国听妈妈话的好少年。让我们一起歌颂祖国:我...
最新或2023(历届)国庆节手...   在歌声中迎着朝阳成长“五星红旗迎风飘扬,胜利歌声多么响亮; 歌唱我们亲爱的祖国,从今走向繁荣富强...
最新或2023(历届)庆祝国庆...   也许我很渺小,所以写不出华丽的辞藻;也许我说的很少,但是我却在认真的思考。国庆节又来到,真心为你...
最新或2023(历届)关于国庆...   小朋友们,我愿我的祝福是小溪,悄悄地流淌在朋友的心上,绵绵不绝;我愿我的关怀是枫叶,轻轻地摇着秋...
最新或2023(历届)关于国庆...   小朋友们大家早上好啊,大家知道为什么要把十月一号定位国庆节呢,并不是中华人民共和国是在这一天建立...
最新或2023(历届)十一国庆...   大家早上好啊,也许在我们每个人的嘴上不是经常说着自己的祖国,可是我相信在我们的心中祖国永远都是很...
最新或2023(历届)最好的国...   每一阵清风细雨,都会让我将你轻轻想起;每一条祝福,都会将我们的心靠得更近;恰逢国庆之际,借此送上...
最新或2023(历届)关于国庆...   国庆节就是一个国家的节日。我们国家的国庆节是哪一天小朋友们知道吗?是农历的十月一日哦,大家会有很...
最新或2023(历届)最好的国...   小朋友们早上好啊,大家觉得在国庆的时候必不可少的是什么,当然是升国旗了,国庆节的时候都会在现在我...
最新或2023(历届)小学生国...   从1949年的中华人民共和国成立到现在,中华人民共和国已经走过了65年的风风雨雨,经过改革开放三...
最新或2023(历届)十一国庆...   国庆节是每个国家的重要节日,但是每个国家的叫法各有不同,有的国家叫国庆节或者是国庆日,还有一些国...
最新或2023(历届)关于国庆...   看到飞舞的五星红旗,大家知道五星红旗是谁设计的吗?是曾联松先生,他的设计稿从全国设计稿中脱颖而出...
最新或2023(历届)十一国庆...   大家早上啊,以前在国际上要是有人说自己是中国人,迎来的肯定是嘲讽和讽刺,但是现在你可以勇敢和骄傲...
关于五一放假的通知公告 杭州中... 劳作创造了前史,劳作创造了将来。下面小编为你精心收拾了几篇五一放假的告诉,期待阅览!  五一放假的告...
关于最新或2023(历届)五一... 五一劳作节即将来临,它是全国际劳作人民一起具有的节日,下面小编为你精心收拾了几篇五一放假的告诉,期待...
最新或2023(历届)学校5.... 中国人民庆祝劳动节的活动可追溯至1918 年。下面小编为你精心整理了几篇五一放假的告诉,欢迎阅览! ...
物业五一放假有通知吗 物业放假... 物业公司五一放假吗?下面小编为你精心整理了几篇五一放假的告诉,期待阅览!  五一放假的告诉【一】  ...
最新或2023(历届)国际劳动...  五一劳作节即将来临,它是全世界劳作人民一起具有的节日,下面小编为你精心收拾了几篇五一放假的告诉,期...