程序员不能逃避的synchronize和volatile

33次阅读

共计 2270 个字符,预计需要花费 6 分钟才能阅读完成。

本博客 猫叔的博客,转载请申明出处

阅读本文约“10 分钟”

适读人群:Java 初级

学习笔记,我也是呆呆做了好久,学了一下 PS,然后继续思考了一会,再开始写出来的,希望可以简明易懂。

原子性

首先是我们彼此都要保持一致的观点:原子(Atomic)操作指相应的操作是单一不可分割的操作

emmmm,这里很牵强的解释下原子性,还是不懂就搜搜其他文章,最好看看一些具体的例子

首先是代码例子

对 int 型变量 conut 执行 counter++ 的操作不是原子操作

这可以分为 3 个操作

  • 1、读取变量 counter 的当前值
  • 2、拿 counter 当前值和 1 做加法运算
  • 3、将 counter 的当前值增加 1 后赋值给 counter 变量

上面的步骤 2,很有可能在执行的时候就已经被其他线程修改了,其所为的“当前值”已经是过期的

或者看看百度百科的例子

我们以 decl(递减指令)为例,这是一个典型的 ” 读-改-写 ” 过程,涉及两次内存访问。设想在不同 CPU 运行的两个进程都在递减某个计数值,可能发生的情况是:

  • ⒈ CPU A(CPU A 上所运行的进程,以下同)从内存单元把当前计数值⑵装载进它的寄存器中;
  • ⒉ CPU B 从内存单元把当前计数值⑵装载进它的寄存器中。
  • ⒊ CPU A 在它的寄存器中将计数值递减为 1;
  • ⒋ CPU B 在它的寄存器中将计数值递减为 1;
  • ⒌ CPU A 把修改后的计数值⑴写回内存单元。
  • ⒍ CPU B 把修改后的计数值⑴写回内存单元。

内存里的计数值应该是 0,然而它却是 1。两个进程都去掉了对该共享资源的引用,但没有一个进程能够释放它 – 两个进程都推断出:计数值是 1,共享资源仍然在被使用

我再举例我呆想到的例子,一个姐姐和一个妹妹一起包饺子

画的很一般,别看我这样,我也是学过 2 小时速成素描的·····

假设我们在一个黑盒环境下,就是两姐妹都在各自小空间包饺子,然后她们把饺子通过各自的小洞口放入一个大盒子里。她们并不知道对方(比如她们两刚刚因为妈妈不给零花钱而生气了)

这个时候她们各自同时边赌气边包了一个饺子,同时放到盒子里,妈妈跑过来问老大,盒子里有多少个了?她只知道一个。再问问老二,她也是回答一个。这个生活例子可能提交特殊,不过偶尔生活中因为信息不对称而导致的预知结果与实际有偏差也是经常发生的

所以他们脑海就是这个情况。其实盒子里已经是 2 个饺子了

那么其实这个场景也像是 JVM

synchronize

synchronize 关键字可以实现操作的原子性,其本质是通过该关键字所包括的临界区的排他性保证在任何一个时刻只有一个线程能够执行临界区中的代码

也就是说,现在妈妈说只有听她的,两姐妹才能有零花钱,所以她叫两个闹脾气的小鬼都到厨房,并拿出了大盒子,让她们重新开始,不过要按照妈妈的要求来

妈妈先让姐姐包了 5 个,因为两姐妹都在厨房,不是各自在房间,所以这次妹妹都看在眼里,接着妈妈让妹妹包 10 个,妹妹显然是有点不乐意了(凭什么我姐才 5 个),不过她还是老实做了,现在他们三人都知道盒子里有 15 个

这里就又牵出了 synchronize 的另一个特点,保证内存的可见性

它保证了一个线程执行临界区中的代码时所修改的变量值对于稍有执行该临界区中的代码的线程来说是可见的,这对于保证多线程的代码是非常重要的

官方的解释下:CPU 执行代码,为了减少变量访问的消耗,会将值缓存到 CPU 缓存区,再次访问的时候,就是从缓存区去读取而不是主内存,这里的缓存区有点类似姐姐脑海 / 妹妹脑海。而且代码对缓存区的修改可能仅修改缓存区,没有被写回主内存。由于 CPU 都有自己的存储区,对于不同 CPU 的存储区内容是不可见的。这也是所谓的内存可见性

volatile

同样这个兄弟也可以保证内存可见性

一个线程对于一个采用 volatile 修改的变量的值的更改对于其他访问该变量的值的线程总是可见的

如果说对比 synchronize 和 volatile 的内存锁,然后说 volatile 是轻量级锁,emmmm,不好不太恰当

volatile 的内部锁并不能保证操作的原子性。

他在内存可见性的核心机制是:修改的值会被写入主内存,且其他 CPU 缓存区的值会因此失效(然后再更新一个最新值),保证其他线程访问 volatile 修饰的变量总是最新值。

当然他也有一个核心作用:禁止指令重排序(Re-order)

你们一般怎么写 5 的?

假如以上是我们的规定与希望

可能编译器和 CPU 为了提供指令的执行效率可能会进行指令重排序(优化)

如果你希望它是按照规定来的话就加上 volatile,虽然可能会导致编译器和 CPU 无法对一些指令做可能的优化,假设上面那样写对于计算机来说算优化:)

用程序来写一个例子:

private SomeOne object = new SomeOne();

你先想一下,你觉得的顺序,好了,我说说计算机可能的顺序

  • 1、分配一段用于存储 SomeOne 的内存空间
  • 2、对该内存空间引用赋值给变量 object
  • 3、创建类 SomeOne

如果当其他线程访问 2、object 变量的时候,仅得到一个指向存储 SomeOne 存储空间的引用,因为 3、SomeOne 还没创建

结语

希望各位兄弟能看到一些新的风景,synchronize 可以保证操作原子性,且保证内存可见性;volatile 仅能保证内存可见性。

synchronize 会导致上下文切换,volatile 不会哦。

关于上下文切换的,可以去看公众号的上一篇文章

我是 MySelf,还在坚持学习技术与产品经理相关的知识,希望本文能给你带来新的知识点。

公众号:Java 猫说

学习交流群:728698035

现架构设计(码农)兼创业技术顾问,不羁平庸,热爱开源,杂谈程序人生与不定期干货。

正文完
 0