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) {//主线程 A

        new Thread(()->{  //副线程 B
            while(num == 0){

            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num = 1;// 修改num的值
        System.out.println(num);
    }
}
复制代码

运行查看结果:

在这里插入图片描述

当主线程A修改变量num=1后,副线程B并没有得到最新的num值,还是原来的num=0,所以副线程B陷入死循环。

当给变量num加上 volatile 关键字后:

private volatile static int num = 0;
复制代码

再次运行查看结果:

在这里插入图片描述

副线程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);
    }
}
复制代码

运行查看结果:

在这里插入图片描述

因为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();//定义原子类AtomicInteger

    public 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);
    }
}
复制代码

在这里插入图片描述

getAndIncrement() 源码:

在这里插入图片描述

在这里插入图片描述

我们取得了旧值,然后把要加的数传过去,调用getAndAddInt () 进行原子更新操作,实际最核心的方法是 compareAndSwapInt(),使用CAS进行更新。

3、volatile与synchronized比较

● volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好; volatile只能修饰变量,而synchronized可以修饰方法,代码块. 随着JDK新版本的发布,synchronized的执行效率也有较大的提升,在开发中使用sychronized的比率还是很大的。

● 多线程访问volatile变量不会发生阻塞,而synchronized可能会阻塞。

● volatile能保证数据的可见性,但是不能保证原子性; 而synchronized可以保证原子性,也可以保证可见性。

● 关键字volatile解决的是变量在多个线程之间的可见性; synchronized关键字解决多个线程之间访问公共资源的同步性。

猜你喜欢

转载自juejin.im/post/7016584491644747813
今日推荐