java里volatile关键字有啥特性?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java里volatile关键字有啥特性?相关的知识,希望对你有一定的参考价值。

参考技术A

Java语言中关键字 volatile 被称作轻量级的 synchronized,与synchronized相比,volatile编码相对简单且运行的时的开销较少,但能够正确合理的应用好 volatile 并不是那么的容易,因为它比使用锁更容易出错,接下来本文主要介绍 volatile 的使用准则,以及使用过程中需注意的地方。

为何使用volatile?

(1)简易性:在某些需要同步的场景下使用volatile变量要比使用锁更加简单

(2)性能:在某些情况下使用volatile同步机制的性能要优于锁

(3)volatile操作不会像锁一样容易造成阻塞

volatile特性

(1)volatile 变量具有 synchronized 的可见性特性,及如果一个字段被声明为volatile,java线程内存模型确保所有的线程看到这个变量的值是一致的

(2)禁止进行指令重排序

(3)不保证原子性

注:

① 重排序:重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段

② 原子性:不可中断的一个或一系列操作

③ 可见性:锁提供了两种主要特性:互斥和可见性,互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。

volatile的实现原理

如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,该Lock指令会使这个变量所在缓存行的数据回写到系统内存,根据缓存一致性协议,每个处理器都会通过嗅探在总线上传输的数据来检查自己缓存的值是否已过期,当处理器发现自己的缓存行对应的地址被修改,就会将当前处理器的缓存行设置成无效状态,在下次访问相同内存地址时,强制执行缓存行填充。

正确使用volatile的场景

volatile 主要用来解决多线程环境中内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,就无法解决线程安全问题。如:

1、不适合使用volatile的场景(非原子性操作)

(1)反例

private static volatile int nextSerialNum = 0;

public static long generateSerialNum()

return nextSerialNum++;

这个方法的目的是要确保每次调用都返回不同的自增值,然而结果并不理想,问题在于增量操作符(++)不是原子操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,如果第二个线程在第一个线程读取旧值和写回新值期间读取这个域,第二个线程与第一个线程就会读取到同一个值。

(2)正例

其实面对上面的反例场景可以使用JDK1.5 java.util.concurrent.atomic中提供的原子包装类型来保证原子性操作

private static AtomicInteger nextSerialNum = new AtomicInteger(0);

public static long generateSerialNum()

return nextSerialNum.getAndIncrement();

2、适合使用volatile的场景

在日常工作当中volatile大多被在状态标志的场景当中,如:

要通过一个线程来终止另外一个线程的场景

(1)反例

private static boolean stopThread;

public static void main(String[] args) throws InterruptedException

Thread th = new Thread(new Runnable()

@Override

public void run()

int i = 0;

while (!stopThread)

i++;

);

th.start();

TimeUnit.SECONDS.sleep(2);

stopThread = true;

运行后发现该程序根本无法终止循环,原因是,java语言规范并不保证一个线程写入的值对另外一个线程是可见的,所以即使主线程main函数修改了共享变量stopThread状态,但是对th线程并不一定可见,最终导致循环无法终止。

(2)正例

private static volatile boolean stopThread;

public static void main(String[] args) throws InterruptedException

Thread th = new Thread(new Runnable()

@Override

public void run()

int i = 0;

while (!stopThread)

i++;

);

th.start();

TimeUnit.SECONDS.sleep(2);

stopThread = true;

通过使用关键字volatile修饰共享变量stopThread,根据volatile的可见性原则可以保证主线程main函数修改了共享变量stopThread状态后对线程th来说是立即可见的,所以在两秒内线程th将停止循环。

线程安全的三大特性(原子性可见性有序性)volatile关键字

一:线程安全的三大特性

1、原子性

原子性是指操作是不可分的。其表现在于对于共享变量的某些操作,应该是不可分的,必须连续完成。

比如 a++ 操作,实际上 JMM 会分 3 步来完成。
①读取变量 a 的值
② a 的值+1
③将值赋予变量 a

这三个操作中任何一个操作过程中,a 的值被人篡改,那么都会出现我们不希望出现的结果。所以我们必须保证这是原子性的

所以想要实现 ++ 这样的原子操作就需要用到 synchronized 或者是 lock 进行加锁处理

如果是基础类的自增操作可以使用 AtomicInteger 这样的原子类来实现(其本质是利用了 CPU 级别的 的 CAS 指令来完成的)。

JDK 1.8中的原子类

2、可见性

可见性是指一个线程对共享变量的修改,另外一个线程能够立刻看到;

我们在编写Java程序的时候,JVM为了提高程序的执行效率,会对我们的程序进行优化,把经常需要被访问的变量存储在我们的缓存当中,也就是CPU中的寄存器(Cache)里,而避免直接去内存里读。

CPU从内存里读数据,需要通过总线发送指令给内存。

这个速度远远比不上让CPU直接从Cache里面读取数据。

但是在多线程的情况下,某一变量可能已经被别的线程改变了,但是该缓存却无法察觉到内存中的变化,从而造成我们的线程读取的数据跟内存里面的数据是不一致的(脏读

通过上述描述我们可以知道:由于多核硬件架构的问题,CPU 高速缓存之间本身是不可见的,必须要实现缓存一致性协议。Java方面提供了两个关键字来保证多线程情况下共享变量的可见性方案(volatile、synchronized)。

3、有序性

有序性是指程序在执行的时候,程序的代码执行顺序和语句的顺序是一致的。
那为什么会出现不一致的情况呢?
这是由于重排序的缘故。

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序;
重排序不会影响单线程的执行结果但是在并发情况下,可能会出现诡异的BUG

举例:

Java 中可以使用 volatile 来保证顺序性,synchronized 和 Lock 也可以来保证有序性,和保证原子性的方式一样,通过同一段时间只能一个线程访问来实现的。

除了通过 volatile 关键字显式的保证顺序之外, JVM 还通过 happen-before 原则来隐式的保证顺序性。其中有一条就是适用于 volatile 关键字的,针对于 volatile 关键字的写操作肯定是在读操作之前,也就是说读取的值肯定是最新的。

二:volatile关键字

volatile 关键字是用来保证数据的可见性。

从上述可见性的描述中,我们可以知道 CPU 高速缓存之间本身是不可见的,那么线程在读取变量的时候就有可能会读到脏数据。而使用 volatile 关键字对变量进行修饰的话,系统每一次用到这个变量的时候会直接从内存里面去取而不会利用缓存。但是volatile并不能保证我们操作的一个原子性(还是要通过加锁来实现),所以它是不能取代 synchronized 的。此外 volatile 关键字会阻止编译器对我们的程序进行优化,所以一般不建议使用 volatile 关键字。

总结:volatile关键字就是在多线程的操作当中,保证变量的一个可见性,避免多线程操作下读到脏数据

三:Java和操作系统是怎么保证内存可见性的?

Java使用 volatile、synchronized 关键字保证内存可见性。

操作系统:
1)总线Lock锁。锁定总线的开销比较大,在缓存更新内存后,其他的cpu都会被锁定住,禁止与内存通信,这样开销就大了。

2)MESI协议。这是缓存一致性协议的具体实现,它通过嗅探技术识别哪个 CPU 想修改主内存缓存行信息,如果该缓存行是共享的,先将该缓存行刷新到主内存,再设置其他 CPU 的高速缓存的缓存行无效,但频繁的嗅探其他CPU 想修改的共享数据,也会导致总线风暴。

以上是关于java里volatile关键字有啥特性?的主要内容,如果未能解决你的问题,请参考以下文章

Java并发关键字Volatile 详解

Java并发多线程编程——volatile关键字

你真的了解volatile关键字吗?

java volatile关键字

Java volatile关键字解惑

线程安全的三大特性(原子性可见性有序性)volatile关键字