Java高并发编程详解系列-十三

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/nihui123/article/details/90702470

在之前的分享中,提到了多线程的类加载机制,说道了线程上下文类加载器,也介绍关于多线程中的内存使用情况,提到了一个关键字volatile,介绍了CPU的缓存机制,介绍了Java内存模型。

下面就来介绍一下并发编程中的最为中要的三个特点。

并发编程三大特性

原子性

  所谓的原子性在之前的博客中或者是在网上其他资料上都有所提及到。是指在一次的操作或者多次的操作过程中,要么所有的需要的操作全部都执行,要么就是所有的操作全不执行。例如:在执行一个账户的转账操作的时候,需要进行的步骤是A账户的钱减少,而B账户的钱增加。那么这个操作就需要两边都执行成功,不能出现A账户的钱减少而B账户的钱没有增加,或者说是A账户钱没有发生变化而B账户的钱发生了变化。
  其实在我们编程操作的过程中,例如一条简单的赋值语句,如果没有保证原子性操作的话就会导致数据的错误。但是有时候会遇到一种情况就是两个原子性的操作合在一起并不一定是原子性的。在Java中volatile关键字不是用来保证原子性的,synchronized关键字可以保证原子性。当然从JDK1.5开始提供了很多的保证原子性的变量。这些原子性的变量就可以保证原子性操作。

可见性

  多线程之间的可见性是指,当一个线程对共享变量进行修改,另外一个线程可以立即看到修改之后的最新值。也就是说,我们在之前的例子中也提到过关于多线程的内存模型,实际上每个线程操作的时候,都是将主存中的一个变量先在缓存中拷贝一份,然后操作完成之后将这个变量重新放回主存中。

有序性

  有序性是指在代码执行的过程中是有先后顺序的,由于Java在编译期间和运行期间都做了优化,导致真实执行的时候会和开发者实际编写的顺序不一样。例如

int a = 10;
int b = 20;
a++;
b=0;

  上面的代码定义了两个变量a和b,并且对a变量进行了自增操作,对b变量进行了赋值操作。从代码编写的角度上执行,也从平时分析代码流程上来分析,这个代码是从上到下执行的。但是JVM真正运行的过程中未必会是这样的顺序。这个情况就是之前所说道的指令重排(Instruction Recorder)。
  一般来说这样的指令重排的操作就是对执行效率进行了一次优化,当然指令重排是严格遵守指令之间的数据依赖关系,并不是可以随意进行重排的。
  就拿上面那段程序来说,在单线程情况下执行的顺序可能就是代码编写的顺序,但是在多线程的情况下就会出现问题
例如

private boolean flag = false;
private Context context;

public Context load(){
	if(!flag){
		context = loadContext();
		flag = true;
	}
	return context;
}

  上面这代码是从网上模拟过来的,也就是说通过flag来控制是否context被加载过,如果在多线程的情况下出现了指令重排,也就是说真正加载Context被排到了flag=true的后面,就会导致第一个线程正常加载完成之后,其他所有的线程都不会进行加载。

JMM保证三大特性

  在上面提到了在高并发中的三个特征,在高并发编程中这三个特征是比较重要的,所以下面就来结合Java 内存模型来看看在Java中通过什么样的方式来保证三大特性。

  在JVM中采用了内存模型的机制来实现各个平台之间访问内存的差异,这个也算是Java实现跨平台的一种内存访问机制,这样的话Java程序在各个平台上可以达到一致性访问内存的效果。例如最简单的就是C语言中的变量在不同平台上的内存时不一样的。但是在Java中所有的平台上都是四个字节 。
  之前也提到了JMM规定的所有变量都在主内存中,每个线程只有自己的本地内存,线程对于变量的操作都是在自己的工作内存中,不能直接对主内存进行操作。

JMM与原子性

  在Java中对于基本数据类型的读取就是原子性的操作,对于引用类型的读取和赋值也是原子性的。所以说原子性的是一荣俱荣的操作。

  1. 变量赋值操作 a=1;
    a=1的操作是原子性的,执行线程首先会将a=1写道工作内存中,然后将其写入到主内存中。在多线程的情况下可能会出现有另外的线程将其赋值其他值的操作,但是最终结果只能是1或者是其他另外一个值。这个就是变量赋值操作的原子性
  2. 变量之间赋值 a=b;
    上面这个操作是一个非原子性的操作,主要包含获取b的值,以及将b的值赋值给a的操作,显然这两个操作分开来讲都是原子性的,但是合在一起就不是原子性操作了。
  3. 自增操作 i++;
    执行过程包含三个步骤,第一个步骤是从主存中读取i的值,然后放到工作内存中,第二步将工作内存中的i的值进行加1操作,第三步将y的值写入到主内存中。

综合上面的三个例子,也就是说只有第一种操作是具备原子性的,其余的操作全部不拘于原子性。也就是说

  1. 多个原子性的操作在一起就不是原子性操作
  2. 对于简单的读取操作与赋值操作是原子性的,但是如果将一个变量的值赋值给另外一个变量就不是原子性的了。
  3. JMM只能保证基本读取操作和赋值操作的原子性,并不能保证其他操作的原子性,如果想要保证其他操作的原子性则需要synchronized关键字和lock操作。
JMM与可见性

  在多线程环境下面如果某个线程需要首次获取某个共享变量,那么就需要先从主存中获取该变量的值,然后进入到工作内存中,通过操作工作内存中的变量执行对应的操作。如果对该变量执行了修改的操作,则需要先将值写入到工作内存中,然后刷到主存中,至于什么时候进行这个操作是不确定的。在 Java中提供以下的三种方式来保证可见性。

  1. 使用关键字volatile,当一个变量被这个关键字修饰之后,对于共享资源的读操作会直接在主存中进行(当然这个操作也会缓存到工作内存中,当其他线程对共享资源进行了修改,则会导致当前线程工作内存中的共享资源失效,所以必须从主存中重新获取),对于共享操作的写操作就需要先进行修改工作内存,然后刷到主存中。
  2. 通过synchronized关键字能够保证可见性,synchronized关键字能够保证同一时间有一个线程获取锁然后执行同步方法,并且在锁发生变化之前就将工作内存中的变量刷到主存中。
  3. 通过JUC提供的lock也可以保证可见性,Lock的lock方法能够保证在同一时间内只会有一个线程获取到同步方法,并且保证在操作完成之后主存中的数据是最新的。
JMM与有序性

  Java内存模型中,允许编译器和处理器进行指令重排,在单线程的情况下之前也说过,不会出现问题,但是现在讨论的是多线程情况下就会导致影响程序运行结果。Java提供了三种保证有序性的方式。

  1. 使用volatile关键字
  2. 使用synchronized关键字
  3. 使用显示所Lock

保证有序性的原理和保证可见性是一样的。当然,JMM也有很多的原生的有序性规则,不许要同步就可以实现,这个规则就是Happens-before原则。如果两个操作执行的顺序不能从happens-before中进行推到,就无法保证有序性。

总结

  通过上面例子得出了三条对应三个特点的关键性的结论。

volatile关键字不具备保证原子性的语义
volatile关键字具有保证可见性的语义
volatile关键字具有保证顺序性的语义。

猜你喜欢

转载自blog.csdn.net/nihui123/article/details/90702470