Java多线程之内存可见性和原子性详解:Synchronized和Volatile

一、 背景

能否保证synchronizedvolatile
原子性×
可见性

二、 可见性介绍

2.1 可见性介绍

2.1.1 定义

  • 可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到
  • 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量

2.1.2 要实现共享变量的可见性,必须保证2点:

  • 线程修改后的共享变量值能够及时从工作内存刷新到主内存中
  • 其他线程能够及时把共享变量的最新值从主内存更新到自己的工作内存中

2.1.3 可见性的实现方式

Java语言层面支持的可见性实现方式:

  • synchronized
  • volatile

2.2 Java内存模型(JMM)

2.2.1 Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。

  • 所有的变量都存储在主内存中
  • 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝) 

2.2.2 两条规定

  • 线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写
  • 不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成

2.2.3 共享变量可见性实现的原理

  • 原本主内存中x=0,线程1和线程2工作内存中x=0
  • 线程1修改当前工作内存1中的x=1,然后刷新都主内存
  • 线程2将主内存中的x读取到自己的工作内存2中,然后再从工作内存读取到寄存器上进行操作 

三、synchronized实现可见性

3.1 synchronized实现可见性原理

3.1.1 synchronized能够实现:

  • 原子性(通过同步来保证)
  • 可见性

3.1.2 JMM关于synchronized的两条规定

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁需要是同一把锁)

==》线程解锁前对共享变量的修改在下次加锁时对其他线程可见

3.1.3 线程执行互斥代码的过程

①获得互斥锁
②清空工作内存
③从主内存拷贝变量的最新副本到工作内存
④执行代码
⑤将更改后的共享变量的值刷新到主内存
⑥释放互斥锁

3.1.4 相关知识点:指令重排序

  • 重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化
①编译器优化的重排序(编译器优化)
②指令级并行重排序(处理器优化)
③内存系统的重排序(处理器优化)--JMM
  • 类似例子
(1)代码顺序
int number = 1;
int result = 0;

(2)实际编译后的代码
int result = 0;
int number = 1;

3.1.5 相关知识点:as-if-serial

  • as-if-serial:无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(Java编译器、运行时和处理器都会保证Java在单线程下遵循as-if-serial语义)
  • 例子
int num1 = 1;          //第1行代码
int num2 = 2;          //第2行代码
int sum = num1 + num2; //第3行代码
  • 单线程:第1、2行的顺序可以重排,但第3行不能
  • 重排序不会给单线程带来内存可见性问题
  • 多线程中程序交错执行时,重排序可能会造成内存可见性问题

3.2 synchronized实现可见性代码

public class SynchronizedDemo {
    //共享变量
    private boolean ready = false;
    private int result = 0;
    private int number = 1;
    //写操作
    public void write() {
        ready = true; //1.1
        number = 2;  //1.2
    }
    //读操作
    public void read() {
        if (ready) { //2.1
            result = number * 3; //2.2
        }
        System.out.println("result的值为:" + result);
    }


    //内部线程类
    private class ReadWriteThread extends Thread {
        private boolean flag;
        public ReadWriteThread(boolean flag) {
            this.flag = flag;
        }

        @Override
        public void run() {
            if (flag) {
                //构造方法中传入true,执行写操作
                write();
            } else {
                //构造方法中传入false,执行读操作
                read();
            }
        }
    }

    public static void main(String[] args) {
        SynchronizedDemo syncDemo = new SynchronizedDemo();
        //启动线程执行写操作
        syncDemo.new ReadWriteThread(true).start();
        //启动线程执行读操作
        syncDemo.new ReadWriteThread(false).start();
    }
}
  • 分析可能结果
(1)执行顺序
1.1->2.1->2.2->1.2
result的值为:3

(2)执行顺序
1.2->2.1->2.2(不执行)->1.1
result的值为:0

3.2.1 可见性分析

导致共享变量在线程间不可见的原因synchronized解决方案
线程的交叉执行原子性(同步执行,保证只有一个线程在执行)
重排序结合线程交叉执行原子性(同步执行,保证只有一个线程在执行)
共享变量更新后的值没有在工作内存与主内存间及时更新可见性

3.2.2 加synchronized的安全代码

//写操作
    public synchronized void write() {
        ready = true; //1.1
        number = 2;  //1.2
    }
    //读操作
    public synchronized void read() {
        if (ready) { //2.1
            result = number * 3; //2.2
        }
        System.out.println("result的值为:" + result);
    }

四、volatile实现可见性

  • 能够保证volatile变量的可见性
  • 不能保证volatile变量复合操作的原子性

4.1 volatile能够保证可见性

4.1.1 volatile如何实现内存可见性:

深入来说:通过加入内存屏障禁止重排序优化来实现的。

  • volatile变量执行写操作时,会在写操作后加入一条store屏障指令
  • volatile变量执行读操作时,会在读操作前加入一条load屏障指令

通俗地讲:volatile变量在每次被线程访问时,都强制从主内存中重读该变量的值; 而当该变量发生变化时,又会强制线程将最新的值刷新到主内存。 这样任何时刻,不同的线程总能看到该变量的最新值

4.1.2 线程写volatile变量的过程:

  • (1)改变线程的工作内存中volatile变量副本的值
  • (2)将改变后的副本的值从工作内存刷新到主内存

4.1.3 线程读volatile变量的过程

  • (1)从主内存中读取volatile变量的最新值到线程的工作内存中
  • (2)从工作内存中读取volatile变量的副本

4.2 volatile不能保证原子性

  • volatile不能保证volatile变量复合操作的原子性
private int number = 0;
number++;  //不是原子操作

1、读取number的值
2、将number的值加1
3、写入最新的number的值


//加入synchronized,只有一个线程执行,变为原子操作
synchronized(this) {
  number++;
}

4.2.1 分析

  • 假设当前number=5
number=5
1、线程A读取number=5
2、线程B读取number=5
3、线程B执行+1操作
4、线程B写入最新的number值
(线程B工作内存:number=6
  主内存:number=6)
5、而此时,线程A工作内存中:number=5(由于线程A没有重新做读操作,所以工作内存中还是5)
6、线程A执行+1操作(5+1)
7、线程A写入最新的number值(写入A的工作内存,然后再刷新到主内存)
(
线程A工作内存:number=6
主内存:number=6
)
AB执行了2次+1操作,但实际上只加了1

4.2.2 保证number自增操作的原子性(解决方案):

  • 使用synchronized关键字
  • 使用ReentrantLock(java.until.concurrent.locks包下)
  • 使用AtomicInteger(java.util.concurrent.atomic包下)

(1)synchronized示例

public class SynchronizedIncreaseDemo {
    private int number = 0;

    public int getNumber() {
        return number;
    }

    public void increase() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized(this) {
            number++;
        }

    }

    public static void main(String[] args) {
        SynchronizedIncreaseDemo volatileDemo = new SynchronizedIncreaseDemo();
        for (int i = 0; i < 500; i++) {
            new Thread(() -> volatileDemo.increase()).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("number: " + volatileDemo.getNumber());
    }
}

//---------输出结果-------
每次都是500

(2)ReentrantLock示例

public class LockIncreaseDemo {
    private Lock lock = new ReentrantLock();
    private int number = 0;

    public int getNumber() {
        return number;
    }

    public void increase() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.lock();
        try {
            number++;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        LockIncreaseDemo volatileDemo = new LockIncreaseDemo();
        for (int i = 0; i < 500; i++) {
            new Thread(() -> volatileDemo.increase()).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("number: " + volatileDemo.getNumber());
    }
}
//---------输出结果-------
每次都是500

(3)AtomicInteger示例

public class AtomicIncreaseDemo {
    private AtomicInteger number = new AtomicInteger();

    public AtomicInteger getNumber() {
        return number;
    }

    public void increase() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        number.getAndIncrement();
    }

    public static void main(String[] args) {
        AtomicIncreaseDemo volatileDemo = new AtomicIncreaseDemo();
        for (int i = 0; i < 500; i++) {
            new Thread(() -> volatileDemo.increase()).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("number: " + volatileDemo.getNumber().get());
    }
}

4.3 volatile适用场合

要在多线程中安全得使用volatile变量,必须同时满足:

4.3.1、对变量的写入操作不依赖其当前值(当前的值与上一次的值没有关系)

  • 不满足:number++、count=count*5
  • 满足:boolean变量、记录温度变化的变量等

4.3.2、该变量没有包含在具有其他变量的不变式中

  • 不满足:不变式low<up(low和up都是volatile变量,volatile不保证原子性。当代码包含其他共享变量,如果被其他线程执行,那么值就会发生改变)
  • 关于不变式,可以参考该文章:如何正确使用 Volatile 变量

4.4 synchronized和volatile比较

  • volatile不需要加锁,比synchronized更轻量级,不会阻塞线程
  • 从内存可见性角度看,volatile读相当于加锁(读操作前加入一条load屏障指令),volatile写相当于解锁(写操作后加入一条store屏障指令)
  • synchronized既能保证可见性,又能保证原子性;而volatile只能保证可见性,无法保证原子性

五、常用操作

5.1 synchronized+volatile+双重检查锁(double checked locking)

  • 假设现在有2个线程,线程A和线程B,线程A先执行getToken,走到1.1,检测到token为空,则执行synchronized加锁进入1.2做初始化token逻辑,赋值了token=“ABCDE”
  • 如果此时线程B进来,执行getToken,由于token变量是volatile的,线程A修改后立刻刷新到主内存了。此时线程B进来就直接从主内存读取,拿到了最新的token值=ABCDE,就返回了
public class DoubleCheckDemo {
    private volatile String token = "";
    public String getToken() {
        if (token.isEmpty()) {
            synchronized (DoubleCheckDemo.class) {
                if (token.isEmpty()) {
                    //做相应的token逻辑
                    token = "ABCDE";
                }
            }
        }
        return token;
    }

    public static void main(String[] args) {
        DoubleCheckDemo demo = new DoubleCheckDemo();
        new Thread(() -> System.out.println(demo.getToken())).start();
        new Thread(() -> System.out.println(demo.getToken())).start();
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Venlenter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值