Java 多线程可见性

在现代操作系统上编写并发程序时,除了要注意线程安全性(多个线程互斥访问临界资源)以外,还要注意多线程对共享变量的可见性,而后者往往容易被人忽略。

可见性是指当一个线程修改了共享变量的值,其它线程能够适时得知这个修改。在单线程环境中,如果在程序前面修改了某个变量的值,后面的程序一定会读取到那个变量的新值。这看起来很自然,然而当变量的写操作和读操作在不同的线程中时,情况却并非如此。

来看看下面的例子吧:

public class NoVisibility {
    private static boolean ready; 
    private static int number;
    
    private static class ReaderThread extends Thread {
        public void run() {
            while(!ready) {
                Thread.yield();
            }
            System.out.println(number);
        }
    }
    
    public static void main(String[] args) {
        new ReaderThread().start(); 
        number = 42;
        ready = true;
    }
}

上面的代码中,主线程和读线程都访问共享变量 readynumber。程序看起来会输出 42,但事实上很可能会输出 0,或者根本无法终止。这是因为上面的程序缺少线程间变量可见性的保证,所以在主线程中写入的变量值,可能无法被读线程感知到。虽然在 主线程 中是先修改 number 变量,再修改 ready 变量,但对于 读线程 来说,ready 变量的修改有可能被重排序到number 变量修改之前。

为什么会出现线程可见性问题?

Java 语言规范要求 JVM 只在单个线程内部维护一种类似串行的语义,即只要程序的最终结果与严格串行环境中执行的结果相同即可。所以在单线程环境中,我们无法察觉到重排序,因为程序重排序后的执行结果与严格按顺序执行的结果相同。但是在编写并发程序时,我们一定要注意重排序对多线程执行结果的影响。

事实上,很多主流程序语言(如C/C++)都存在多线程可见性的问题,这些语言是借助物理硬件和操作系统的内存模型来处理多线程可见性问题的,因此不同平台上内存模型的差异,会影响到程序的执行结果。Java 虚拟机规范定义了自己的内存模型 JMM(Java Memory Model) 来屏蔽掉不同硬件和操作系统的内存模型差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问结果。所以对于 Java 程序员,无需了解底层硬件和操作系统内存模型的知识,只要关注Java 自己的内存模型,就能够解决 Java 语言中的内存可见性问题了。

那如何解决 Java 中多线程共享变量的可见性问题呢?

即当一个多线程共享变量被某个线程修改后,如何让这个修改被需要读取这个变量的其他线程感知到呢?

JMM 定义了 Happens-Before 原则。只要我们理解了Happens-Before 原则,无需了解 JVM 底层的内存操作,就可以解决在并发编程中遇到的变量可见性问题。对于两个操作 A 和 B,这两个操作可以在不同的线程中执行。如果 A Happens-Before B,那么可以保证,当 A 操作执行完后,A 操作的执行结果对 B 操作是可见的。

Happens-Before 的规则包括:

1,程序顺序规则

按照程序代码的书写顺序,如果多个操作之间有先后依赖关系,则不允许对这些操作进行重排序。

2,锁定规则

class HappensBeforeLock {
    private int value = 0;
    
    public synchronized void setValue(int value) {
        this.value = value;
    }
    
    public synchronized int getValue() {
        return value;
    }
}

setValuegetValue 两个方法共享同一个监视器锁。假设 setValue 方法在线程 A 中执行,getValue 方法在线程B 中执行。setValue 方法会先对value 变量赋值,然后释放锁。getValue 方法会先获取到同一个锁后,再读取 value 的值。所以根据锁定原则,线程 A 中对 value 变量的修改,可以被线程 B 感知到。在锁被释放时,A 线程会把释放锁之前所有的操作结果同步到主内存中,而在获取锁时,B 线程会使自己 CPU 的缓存失效,重新从主内存中读取变量的值。这样,A 线程中的操作结果就会被 B 线程感知到了。

3,volatile 变量规则

volatile 变量可以保证可见性,禁止对变量的操作进行重排序

4,线程启动规则

调用 start() 方法时,会将 start() 方法之前所有操作的结果同步到主内存中,新线程创建好后,需要从主内存获取数据。这样在 start() 方法调用之前的所有操作结果对于新创建的线程都是可见的。

5, 线程结束规则

假设两个线程A、B。在线程 B 中调用 a.join()方法。则线程 B 会被挂起,等待A 线程运行结束才能恢复执行。当 a.join() 成功返回时,B 线程就知道 A 线程已经结束了。在 A 线程中对共享变量的修改,对 B 线程都是可见的。当一个线程结束时,会把自己所有操作的结果都同步到主内存。而任何其它线程当发现这个线程已经执行结束了,就会从主内存中重新刷新最新的变量值。 所以结束的线程 A 对共享变量的修改,对于其它检测了A 线程是否结束的线程是可见的。(Thread.isAlive() 方法可以检测到一个线程是否结束)

6, 中断规则

假设两个线程 A 和 B,A 先做了一些操作,然后调用 B 线程的 interrupt() 方法。当 B 线程感知到自己的中断标识被设置时(通过抛出 InterruptedException,或调用 interruptedisInterrupted ),A 中的操作结果对 B 都是可见的。

7,终结器规则

一个对象的构造函数必须在它的 finalize() 方法调用时执行完。
根据这条原则,可以确保在对象的 finalize() 方法执行时,该对象的所有字段值都是可见的。

8,传递性规则

如果操作A Happens-Before B,B Happens-Before C,那么可以得出操作 A Happens-Before C。

下面我们在深入思考一下,Happens-Before 原则到底是如何解决变量可见性问题的?

我们已经知道,导致多线程间可见性问题的两个“罪魁祸首”是 CPU缓存重排序。那么如果要保证多个线程间共享的变量对每个线程都及时可见,一种极端的做法就是禁止使用所有的重排序和CPU缓存。即关闭所有的编译器、操作系统和处理器的优化,所有指令顺序全部按照程序代码书写的顺序执行。去掉 CPU 高速缓存,让 CPU 的每次读写操作都直接与主存交互。
当然,上面的这种极端方案是绝对不可取的,因为这会极大影响处理器的计算性能,并且对于那些非多线程共享的变量是不公平的。

重排序和 CPU 高速缓存有利于计算机性能的提高,但却对多CPU处理的一致性带来了影响。为了解决这个矛盾,我们可以采取一种折中的办法。我们用分割线把整个程序划分成几个程序块,在每个程序块内部的指令是可以重排序的,但是分割线上的指令与程序块的其它指令之间是不可以重排序的。在一个程序块内部,CPU 不用每次都与主内存进行交互,只需要在 CPU 缓存中执行读写操作即可,但是当程序执行到分割线处,CPU 必须将执行结果同步到主内存或从主内存读取最新的变量值。那么,Happens-Before 规则就是定义了这些程序块的分割线。

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

推荐阅读更多精彩内容