4个点说清楚Java中synchronized和volatile的区别

作者 : Hollis

回顾一下两个关键字:synchronized和volatile

1、Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized、volatile、final、concurren包等。

2、synchronized通过加锁的方式,使得其在需要原子性、可见性和有序性这三种特性的时候都可以作为其中一种解决方案,看起来是“万能”的。的确,大部分并发控制操作都能使用synchronized来完成。

3、volatile通过在volatile变量的操作前后插入内存屏障的方式,保证了变量在并发场景下的可见性和有序性。

4、volatile关键字是无法保证原子性的,而synchronized通过monitorenter和monitorexit两个指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,即可保证不会出现CPU时间片在多个线程间切换,即可保证原子性。

那么,我们知道,synchronized和volatile两个关键字是Java并发编程中经常用到的两个关键字,而且,通过前面的回顾,我们知道synchronized可以保证并发编程中不会出现原子性、可见性和有序性问题,而volatile只能保证可见性和有序性,那么,既生synchronized、何生volatile?

接下来,本文就来论述一下,为什么Java中已经有了synchronized关键字,还要提供volatile关键字。

synchronized的问题

我们都知道synchronized其实是一种加锁机制,那么既然是锁,天然就具备以下几个缺点:

1、有性能损耗

虽然在JDK 1.6中对synchronized做了很多优化,如如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等,但是他毕竟还是一种锁。

以上这几种优化,都是尽量想办法避免对Monitor进行加锁,但是,并不是所有情况都可以优化的,况且就算是经过优化,优化的过程也是有一定的耗时的。

所以,无论是使用同步方法还是同步代码块,在同步操作之前还是要进行加锁,同步操作之后需要进行解锁,这个加锁、解锁的过程是要有性能损耗的。

关于二者的性能对比,由于虚拟机对锁实行的许多消除和优化,使得我们很难量化这两者之间的性能差距,但是我们可以确定的一个基本原则是:volatile变量的读操作的性能小号普通变量几乎无差别,但是写操作由于需要插入内存屏障所以会慢一些,即便如此,volatile在大多数场景下也比锁的开销要低。

2、产生阻塞

关于synchronize的实现原理,无论是同步方法还是同步代码块,无论是ACC_SYNCHRONIZED还是monitorenter、monitorexit都是基于Monitor实现的。

基于Monitor对象,当多个线程同时访问一段同步代码时,首先会进入Entry Set,当有一个线程获取到对象的锁之后,才能进行The Owner区域,其他线程还会继续在Entry Set等待。并且当某个线程调用了wait方法后,会释放锁并进入Wait Set等待。

所以,synchronize实现的锁本质上是一种阻塞锁,也就是说多个线程要排队访问同一个共享对象。

而volatile是Java虚拟机提供的一种轻量级同步机制,他是基于内存屏障实现的。说到底,他并不是锁,所以他不会有synchronized带来的阻塞和性能损耗的问题。

volatile的附加功能

除了前面我们提到的volatile比synchronized性能好以外,volatile其实还有一个很好的附加功能,那就是禁止指令重排。

我们先来举一个例子,看一下如果只使用synchronized而不使用volatile会发生什么问题,就拿我们比较熟悉的单例模式来看。

我们通过双重校验锁的方式实现一个单例,这里不使用volatile关键字:

1 public class Singleton { 2 private static Singleton singleton; 3 private Singleton (){} 4 public static Singleton getSingleton() { 5 if (singleton == null) { 6 synchronized (Singleton.class) { 7 if (singleton == null) { 8 singleton = new Singleton(); 9 } 10 } 11 } 12 return singleton; 13 } 14 }

以上代码,我们通过使用synchronized对Singleton.class进行加锁,可以保证同一时间只有一个线程可以执行到同步代码块中的内容,也就是说singleton = new Singleton()这个操作只会执行一次,这就是实现了一个单例。

但是,当我们在代码中使用上述单例对象的时候有可能发生空指针异常。这是一个比较诡异的情况。

我们假设Thread1 和 Thread2两个线程同时请求Singleton.getSingleton方法的时候:

  • Step1 ,Thread1执行到第8行,开始进行对象的初始化。
  • Step2 ,Thread2执行到第5行,判断singleton == null。
  • Step3 ,Thread2经过判断发现singleton != null,所以执行第12行,返回singleton。
  • Step4 ,Thread2拿到singleton对象之后,开始执行后续的操作,比如调用singleton.call()。

以上过程,看上去并没有什么问题,但是,其实,在Step4,Thread2在调用singleton.call()的时候,是有可能抛出空指针异常的。

之所有会有NPE抛出,是因为在Step3,Thread2拿到的singleton对象并不是一个完整的对象。

什么叫做不完整对象,这个怎么理解呢?

我们这里来先来看一下,singleton = new Singleton();这行代码到底做了什么事情,大致过程如下:

  • 1、虚拟机遇到new指令,到常量池定位到这个类的符号引用。
  • 2、检查符号引用代表的类是否被加载、解析、初始化过。
  • 3、虚拟机为对象分配内存。
  • 4、虚拟机将分配到的内存空间都初始化为零值。
  • 5、虚拟机对对象进行必要的设置。
  • 6、执行方法,成员变量进行初始化。
  • 7、将对象的引用指向这个内存区域。

我们把这个过程简化一下,简化成3个步骤:

  • a、JVM为对象分配一块内存M
  • b、在内存M上为对象进行初始化
  • c、将内存M的地址复制给singleton变量

如下图:

因为将内存的地址赋值给singleton变量是最后一步,所以Thread1在这一步骤执行之前,Thread2在对singleton==null进行判断一直都是true的,那么他会一直阻塞,直到Thread1将这一步骤执行完。

但是,问题就出在以上过程并不是一个原子操作,并且编译器可能会进行重排序,如果以上步骤被重排成:

  • a、JVM为对象分配一块内存M
  • c、将内存的地址复制给singleton变量
  • b、在内存M上为对象进行初始化

如下图:

这样的话,Thread1会先执行内存分配,在执行变量赋值,最后执行对象的初始化,那么,也就是说,在Thread1还没有为对象进行初始化的时候,Thread2进来判断singleton==null就可能提前得到一个false,则会返回一个不完整的sigleton对象,因为他还未完成初始化操作。

这种情况一旦发生,我们拿到了一个不完整的singleton对象,当尝试使用这个对象的时候就极有可能发生NPE异常。

那么,怎么解决这个问题呢?因为指令重排导致了这个问题,那就避免指令重排就行了。

所以,volatile就派上用场了,因为volatile可以避免指令重排。只要将代码改成以下代码,就可以解决这个问题:

1 public class Singleton { 2 private volatile static Singleton singleton; 3 private Singleton (){} 4 public static Singleton getSingleton() { 5 if (singleton == null) { 6 synchronized (Singleton.class) { 7 if (singleton == null) { 8 singleton = new Singleton(); 9 } 10 } 11 } 12 return singleton; 13 } 14 }

对singleton使用volatile约束,保证他的初始化过程不会被指令重排。这样就可以保Thread2 要不然就是拿不到对象,要不然就是拿到一个完整的对象。

synchronized的有序性保证呢?

看到这里可能有朋友会问了,说到底上面问题是发生了指令重排,其实还是个有序性的问题,不是说synchronized是可以保证有序性的么,这里为什么就不行了呢?

首先,可以明确的一点是:synchronized是无法禁止指令重排和处理器优化的。那么他是如何保证的有序性呢?

这就要再把有序性的概念扩展一下了。Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的。

以上这句话也是《深入理解Java虚拟机》中的原句,但是怎么理解呢?周志明并没有详细的解释。这里我简单扩展一下,这其实和as-if-serial语义有关。

as-if-serial语义的意思指:不管怎么重排序,单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。

这里不对as-if-serial语义详细展开了,简单说就是,as-if-serial语义保证了单线程中,不管指令怎么重排,最终的执行结果是不能被改变的。

那么,我们回到刚刚那个双重校验锁的例子,站在单线程的角度,也就是只看Thread1的话,因为编译器会遵守as-if-serial语义,所以这种优化不会有任何问题,对于这个线程的执行结果也不会有任何影响。

但是,Thread1内部的指令重排却对Thread2产生了影响。

那么,我们可以说,synchronized保证的有序性是多个线程之间的有序性,即被加锁的内容要按照顺序被多个线程执行。但是其内部的同步代码还是会发生重排序,只不过由于编译器和处理器都遵循as-if-serial语义,所以我们可以认为这些重排序在单线程内部可忽略。

总结

本文从两方面论述了volatile的重要性以及不可替代性:

一方面是因为synchronized是一种锁机制,存在阻塞问题和性能问题,而volatile并不是锁,所以不存在阻塞和性能问题。

另外一方面,因为volatile借助了内存屏障来帮助其解决可见性和有序性问题,而内存屏障的使用还为其带来了一个禁止指令重排的附件功能,所以在有些场景中是可以避免发生指令重排的问题的。

所以,在日后需要做并发控制的时候,如果不涉及到原子性的问题,可以优先考虑使用volatile关键字。

原文地址:https://www.cnblogs.com/zhuifeng523/p/11421047.html

时间: 2024-08-27 22:44:53

4个点说清楚Java中synchronized和volatile的区别的相关文章

Java中synchronized和Lock的区别

synchronized和Lock的区别synchronize锁对象可以是任意对象,由于监视器方法必须要拥有锁对象那么任意对象都可以调用的方法所以将其抽取到Object类中去定义监视器方法这样锁对象和监视器对象是同一个,只要创建了锁对象它既是锁对象同时也是监视器对象这样不能实现在一个锁对象上绑定多个监视器对象jdk1.5中Lock对象仅仅是一个锁对象监视器方法被封装到了Condition对象中这样实现了锁对象和监视器对象进行了分离更加的面向对象这样可以实现在一个锁对象上绑定多个监视器对象 在一个

java中 synchronized 的使用,确保异步执行某一段代码。

最近看了个有关访问网络url和下载的例子,里面有几个synchronized的地方,系统学习下,以下内容很重要,记下来. Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行.另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块. 二.然而,当一个线程访问object的一个synchroniz

java中synchronized的用法与详解

Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行.另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块. 二.然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块.

java中synchronize锁 volatile thread.join()方法的使用

对于并发工作,你永远不知道一个线程何时运行,你需要某种方式来避免两个任务访问相同的资源,即要避免资源竞争,至少在关键代码上不能出现这样的情况,否则多个线程同时对某个内存区域操作会导致数据破坏. 程序代码中的临界区是需要互斥访问的,同一时刻只能有一个线程来访问临界区,也就是线程对临界区的访问时互斥的. 竞争条件:当多个线程同时访问某个共享的内存区域并且对其进行读写操作时,就会出现数据破坏.这就是竞争条件.避免竞争条件的方法是synchronized加锁. 样例,设有一个现成,该线程的任务是对共享变

java学习笔记——Java中HashMap和TreeMap的区别深入理解

本文转载自Java中HashMap和TreeMap的区别深入理解 首先介绍一下什么是Map.在数组中我们是通过数组下标来对其内容索引的,而在Map中我们通过对象来对对象进行索引,用来索引的对象叫做key,其对应的对象叫做value.这就是我们平时说的键值对. HashMap通过hashcode对其内容进行快速查找,而 TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的). HashMap 非线程安全 

java中List 和 Set 的区别

a. 特性 两个接口都是继承自Collection?,是常用来存放数据项的集合,主要区别如下:      ① List和Set之间很重要的一个区别是是否允许重复元素的存在,在List中允许插入重复的元素,而在Set中不允许重复元素存在. ? ② 与元素先后存放顺序有关,List是有序集合,会保留元素插入时的顺序,Set是无序集合. ? ③ List可以通过下标来访问,而Set不能. b. 常见实现类 ? (1) List接口 常见实现类如下: ? ArrayList(数组实现):允许对元素进行快

【翻译】JAVA中抽象类和接口的区别

不知道有没有人翻译过了,这篇是挺简单的,权当复习一遍内容吧. 很多常见的面试问题如"抽象类和接口有什么区别","你在什么时候用抽象类什么时候用接口".所以在这篇文章里面,我们会讨论一下这个话题. 在开始讨论它们的区别之前,我们先看看它们的介绍. Abstract class 抽象类 抽象类被用来抽象出子类的共同特征.它是不能被实例化的,它只能被子类继承作为超类使用.抽象类被用作它的子类下的层次结构创建模板(Abstract classes are used to c

java中String与StringBuilder的区别

本篇文章介绍了,java中String与StringBuilder的区别. 相信大家对 String 和 StringBuffer 的区别也已经很了解了,但是估计还是会有很多同志对这两个类的工作原理有些不清楚的地方,今天我在这里重新把这个概念给大家复习一下,顺便牵出 J2SE 5.0 里面带来的一个新的字符操作的类—— StringBuilder (先别忙着扔我砖头,我还算清醒,我这里说的不是 C #, Java 也有 StringBuilder 类).那么这个 StringBuilder 和

java中extends与implements的区别

学了java很久了,久不用之后给同学解决一个java问题的时候,就卡在这个标题上了. 下面是java中extends与implements的区别: 1. 在类的声明中,通过关键字extends来创建一个类的子类.一个类通过关键字implements声明自己使用一个或者多个接口. extends 是继承某个类, 继承之后可以使用父类的方法, 也可以重写父类的方法; implements 是实现多个接口, 接口的方法一般为空的, 必须重写才能使用 2.extends是继承父类,只要那个类不是声明为f