JUC并发编程|JUC并发编程—— volatile 关键字详解


文章目录

  • JUC并发编程—— volatile 关键字详解
    • 1、volatile 简介
    • 2、可见性和非原子性验证
    • 3、volatile与synchronized比较

JUC并发编程—— volatile 关键字详解 1、volatile 简介 并发编程三个特性:
1、原子性:一段操作一旦开始就会一直运行到底,中间不会被其它线程打断,这段操作可以是一个操作,也可以是多个操作。
2、可见性:当一个线程修改了共享变量的值,其它线程能立即感知到这种变化。
3、有序性:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。
volatile 是Java提供的一种轻量级的同步机制。
相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。
volatile 特性:
  • 保证了可见性,不保证原子性
  • 禁止指令重排
    指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序.
2、可见性和非原子性验证 volatile 保证了可见性,不保证原子性
  • 当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;
  • 这个写会操作会导致其他线程中的volatile变量缓存无效。
可见性验证
假设A,B两个线程操作主存中的同一变量,当A线程修改了变量后,并把修改后的变量重新写入主存,此时B线程并不知道变量已被修改:
public class Demo01 { private static int num = 0; //共享变量 public static void main(String[] args) {//主线程 Anew Thread(()->{//副线程 B while(num == 0){} }).start(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } num = 1; // 修改num的值 System.out.println(num); } }

运行查看结果:
JUC并发编程|JUC并发编程—— volatile 关键字详解
文章图片

当主线程A修改变量num=1后,副线程B并没有得到最新的num值,还是原来的num=0,所以副线程B陷入死循环。
当给变量num加上 volatile 关键字后:
private volatile static int num = 0;

再次运行查看结果:
JUC并发编程|JUC并发编程—— volatile 关键字详解
文章图片

副线程B得到最新的num值,退出while循环。
当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。
验证非原子性
原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。
a++可以分解为三个操作:
  1. 先获得 a 这个值
  2. 然后 a 加1
  3. 最后把 a 写回内存
package com.cheng.volatiletest; import java.util.concurrent.TimeUnit; public class Demo02 { private static int num = 0; public static void add(){ num++; //非原子性操作 }public static void main(String[] args) {for (int i = 0; i < 20; i++) {//创建20个线程 new Thread(()->{ for (int j = 0; j < 1000; j++) {//每个线程执行100次add()方法 add(); } }).start(); } //如果当前存活线程大于两个 while (Thread.activeCount() > 2){// main,GC Thread.yield(); //让出cpu使用权 } System.out.println(Thread.currentThread().getName()+""+num); } }

运行查看结果:
【JUC并发编程|JUC并发编程—— volatile 关键字详解】JUC并发编程|JUC并发编程—— volatile 关键字详解
文章图片

因为add方法是个非原子性操作,所以线程在执行add方法的操作时,会被其他线程插入,导致执行出现问题。
使用 Synchronized 或 Lock 可以保证原子性,在执行add方法时,将不会被其他线程插队。
使用原子类
除了使用 Synchronized 和 Lock ,JUC包下的原子类也可以保证原子性。
package com.cheng.volatiletest; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class Demo02 { private static AtomicInteger num = new AtomicInteger(); //定义原子类AtomicIntegerpublic static void add(){ num.getAndIncrement(); //以原子的方式将当前值加 1 }public static void main(String[] args) {for (int i = 0; i < 20; i++) {//创建20个线程 new Thread(()->{ for (int j = 0; j < 1000; j++) {//每个线程执行100次add()方法 add(); } }).start(); } //如果当前存活线程大于两个 while (Thread.activeCount() > 2){// main,GC Thread.yield(); //让出cpu使用权 } System.out.println(Thread.currentThread().getName()+""+num); } }

JUC并发编程|JUC并发编程—— volatile 关键字详解
文章图片

getAndIncrement() 源码:
JUC并发编程|JUC并发编程—— volatile 关键字详解
文章图片

JUC并发编程|JUC并发编程—— volatile 关键字详解
文章图片

我们取得了旧值,然后把要加的数传过去,调用getAndAddInt () 进行原子更新操作,实际最核心的方法是 compareAndSwapInt(),使用CAS进行更新。
3、volatile与synchronized比较 ● volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好; volatile只能修饰变量,而synchronized可以修饰方法,代码块. 随着JDK新版本的发布,synchronized的执行效率也有较大的提升,在开发中使用sychronized的比率还是很大的。
● 多线程访问volatile变量不会发生阻塞,而synchronized可能会阻塞。
● volatile能保证数据的可见性,但是不能保证原子性; 而synchronized可以保证原子性,也可以保证可见性。
● 关键字volatile解决的是变量在多个线程之间的可见性; synchronized关键字解决多个线程之间访问公共资源的同步性。

    推荐阅读