Java 多线程 --- 线程同步 volatile关键字

Java 多线程 --- 线程同步 volatile关键字

volatile keyword

  • Volatile是Java虚拟机提供的轻量级的同步机制
  • Volatile可以保证可见性, 禁止指令重排, 但是不保证原子性
  • volatile利用MESI协议和snooping保证可见性
  • volatile不保证原子性, 比如 num++ 这个操作实际上分为三步, 拿到num值, 对num值加一, 放回num值.
  • volatile关键字保证了拿到number的值是正确的,但是在执行对num值加一, 放回num值这些指令的时候,其他线程可能已经把number的值改变了,而操作栈顶的值就变成了过期的数据,所以就可能把较小的number值同步回主内存之中.
  • 如果要实现原子性, 可以使用synchronized关键字, 或者使用 Java并发包(JUC)中的AtomicInterger等类
  • 可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改
  • 通过之前对synchronzed内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。从而,synchronized具有可见性。
  • 同样的在volatile分析中,会通过在指令中添加lock指令,以实现内存可见性。因此, volatile具有可见性

使用volatile保证可见性

import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

public class Solution {
	
	static class ShareData {
		int number = 0;
		
		public void setNumberTo100() {
			this.number = 100;
		}
	}
	
	public static void main(String[] args) {
        // 资源类
        ShareData shareData = new ShareData();
 
        // 子线程 实现了Runnable接口的,lambda表达式
        new Thread(() -> {
 
            System.out.println(Thread.currentThread().getName() + "\t come in");
 
            // 线程睡眠3秒,假设在进行运算
            try {
                Thread.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改number的值
            shareData.setNumberTo100();
 
            // 输出修改后的值
            System.out.println(Thread.currentThread().getName() + "\t update number value:" + shareData.number);
 
        }, "child Thread: ").start();
 
        while(shareData.number == 0) {
        	//System.out.println(Thread.currentThread().getName() + "等待number更新为100");
        }
 
        //这句话输出不出来, 因为子线程更改number值后, main线程没有感知到
        System.out.println(Thread.currentThread().getName() + "\t 主线程感知到了 number 不等于 0");
    }
}
  • 最后线程没有停止,没有输出"主线程知道了 number 不等于0"这句话,说明没有用volatile修饰的变量,变量的更新是不可见的
    在这里插入图片描述
  • 将number 声明为 volatile: volatile int number = 0;
    在这里插入图片描述

使用volatile禁止指令重排

  • 计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。
  • 有三种指令重排:
  • .编译器优化重排:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • .指令级的并行重排:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • .内存系统的重排:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行

Example: 双重检测锁定的单例模式

package com.jackson0714.passjava.threads;
/**
 演示volatile 单例模式应用(双边检测)
 * @author: 悟空聊架构
 * @create: 2020-08-17
 */
 
class VolatileSingleton {
    private static VolatileSingleton instance = null;
    private VolatileSingleton() {
        System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
    }
    public static VolatileSingleton getInstance() {
        // 第一重检测
        if(instance == null) {
            // 锁定代码块
            synchronized (VolatileSingleton.class) {
                // 第二重检测
                if(instance == null) {
                    // 实例化对象
                    instance = new VolatileSingleton();
                }
            }
        }
        return instance;
    }

}

  • 代码看起来没有问题,但是 instance = new VolatileSingleton();其实可以看作三条伪代码:
memory = allocate(); // 1、分配对象内存空间
instance(memory); // 2、初始化对象
instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null
  • 步骤2 和 步骤3之间不存在 数据依赖关系,而且无论重排前 还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate(); // 1、分配对象内存空间
instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
instance(memory); // 2、初始化对象

在这里插入图片描述

可以使用volatile: private static volatile VolatileSingleton instance = null;

注意:当且仅当满足以下所有条件时,才应该用volatile变量

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
  • 该变量不会与其他的状态一起纳入不变性条件中。
  • 在访问变量时不需要加锁。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值