理解volatile关键字与synchronize

       本文意在分析java多线程编程中的volatile作用,除了介绍volatile基本信息与使用,还会从多个层面分析volatile的原理,分别从JMM实现规范与内存语义等多个层面解析volatile,做到知其然更知其所以然,才能在多线程编程中更合理的使用volatile关键字;本文也会对和volatile类似的synchronize关键字从多个层面做分析,主要是讲解jdk1.6之后对synchronize关键字的优化。

多线程编程真的比单线程要快么?

       java支持多线程目的是为了让程序异步执行,从而更快的对结果进行处理响应,在大多数情况下,多线程代码比在单线程代码环境中执行的要快,但世事无绝对,多线程代码并不是绝对比单线程快(上下文切换导致性能开销),如何让自己写的多线程代码执行效率高,这就是多线程编程所要面临的挑战。

1.并发编程的挑战

       如上文所言,并发编程的目的是为了让程序运行的更快。但是,并不是启动更多的线程就能让程序最大限度地并发执行,在进行并发编程时,如果希望通过多线程执行任务让程序运行得更快,会面临非常多的挑战,比如上下文切换的问题,死锁的问题,以及受限于硬件和软件的资源限制问题,下面会介绍这些在并发编程中要面临的问题。

1.1上下文切换
cpu分配时间片执行多线程程序示意图

       即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配时间片来实现此机制。时间片是CPU分配给各个线程的执行时间,因为时间片非常短,所以CPU要通过不停的切换线程执行(这让程序员产生一种幻觉:多个线程是同时在执行的)。在上图中,线程程序1切换到下一线程前,会保存当前任务的状态,以便下一次循环到自己时才能继续恢复执行任务。所以任务从保存到再加载的过程就是一次上下文切换。

       这就像我们同时阅读两本书,在阅读英文书时发现某个单词不认识,去字典查,但在查字典前需要我们先记住当前英文书阅读到了第几页等信息,才能查字典,这样我们才能在查完字典之后继续恢复阅读英文书。这样的切换会影响我们的读书效率,同理上下文切换也会影响多线程的执行速度。

1.2 volatile 简介

       在多线程编程中有两个重要的角色,volatile和synchronize,其中volatile相当于轻量级的synchronize,volatile在多线程并发编程中保证了共享变量的“可见行”,可见行即当一个线程修改这个共享变量时,另一个线程能够读到这个修改的值。volatile使用恰当能够,会比synchronize使用执行成本更低(即使synchronize在1.6之后做了优化),主要原因是volatile不会引起线程上下文的切换和调度。

1.3 volatile 定义与实现原理

       volatile定义如下:
       java允许线程访问共享变量,为了确保共享变量能够被准确且一致的更新,线程应该用排他锁获得这个变量,java提供了volatile,在某些情况下使用volatile比使用锁更加方便轻量,如果一个共享变量被声明为volatile,java内存模型确保所有线程看到这个变量的值是一致的。

       volatile 实现原理
       在使用volatile变量进行写操作时,CPU会做如下事情:
       java代码如下:

volatile Boolean flag = true;

       转换为汇编语言大致如下:


image.png

       lock指令会在多核处理器下引发两件事情:
       1. 将当前处理器缓存行的数据写回到内存中
       2. 这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效

多个线程内存与主内存的关系:
image.png

从上图可知,线程A若要与线程B进行通信的话,在java中需要如下两步:

  1. 线程A将本地内存A中更新过的共享变量刷到主内存中;
  2. 线程B去主内存中读取线程A已经更新到主内存中的共享变量值。

此过程示意图如下:


image.png

以上就是volatile实现原理的大体流程,但如果想更深层次的了解volatile的原理实现,就不得不从volatile的内存语义,与JSR-qee内存模型两个层面开始说起,在从这两个层面开始谈volatile内存语义之前,需要先了解几个概念与现象

1.4 重排序

java程序在执行时,为了提高性能,编译器和处理器常常会对指令进行重排序,重排序分3种类型:

  1. 编译器优化的重排序,编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
  2. 指令级并行的重排序。
  3. 内存系统的重排序,由于处理器使用缓存和读/写缓冲区,,使得加载和存储操作看上去是在乱序执行


    image.png

上图的1属于编译器重排序,2,3属于处理器重排序。这些重排序会导致多线程程序出现内存可见行问题
举例如下:

//线程A
a=1;  //A1
x=b;  //A2

//线程B
b=2;
y=a;

假设处理器A和处理器B按程序的顺序并行执行内存访问,最终可能的得到x=y=0;的结果。出现此结果的原因如下:


image.png

虽然处理器执行内存操作的顺序是A1->A2,但内存实际操作的顺序却是A2->A1,此时,处理器A的内存操作顺序被重排序了

image.png

你可能感兴趣的:(理解volatile关键字与synchronize)