volatile和synchronized

版权声明:本文为博主原创文章,未经博主允许不得转载。http://www.cnblogs.com/jokermo/

0. 前言

转载请注明出处:http://www.cnblogs.com/jokermo/

volatile和synchronized都是解决多线程安全问题的方法,为了了解与使用这两个修饰符,首先需要了解什么是多线程安全问题。即多线程安全问题发生的原因。多线程安全问题产生的原因可以概括为两点:

  • 线程任务中处理到共享数据
  • 线程任务中有多条共享数据操作,一条线程在操作共享数据的过程时,另一条线程参与了运算,造成了数据错误。

解决多线程问题的思想:

  • 只要保证多条操作共享的代码在某一时间段内被同一条线程操作,在执行期间不允许其他线程参与运算。这下就运用到了volatilesynchronized了。

1.Java内存模型简称JMM(Java Memory Model)

JMM用来屏蔽不同硬件和操作系统的内存访问差异,期望Java程序在各种平台上都能实现一致的内存访问效果。

  • 主内存:主内存可以简单理解为计算机当中的内存,但又不完全等同。主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。

工作内存:

工作内存可以简单理解为计算机当中的CPU高速缓存,但又不完全等同。每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。

线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。因为直接操作主内存太慢,所以JVM利用性能更好的工作内存,这里可以类比成CPU,内存,高速缓存.不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行。

2.synchronized

synchronized可作用于一段代码或方法,既可以保证可见性,又能够保证原子性。

  • 用于方法上称为同步函数,同步函数的锁是this
  • 用在方法上称为同步代码块,以唯一的对象为锁

可见性体现在:通过synchronized或者Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存中。

原子性表现在:要么不执行,要么执行到底。

推荐使用同步代码块,因为对函数同步时,函数里面可能包括了非共享资源,导致其他线程访问不到。性能会比同步代码块差。

3.volatile

volatile关键字具有许多特性,其中最重要的特性就是保证了用volatile修饰的变量对所有线程的可见性。这里的可见性的意思是当一条线程对变量进行操作时,新的值会立刻更新到主内存中。当另一条线程访问这个变量时,取到的是主内存里面的最新值。这得益于java的先行发生原则

但是volatile不保证变量的原子性。因为volatile不保证变量的原子性,并发处理时可能会出错,所以使用volatile是需要条件的,以下情况适合使用。

  • 运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。也就是多条线程交叉处理共享资源变量。
  • 变量不需要与其他的状态变量共同参与不变约束。

第一条很好了解,来看看第二条。

volatile static int start = 3;

volatile static int end = 6

线程A执行如下代码:

while (start < end){

  //do something

}

线程B执行如下代码:

start+=3;

end+=3;

这种情况下,一旦在线程A的循环中执行了线程B,start有可能先更新成6,造成了一瞬间 start == end,从而跳出while循环的可能性。

 

4.指令重排序

什么是指令重排?

指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。

指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。

 因为多线程情况下指令重排会出现错误结果。

/*线程A*/
boolean b = false;
contex = test();
b = true;
/*线程B*/
while(!b){
sleep(2000);
...
}
doAfter(contex);

以上代码正常执行时不会影响最终结果,但是当发生指令重排时,如下代码:

/*线程A*/
boolean b = false;
b = true;
contex = test();

/*线程B*/
while(!b){
sleep(2000);
...
}
doAfter(contex);

这个时候,很可能context对象还没有加载完成,变量b已经为true,线程B直接跳出了循环等待,开始执行doAfter()方法,结果自然会出现错误。

5.内存屏障

解决指令重排问题,使用了内存屏幕,内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。 这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。

内存屏幕分四种:

  • LoadLoad屏障

    抽象场景:Load1; LoadLoad; Load2

    Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

  • StoreStore屏障:

    抽象场景:Store1; StoreStore; Store2

    Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见

  • LoadStore屏障:

    抽象场景:Load1; LoadStore; Store2

    在Store2被写入前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障:

抽象场景:Store1; StoreLoad; Load2

在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的。

5.valatile指令重排

volatile做了什么?

 

在一个变量被volatile修饰后,JVM会为我们做两件事:

1在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。

2.在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。

或许这样说有些抽象,我们看一看刚才线程A代码的例子:

boolean b = false;
b = true;
contex = test();

我们给b 增加volatile修饰符,会带来什么效果呢?

volatile boolean b = false;
StoreStore屏障 b = true;
StoreLoad屏障 contex = test();
由于加入了StoreStore屏障,屏障上方的普通写入语句 context = test()  和屏障下方的volatile写入语句 b = true 无法交换顺序,从而成功阻止了指令重排序。

 总结:

1.volatile特性一

 保证变量在线程之间的可见性。可见性的保证是基于CPU的内存屏障指令,被JSR-133抽象为happens-before原则。

2.volatile特性二

阻止编译时和运行时的指令重排。编译时JVM编译器遵循内存屏障的约束,运行时依靠CPU屏障指令来阻止重排。

注:大部分内容来自”码农翻身“,我是搬运工。

猜你喜欢

转载自www.cnblogs.com/jokermo/p/8922200.html