T:hi,J。
J:hi,T。最近忙什么呢?
T:我最近在研究Java并发编程,刚学习了两个重要的特性:原子性和可见性。
J:哦,能解释一下吗?
T:你知道什么是原子吗?
J:这个我知道,原子就是构成物质的基本单位。
T:对,在编程中,原子操作就是指不可再分的操作,原子性就是指一段代码象原子一样不可再分,一次只能有一个线程执行这段代码,即代码的执行是互斥的。
J:我明白了,那可见性呢?
T:可见性指一个变量在一个线程中的修改能够被其它所有使用该变量的线程看到。
J:我大概明白他们是什么意思了,但还不清楚在什么情况下会使用到它们。
T:只有正确的理解了他们才能正确的使用他们,下面我将对它们做详细的讲解。我们从原子性开始吧。

原子性

先看一个例子:

public class test {
	private int a = 0;
	
	public void increase(){
		a++;
	}
}

现在假定我们在多个线程中调用该类的同一个实例的increase方法,那么,会出现什么情况呢?
首先,a++并不是一个原子操作,它分为3个步骤:1)获取a的值;2)为a加1;3)存入a的值。因此,在多个线程同时执行时,就可能出现一个线程执行的步骤1和步骤2之间,另一个线程插入开始执行步骤1,如下:
 获取a------>加1------>存入
     获取a------>加1------>存入
这样,两个线程获取到的a的初始值一样,都为a加1,然后依次存入,最终a的值变为初始值加1,并不是增加了2。发生这种情况的主要原因在于a++不具备原子性。
J:哦,我明白了,就是在多线程下,不具备原子性的操作在多个线程之间调用,每两步之间都可能会有其它线程插入操作,导致bug。
T:非常正确
J:那么这个问题怎么解决呢?
T:在Java中,可以通过为该方法加锁来解决这个问题:

public class test {
	private int a = 0;
	
	public synchronized void increase(){
		a++;
	}
}

这样,increase就具备原子性了,increase就是线程安全的了。
J:原子性我大概了解了,那可见性呢?
T:我们还是从一个例子开始:

可见性

public class test {
	private int value = 0;
	
	public void set(int value){
		this.value = value;
	}
	
	public int get(){
		return this.value;
	}
}

假定我们在一个线程中调用set方法,而在另一个线程中调用get方法,那么get方法是否始终都能得到最新的数据了?
J:这。。。
T:不一定。
J:为什么?
T:在可共享内存的多处理器体系架构中,每个处理器都有它自己的缓存,并且周期性地与主存协调一致。处理器架构提供了不同级别的缓存一致性;有些只提供了最小的保证,几乎在任何时间内,都允许不同的处理器在相同的存储位置上看到不同的值。因此,上面的程序中,当一个线程调用set方法后,并不能保证当另一个线程获取value的值时,value的值已经同步到主存中,获取到的任然可能是旧值。
J:但为什么要这么做呢?
T:因为要想保证每个处理器能在任意时间内获知其他处理器正在进行的工作,其代价非常高。并且大多数时间里这些信息都是没用的,所以处理器会牺牲存储一致性的保证,来换取性能的提升。
J:哦,但我们需要一些手段来保证存储一致性,对吧?
T:是的,多处理器体系架构的存储模型会告诉应用程序可以从它的存储系统中获得何种担保,同时详细定义了一些特殊的指令称为存储关卡或栅栏,用以在需要共享数据时,得到额外的存储协调保证。而在Java中,为了帮助Java开发者屏蔽这些跨架构的存储模型之间的不同,Java提供了自己的存储模型,JVM会通过在适当的位置上插入存储卡片,来解决JMM与底层平台存储模型之间的差异。
J:那具体我们怎么做呢?
T:就是在适当的位置加上同步操作,例如:

public class test {
	private int value = 0;
	
	public synchronized void set(int value){
		this.value = value;
	}
	
	public synchronized int get(){
		return this.value;
	}
}

J:哦,我明白了。
T:有时候我么也需要特别小心,例如,J,你觉得下面这段代码正确吗?

public class test {
	private int value = 0;
	
	public synchronized void set(int value){
		this.value = value;
	}
	
	public int get(){
		return this.value;
	}
}

J:恩,好像有点问题。
T:是的,这段代码任然存在问题,为set方法增加同步后,保证了对变量的修改会同步到主内存区域,但是由于get方法没有同步,get任然会从本地缓存中获取变量的值,因此变量的正确性无法得到保证。
J:看来,正确的使用同步还真不简单。
T:是的。
J:我来总结一下吧:原子性保证代码之间的互斥,而可见性保证变量的修改能够被其它线程看到;在并发编程中,对需要保证原子性和可见性的操作我们都应该添加同步。
T:非常好,今天就到这里了,再见,J。
J:再见。