volatile是Java提供的一种轻量级的同步机制。Java语言包含两种内在的同步机制:同步块(或方法)和volatile变量,相比于synchronized (synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
定义:即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:
定义︰指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatle修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然, synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
定义:即程序执行的顺序按照代码的先后顺序执行。
Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。
"如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的"。以上这句话也是《深入理解Java虚拟机》中的原句,但是怎么理解呢?周志明并没有详细的解释。这里我简单扩展一下,这其实和as-if-serial语义有关。
as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。
这里不对as-if-serial语义详细展开了,简单说就是,as-if-serial语义保证了单线程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的。当然,实际上还是有重排的,只不过我们无须关心这种重排的干扰。
在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性, synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
锁提供了两种主要特性:互斥(mutual exclusion)和可见性(visibility)。
(1)互斥即一次只允许一个线程持有某个特定的锁,一次就只有一个线程能够使用该共享数据。
(2)可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。也即当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。要使volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
a.对变量的写操作不依赖于当前值。
b.该变量没有包含在具有其他变量的不变式中。
实际上,这些条件表明,可以被写入volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。事实上就是保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(MainMemory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
对于普通的共享变量来讲,线程A将其修改为某个值发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile。
需要注意的是,JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,并不一定就真实的对应cpu缓存和物理内存
1.保证可见性,不保证原子性
(1)当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;
(2)这个写会操作会导致其他线程中的volatile变量缓存无效。
2.禁止指令重排
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则;
(1)重排序操作不会对存在数据依赖关系的操作进行重排序。
比如: a=1;b=a;这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
(2)重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
比如: a=1:b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,下例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。
public class TestVolatile{
int a = 1;
boolean status = false;
//状态切换为true
public void changeStatus{
a = 2; //1
status = true; //2
}
//若状态为true,则为running
public void run(){
if(status){
//3
int b = a + 1;
//4
System.out.println(b);
}
}
}
使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则;
a.当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
b.在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。
例如, inc++不是一个原子性操作,可以由读取、加、赋值3步组成,所以结果并不能达到10000。
public class Test{
public volatile int inc = 0;
public void increase(){
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for( int i = 0; i<10; i++){
new Thread (){
public void run(){
for(int j = 0; j<1000; j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>2)
//保证前面的线程都执行完Thread.yield();
System.out.println(test.inc);
}
}
public class Test {
public int inc = 0;
public synchronized void increase(){
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for( int i = 0; i<10; i++){
new Thread (){
public void run(){
for(int j = 0; j<1000; j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) {
//保证前面的线程都执行完Thread.yield();
System.out.println(test.inc);
}
}
}
public class Test {
public int inc = 0;
Lock lock = new ReentrantLock();
public synchronized void increase(){
lock.lock();
try {
inc++;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final Test test = new Test();
for( int i = 0; i<10; i++){
new Thread (){
public void run(){
for(int j = 0; j<1000; j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>2) {
//保证前面的线程都执行完Thread.yield();
Thread.yield();
}
System.out.println(test.inc);
}
}
public class Test {
public AtomicInteger inc = new AtomicInteger();
public synchronized void increase(){
inc.getAndIncrement();
}
public static void main(String[] args) {
final Test test = new Test();
for( int i = 0; i<10; i++){
new Thread (){
public void run(){
for(int j = 0; j<1000; j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>2) {
//保证前面的线程都执行完Thread.yield();
Thread.yield();
}
System.out.println(test.inc);
}
}
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatle是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatle关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障((也成内存栅栏),内存屏障会提供3个功能:
(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
(2)它会强制将对缓存的修改操作立即写入主存;
(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。