Volatile与Synchronized的区别

java线程的内存模型

java的线程内存模型中定义了每个线程都有一份自己的共享变量副本(本地内存),里面存放自己私有的数据,其他线程不能直接访问,而一些共享变量则存在主内存中,供所有线程访问。
上图中,如果线程A和线程B要进行通信,就要经过主内存,比如线程B要获取线程A修改后的共享变量的值,要经过下面两步:
(1)、线程A修改自己的共享变量副本,并刷新到了主内存中。
(2)、线程B读取主内存中被A更新过的共享变量的值,同步到自己的共享变量副本中。
总结:在java内存模型中,共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。

java多线程中的三个特性:

  原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。一个很经典的例子就是银行账户转账问题:比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证不出现一些意外的问题。
  可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  有序性:就是程序执行的顺序按照代码的先后顺序执行。一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。如下:
      int a = 10; //语句1
      int r = 2; //语句2
      a = a + 3; //语句3
      r = a*a; //语句4
因为重排序,他还可能执行顺序为 2-1-3-4,1-3-2-4。但绝不可能 2-1-4-3,因为这打破了依赖关系。显然重排序对单线程运行是不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。

Volatile关键字的作用
  其实volatile关键字的作用就是保证了可见性和有序性(不保证原子性),如果一个共享变量被volatile关键字修饰,那么如果一个线程修改了这个共享变量后,其他线程是立马可知的。如果线程A修改了自己的共享变量副本,这时如果该共享变量没有被volatile修饰,那么本次修改不一定会马上将修改结果刷新到主存中,如果此时B去主存中读取共享变量的值,那么这个值就是没有被A修改之前的值。如果该共享变量被volatile修饰了,那么本次修改结果会强制立刻刷新到主存中,如果此时B去主存中读取共享变量的值,那么这个值就是被A修改之后的值了。
  volatile禁止指令重排序优化,在指令重排序优化时,在volatile变量之前的指令不能在volatile之后执行,在volatile之后的指令也不能在volatile之前执行,所以它保证了有序性。
  volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令(是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。)来保证处理器不发生乱序执行。

synchronized关键字的作用:  
  synchronized提供了同步锁的概念,被synchronized修饰的代码段可以防止被多个线程同时执行,必须一个线程把synchronized修饰的代码段都执行完毕了,其他的线程才能开始执行这段代码。 因为synchronized保证了在同一时刻,只能有一个线程执行同步代码块,所以执行同步代码块的时候相当于是单线程操作了,那么线程的可见性、原子性、有序性(线程之间的执行顺序)它都能保证了。synchronized并没有禁止重排序,但是synchronized相当于是一个单线程了,所以有没有重排序对程序都是没有影响的。

Volatile和synchronized的区别: 
  (1)、volatile只能作用于变量,使用范围较小。synchronized可以用在变量、方法、类、同步代码块等,使用范围比较广。
  (2)、volatile只能保证可见性和有序性,不能保证原子性。而可见性、有序性、原子性synchronized都可以包证。
  (3)、volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。
  (4)、在性能方面synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized。

什么是重排序
  重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。但是重排序可以保证最终执行的结果是与程序顺序执行的结果一致,并且只会对不存在数据依赖性的指令进行重排序,这个重排序在单线程下对最终执行结果是没有影响的,但是在多线程下就会存在问题。

可以看一个例子: 

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 
}}}

如上面代码,如果两个线程同时执行在没有发生重排序的时候int i =1,如果发生了重排序那么1,2的位置因为不存在数据依赖可以会发生位置的互换。那么这时候int i =0;当然这个在单线程是没有问题的。只有在多线程才会发生这种情况

 volatile int a = 0;
    volatile boolean flag = false;

我们只需要加上volatile关键字也是可以避免这种问题的,volatile是禁止重排序的。

什么是数据依赖?

int a = 1;(1)
int b = 2;(2)
int c= a + b;(3)

这里面第三步就存在数据依赖。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。所以这里面无论(1)(2)有没有发生重排序,(3)都是在他们之后执行。这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

s-if-serial语义
  无论怎么排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。
  为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。

happens-before语义
  如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

具体定义
  1、如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2、两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照 happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(如果A happens- before B,JMM并不要求A一定要在B之前执行。JMM仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。这里操作A的执行结果不需要对操作B可见;而且重排序操作A和操作B后的执行结果,与操作A和操作B按happens- before顺序执行的结果一致。在这种情况下,JMM会认为这种重排序并不非法(not illegal),JMM允许这种重排序)。 

happens-before规则如下:
  程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

posted @ 2020-04-07 21:59  无话可说丶  阅读(1905)  评论(0编辑  收藏  举报