前言:今天看到一道经典的面试题:Volatile 和 synchronized的区别 , 以前对它的理解知识很片面,很浅。这几天在查阅了很多资料后,终于对它底层原理也清晰了,在这里记录一下。
首先,我们要知道一个概念 Java 内存模型即 Java Memory Model , 简称 JMM 。JMM 定义了 Java 虚拟机 ( JVM ) 在计算机内存 ( RAM ) 中的工作方式。
多线程并发过程中, 我们需要处理的三个问题是: 可见性,原子性,有序性。
并发编程的两个关键问题:
1:线程之间如何通信。
概念:线程的通信是指线程之间以何种机制来交换信息。
-
内存共享。
即:线程之间共享程序的公开状态,线程之间通过 写-读 内存中的公共状态来隐式的进行通信。比如通过共享对象进行通信。 -
消息传递;
即:线程之间没有公共状态,线程之间必须通过明确的发送消息来显示进行通信,在JAVA中典型的消息传递方式为: wait() 和 notify()。
2:线程之间如何同步
概念:同步是指程序用于控制不同线程之间操作发生相对顺序的机制。
-
在共享内存的并发模式中,同步是显示做的,比如:synchronized。也就是程序员必须显示指定某个方法或某段代码需要在线程之间互斥执行。
-
在消息传递的并发模式中,由于消息的发送必须在消息接受之前,所以同步是隐式做的。
2 : 内存模型:
JMM决定了一个线程对共享变量的写入何时对另一个线程可见。从抽象角度来看,JMM定义了线程和主内存之间的抽象关系。即:线程之间的共享变量在主内存(main memory)中,每一个线程都有一个自己私有的本地内存(local memory),本地内存中存储了该线程以 读/写 共享变量的副本。本地内存是一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
如图所示:
从上图看,线程 A 要和线程 B 之间如果要通信的话,必须要经历下面2个步骤:
1:首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2:然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明这两个步骤:
如上图所示,本地内存 A 和 B 有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为 0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存 A 中。当线程 A 和线程 B 需要通信时,线程 A 首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程 B 到主内存中去读取线程 A 更新后的 x 值,此时线程 B 的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 java 程序员提供内存可见性保证。
当对象和变量存储到计算机的各个内存区域时,必然会面临一些问题,其中最主要的两个问题是:
1. 共享对象对各个线程的可见性
2. 共享对象的竞争现象
共享对象的可见性
当多个线程同时操作同一个共享对象时,如果不加以控制,那么一个线程对共享对象的更新就有可能导致其他线程的不可见。
要解决共享对象可见性这个问题,我们可以使用java volatile关键字。volatile 关键字可以保证变量会直接从主存读取,而对变量的更新也会直接写到主存。volatile 原理是基于 CPU 内存屏障指令实现的.
竞争现象
如果多个线程共享一个对象,如果它们同时修改这个共享对象,这就产生了竞争现象。
要解决上面的问题我们可以使用 java synchronized 代码块。synchronized 代码块可以保证同一个时刻只能有一个线程进入代码竞争区, synchronized 代码块也能保证代码块中所有变量都将会从主存中读,当线程退出代码块时,对所有变量的更新将会 flush 到主存,不管这些变量是不是 volatile 类型的。
总结:
1:Volatile 仅能使用在变量级别;
synchronized 则可以使用在变量、方法、和类级别的.
2:Volatile仅能实现变量的修改可见性,并不能保证原子性(复合操作的原子性);
synchronized则可以保证变量的修改可见性和原子性。
3:Volatile不会造成线程的阻塞;
synchronized可能会造成线程的阻塞。
4:Volatile标记的变量不会被编译器优化(因为这是由cpu指令完成);
synchronized标记的变量可以被编译器优化(JAVA1.6后性能优化很多)