并发编程之:JMM

大家好,我是小黑,一个在互联网苟且偷生的农民工。

上一期给大家分享了关于Java中线程相关的一些基础知识。在关于线程终止的例子中,第一个方法讲到要想终止一个线程,可以使用标志位的方法,我们再来回顾一下代码。

class MyRunnable implements Runnable {
// volatile关键字,保证主线程修改后当前线程能够看到被改后的值(可见性)
private volatile boolean exit = false;
@Override
public void run() {
while (!exit) { // 循环判断标识位,是否需要退出
System.out.println("这是我自定义的线程");
}
}
public void setExit(boolean exit) {
this.exit = exit;
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
new Thread(runnable).start();
runnable.setExit(true); //修改标志位,退出线程
}
}

在这个代码中,标志位exit字段在声明时使用了volatile关机字修饰,目的是为了保证在另外一个线程修改后当前线程能够感知到变化,那么这个关键字到底做了些什么呢?这一期我们来详细聊一聊。

在开始讲volatile关键字之前,需要先和大家聊一聊计算机的内存模型这个玩意儿。

计算机的内存模型

所谓内存模型,英文描述是Memory Model,这玩意儿是一个比较底层的东西,它是与计算机硬件有关的一个概念。

我们都知道,计算机在执行程序的时候,最终是一条条的指令在CPU中执行,在执行过程中往往会存在数据的传递。而数据是存放在主内存上的,对,就是你那个内存条。

在刚开始CPU的的执行速度还不够快的时候并没有什么问题,但随着CPU技术的不断发展,CPU计算的速度越来越快,但是呢,从主内存上读取和写入数据的速度有点拉胯,跟不上呀,这就导致CPU每次操作主内存都要花费很多的等待时间。

技术总是要往前发展的,不能因为内存读写慢CPU就不发展了吧,也不能让主内存的读写速度成为瓶颈。

想必这里大家也应该想到了,就是在CPU和主内存之间加一个高速缓存,将需要的数据在这个高速缓存上复制一份,而这个高速缓存的特点就是读写很快,然后定期的将缓存中的数据和主内存同步。

到这里问题就解决了吗? too young,too simple啊,这种结构在但线程的情况下是没有问题的,随着计算机能力不断提升,开始支持多线程了,并且CPU牛逼到支持多核,到现在的4核8核16核,在这种情况下是会存在一些问题的,我们来分析一下。

单核多线程情况:多个线程同时访问一个共享数据,CPU将数据从主内存加载到高速缓存中,多个线程会访问高速缓存中的同一个地址,这样即使在线程切换时,缓存数据也不会失效,因为在单核CPU同一时间只能有一个线程在执行,所以也不会有数据访问的冲突。

多核多线程情况:每个CPU内核都会复制一份数据到自己的高速缓存,这样的话在不同内核上的两个线程是并行的,这样就会导致两个内核各自缓存的数据发生不一致。这个问题就叫做缓存一致性问题

除了上面说到的缓存一致性问题,计算机为了使CPU的算力能够被充分利用,会对输入的指令进行乱序处理,叫做处理器优化。很多的编程语言为了提高执行效率,也会对代码的执行顺序重新排序,比如咱们Java虚拟机的即时编译器(JIT)也会做,这个动作叫做指令重排

int a = 1;
int b = 2;
int c = a + b;
int d = a - b;

比如我们写的这段代码,第三行和第四行的执行顺序就有可能发生改变,这在单线程中并没有问题,但是在多线程情况下,会产生和我们预期不一样的结果。

其实上面提出的缓存一致性问题,处理器优化,指令重排就对应我们并发编程中的可见性问题,原子性问题,有序性问题。带着这些问题,我们再来看看,在Java中是如何来解决的。

因为存在这些问题,那么肯定要有一种机制来解决。这种解决的机制就是内存模型

内存模型定义了一个规范,用来保证共享内存的可见性,有序性,原子性。内存模型是怎么解决的呢?主要采取两种方式:限制处理器优化内存屏障。这里我们先不深究底层原理。

JMM

从前面我们知道内存模型是一个规范,用来解决并发情况下的一些问题。不同的编程语言对于这个规范都有对应的实现。那么JMM(Java Memory Model)就是Java语言对于这一规范的具体实现。

那么JMM具体是如何解决这写问题的呢?我们先来看下面这张图。

内存可见性问题

我们一个一个问题来看,首先,如何解决可见性问题

如上图所示,在JMM中,一个线程对于一个数据的操作,分成了6个步骤。

分别是:read,load,use,assign,write,store.

如果说这个变量在声明时,没有使用volatile关键字,那么两个线程是各自复制一份到工作内存,线程B将flag赋值为true,线程A是不可见的。

那么要想线程A可见,就需要在声明flag这个变量时,加上volatile关键字。那么加了关键字之后JMM是怎么做的呢?这里要分读和写两个情况。

  1. 线程在读取一个volatile变量时,JMM会把工作内存中的该变量置为无效,重新从主内存中读取;
  2. 线程在写一个volatile变量时,会立刻将工作内存中的值刷新到主内存中。

也就是说,对于volatile关键字修饰的变量,在read,load,use操作必须是一起执行的;assign,write,store操作时一起执行。

通过这样的方式,就能够解决内存可见性的问题。

指令重排

而指令重排这个问题,对于编译器来说,只要该对象声明为volatile的,那么就不会对它进行指令重排的优化。

而volatile禁止指令重排的这种规则是符合一个叫做happens-before的规则。

happens-before除了在volatile变量规则外,还有一些其他规则。

程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变。

管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)

volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。

线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。

线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。也称线程join()规则。

线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。

传递性规则:happens-before原则具有传递性,即hb(A, B) , hb(B, C),那么hb(A, C)。

对象终结规则:一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。

竞态条件

到这里,大家是不是感觉问题已经都解决了?emmm,我们来看下面这个场景:

假设上图中的线程A和线程B执行在两个CPU核心上,是并行执行的,它们一起读取到i的值等于0,然后各自加1,然后一起往主内存写。如果线程A和线程B是有先后顺序执行的,i的值最后应该是等于2才对,但是并行情况下是有可能同时操作的,最后写回到主内存中的值只被增加了一次。

这就好比你的银行卡收到了两笔100块的转账,但是账户上只多了100块。

对于这种问题通过volatile是无法解决的,volatile不会保证该变量操作的原子性。那我们应该怎么解决呢,就需要使用synchronized对这个操作加锁,保证同一时刻只能有一个线程进行操作。

总结

因为CPU和内存之间存在着高速缓存,在多线程并发情况下,可能会存在缓存一致性问题;而CPU对于输入的指令会做一些处理器优化,一些高级语言的编译器也会做指令重排。因为这些问题,会导致我们在并发情况下存在内存可见性问题,有序性问题,而JMM就是Java中为了解决这些问题而出现的。通过volatile关键字可以保证内存可见性,并且会禁止指令重排。但是volatile只能保证操作的有序性,无法保证操作的原子性,所以,为了安全,我们对于共享变量的并发处理要进行加锁。


好的,今天的内容就到这里,我们下期再见。

并发编程之:JMM的更多相关文章

  1. 并发编程之JMM&Volatile(一)

    并发 很多程序员应该对并发一词并不陌生,并发如同一把双刃剑,如果使用得当,可以帮助我们更好的压榨硬件的性能,反之,也会产生一些难以排查的问题.这里,先简单介绍下并发的几个基本概念. 进程与线程 进程: ...

  2. [转载]并发编程之Operation Queue和GCD

    并发编程之Operation Queue http://www.cocoachina.com/applenews/devnews/2013/1210/7506.html 随着移动设备的更新换代,移动设 ...

  3. Java并发编程之CAS

    CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替 ...

  4. 并发编程之wait()、notify()

    前面的并发编程之volatile中我们用程序模拟了一个场景:在main方法中开启两个线程,其中一个线程t1往list里循环添加元素,另一个线程t2监听list中的size,当size等于5时,t2线程 ...

  5. 并发编程之 Exchanger 源码分析

    前言 JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 还有一个重要的工具,只不过相对而言使用的不多,什么呢? Exchange -- 交换器.用于 ...

  6. 并发编程之 Condition 源码分析

    前言 Condition 是 Lock 的伴侣,至于如何使用,我们之前也写了一些文章来说,例如 使用 ReentrantLock 和 Condition 实现一个阻塞队列,并发编程之 Java 三把锁 ...

  7. python并发编程之Queue线程、进程、协程通信(五)

    单线程.多线程之间.进程之间.协程之间很多时候需要协同完成工作,这个时候它们需要进行通讯.或者说为了解耦,普遍采用Queue,生产消费模式. 系列文章 python并发编程之threading线程(一 ...

  8. python并发编程之gevent协程(四)

    协程的含义就不再提,在py2和py3的早期版本中,python协程的主流实现方法是使用gevent模块.由于协程对于操作系统是无感知的,所以其切换需要程序员自己去完成. 系列文章 python并发编程 ...

  9. python并发编程之asyncio协程(三)

    协程实现了在单线程下的并发,每个协程共享线程的几乎所有的资源,除了协程自己私有的上下文栈:协程的切换属于程序级别的切换,对于操作系统来说是无感知的,因此切换速度更快.开销更小.效率更高,在有多IO操作 ...

随机推荐

  1. java网络编程基础——TCP网络编程二

    1.半关闭的Socket 前面的服务器和客户端通信时总是以行为最小数据单位,但是在某些协议里,通信的数据单位可能是多行的,当出现多行数据时就 出现一个问题:Socket输出流如何表示输出数据已经结束. ...

  2. pip批量安装库

    将需要安装的库名和版本号都写在一个txt文档中,每个库名占一行,例如requests==2.24.0. 然后在用pip install -r命令去找到这个txt文档批量安装里面填写的库,如果嫌速度太慢 ...

  3. proteus8.1 pro 中文版安装破解教程

    Proteus8 Pro是非常有名的EDA工具(仿真软件),从原理图布图.代码调试到单片机与外围电路协同仿真,一键切换到PCB设计,真正实现了从概念到产品的完整设计.是唯一将电路仿真软件.PCB设计软 ...

  4. jvm源码解读--15 oop对象详解

    (gdb) p obj $15 = (oopDesc *) 0xf3885d08 (gdb) p * obj $16 = { _mark = 0x70dea4e01, _metadata = { _k ...

  5. NPM 所有的指令已经问题 使用淘宝镜像 出现code EAI_AGAIN

    windows怎么卸载cnpm? npm uninstall cnpm -g windows怎么检测cnpm是否安装成功 cnpm -v 我们直接将node的仓库地址换成淘宝仓库地址即可 单次使用 n ...

  6. Java 正则表达式 简单用法

    正则表达式的具体写法网上有很多了,这里只记录在 Java 中怎么使用. java.util.regex.Matcher.java.util.regex.Pattern 主要有: String.matc ...

  7. 利用swagger和API Version实现api版本控制

    场景: 在利用.net core进行api接口开发时,经常会因为需求,要开发实现统一功能的多版本的接口.比如版本V1是给之前用户使用,然后新用户有新需求,这时候可以单独给这个用户写接口,也可以在V1基 ...

  8. Java 反射(一)反射简介、原理和应用场景

    目录 一.动态语言和动态语言的比较 动态语言 静态语言 二.反射 简介 反射的常见使用 1. 代码编辑器 2. Spring等框架的IoC容器 3. 和注解的配合使用 原理 反射优缺点 调试查看 Cl ...

  9. 蓝凌OA前台任意文件读取漏洞利用

    近期CNVD爆出漏洞编号:CNVD-2021-28277,首次公开日期为2021-04-15,蓝凌oa存在多个漏洞,攻击者可利用该漏洞获取服务器控制权.今天挑选一个蓝凌OA前台任意文件读取漏洞进行分析 ...

  10. Vulhub-Phpmyadmin 4.8.1远程文件包含漏洞

    前言:Phpmyadmin是一个用PHP编写的免费软件工具,旨在处理Web上的MySQL管理. 该漏洞在index.php中,导致文件包含漏洞 漏洞环境框架搭建: cd vulhub-master/p ...