【剧前爆米花--爪哇岛寻宝】java--线程不安全的原因及解决方法
创始人
2025-05-31 07:59:10
0

作者:困了电视剧

专栏:《JavaEE初阶》

文章分布:这是关于线程安全相关的文章,在该文章中,我梳理了造成线程不安全的原因和使线程变安全的方法,希望对你有所帮助!

 

目录

线程的安全问题

什么是线程安全

线程不安全的原因

修改共享数据 

原子性

可见性

代码顺序性 

线程安全问题的解决

synchronized关键字

互斥

可重入

volatile关键字


线程的安全问题

我们在单线程的情况下,一般不会遇到线程的安全问题,但当我们进行多线程的编程时,多线程之间的并发并行机制,以及线程之间对CPU资源的抢占都会可能导致我们得到一些意料之外的结果。

什么是线程安全

想给出一个线程安全的确切定义是复杂的,但我们可以这样认为: 如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线 程安全的。

线程不安全的原因

修改共享数据 

这一点可以分为三个小类:

1.抢占式执行(根本原因)

2.多个线程修改同一个变量

   1)一个线程修改一个变量安全

   2)多个线程读取同一个变量安全

   3)多个线程修改不同的变量安全

3.修改操作不是原子的

举个栗子:

class Counter{public int count = 0;public void add(){count++;}
}public class ThreadDemo2 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread( ()->{for ( int i=0;i<10000;i++ ){counter.add();}});Thread t2 = new Thread( ()->{for ( int i=0;i<10000;i++ ){counter.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}
}

现在有这样的一段代码,我想要实现的功能就是设置两个线程,让每一个线程都做累加count一万次的功能,按照我们的逻辑来思考,最后主线程在两个线程执行完后输出的count值应该是两万,这是我们在单线程中的思维。但结果确实如此吗?

我们可以看到结果并不是两万,而是一个我们意料之外的数字,并且随着我们每次运行,这个结果都不同,这是为什么?

这就需要从计算机的底层来进行剖析了,我们在代码中执行“count++”这一句代码时,反应到计算机内部大致就是:

计算机先通过load指令将count的值从内存中取出来存到寄存器当中,然后再通过运算逻辑部件对寄存器中的count进行加一操作,完成后,再将寄存器中的值放回到内存中保存

在分析上大概是这种,我们的理想情况是t1线程执行完后,再执行t2线程,然后t2线程完整的执行完后在执行t1线程,但是在真实的计算机内部并不是这样的,由于线程的并发执行,这就导致一个count++代码可能并没有执行完就切换到另一个线程去了,这就导致了很多种不确定的情况,比如这种:

 

当出现这种情况的时候就会发现,虽然我们的count++执行了两次,但最后保存到内存中时,只会保存执行一次的结果,还有很多种其他的情况,这些情况有的会影响结果有的不会,在这种混乱的状况下,我们根本无法得到一个准确的值,更别说我们想要的值了。 

原子性

这个原子性和之前的事物的原子性类似,都是表示一种不能分的概念。

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。 那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。

和上述举得count的例子一样,我们要想解决这类问题必须要让我们进行的操作具备一种原子性,即不执行完不能进行其他的操作。 

可见性

可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.

                                                                                                                                               

线程之间的共享变量存在 主内存 (Main Memory). 每一个线程都有自己的 "工作内存" (Working Memory) . 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据. 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

这里的主内存就是硬件角度的内存,而这里的工作内存就是寄存器,这里的代码不安全具体体现在,主内存对数据的修改无法及时的更新,举个栗子:

线程1需要对a进行修改,线程2也需要a这个数据,假设a的大小是10,然后进行修改后变成了20,由于工作内存的速度远远大于主内存的读写速度,所以此时修改后的20并不会及时地传入主内存中,于是在这期间线程2取得值还是10,这就造成了错误,也就是线程不安全。

代码顺序性 

代码的顺序性比较复杂,这里通过一个栗子来进行解释:

比如说我现在需要干三件事。1.去前台拿钥匙。2.完成一个试卷。3.去前台拿一盒粉笔。

如果 我们是单线程,那么计算机会自动的帮我们进行优化,即不按123的顺序执行而是按132的顺序执行,这样我们就会节约一次去前台的时间,而如果我们是多线程,当我们不按顺序执行,未执行2而执行了3这样就可能会造成一些问题,比如在完成两个任务的时间后其他线程需要进行批改试卷,而此时这个线程的试卷还没开始写...

这就造成了线程安全的问题。

线程安全问题的解决

synchronized关键字

互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待. 进入 synchronized 修饰的代码块, 相当于 加锁 退出 synchronized 修饰的代码块, 相当于 解锁
理解 "阻塞等待". 针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁。

换个角度思考,加上锁的代码块就是让这个代码块具有原子性,即对于这个代码块来说,必须要等当前线程执行完代码段内的操作其他线程才能执行,对于这个代码块中的内容cpu只能串行执行。 

这时候可能有人会问了,这和join有什么区别?

这是个好问题,首先最重要的一点是,初心不一样:synchronized通过给代码段上锁,赋予一段操作原子性,然后当这段代码执行结束时,其他被synchronized修饰的代码段再通过锁竞争进行执行,其本质是为了保证线程安全,而join则是完全等待另一个线程执行完,可能是另一个线程有当前线程需要的内容等等,总之不是为了线程安全考虑。

其次对于线程的并发而言,synchronized只是将那一段代码块进行上锁,即串行,其他需要执行的依然会并发执行,而join则是让整个线程进行等待,效率比上锁更慢。

可重入

volatile关键字

volatile 修饰的变量, 能够保证 "内存可见性"。 代码在写入 volatile 修饰的变量的时候, ->改变线程工作内存中volatile变量副本的值 ->将改变后的副本的值从工作内存刷新到主内存 代码在读取 volatile 修饰的变量的时候, ->从主内存中读取volatile变量的最新值到线程的工作内存中 ->从工作内存中读取volatile变量的副本

归根结底,volatile关键字修饰的变量就是,让其每次读写都强制访问主内存,而不仅仅是工作内存,这样虽然降低了运行的效率,但是却也避免了代码可见性相关的问题,是线程安全。

举个栗子:

public class ThreadDemo2 {public static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread( ()->{while (flag == 0){//空循环}System.out.println("结束");});Thread t2 = new Thread( ()->{Scanner reader = new Scanner(System.in);System.out.println("输入flag");flag = reader.nextInt();});t1.start();t2.start();}
}

对于这段代码会发现,当我输入0时,t1线程并没有结束,这是为什么?

原因是,由于在t1的循环中我的是空循环,所以while()中的判断语句的执行时间远远大于循环体的执行时间,计算机为了提高效率就会进行优化,他不在每次都从主内存中读取flag而是直接读取工作内存中flag的副本以此来加快速度,所以只要我们加上volatile修饰就好。

以上就是本篇博客的全部内容,如有疏漏还请指正! 

相关内容

热门资讯

光棍节短信祝福语 把忧愁赶光光... 1.光棍节要到了,光棍们千万别忧愁。光棍也有光棍的好,工资不用月月交,聚会不用管时间,早上可以睡懒觉...
Python数据结构与算法(p... 主要内容:什么是列表查找顺序查找二分查找一、什么是查找?(...
关于职业规划,你必须熟知的几个... 一、自我评估/了解自己  审视自己,发现你的兴趣,技能,个性特征和价值观。当你和自己的朋友,家人,教...
软件测试工程师职业生涯规划 软...  软件测试工程师职业生涯规划  软件测试工程师职业生涯阶段发展方向分析:  由于新时代下国内软件测试...
最新或2023(历届)教师职业... 【篇一】  步入教堂,作为一名人民教师,让我拥有神圣而光荣的使命。在实际教学工作中我以先进的教育理念...
大学生职业规划书范文 工程造价... 范文一】  姓名:杨春梅  专业:工程造价(四)  学号:12607最新或2023(历届)1433 ...
职业生涯规划书模板范例 职业生... 第一章 认识自我  1.职业兴趣  我是一个相对来说不是很外向的男孩,因而我不是很喜欢过于外向的工作...
大学生职业生涯规划书范文精选 ...  大学生职业规划书模板  【1】会计专业大学生的职业生涯规划书  (一)形式分析  在我国现阶段,全...
大学生职业规划书模板 澶у鐢... 【1】  在大学生就业形势越来越严峻的今天,职业规划开始成为大学生争夺战中的另一重要利器。对企业而言...
如何制定大学生职业生涯规划 职...  职业生涯规划(career planning)简称生涯规划,又叫职业生涯设计,是指个人与组织相结合...
最新版大学职业生涯规划书范文 ...   大学职业生涯规划  一个人职业是否有升迁发展机会,是由他的智慧能力、父母经济社会地位、本人对权势...
[JAVA]封装 目录 1.封装的初步认识 2.封装的实现 2.1private访问限定符 2.2封装的实现举例 ...
Python 学习 Python环境搭建conda 基础命令python 基础命令if __name__ =&#...
精选关于大学生职业规划范文 大...   大学生职业规划  【1】大学生职业生涯规划书范文  一、前言  俗话说:"志不立,天下无可成之事...
关于大学生职业生涯规划范文 数...   大学生职业生涯规划  【1】  一、自我认知  通过人才测评分析结果以及本人对自己的认识、朋友对...
护理职业生涯规划书范文 大专护...   护理职业生涯规划书范文  【1】护理系大学生职业生涯规划书范文  前言  在今天这个人才竞争的时...
大学生职业规划书范文【推荐】 ... 一 个人概况  姓名:张银平 性别: 女  出生年月:1989年12月5日 健康状况: 良好  毕业...
会计职业生涯规划书模板精选 会... 【1】会计职业生涯规划书范文  人生因为有梦想而精彩,一个人有梦想并且不懈的追求的人,才能有亮丽而耀...
Redis 内存管理(淘汰策略... 目录Redis 内存管理(淘汰策略/删除策略)一、淘汰算法1、LRU算法...
用问答的方式学习Require 我需要在vue3中引入require,该怎么导入? 在 Vue 3 中...