Java并发编程(一)并发特性

最近想总结一些Java并发相关的内容,先写吧,写到哪儿就是哪[捂脸]java

一、物理计算机的并发问题

在说明Java并发特性以前,先简单了解一下物理计算机中的并发问题,这两者有很多类似之处。物理机对并发的处理方案对于虚拟机也有很大的参考意义。缓存

“并发”在计算机领域内,一直是比较头疼。由于并发不只仅是计算的事情,也是存储的事情。咱们在处理并发时,不可能只靠CPU就能完成,也须要与内存交互,好比读取运算数据,存储运算结果等。多线程

可是,因为CPU的处理效率和内存的处理效率差了几个数量级,计算机不得不引入高速缓存做为内存和CPU之间的缓冲,将运算须要使用的数据复制到缓存中,减小I/O瓶颈,加速运算,当运算完成以后,再将数据从缓存同步回内存中,这样可以提高很多处理的效率。并发

不过,在引入高速缓存的同时,也带来了另一个问题——缓存一致性。每一个处理器都有本身的高速缓存,而他们又共享同一主内存,当多个处理器任务都是涉及到同一块主内存区域时,就会出现缓存数据不一致的问题。优化

同时,为了解决一致性的问题,高速缓存就须要遵照一些一致性协议(MSI等协议)来规范对数据的读写。spa

具体示意图,以下:操作系统

图1

注:引入物理计算机并发的概念,主要是为了提供一种思路,实际上的实现远比描述的要复杂。线程

二、Java的内存模型

众所周知,Java自己的运行是基于虚拟机的,在虚拟机的规范中,Java定义了一种内存模型,来屏蔽掉硬件和操做系统的内存访问差别,以实现让Java程序在各类平台下都能达到一致的内存访问效果。3d

模型分为主内存和工做内存,全部的变量(局部变量除外,局部变量都是线程私有的,不存在并发问题)都存储在主内存中。每条线程具备本身的工做内存,其中工做内存中保存了线程使用到的变量的主内存副本拷贝,线程对变量的全部操做(读取、赋值等)都必须在工做内存中进行,而不能直接操做主内存中的变量。不一样线程之间是没法访问对方的工做内存,线程间变量值的传递均须要经过主内存来完成,示意图以下:code

图2

注:这里提到的主内存和工做内存,实际上和咱们常说的Java内存分为堆、栈、方法区等并非同一层次的划分,两者基本上没有直接联系。若是必定要勉强对应的话,那主内存主要对应于Java堆中的对象实例部分,而工做内存则对应于虚拟机栈中的部分区域。从更低层次上说,主内存直接对应于物理硬件的内存,而工做内存可能优先存储于高速缓存中。

关于主内存与工做内存之间具体的交互协议,也就是说,一个变量如何从主内存拷贝到工做内存,又是如何从工做内存同步回到主内存的。Java定义了8种操做来实现的,而且虚拟机保证每一种操做都是原子的。

注:8种操做分别是lock、unlock、read、load、use、assign、store、write.

图3

上图所示,是两组操做,一组是读取,一组是写入。

值得注意的是,Java模型只要求这两个操做必须是顺序执行,但并无保证是连续执行,这点区别是很是大的。

也就是说,read和load之间、store和write之间是能够插入其余指令的。

接下来,咱们关注一下,Java并发中的三个特性,原子性、可见性和有序性

原子性

由Java内存模型,咱们能够得知,在工做内存和主内存交互时,尽管每一条指令是原子性的,可是每一组指令并非顺序的。

好比,咱们对主内存中的变量a、b进行访问时,一种可能出现的顺序是

read a、read b、load b、load a.

所以,在多线程的环境下,会出现并发访问主内存数据的问题。

那么Java是如何知足原子性的需求呢?

Java内存模型中提供了lock和unlock操做来知足原子性的需求。

尽管虚拟机没有直接给用户提供操做,可是提供了更高层次的语法,这就是Java代码中的同步块——synchronized。

固然了,使用ReentrantLock也能够知足原子性。

注:

基础类型变量(byte、short、int、float、boolean等)都是原子性操做,不存在并发问题,经过反编译成汇编语言,能够看到基础类型变量的操做只有一条汇编语句。

可是,对于long和dobule型,未必是原子性操做,主要缘由仍是由于这两个都是8个字节(64位),Java规范容许将64位数据的读写操做划分为两次32位的操做。

可见性

可见性指的是当一个线程修改了共享变量的值,其余线程可以当即得知这个修改。Java内存模型是经过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值。这种依赖主内存做为传递媒介的方式来实现可见性的。

对于普通的变量来讲,Java是不会保证可见性的,以下图所示:

图4

线程1和线程2都将x读取到工做内存中,可是线程2将x的值改为b,并无及时更新主内存,此时工做内存1仍然取值a。这就出现了可见性的问题。

Java是使用volatile关键字实现变量的可见性的。

volatile类型的变量,在修改值以后,会当即刷新到主内存。而且每次使用前都是从主内存中刷新。

注:synchronized同时也能实现可见性

有序性

为了提高效率,尽可能充分利用计算能力,Java虚拟机的即时编译器会对指令进行从新排序优化。

指令的从新排序,不保证程序中的各个语句执行的前后顺序同代码中的顺序一致,可是它会保证程序最终结果执行结果和代码顺序执行的结果是一致的。

//同一个线程内

int a=10;//语句1

int r = 2;//语句2

a = a+3;//语句3

r= a*a;//语句4

复制代码

执行的顺序多是这样的:

图5

那有没有可能语句4和语句3调换一下?

这种是不可能的,由于语句4依赖于语句3,因此语句4只能在语句3以后执行。

以上是单线程的状况,在单线程中,尽管指令多是无序的,可是最终执行的结果是有序的。

那,换到多线程的状况下,就不同了,咱们看一个例子。

//线程1

context = loadContext(); //语句1

inited = true; // 语句2

//线程2

while(!inited) {

    sleep();

}

doSomething(context);
复制代码

例子中,线程1的语句1和2没有依赖性,所以可能会发生重排序。

假如发生了重排序,在线程1执行过程当中先执行了2,而此时线程2觉得初始化工做已经完成,那么会跳出循环,执行doSomething(context)方法,而此时context并未初始化,会致使程序报错。

这也就是无序会致使的并发问题。

Java提供了有序性保证的机制,经过volatile和synchronized均可以实现。

咱们将上面的代码改造一下:

//线程1

context = loadContext(); //语句1

volatile inited = true; // 语句2

//线程2

while(!inited) {

    sleep();

}

doSomething(context);
复制代码

这样的话,语句2和语句1的顺序就会有保证了。

本文主要参考

《深刻理解Java虚拟机第二版》 周志明

相关文章
相关标签/搜索