线程安全问题及解决办法(synchronized 、volatile)

我在前面介绍过线程的概念在此就不在赘述。线程的安全性对弈一个程序员来说是一个为重要的事情,线程不安全轻则导致结果出错,重则导致进程崩溃,并且线程安全问题比较隐蔽,所以我们必须要重视。下面我将分享一些关于线程安全问题的原因和解决办法。

一、线程不安全(其他线程会对当前线程的值进行覆盖导致bug)

        当两个线程针对一个可以分步的操作时,有可能会出现两个线程针对同一个对象多次覆盖,即线程1对于这个对象进行了操作还没保存的时候线程2读到的是线程1未改动之前的值(因为线程1改动没有保存)然后线程2针对初始值再进行改动1,然后这两个线程随机先后保存,但是这个改动只有其中一个生效(另一个相当于被覆盖了),此时导致与预期不符,程序出bug。 这个问题的解决办法是针对方法使用synchronized 关键字进行加锁,将方法变为“原子”的(即步骤不可分割),此时这个问题迎刃而解。

加锁:可以理解为把多个步骤约定再一起执行,而约定的这个步骤我们可以认为就是加锁。

代码示例:

我们可以看到同一个代码运行三次的结果都不符合预期(2w),并且每次都不一样

这时我们给方法上锁

 加锁后符合预期,线程安全。

二、死锁问题(线程之间互相加锁,无法进行下一步)

死锁详见这一篇博客

如何理解死锁?【死锁是什么】【死锁的三个典型情况】【死锁的四个必要条件】【如何破解死锁】_不会写代码的英专生不是好摄影师的博客-CSDN博客

三、内存可见性问题(编译器错误优化)

问题:根据1我们可以知道当我们使用synchronized 关键字对方法上锁的时候,保证了一个方法的原子性,但是这时候并不能完全的保证线程是安全的。在这里有一个内存可见性问题(也叫编译器错误优化):假设一个场景: 一个线程对一个变量进行读取操作,另外一个线程针对这个变量进行修改,此时读到的值,不一定是修改后的值

原因,我们可以认为读操作有两个步骤,load和cmp, load把内存中flag的值读到寄存器里,cmp把寄存器中的值和0进行比较,根据比较结果,决定下一步往哪个地方执行,但是在这个读和比较的操作循环中,循环速度极快,一秒内执行百万次以上,在这么多次循环中t2只对flag值修改过了一遍,此时编译器在读取到这个flag之前会自以为是地判断这个flag值不会被修改,所以就只读取了第一次,后面的不读了,对编译器进行错误的优化。)

解决办法:volatile关键字。 这个关键字加给变量,提醒编译器在这个情况下不要自行优化。

这个关键字能解决 内存可见性问题,也能解决指令重排序问题。

【volatile适用范围】:类似于一个线程读一个线程改这样的操作时就需要使用

        但是volatile不能保证原子性问题,所以还是需要搭配synchronized使用。(内存可见性问题,不一定100%出现,编译器优化(误判)不一定每次都会出现。根据自己写的代码~~玄学问题~~稳妥的办法就是给每个能加volatile 的地方都加上,不要去赌)

指令重排序(同样也是编译器优化的一种方式,当优化出问题时就是错误优化。)

例如举一个生动的例子: 我去食堂打饭,饭堂阿姨应该把饭打在盘子里再给我。这里把1.打饭 2 给盘子,但是在单线程模式下,相当于只有我一个人打饭,那么从结果的角度来看,阿姨先给我饭或者盘子都能达到我吃到饭的目的,但是,在多线程情况下,假如阿姨把给盘子的操作放在了打饭前面 ,当我得到的盘子里还没有饭的时候我被调度走了,那么这次打饭操作就无效,线程不安全。Volatile关键字同样可以避免这个错误优化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值