在360“救了五年火的程序猿”聊Java多线程与并发模型之共享对象

互联网上充斥着对Java多线程编程的介绍,每篇文章都从不同的角度介绍并总结了该领域的内容。但大部分文章都没有说明多线程的实现本质,没能让开发者真正“过瘾”。

以下内容如无特殊说明均指代Java环境。

共享对象

使用Java编写线程安全的程序关键在于正确的使用共享对象,以及安全的对其进行访问管理。在第一章我们谈到Java的内置锁可以保障线程安全,对于其他的应用来说并发的安全性是在内置锁这个“黑盒子”内保障了线程变量使用的边界。谈到线程的边界问题,随之而来的是Java内存模型另外的一个重要的含义,可见性。Java对可见性提供的原生支持是volatile关键字。

volatile关键字

volatile 变量具备两种特性,其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。其二 volatile 禁止了指令重排。

虽然 volatile 变量具有可见性和禁止指令重排序,但是并不能说 volatile 变量能确保并发安全。

public class VolatileTest {public static volatile int a = 0;public static final int THREAD_COUNT = 20;public static void increase() {a++;}public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[THREAD_COUNT];for (int i = 0; i < THREAD_COUNT; i++) {threads[i] = new Thread(new Runnable() {public void run() {for (int i = 0; i < 1000; i++) {increase();}}});threads[i].start();}while (Thread.activeCount() > 2) {Thread.yield();}System.out.println(a);}}

按照我们的预期,它应该返回 20000 ,但是很可惜,该程序的返回结果几乎每次都不一样。

问题主要出在 a++ 上,复合操作并不具备原子性, 虽然这里利用 volatile 定义了 a ,但是在做 a++ 时, 先获取到最新的 a 值,比如这时候最新的可能是 50,然后再让 a 增加,但是在增加的过程中,其他线程很可能已经将 a 的值改变了,或许已经成为 52、53 ,但是该线程作自增时,还是使用的旧值,所以会出现结果往往小于预期的 2000。如果要解决这个问题,可以对 increase() 方法加锁。

volatile 适用场景

volatile 适用于程序运算结果不依赖于变量的当前值,也相当于说,上述程序的 a 不要自增,或者说仅仅是赋值运算,例如 boolean flag = true 这样的操作。

volatile boolean shutDown = false;public void shutDown() {shutDown = true;}public void doWork() {while (!shutDown) {System.out.println("Do work " + Thread.currentThread().getId());}}

代码2.1:变量的可见性问题

在代码2.1中,可以看到按照正常的逻辑应该打印10之后线程停止,但是实际的情况可能是打印出0或者程序永远不会被终止掉。其原因是没有使用恰当的同步机制以保障线程的写入操作对所有线程都是可见的。

我们一般将volatile理解为synchronized的轻量级实现,在多核处理器中可以保障共享变量的“可见性”,但是不能保障原子性。关于原子性问题在该章节的程序变量规则会加以说明,下面我们先看下Java的内存模型实现以了解JVM和计算机硬件是如何协调共享变量的以及volatile变量的可见性。

Java内存模型

我们都知道现代计算机都是冯诺依曼结构的,所有的代码都是顺序执行的。如果计算机需要在CPU中运算某个指令,势必就会涉及对数据的读取和写入操作。由于程序数据的大部分内容都是存储在主内存(RAM)中的,在这当中就存在着一个读取速度的问题,CPU很快而主内存相对来说(相对CPU)就会慢上很多,为了解决这个速度阶梯问题,各个CPU厂商都在CPU里面引入了高速缓存来优化主内存和CPU的数据交互。针对上面的技术我特意整理了一下,有很多技术不是靠几句话能讲清楚,所以干脆找朋友录制了一些视频,很多问题其实答案很简单,但是背后的思考和逻辑不简单,要做到知其然还要知其所以然。如果想学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶群:591240817,群里有大牛直播讲解技术,以及Java大型互联网技术的视频免费分享

此时当CPU需要从主内存获取数据时,会拷贝一份到高速缓存中,CPU计算时就可以直接在高速缓存中进行数据的读取和写入,提高吞吐量。当数据运行完成后,再将高速缓存的内容刷新到主内存中,此时其他CPU看到的才是执行之后的结果,但在这之间存在着时间差。

看这个例子:

int counter = 0; counter = counter + 1;

代码2.2:自增不一致问题

代码2.2在运行时,CPU会从主内存中读取counter的值,复制一份到当前CPU核心的高速缓存中,在CPU执行完成加1的指令之后,将结果1写入高速缓存中,最后将高速缓存刷新到主内存中。这个例子代码在单线程的程序中将正确的运行下去。

但我们试想这样一种情况,现在有两个线程共同运行该段代码,初始化时两个线程分别从主内存中读取了counter的值0到各自的高速缓存中,线程1在CPU1中运算完成后写入高速缓存Cache1,线程2在CPU2中运算完成后写入高速缓存Cache2,此时counter的值在两个CPU的高速缓存中的值都是1。

此时CPU1将值刷新到主内存中,counter的值为1,之后CPU2将counter的值也刷新到主内存,counter的值覆盖为1,最终的结果计算counter为1(正确的两次计算结果相加应为2)。这就是缓存不一致性问题。这会在多线程访问共享变量时出现。

解决缓存不一致问题的方案:

通过总线锁LOCK#方式。

通过缓存一致性协议。

图2.1 :缓存不一致问题

图2.1中提到的两种内存一致性协议都是从计算机硬件层面上提供的保障。CPU一般是通过在总线上增加LOCK#锁的方式,锁住对内存的访问来达到目的,也就是阻塞其他CPU对内存的访问,从而使只有一个CPU能访问该主内存。因此需要用总线进行内存锁定,可以分析得到此种做法对CPU的吞吐率造成的损害很严重,效率低下。

随着技术升级带来了缓存一致性协议,市场占有率较大的Intel的CPU使用的是MESI协议,该协议可以保障各个高速缓存使用的共享变量的副本是一致的。其实现的核心思想是:当在多核心CPU中访问的变量是共享变量时,某个线程在CPU中修改共享变量数据时,会通知其他也存储了该变量副本的CPU将缓存置为无效状态,因此其他CPU读取该高速缓存中的变量时,发现该共享变量副本为无效状态,会从主内存中重新加载。但当缓存一致性协议无法发挥作用时,CPU还是会降级使用总线锁的方式进行锁定处理。

一个小插曲:为什么volatile无法保障的原子性

我们看下图2.2,CPU在主内存中读取一个变量之后,拷贝副本到高速缓存,CPU在执行期间虽然识别了变量的“易变性”,但是只能保障最后一步store操作的原子性,在load,use期间并未实现其原子性操作。

图2.2:数据加载和内存屏障

JVM为了使我们的代码得到最优的执行体验,在进行自我优化时,并不保障代码的先后执行顺序(满足Happen-Before规则的除外),这就是“指令重排”,而上面提到的store操作保障了原子性,JVM是如何实现的呢?其原因是这里存在一个“内存屏障”的指令(以后我们会谈到整个内容),这个是CPU支持的一个指令,该指令只能保障store时的原子性,但是不能保障整个操作的原子性。

从整个小插曲中,我们看到了volatile虽然有可见性的语义,但是并不能真正的保证线程安全。如果要保证并发线程的安全访问,需要符合并发程序变量的访问规则。

并发程序变量的访问规则

1. 原子性

程序的原子性和数据库事务的原子性有着同样的意义,可以保障一次操作要么全部执行成功,要不全部都不执行。

2. 可见性

可见性是微妙的,因为最终的结果总是和我们的直觉大相径庭,当多个线程共同修改一个共享变量的值时,由于存在高速缓存中的变量副本操作,不能及时将数据刷新到主内存,导致当前线程在CP中的操作结果对其他CPU是不可见状态。

3. 有序性

有序性通俗的理解就是程序在JVM中是按照顺序执行的,但是前面已经提到了JVM为了优化代码的执行速度,会进行“指令重排”。在单线程中“指令重排”并不会带来安全问题,但在并发程序中,由于程序的顺序不能保障,运行过程中可能会出现不安全的线程访问问题。

综上,要想在并发编程环境中安全的运行程序,就必须满足原子性、可见性和有序性。只要以上任何一点没有保障,那程序运行就可能出现不可预知的错误。最后我们介绍一下Java并发的“杀手锏”,Happens-Before法则,符合该法则的情况下可以保障并发环境下变量的访问规则。

happens-before语义

Java内存模型使用了各种操作来定义的,包括对变量的读写,监视器的获取释放等,JMM中使用了

happens-before

语义阐述了操作之间的内存可见性。如果想要保证执行操作B的线程看到操作A的结构(无论AB是否在同一线程),那么A,B必须满足

happens-before

关系。如果两个操作之间缺乏

happens-before

Happens-Before法则:

程序次序法则:线程中的每个动作A都Happens-Before于该线程中的每一个动作B,在程序中,所有的动作B都出现在动作A之后。

Lock法则:对于一个Lock的解锁操作总是Happens-Before于每一个后续对该Lock的加锁操作。

volatile变量法则:对于volatile变量的写入操作Happens-Before于后续对同一个变量的读操作。

线程启动法则:在一个线程里,对Thread.start()函数的调用会Happens-Before于每一个启动线程中的动作。

线程终结法则:线程中的任何动作都Happens-Before于其他线程检测到这个线程已经终结或者从Thread.join()函数调用中成功返回或者Thread.isAlive()函数返回false。

中断法则:一个线程调用另一个线程的interrupt总是Happens-Before于被中断的线程发现中断。

终结法则:一个对象的构造函数的结束总是Happens-Before于这个对象的finalizer(Java没有直接的类似C的析构函数)的开始。

传递性法则:如果A事件Happens-Before于B事件,并且B事件Happens-Before于C事件,那么A事件Happens-Before于C事件。

当一个变量在多线程竞争中被读取和存储,如果并未按照Happens-Before的法则,那么他就会存在数据竞争关系。

总结

给大家关于Java的共享变量的内容就介绍到这里,现在你已经明白Java的volatile关键字的含义了,了解了为什么volatile不能保障原子性的原因了,了解了Happens-Before规则能让我们的Java程序运行的更加安全。

在这里给大家提供一个学习交流的平台,java架构师群671017482

具有1-5工作经验的,面对目前流行的技术不知从何下手,需要突破技术瓶颈的可以加群。

在公司待久了,过得很安逸,但跳槽时面试碰壁。需要在短时间内进修、跳槽拿高薪的可以加群。

如果没有工作经验,但基础非常扎实,对java工作机制,常用设计思想,常用java开发框架掌握熟练的可以加群。

通过这节内容希望可以帮助你更深入的了解Java的并发概念中的内置锁和共享变量。Java的并发内容还有很多,例如在某些场景下比synchronized效率要更高的Lock,阻塞队列,同步器等。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在...
    加油小杜阅读 539评论 0 2
  • 这周许多前期保持强势上涨的白马开始回调,那么这次回调是机会还是风险? 要回答这个问题,我们先要搞清楚卖出白马的这些...
    Leo凯文阅读 101评论 0 0
  • #21天写作计划#—10— —Day18— 某天清晨,出门晨读英语,低头看地面,发现很多落叶,黄色、绿色、褐色、橙...
    喜悦Iris阅读 484评论 0 0
  • ​有些人想减肥,但就是动不起来,减不下来。空有一颗减肥的心,却一直没有行动。有些人因为一时热情行动了,但是热情一去...
    Cody小安阅读 308评论 2 0
  • json 1、json是一种轻量级数据交换格式,主要是跨平台交流数据用的2、他是一种严格的js对象的格式,json...
    Man僵小鱼阅读 259评论 0 0