目录

前言

一、引发线程安全的原因

1.抢占式执行

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

3.操作是原子的

4.指令重排序

5.内存的可见性问题

对于线程不安全问题,如何解决?

Java的代码中如何进行加锁呢?

这个锁具体是怎么执行的呢?

思考:要加锁的代码不是在一个方法里,怎么办?


前言

本篇围绕理解引发线程安全的原因以及如何解决;


一、引发线程安全的原因

1.抢占式执行

多线程调度的过程,可以是认为“随机”的,没有规律;

例如:你定义了一个变量count,执行了count++这种操作,本质上是三个CPU指令,load、add、save,而CPU执行指令都是以一个指令为单位顺序进行的,试想,有两个线程同时执行count++操作,这些一个一个的指令就会抢占执行,线程一的add的操作刚完,线程二的add就抢占了下一个位置...

总结:线程抢占式执行,是线程的不安全的万恶之首,并且是内核实现,咱是无能为力的~

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

一个线程修改一个变量,没事。多线程修改不同变量,也没事,多个线程读取一个变量,还没事,但如果多线程修改同一个变量,那就有问题了;

就像刚刚提到的抢占式执行的例子,如果一个变量count,进行count++这种操作,分load、add、save,要说线程一二修改不同变量倒也没事,互不干扰,然如果修改同一变量,就会出现以下情况:

穿插一个问题:String是不可变的对象,这样设计有什么好处?

        这里好处很多,其中一点就是“线程安全”,因为这个线程安全问题,甚至还有的编程语言(尤其是“函数式编程”语言)就广泛的使用了不可变的概念,比如erlang,就没有变量这个东西,所有数据都是不可变的,要想修改数据,很抱歉,不行~只能重新创建一个,还有人可能就会说,那太占内存了~其实,格局打开,现在可是21实际,咱缺的是内存吗?时代变了,内存以及不值钱了;

总结:这里确实可以通过调整代码,来避免线程安全问题,但是以及适用性不高;

3.操作是原子的

实际上理解了多线程修改同一变量这里我引出的例子和画的图,这个也就不难理解,首先什么是原子?原子表示不可分割的最小单位,CPU执行指令是一条一条执行的,这一条一条的指令就可以理解为原子,也正因为这是原子的才会引发上述的多线程修改同一变量会引发线程安全;

结论:既然上述1,2都没有方法很好的解决线程安全问题,那么咱就试试从这入手——修改操作,使其不是原子的~(不卖关子了,实际上从修改操作,使其不是原子的,也是最常见的办法),也就是说,咱可以把这些多个原子操作包装成一个原子操作!(例如可以把刚刚所说的count++这个例子的的三条指令包装成一个);

4.指令重排序

什么是指令重排序?假设咱写了一段Java代码,并且希望运行程序的时候代码的执行顺序与我们所写的顺序一致,但实际上,编译器、JVM、CPU处于优化目的对实际指令进行顺序上的调整,这便是指令重排序;JVM的代码优化本身没什么太大的问题,但是一旦使用了多线程,这里的优化就会引发线程不安全;

JVM的代码优化,这里的优化又是什么呢?

举个栗子:就像程序员敲代码,总会大佬和菜鸡,大佬写出的代码,往往是很高效的,而菜鸡呢,写出的往往是一些很低效的代码,这时候写编译器的大佬就想到,让编译器具有一定的代码优化功能,将菜鸡写出的代码在逻辑不变的前提下优化成大佬那种高效的代码,这便是编译器优化;

再举个栗子:你去leetcode上刷题

面对这样一个页面,作为一个小白,肯定是先一同瞎点,最后得出经验,哦,原来要这样去刷题,但如果然后有人站出来说,你因该怎么怎么去刷题,比如,对于新手,应当上来选择刷简单题:

然后呢,选择通过率高的,出现频率高的,然后再去慢慢刷中等,困难题...这样下来,少走很多弯路,就很高效了;

        话又说回来,但这里优化是优化了,对于多线程很多不可预测性的问题,还是不能很好的解决~

总结:JVM的代码优化再多线程情况下,也会带来一些BUG;

5.内存的可见性问题

内存可见性问题就是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化,若是看不到,那就要出问题了;

例如,一个线程负责读数据,另一个线程负责修改数据;

先来看看如下代码:

这里的while(test.count == 0),就是要先从内存中读取count的值(LOAD操作),再到寄存器中与0进行比较(CMP操作),这里while会循环的进行这个操作(非常快),而我们知道的是,CPU读写数据最快,内存次之(与CPU差3~4个数量级),硬盘最慢(与内存差3~4个数量级);所以LOAD从内存中读取数据操作的速度相对于在CPU上进行CMP操作就要慢的多,那么编译器就要偷懒了,既然频繁的LOAD读取count这个数据,多次执行的结果还都是一样,干脆LOAD就只执行一次;因为一般没有人改这个代码,编译器就认为读到的结果都是固定的,就做出了一个大胆的优化——只读一次,效率大大提升 !

        这时候又出现了个线程t2,他就说:谁说没人改这个代码?俺就来~

如下代码:

这时我们运行代码,来看看效果:

分析:这时可以发现, 当输入数字6时,相当于修改了count这个变量的值为6,按理来说t1线程的run方法中count只要不等于0就会停下来,可是程序依旧没有停止,就出现了内存可见性问题;有人可能就要问了,编译器优化,不是因该在代码逻辑不变的情况下优化吗?这里的优化却让其逻辑变了?这里要注意,编译器优化,在多线程情况下可能存在误判!既然编译器自己判定不了,这时候就该我们程序猿出场了——使用volatile关键字(博主下一章会整理出专门的博客,来讲讲volatile)

        


对于线程不安全问题,如何解决?

上面提到操作是原子的,我们可以从这里入手,将count++这个操作的三个布置包装成一个步骤,如何做呢——“加锁”

举个栗子,刘华强前来买瓜,给老板指着那个最好的瓜说:老板,这瓜我要了,我先去办点事,一会再来拿走;老板同意了,这就相当于是从华强买瓜离开办事,到华强办完事来取瓜这个区间加锁,而华强不在的这个期间,别人不可以动这个瓜;

类似的count++之前加锁,count++之后再解锁,别的线程若是想在加锁和解锁之间进行需修改,很抱歉,修改不了,别的线程只能处于阻塞等待的线程状态(BLOCKED状态);

Java的代码中如何进行加锁呢?

        使用synchronized关键字,这时最基本的使用,它用来修饰一个普通方法,当进入方法的时候,就会加锁,方法执行完毕,就会解锁;如下图

这个锁具体是怎么执行的呢

锁具有抢占特性,如果这个锁没人加,有人想加,就可以立即加上,若这个锁以及被人加上了,加锁操作就会阻塞等待;如刚才的栗子,count++分三步进行,load、add、save,而线程调度是随机的过程,一旦这两个线程同时调用,这两组三个操作就会进行排列组合,就会产生线程不安全,现在使用锁,就可以使这三个操作串行执行了;如下

分析:这个操作就将“并发执行”变成了串行执行,这个操作就会减慢执行效率,但是保证了线程安全,正所谓鱼与熊掌不可兼得也~

值得注意的是,加锁(lock->unlock这个区间)不是说CPU一鼓作气执行完,中间也是有调度切换的,即使线程一切换走了(比如执行到add切换走了),线程二仍然是BOLCKED状态,无法在CPU上运行的;

思考:要加锁的代码不是在一个方法里,怎么办?

synchronized除了修饰方法,还可以用来修饰代码块,把要进行加锁的逻辑放到代码块之中,就能起到加锁的效果:

这个()里需要填什么呢?填的东西就是你要针对哪个对象进行加锁(被加锁的对象称为“锁对象”);

比如线程一和线程二要对同一个对象的加锁,就会产生锁竞争,也就是说,如果线程一加锁成功了,线程二只能阻塞等待,若两个线程对不同对象加锁,就不会产生锁竞争,各执其职,不会有阻塞等待

在Java中,任何对象都可以作为锁对象,所以写多线程代码的时候,不关心锁对象是谁,是那种形态。只是关心,两个线程是否锁的是同一个对象,只要锁的是同一个对象,就会产生锁竞争,有了锁竞争,就保证了线程安全,如下,我任意定义一个锁对象是谁都无所谓:

还有一种常用的写法——this,这便是谁调用了add方法,谁就是this~

最后注意:多线程的的代码,切勿无脑操作,很多情况下写this都没什么问题,具体还是要看实际需求,希望在什么场景下产生竞争,那些场景下不需要竞争,对于锁对象的设置都是不同的!

引发线程安全的原因是什么?怎么解决?程序员一定要掌握的东西相关推荐

  1. 程序员加班一般是有原因的,但是有些程序员却表示:我是自愿的!

    有人说,程序员就是把咖啡变成代码的机器.我想说,程序员就是满天星辰下敲着代码.喝咖啡的机器. 在编程界,加班就是潜规则.程序员加班还有加班费,一个月下来薪资收入颇为丰厚. 程序员为什么经常加班呢?下面 ...

  2. 5大原因告诉你,Python程序员为何如此难招!

    现在程序员的现状是什么样的?程序员有很多,好的Python程序员还是供不应求的.Python开发的工资一般多少?一般而言,Python程序员的收入水平不低.在一线城市,程序员的平均收入应该都能达到该市 ...

  3. python软件是什么原因引起的_Python对程序员重要的原因在哪里?

    Python 之父Fredrik Haard最近发表了一篇"为什么Python对你如此重要"的文章,引起了开发者的热烈讨论. 我相信Python对软件开发人员很重要.现今已经诞生了 ...

  4. 程序员离职原因的最佳回答_程序员面试被问离职原因,如实回答不适应996,面试官答复尴尬了...

    跳槽面试是一个技术活,特别是面试环节,需要在简短的时间里回答大量问题.如果稍不小心,可能就会导致哭笑不得的结局. 最近,有个程序员就在网上吐槽说,自己原单位加班很严重,长期996(即早上9嗲上班,晚上 ...

  5. 是什么原因让你选择做程序员

    穷! 这是大多数程序员给出的答案,程序员,一不拼爹妈背景,二不拼社会关系,三不要本地户口,四不用花钱读班,只要买台电脑,能上网找资料看公开课,自己努力点,代码写好了,又肯加班,就能挣得多. 你,一无才 ...

  6. 那些程序员身上共有的属性,这就是他为什么比你进步快的原因!

    1. 发展全面. 深入了解一门技术虽然很好,但是现实世界中的问题从来都无法仅靠一种技术就能够解决.即使别人雇佣你为专业技术人员,你仍然需要明白你所掌握的技术如何与组成应用生态系统的其他软件.硬件和网络 ...

  7. 程序员“不会”修电脑的原因

    老生长谈的问题,程序员会不会修电脑.(修电脑指一般简单软件安装,硬件组装) 程序员应该会,为什么,因为计算机专业基础课程有一门是计算机组成原理,学完也对计算机有些一些基本的了解吧.从硬件来看,简单一些 ...

  8. 调试JDK源码-Hashtable实现原理以及线程安全的原因

    调试JDK源码-一步一步看HashMap怎么Hash和扩容 调试JDK源码-ConcurrentHashMap实现原理 调试JDK源码-HashSet实现原理 调试JDK源码-调试JDK源码-Hash ...

  9. java 线程安全的原因_Java并发编程——线程安全性深层原因

    线程安全性深层原因 这里我们将会从计算机硬件和编辑器等方面来详细了解线程安全产生的深层原因. 缓存一致性问题 CPU内存架构 随着CPU的发展,而因为CPU的速度和内存速度不匹配的问题(CPU寄存器的 ...

最新文章

  1. 使用OpenCV执行图像算法(加法和减法)以提亮图像或者使图像变暗
  2. jquery实现抽奖系统
  3. boost::tuple用法的测试程序
  4. java 编写代码_Java 7:如何编写非常快速的Java代码
  5. 论文浅尝 | 通过文本到文本神经问题生成的机器理解
  6. Android官方开发文档Training系列课程中文版:管理音频播放之音频输出硬件的处理
  7. 华北水利水电大学c语言程序设计四_我校代表队在“中国高等计算机大赛——团体程序设计天梯赛” 中喜获佳绩...
  8. OpenCv之Canny边界检测(笔记13)
  9. java 将 ResultSet 转化为 json格式
  10. Android报错:FAILED:_nl_intern_locale_data: ?? ‘cnt < (sizeof (_nl_value_type_LC_TIME)
  11. 数据库语句删除数据库
  12. lcd1602c语言编程原理,lcd1602工作原理是什么?
  13. 微信8.0下载(可抓包)
  14. IDEA配置JAVA11
  15. 解决微信小程序wx:if使用不了函数,WXS使用方法以及防踩坑
  16. 音响的灵魂! 世界顶级扬声器品牌介绍
  17. VS2019+WDK10编写xp平台的驱动
  18. 【LiteOS】华为LiteOS开发初体验
  19. 0321 复利计算—贷款
  20. Linux的基础配置

热门文章

  1. Python输出字典的键和值
  2. 手机上的悬浮球这么好用,简直就是宝藏功能,难怪这么多人都在用
  3. 香港Paypal申请指南
  4. ANTLR 权威参考 第一章 开始antlr
  5. C#接口与抽象类的区别
  6. 卖身、离场、坚持、转机:属于智能手机的2018
  7. swig java_2019-02-01 使用swig转化C++到Java
  8. Android的WakeLock机制
  9. java 关闭输入密码_为什么不能实现输入密码 3 次错误后不能自动关闭页面
  10. 用php制作中奖系统,php简单中奖算法(实例)