Volatile 与 Synchronized的区别

引言

    在研究并发程序时,我们可能都知道volatile和synchronized是用于多线程中,用于线程安全和变量可见性的,但是具体两者怎么使用,有何区别可能还是稀里糊涂一知半解,在此就自己简单的理解总结一下二者的区别,和大家一块儿学习!我们需要了解java中关键字volatile和synchronized关键字的使用以及lock类的用法。

    首先,了解下java的内存模型

  

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

 

java多线程中的三个特性:

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

int a = 20; //语句1
int r = 3; //语句2
a = a + 5; //语句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

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

可以看一个例子: 

 1 class TestExample {
 2  int a = 0;
 3  boolean flag = false;
 4 
 5 // 写入的线程
 6 public void writer() {
 7  a = 1; // 1
 8  flag = true; // 2
 9 
10 }
11 //读取的线程 12 public void reader() {
13 if (flag) { // 3 14 int i = a * a; // 4 15 }}}

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

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

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

什么是数据依赖?

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

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

 

posted @ 2020-12-31 09:01  zsq_fengchen  阅读(506)  评论(0编辑  收藏  举报