多线程积累:JMM模型

(一)前言

学习多线程,要理解java内存模型,才能理解多线程情况下,数据的变化,指令的运行等,才能更好的了解多线程的运行情况和日常使用的注意点。

(二)JMM与硬件内存结构

java内存模型与硬件内存结构.png

如上图所示,可以看到JMM的大概结构与硬件内存结构之间的关系,每个线程只能访问自己工作内存的数据,工作内存中存储着主内存中变量复制的副本,这两个内存的数据可以存储在硬件内存中的任一地方,并没有特殊划分。
JMM只是一种抽象的概念,是一种规则,并不真实存在,对于计算机而言,并不划分工作内存和主内存,而是都存储在计算机主内存中。

(三)JMM的三种特性

1.原子性

在多线程环境下,一个操作一旦开始就不会被其他线程影响。
比如一个静态变量,被两个线程同时进行操作,无论如何运行,最后的结构必定是两个线程中的一种结果。
特例:32位的系统,如果操作long或者double,由于操作位数问题,最终的结果可能并不是两个线程中的任一结果。
其实,在上述描述中,有一点无论如何运行,在计算机执行程序中,为了提高性能,编译器和处理器会对指令进行重排。

指令重排
  • (1)编译器重排
    简单的举个例子:
    主线程:
d=3;
c=3;

线程A:

a=c;
d=1;

线程B:

b=d;
c=2;

在以上两个线程之前,对c和d进行赋值,从程序的执行顺序来说,似乎不可能存在a=2,b=1的情况,但是指令重排之后,可能存在:
线程A:

d=1;
a=c;

线程B:

c=2;
b=d;

此时,看起来就更可能存在a=2,b=1的情况,所以,多线程情况下,对变量能否保持一致是不可预知的。

  • (2)处理器重排
    简单举个例子:
a=b+c;
d=a-e

在上述代码里面,落实到指令可以理解为:

  • 1.把b的值加载到寄存器
  • 2.把c的值加载到寄存器
  • 3.将b和c相加得到a
  • 4.将a加载到寄存器
  • 5.把e的值加载到寄存器
  • 6.将a减e得到d
  • 7.将d加载到寄存器。

其实上面的指令有个优化的点,就是将步骤5提前到2之后,因为步骤3和4都需要前面数据准备好之后才能进行,所以会进行中断,此时中断,会影响5的运行,将5提前,可以提高CPU的性能。
重排保证了串行语义的执行,但是在多线程的环境下,这样是毁灭性的,导致结果的不可预知性。

如下代码:

class MixedOrder{
    int a = 0;
    boolean flag = false;
    public void writer(){
        a = 1;
        flag = true;
    }

    public void read(){
        if(flag){
            int i = a + 1;
        }
    }
}

在单线程的场景下,先调用writer(),再次调用read(),得到的结果是i=2
在多线程的场景下,指令重排之后,read()方法在读到flagtrue的情况下,可能误读a=0,此时得到的结果为i=1

2.有序性

有序性是指在单线程的执行代码,我们可以认为代码的执行是按照顺序执行的,但是在多线程场景下,因为指令重排,导致最终的指令可能是乱序的,在本线程内,所有操作都视为有序的,但是多线程下,存在共享变量,一个线程需要观察另一个线程,所以操作都是无序的。

3.可见性

可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。这个概念仅代表在并发程序上的概念。由于每个线程会将共享变量拷贝到自己的工作线程中,由于指令重排的情况,也会存在可见性的问题,导致结果不是预期的结果。

(四)JMM提供的解决方案

针对以上的三种特性在多线程环境下的问题,JMM提供了相应的解决方案。

  • 原子性问题
    除了JVM自身提供的对基本数据类型读写操作的原子性外,对于方法级别或者代码块级别的原子性操作,可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性。
  • 可见性问题
    可见性问题,可以使用synchronized关键字或者volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
  • 有序性问题
    对于指令重排导致的可见性问题和有序性问题,则可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化,关于volatile稍后会进一步分析。

同时,JMM内部还定义一套happens-before 原则来保证多线程环境下两个操作间的原子性、可见性以及有序性。

happens-before 原则

  • 1.程序顺序原则
    即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
  • 2.锁规则
    解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
  • 3.volatile规则
    volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
  • 4.线程启动规则
    线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见。
  • 5.传递性
    A先于B ,B先于C 那么A必然先于C
  • 6.线程终止规则
    线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
  • 7.线程中断规则
    对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
  • 8.对象终结规则
    对象的构造函数执行,结束先于finalize()方法

(五)volatile

volatile是Java虚拟机提供的轻量级的同步机制。volatile关键字有如下两个作用:

  • 保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总数可以被其他线程立即得知。
  • 禁止指令重排序优化。

volatile的可见性

关于volatile的可见性作用,我们必须意识到被volatile修饰的变量对所有线程总数立即可见的,对volatile变量的所有写操作总是能立刻反应到其他线程中,但是对于volatile变量运算操作在多线程环境并不保证安全性。

volatile禁止重排优化

禁止重排其实在单例模式中已经有提现,就是单例模式中的双重校验锁模式。
instance = new Singleton();伪代码如下:

memory = allocate(); //1.分配对象内存空间
instance(memory);    //2.初始化对象
instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null

如果去掉volatile,则可重排优化为:

memory = allocate(); //1.分配对象内存空间
instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory);    //2.初始化对象

以上可以发现,当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。

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

推荐阅读更多精彩内容