Java并发:java内存模型(JMM)设计思想(一), 一分钟搞懂并发问题

文:GentlemanTsao

前言:

Debug并发的bug通常十分困难,这些bug在测试阶段一般无法暴露,直到程序高负载时才被发现,而且很难复制和追踪问题。解决并发bug的关键不在于问题暴露以后,而是在设计时花更多的精力确保程序已经正确的同步了,这比debug一个漏洞百出的并发程序要轻松的多。

一个模型通常是为了解决一类问题而设计。Java内存模型(JMM)是为了解决并发中遇到的同步问题而产生的。所以,首先要讨论的是,程序并发执行时遇到了哪些问题?

1.可见性问题:麦琪的礼物

欧亨利的短篇小说《麦琪的礼物》讲述了这样的故事:麦琪和丈夫生活穷困,为了给彼此送圣诞礼物,麦琪卖掉长发换来表链,麦琪的丈夫卖掉金表换来梳子。他们都出于为对方着想,结果却都买了无用的礼物。为什么会这样?
在这里插入图片描述
假如把麦琪和麦琪的丈夫看作两个线程,长发和表链是两个变量的值。当其中一个线程更改变量的值后,另一个线程没能及时“看到”。
以下面的伪代码来表示

class 圣诞快乐{
    String x = “长发”;	
    String y = “金表”;
    public void 给丈夫买礼物{
        //有金表,缺表链,所以用长发换表链
    	if(“金表”.equals(y){
    	    x = "表链"}
    }
    
    public void  给麦琪买礼物{
        //有长发,缺梳子,所以用金表换梳子
        if(“长发”.equals(x){
            y = "梳子"}
    }
}

线程“麦琪”调用方法“给丈夫买礼物”,将x的值改为“表链”;
同时另一个线程“丈夫”调用方法“给麦琪买礼物”,但仍然以为x的值是“长发”!因而又将y的值改为“梳子”。

为什么线程“丈夫”看不到x的改变?

缓存的设计

多核系统中CPU通常有多层缓存(cache),cache的目的是为了改善读取速度(因为缓存速度更快)以及减少内存总线的占用(对cache操作代替了内存操作)。Cache的设计极大的提升了性能,但是同时带来了问题。当多个核心同时访问同一块内存时,因为cache的存在,它们看到的值可能是不一样的。这是从CPU层面看可见性问题。

JMM设计思想1:
从CPU层面上,java内存模型定义了一个充分必要条件,让当前核心能够看到其他核心对内存的修改,并且其他核心也能够看到当前核心对内存的修改。

2. 有序性问题:编译器大厨的排序自由

一段代码的执行顺序并不一定是完全按照代码书写顺序来执行的。
不妨将书写代码想象成前台点菜,你以为后厨按照你的菜单顺序出菜。你点了下面的菜单:

class 顾客{
    String x,y;
    public void 点菜{
    	x = “水煮鱼”;
    	y = “番茄炒蛋”;
    }
    public void 去厨房端菜{
        String plate1 = x;
        String plate2 = y; // plate2有值的情况下,plate1一定有值吗?
    }
}

在这里插入图片描述
在这里插入图片描述

编译器大厨接到菜单,认为“水煮鱼”这个菜很耗时,可以放到后面做,先把“番茄炒蛋”烧起来。因为出于效率考虑,编译器大厨可以自由安排炒菜顺序。
某一时刻你发现plate2的值是“番茄炒蛋”,这表示“番茄炒蛋”已经做好装盘了,这时我们会以为“水煮鱼”也应该做好了,因为从代码的书写顺序上看,它排在前面。可实际上并非如此,plate1的值可能还是null.

在只有一个线程的时候,这个问题不会被发现;但多个线程的情况下,编译器的重排序会带来错乱。
事实上,不仅编译器,CPU也可以对指令重排,只要两个指令之间没有先后依赖关系。
JMM设计思想2:
Java内存模型描述了程序中的变量和计算机系统底层的内存、寄存器存取操作的对应关系。它定义了多线程代码的合法行为,以及线程和内存的交互方式。

3.原子性问题:ATM机取款的安全保障

“原子”的涵义是不可分割,指的是一个操作不可中断,或者完成,或者没有开始。我们以ATM机取款为例。银行认为ATM取款(插卡,取款,拔卡)应该是一个原子操作,不可以分割。换句话说,必须防止客户A插入了银行卡以后,客户B过来取款的事情发生。
在这里插入图片描述
如果把每个取款客户看作一个线程,原子操作保证了线程之间不会干扰该操作,所以该操作是线程安全的。

Java中的原子操作是对基本数据类型变量的读取和赋值。
注意点:
必须是对基本数据类型,例如int,Boolean;
只包括读取和赋值,不包括加减等运算
例如:

x = 1//原子
y = x; //不是原子,包含读取x和给y赋值两个操作,虽然这两个操作是原子的,但合起来却不是。

在下一篇我们将讨论JMM是如何解决以上的并发问题的。

想了解更多?访问这个专栏:
java并发和多线程教程2020版

本文为博主原创。请帮点赞、评论,让我们共同进步。

原创文章 31 获赞 24 访问量 2万+

猜你喜欢

转载自blog.csdn.net/GentelmanTsao/article/details/105111033