文档章节

深入Java多线程——Java内存模型深入(1)

江左煤郎
 江左煤郎
发布于 06/14 18:35
字数 8384
阅读 5
收藏 3
点赞 0
评论 2

1.重排序

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

1.1 数据依赖性

    1.如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分为下列3种类型:

编译器和处理器可能会对操作做重排序,在上面3种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。所以编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作, 不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

1.2 as-if-serial语义

1.as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被 编译器和处理器重排序。比如下列求圆面积的代码:

double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C

    操作A与C有依赖关系,B也与C有数据依赖关系,所以C必须在A,B执行之后,也就是C一定不会重排序到A,B指令之后。但是A与B指令并不存在数据依赖,所以编译器或处理器可以重排序A,B指令的执行顺序。但是无论是ABC顺序还是BAC顺序,最后的得到的结果不会变化。

    as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器、runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。asif-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。

1.3 程序顺序规则

    1. 如果根据happens-before的程序顺序规则,上面计算圆的面积的示例代码存在3个happensbefore关系:

1)A happens-before B。 (程序顺序规则)

2)B happens-before C。 (程序顺序规则)

3)A happens-before C。(传递性规则)

    这里A happens-before B,但实际执行时B却可以排在A之前执行(看上面的重排序后的执行顺序)。如果A happens-before B,JMM并不要求A一定要在B之前执行。适用happens-before的情况是要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。这里操作A的执行结果不需要对操作B可见;而且重排序操作A和操作B后的执行结果,与操作A和操作B 按happens-before顺序执行的结果一致。在这种情况下,JMM会认为这种重排序并不非法(not illegal),JMM允许这种重排序。

1.4 重排序对多线程的影响

    1.对如下代码

public class ReorderExample {
	int a = 0;
	boolean flag = false;
	public void writer() {
		a = 1; // 1
		flag = true; // 2
	}
	public void reader() {
		if (flag) { // 3
			int i = a * a; // 4
		}
	}
}

    flag变量是个标记,用来标识变量a是否已被写入。这里假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法。线程B在执行操作4时,是不一定能看到线程A中操作1对a的写入的。

    由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。

    如果操作1和操作2做了重排序。程序执行时,线程A首先写标记变量flag,随后线程B读这个变量。由于条件判断为真,线程B将读取变量a。此时,变量a还没有被线程A写入,在这里多线程程序的语义被重排序破坏了。

    如果操作3和操作4发生了重排序。程序中,操作3和操作4存在控制依赖关系,当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提前读取a并计算a*a,然后把计算结果临时保存到一个名为重排序缓冲的硬件缓存中。当操作3的条件判断为真时,就把该计算结果写入变量i中。猜测执行实质上对操作3和4做了重排序。重排序在这里也破坏了多线程程序的语义。

    在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial 语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

2.顺序一致性

2.1 数据竞争与顺序一致性

当程序未正确同步时,就可能会存在数据竞争。Java内存模型规范对数据竞争的定义为: 在一个线程中写一个变量, 在另一个线程读同一个变量, 而且写和读没有通过同步来排序。

    1.当代码中包含数据竞争时,程序的执行往往产生违反直觉的结果,如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。

    2.Java内存模型对正确同步的多线程程序的内存一致性做了以下保证:如果程序是正确同步的,程序的执行将具有顺序一致性(Sequentially Consistent)——即程 序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。

2.2 顺序一致性内存模型

    1.顺序一致性内存模型是一个被想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性:

(1)一个线程中的所有操作必须按照程序的顺序来执行。

(2)无论程序是否同步,所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

以程序员角度来看如下图所示

    在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程,同时每一个线程必须按照程序的顺序来执行内存读/写操作。从上面的示意图可以看出,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化(即在顺序一致性模型中, 所有操作之间具有全序关系)。

    2.通过以下这个例子来进一步解释什么是顺序一致性内存模型。

    有两个线程A和B并发执行。其中A线程有3个操作,它们在程序中的顺序是: A1→A2→A3。B线程也有3个操作,它们在程序中的顺序是:B1→B2→B3。

     假设一:这两个线程使用监视器锁来正确同步:A线程的3个操作执行后释放监视器锁,随后B线程获取同一个监视器锁。那么最后的执行效果应该是: A1→A2→A3→B1→B2→B3,即操作的执行整体上有序,所有线程都只能看到一个一致的整体执行顺序,且线程中的程序顺序不变。

    假设二:这两个线程没有使用监视器锁来进行同步,那么最后的执行效果将是随机的,结果有可能是:A1→B1→A2→A3→B2→B3,即操作的执行整体上无序,但线程中的程序顺序不变且所有线程都只能看到一个一致的整体执行顺序。而能够保证所有线程都只能看到一个一致的整体执行顺序,就是因为顺序一致性内存模型中的每个操作必须对每个线程都可见。正如上图中所示。

    3.在Java内存模型中,未同步程序无法保证所有线程都只能看到一个一致的整体执行顺序,未同步程序在JMM中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。比如,在当前线程把写过的数据缓存在本地内存中,在没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,会认为这个写操作根本没有被当前线程执行。只有当前线程把本地内存中写过的数据刷新到主内存之后,这个写操作才能对其他线程可见。所以就导致了当前线程与其他线程看到的操作执行顺序不同。

2.3 同步程序的顺序一致性效果

public class ReorderExample {
	int a = 0;
	boolean flag = false;
	public synchronized void writer() {//获取锁
		a = 1; // 1
		flag = true; // 2
	}								   //释放锁
	public synchronized void reader() {//获取锁
		if (flag) { // 3
			int i = a * a; // 4
			//do something
		}
	}								   //释放锁
}

    在上面示例代码中,假设A线程执行writer()方法后,B线程执行reader()方法。这是一个正确同步的多线程程序,该程序的执行结果将与该程序在顺序一致性模型中的执行结果相同。

    顺序一致性模型中,所有操作完全按程序的顺序串行执行。而在JMM中,临界区内的代码可以重排序(但JMM不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)。JMM会在退出临界区和进入临界区这两个关键时间点做一些特别处理,使得线程在这两个时间点具有与顺序一致性模型相同的内存视图。虽然线程A在临界区内做了重排序,但由于监视器互斥执行的特性,这里的线程B根本无法“观察”到线程A在临界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。

从这里可以看出,JMM在具体实现上的基本方针为:在不改变(正确同步的)程序执行结果的前提下,尽可能地为编译器和处理器的优化打开方便之门。

2.4 未同步程序的执行特性

    1.对于未同步或未正确同步的多线程程序,JMM只提供最小安全性:线程执行时读取到的 值,要么是之前某个线程写入的值,要么是默认值(0,Null,False),JMM保证线程读操作读取到的值不会无中生有(Out Of Thin Air)的冒出来。为了实现最小安全性,JVM在堆上分配对象时,首先会对内存空间进行清零,然后才会在上面分配对象(JVM内部会同步这两个操作)。因此,在已清零的内存空间(Pre-zeroed Memory)分配对象时,域的默认初始化已经完成了。

    2.JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。因为如果想要保证执行结果一致,JMM需要禁止大量的处理器和编译器的优化,这对程序的执行性能会产生很大的影响。而且未同步程序在顺序一致性模型中执行时,整体是无序的,其执行结果无法预知。而且,保证未同步程序在这两个模型中的执行结果一致没什么意义。

    3.未同步程序在JMM中的执行时,整体上是无序的,其执行结果无法预知。未同步程序在两个模型中的执行特性有如下几个差异:

(1)顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。

(2)顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。

(3)JMM不保证对64位的long型和double型变量的写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。

    第三个差异预处理器总显得工作机制密切相关,在计算机中,数据通过总线在处理器和内存之间传递。每次处理器和内存之间的数据传递都是通过一系列步骤来完成的,这一系列步骤称之为总线事务(Bus Transaction)。总线事务包括读事务(Read Transaction)和写事务(Write Transaction)。读事务从内存传送数据到处理器,写事务从处理器传送数据到内存,每个事务会读/写内存中一个或多个物理上连续的字。这里的关键是,总线会同步试图并发使用总线的事务。在一个处理器执行总线事务期间,总线会禁止其他的处理器和I/O设备执行内存的读/写。示意图如下

    假设处理器A,B,C,D同时向总线发起总线事务,此时总线仲裁会对竞争作出裁定,若总线裁定处理器A竞争成功,则此时A处理器将继续它的总线事务,而其他处理器的总线事务将等到处理器A执行完成后再次进行竞争进行内存访问。而在处理器A执行总线事务期间(无论是读事务还是写事务),其他处理器的总线事务请求都会被禁止。

    总线的这些工作机制可以把所有处理器对内存的访问以串行化的方式来执行。在任意时间点,最多只能有一个处理器可以访问内存。这个特性确保了单个总线事务之中的内存读/写操作具有原子性。

    在一些32位的处理器上,如果要求对64位数据的写操作具有原子性,会有比较大的开销。 当JVM在这种处理器上运行时,可能会把一个64位long/double型变量的写操作拆分为两个32位的写操作来执行。这两个32位的写操作可能会被分配到不同的总线事务中执行,此时对这个64位变量的写操作将不具有原子性。 当单个内存操作不具有原子性时,可能会产生意想不到后果。

    比如处理器A写一个long型变量,同时处理器B要读这个long型变量。处理器A中64位的写操作被拆分为两个32位的写操作,且这两个32位的写操作被分配到不同的写事务中执行。同时,处理器B中64位的读操作被分配到单个的读事务中执行。如果当处理器A第一次竞争到总线成功,则将会对变量的高32位进行写,即第一次写事务,执行完成后处理器A和B会再次竞争总线,如果此次处理器B竞争成功,则会执行读事务读取变量,而此时处理器B将看到仅仅被处理器A“写了一半”的无效值。读取完成后,执行处理器A第二个写事务,对变量进行第二次写事务。

   JDK5之后的Java虚拟机对64位的long、double类型数据的写操作会被分割成两个32位的写操作来执行,但任意的读操作都具有原子性,也就是说任意的读操作都会被放在单个读事务中进行。

3. volatile的内存语义

3.1 volatile的特性

    1.两大特性:

(1)可见性:对一个volatile变量的读,总是能看到任意一个线程对这个变量最后的写,也就是说一个线程如果对volatile变量进行了写(赋值)操作,那么其他线程中的该变量都能立即感知到这个操作。

(2)原子性:对任意单个volatile变量的读或写操作具有原子性,注意,i++这种复合操作不具有原子性。

3.2 volatile写-读建立的happens-before关系

    1.依据volatile变量的可见性,可以利用其实现线程间的通信。从内存语义的角度来说,volatile的写-读与锁的释放-获取有相同的内存效果:volatile写和锁的释放有相同的内存语义;volatile读与锁的获取有相同的内存语义。

class VolatileExample{
	int i=0;
	volatile boolean flag=false;
	public void wtirer(){
		i=1;				//1
		flag=true;			//2
	}
	public void reader(){
		if(flag){		//3
			int a=i;	//4
			//do something
		}
	}
}

    假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens-before规则,这个过程建立的happens-before关系可以分为3类:

(1)根据程序次序规则,1 happens-before 2;3 happens-before 4。

(2)根据volatile规则,2 happens-before 3。

(3)根据happens-before的传递性规则,1 happens-before 4。

A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。

3.3 volatile写-读的内存语义

    1. volatile写的内存语义为:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存。

    2。volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

3.4 volatile内存语义的实现

    1. 编译器和处理器为了优化程序性能而对指令序列进行重新排序,也就是编译器重排序重排序和处理器重排序,但是Java内存模型为了实现volatile会限制这两种重排序,规则如下图所示

    也就是说,对于volatile变量,其重排序规则限制为:

(1)当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile写之前的操作不会被编译器重排序到volatile写之后 。

(2)当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。

(3)当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

    2. 为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:

(1)在每个volatile写操作的前面插入一个StoreStore屏障。

(2)在每个volatile写操作的后面插入一个StoreLoad屏障。

(3)在每个volatile读操作的后面插入一个LoadLoad屏障。

(4)在每个volatile读操作的后面插入一个LoadStore屏障。

3.下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图

 

    (1)StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。

    (2)而volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序(准确的说是避免读操作,因为volatile写操作前面会有StoreStore屏障,也能保证不会重排序)。因为编译器常常无法准确判断在一个volatile写的后面是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。为了保证能正确实现volatile的内存语义,JMM在采取了保守策略:在每个volatile写的后面,或者在每个volatile读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM最终选择了在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时, 选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里可以看到JMM在实现上的一个特点:首先确保正确性,然后再去追求执行效率。

    4.下面是保守策略下,volatile读插入内存屏障后生成的指令序列示意图,

    (1)LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。

    (2)LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

    上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变 volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。比如如下代码

class VolatileBarrierExample {
	int a;
	volatile int v1 = 1;
	volatile int v2 = 2;

	void readAndWrite() {
		int i = v1; // 第一个volatile读
		int j = v2; // 第二个volatile读
		a = i + j; // 普通写
		v1 = i + 1; // 第一个volatile写
		v2 = j * 2; // 第二个 volatile写
	}
}

其指令序列可以优化为:

    上面的优化针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。比如X86处理器仅会对写-读操作做重排序。X86不会对读-读、读-写和写-写操作做重排序,因此在X86处理器中会省略掉这3种操作类型对应的内存屏障。在X86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。这意味着在X86处理器中,volatile写的开销比volatile读的开销会大很多(因为执行StoreLoad屏障开销会比较大)。

3.4 增强volatile的内存语义

    1.的旧Java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量重排序。也就是说,在旧的内存模型中,volatile的写-读没有锁的释放-获所具有的内存语义。为了提供 一种比锁更轻量级的线程之间通信的机制,故增强了volatile的内存语义,严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。

    2.volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,锁比volatile更强大;在可伸缩性和执行性能上,volatile更有优势,换句话说,volatile是更轻量级的锁。

4.锁的内存语义

4.1 锁的释放-获取建立的happens-before关系

    1.锁是Java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。

class VolatileExample{
	int i=0;
	public synchronized void wtirer(){//1	
		i=1;					//2
	}                           //3
	public synchronized void reader(){//4
		int a=i;	//5

	}//6
}

    2.假设线程A执行writer()方法,随后线程B执行reader()方法。根据happens-before规则,这个过程包含的happens-before关系可以分为3类:

(1)根据程序次序规则,1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happensbefore 6。

(2)根据监视器锁规则,3 happens-before 4。

(3)根据happens-before的传递性,2 happens-before 5。

线程A在释放锁之前所有可见的共享变量,在线程B获取同一个锁之后,将立刻变得对B线程可见。

4.2 锁的释放和获取的内存语义

    当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。

    当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变。

    1. 对比锁释放-获取的内存语义与volatile写-读的内存语义可以看出:锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。

    2.内存语义:

(1)线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。

(2)线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。

(3)线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。

4.3 锁内存语义的实现

    1.以ReentrantLock的源代码为例,来分析锁内存语义的具体实现机制。

    在ReentrantLock中,调用lock()方法获取锁;调用unlock()方法释放锁。ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(简称之为 AQS)。AQS使用一个整型的volatile变量(命名为state)来维护同步状态,这个volatile变量是ReentrantLock内存语义实现的关键。

    ReentrantLock分为公平锁和非公平锁。

    1.首先分析公平锁。

     使用公平锁时,加锁方法lock()调用轨迹如下:

 1)ReentrantLock:lock()。

2)FairSync:lock()。

3)AbstractQueuedSynchronizer:acquire(int arg)。

4)FairSync:tryAcquire(int acquires)。

    在第4步真正开始加锁,下面是该方法的源代码。

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState(); //获取锁的开始,尝试获取同步状态
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

    在使用公平锁时,解锁方法unlock()调用轨迹如下。

1)ReentrantLock:unlock()。

2)AbstractQueuedSynchronizer:release(int arg)。

3)Sync:tryRelease(int releases)。

    在第3步真正开始释放锁,下面是该方法的源代码。

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;//获取同步状态,开始释放锁
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);//释放锁,设置同步状态
            return free;
        }

    从以上两个锁的获取和释放源码中可以看出:公平锁在释放锁的最后写volatile变量state,在获取锁时首先读这个volatile变量。根据volatile的happens-before规则,释放锁的线程在写volatile变量操作(也就是setState(c)代码)之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见。

    2.非公平锁的内存语义的实现。

    非公平锁的释放和公平锁完全一样,所以这里仅仅分析非公平锁的获取。使用非公平锁时,加锁方法lock()调用轨迹如下:

1)ReentrantLock:lock()。

2)NonfairSync:lock()。

3)AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)。

    在第3步真正开始加锁,下面是该方法的源代码。

    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

    该方法以原子操作的方式更新state变量,也就是compareAndSet()方法调用,该方法简称为 CAS。该方法表示:如果当前状态值等于预期值,则以原子方式将同步状态 设置为给定的更新值。此操作具有volatile读和写的内存语义。

    编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序。组合这两个条件,意味着为了同时实现volatile读和volatile写的内存语义,编译器不能对CAS与CAS前面和后面的任意内存操作重排序。

    为了实现与volatile相同的内存语义,CAS的底层实现是通过一个C++实现的本地方法,在多处理器情况下,会在该操作的处理器指令前加上一个Lock前缀来添加一个内存屏障,如果是单处理器就无需Lock前缀。

    对于Lock前缀,作用是:

(1)确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4、Intel Xeon及P6处理器开始,Intel使用缓存锁定(Cache Locking) 来保证指令执行的原子性。缓存锁定将大大降低lock前缀指令的执行开销。

(2)禁止该指令,与之前和之后的读和写指令重排序。

(3)把写缓冲区中的所有数据立即刷新到内存中。

    第2点和第3点所具有的内存屏障效果,足以同时实现volatile读和volatile写的内存语义。

    2. 根据以上分析,锁释放-获取的内存语义的实现至少有下面两种方式:

(1) 利用volatile变量的写-读所具有的内存语义。

(2)利用CAS所附带的volatile读和volatile写的内存语义。

4.4 concurrent包的实现

    1.由于Java的CAS同时具有volatile读和volatile写的内存语义,因此Java线程之间的通信现在有了下面4种方式:

    (1)A线程写volatile变量,随后B线程读这个volatile变量。

    (2)A线程写volatile变量,随后B线程用CAS更新这个volatile变量。

    (3)A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。

    (4)A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

    volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。

    2.观察concurrent包的源代码实现,会发现一个通用化的实现模式:

    首先,声明共享变量为volatile。

    然后,使用CAS的原子条件更新来实现线程之间的同步。

    同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

    AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent 包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类 来实现的。从整体来看,concurrent包的实现示意图如下

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

© 著作权归作者所有

共有 人打赏支持
江左煤郎
粉丝 8
博文 22
码字总数 63304
作品 0
西安
后端工程师
加载中

评论(2)

江左煤郎
江左煤郎
谢谢,主要是来自于《Java并发编程的艺术》一书,根据自己的理解进行了一些删减和修改,把一些复杂重复的地方简单化,方便理解,建议对原书进行深入学习,能有自己的见解
davinking
davinking
文章不错 希望能继续
《成神之路-基础篇》JVM——JVM内存结构(已完结)

Java内存模型,Java内存管理,Java堆和栈,垃圾回收 本文是《成神之路系列文章》的第一篇,主要是关于JVM的一些介绍。 持续更新中 参考文章: Java虚拟机的内存组成以及堆内存介绍 Java堆和栈...

⋅ 05/05 ⋅ 0

面试中关于Java虚拟机(jvm)的问题看这篇就够了

最近看书的过程中整理了一些面试题,面试题以及答案都在我的文章中有所提到,希望你能在以问题为导向的过程中掌握虚拟机的核心知识。面试毕竟是面试,核心知识我们还是要掌握的,加油~~~ 下面...

snailclimb ⋅ 05/12 ⋅ 0

大型互联网架构必备技术——性能调优专题

性能调优 深入内核,直击故障 ,拒绝蒙圈 性能优化如何理解 1、性能基准 2、什么是性能优化 3、衡量标准 JVM调优 1、Jvm虚拟机内存剖析 2、垃圾收集器 3、实战调优案例与解决方案 4、Jvm运行...

Java高级架构 ⋅ 04/15 ⋅ 0

《成神之路-基础篇》JVM——JVM参数及调优(已完结)

Java内存模型,Java内存管理,Java堆和栈,垃圾回收 本文是[《成神之路系列文章》][1]的第一篇,主要是关于JVM的一些介绍。 持续更新中 JVM参数及调优 JVM实用参数系列 成为Java GC专家(5)...

⋅ 05/05 ⋅ 0

《成神之路-基础篇》JVM——垃圾回收(已完结)

Java内存模型,Java内存管理,Java堆和栈,垃圾回收 本文是[《成神之路系列文章》][1]的第一篇,主要是关于JVM的一些介绍。 持续更新中 Java之美[从菜鸟到高手演变]之JVM内存管理及垃圾回收 ...

⋅ 05/05 ⋅ 0

2018年Java编程学习面试最全知识点总结

Java是一种可以撰写跨平台应用软件的面向对象的程序设计语言。Java 技术具有卓越的通用性、高效性、平台移植性和安全性,广泛应用于PC、数据中心、游戏控制台、科学超级计算机、移动电话和互...

Java小辰 ⋅ 05/14 ⋅ 0

002. 深入JVM学习—JVM对象访问模式

Object obj = new Object(); 分析 --- Object obj:描述的是保存在栈内存之中,而后保存有堆内存的引用,这个数据会保存在本地变量表中(变量表描述有哪些对象,保存对象栈的位置,栈对应着堆...

影狼 ⋅ 06/22 ⋅ 0

Java 使用 happen-before 规则实现共享变量的同步操作

前言 熟悉 Java 并发编程的都知道,JMM(Java 内存模型) 中的 happen-before(简称 hb)规则,该规则定义了 Java 多线程操作的有序性和可见性,防止了编译器重排序对程序结果的影响。按照官方的...

stateIs0 ⋅ 01/20 ⋅ 0

【JVM】 java内存区域与内存溢出异常

前言 此系列博客是读《深入理解java虚拟机》所做的笔记整理。 No1. JVM内存管理这堵墙? 对C和C++的开发人员来说,在内存管理领域,他们既拥有每一个对象的“所有权”,也担负着每一个对象生...

binggetong ⋅ 05/07 ⋅ 0

深入理解Java虚拟机的体系结构

JAVA虚拟机的生命周期 一个运行时的Java虚拟机实例的天职是:负责运行一个java程序。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。如果...

java进阶 ⋅ 06/22 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

Linux中的端口大全

1 被LANA定义的端口 端口 名称 描述 1 tcpmux TCP 端口服务多路复用 5 rje 远程作业入口 7 echo Echo 服务 9 discard 用于连接测试的空服务 11 systat 用于列举连接了的端口的系统状态 13 d...

寰宇01 ⋅ 13分钟前 ⋅ 0

Confluence 6 如何备份存储文件和页面信息

备份的 ZIP 文件包含有 entities.xml,这个 XML 文件包含有 Confluence 的所有页面内容和存储附件的目录。 备份 Zip 文件结构 页面的附件是存储在附件存储目录中的,通过页面和附件 ID 进行识...

honeymose ⋅ 15分钟前 ⋅ 0

【每天一个JQuery特效】根据状态确定是否滑入或滑出被选元素

主要效果: 本文主要采用slideToggle()方法实现以一行代码同时实现以展开或收缩的方式显示或隐藏被选元素。 主要代码如下: <!DOCTYPE html><html><head><meta charset="UTF-8">...

Rhymo-Wu ⋅ 19分钟前 ⋅ 0

度量.net framework 迁移到.net core的工作量

把现有的.net framework程序迁移到.net core上,是一个非常复杂的工作,特别是一些API在两个平台上还不能同时支持。两个类库的差异性,通过人工很难识别全。好在微软的工程师们考虑到了我们顾...

李朝强 ⋅ 24分钟前 ⋅ 0

请不要在“微服务”的狂热中迷失自我!

微服务在过去几年一直是一个非常热门的话题(附录1)。何为“微服务的疯狂”,举个例子: 众所周知,Netflix在DevOps上的表现非常棒。Netfix可以做微服务。因此:如果我做微服务,我也将非常...

harries ⋅ 26分钟前 ⋅ 0

oAuth2 升级Spring Cloud Finchley.RELEASE踩坑分享

背景 6.19号,spring团队发布了期待已久的 Spring Cloud Finchley.RELEASE 版本。 重要变化: 基于Spring Boot 2.0.X 不兼容 Spring Boot 1.5.X 期间踩过几个坑,分享出来给大伙,主要是关于...

冷冷gg ⋅ 56分钟前 ⋅ 0

OSChina 周一乱弹 —— 理发师小姐姐的魔法

Osc乱弹歌单(2018)请戳(这里) 【今日歌曲】 @冰冰棒- :分享田馥甄的单曲《My Love》 《My Love》- 田馥甄 手机党少年们想听歌,请使劲儿戳(这里) @Li-Wang :哎,头发又长了。。。又要...

小小编辑 ⋅ 今天 ⋅ 8

Kafka1.0.X_消费者API详解2

偏移量由消费者管理 kafka Consumer Api还提供了自己存储offset的功能,将offset和data做到原子性,可以让消费具有Exactly Once 的语义,比kafka默认的At-least Once更强大 消费者从指定分区...

特拉仔 ⋅ 今天 ⋅ 0

NEO智能合约之发布和升级(二)

接NEO智能合约之发布和升级(一),我们接下来说说智能合约的升级功能。 一 准备工作 合约的升级需要在合约内预先设置好升级接口,以方便在升级时调用。接下来我们对NEO智能合约之发布和升级...

红烧飞鱼 ⋅ 今天 ⋅ 0

个人博客的运营模式能否学习TMALL天猫质量为上?

心情随笔|个人博客的运营模式能否学习TMALL天猫质量为上? 中国的互联网已经发展了很多年了,记得在十年前,个人博客十分流行,大量的人都在写博客,而且质量还不错,很多高质量的文章都是在...

原创小博客 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部