JAVA基础 - volatile

本文探讨了Java中volatile关键字的作用,分析了其解决的不可见性问题,以及为何不能保证原子性。同时,介绍了重排的概念及其可能引发的问题,并讲解了Happens-before原则在多线程内存可见性中的意义。
摘要由CSDN通过智能技术生成

目录:

  1. 不可见性是什么?
  2. volatile 可以保证原子性吗?
  3. . 重排的示例和作用?
  4. Happends - before 是什么?
  5. volatile与synchronized 区别?
  6. 参考

1.不可见性是什么?

1.1不可见性案例

/**
 * 多线程修改变量 会出现 修改值之后不可见性
 */
public class VisibilityDemo1 {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
        while (true) {
            if (thread.isFlag()) {
                System.out.println(" 进入 -- ");
            }
        }
    }
}
class MyThread extends Thread {
    private boolean flag = false;
    @Override
    public void run() {
        try {
            Thread.sleep(1000);  // 标记1
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println(" flag = " + flag);
    }
    public boolean isFlag() {
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

控制台会不会打印输出:" 进入 –" ?,我估计应该有一部分读者会认为答应。
实际的输出如下:
可见性案例

1.2 如何解决这个问题

   解决方案1:    去掉 标记1 代码
   解决方案2private volatile boolean flag = false;
   解决方案3(下一篇 synchronized 进行分析 ):	加入 synchronized
    while (true) { 
            synchronized (VisibilityDemo1.class) {
                if (thread.isFlag()) {
                    System.out.println(" 进入 -- ");
                }
            }
        }	

1.3 原理分析

	分析之前需要知道JMM(JAVA 内存模型) :
		1.JAVA 内存模型 描述了java 程序中 各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
		2. JMM 有以下规定:
			  1. 所有的共享变量都存储于主内存。这是所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量时线程私有的。因此不存在竞争问题。
			  2.每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
 			  3. 线程对变量的所有的操作(读,取) 都必须在工作内存中完成,而不能直接读写主内存中的变量。
 			  4. 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值得传递需要通过主内存中转完成

3. 主内存 和 工作内存示意图

本地内存和主内存的关系

  1. 开始问题分析

    在这里插入图片描述

    问题发生的路径:
    1. 子线程 thread 从主内存读取到数据放入其对应的工作内存
    2.将 flag 的值 更改为true , 但是这个时候 flag的值还没有写回主内存
    3.此时main 方法读取到了flag的值为false
    4. 当子线程t将 flag的值写回去后,但是main 函数里面的while(true) 调用的是系统比较底层
    的代码,速度快,快到没有时间再去读取主内存中的值
    所以while(true) 读取到点值一直是false

分析 解决方案2:(重要 重要 重要)
1.1 子线程t 从主内存读取到数据放入其对应的工作
1.2 将flag的值更改为true ,但是这个时候flag的值还没有写主内存
1.3 此时main方法 读取到了flag的值为false
1.4 当子线程t将flag的值写回去后,失效( 嗅探技术 )其他线程对此变量副本
1.5 再次对flag进行 操作的时候线程会从主内存读取最新的值,放入到工作线程中

           总结: volatile 保证不通线程对共享变量操作的可见性,也就是说一个线程修改了volatile的修饰的变量,当修改写会内存时,另外一个线程立即看到最新值.

在这里插入图片描述

2. volatile 可以保证原子性吗??

2.1  volatile 可以保证原子性吗?
	答:不可以,  volatile是可见但是线程不安全。   // 解决办法  synchronized 修饰
 public static void main(String[] args) {
        Runnable target = new MyTarget();
        for (int i = 1; i <= 100; i++) {
            new Thread(target, " 第" + i + "线程").start();
        }
    }
}
class MyTarget implements Runnable {
    private   volatile  int count = 0;
    @Override
    public void run() {
        for (int i = 1; i <= 10000; i++) {
            count++;
            System.out.println(Thread.currentThread().getName() + " count  =  " + count);
        }
    }
}

// 输入结果  基本不可能等于 100*10000

分析一下:
1、从主内存中读取数据到工作内存
2、对工作内存中的数据进行++ 操作
3、将工作内存中的数据写回到主内存
也可以从编译的字节码看下
在这里插入图片描述
描述:
1. 假设此时X的值100,线程A需要对改变量进行自增1的操作,
首先它需要从主内存中读取变量x的值。由于cpu的切换关系,此时cpu的执行权被切换了B线程。 A线程就需出于就绪状态。B 线程出于运行状态
2 线程b也需要从主内存中读取X变量的值,由于线程A没有对X值做任何
因此B读取到点数据还是100
3 线程b工作内存中x 执行了+1操作,但是未刷新之主内存中
4 此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主 内存,因 此A线程工作内存中的变量值还是1-- 没有失效。
5 线程B将101 写入到主内存
6 线程A将101 写入到主内存
虽然计算量2次,但是只对A进行了1次修改。

3.重排的实例和作用?

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

    在这里插入图片描述

重点:重排序不是万能的。 也会出问题。

在这里插入图片描述
其实这个地方还有 重要点 重要点 重要点.
重排序 返回内存地址引用 和 初始化构造方法详细情况.

Happends - before 是什么?

通过 happens-before的概念,描述操作之间内存的可见性。
如果一个操作执行的结果需要对另外一个操作可见,那么这两个操作之间必须存在happeds-before关系。
两个操作可以是在一个线程内,也可以在不同线程之间。

一共有六项规则:
1.程序顺序规则(单线程规则)
解释:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 同一个线程中前面的所有写操作对后面的操作可见。

	2. 锁规则(Synchronized, Lock 等)
    	解释: 对一个锁的解锁,happens-before于随后对这个锁的加锁。
    		如果线程1解锁monitor,接着线程2锁定了a,那么线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一线程)
	
	3. volatile 变量规则
		 解释 对一个锁的解锁,happends-before于随后对这个锁的加锁。
		 如果线程1写入了 volatile变量v(临界资源) , 接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一线程)
	4: 传递性:
	     解释: 如果A happens-before B  且 B happens-before C ,那么 A happens-before C
	               A -> B   ,  B ->C 且  A-C
	
	5 start()规则
		解释: 如果线程A执行操作ThreadB.start() 启动线程B,那么线程A的TheadB.start() 可以看到 A线程的变量。
		假定 线程线程A在执行过程中,通过执行ThreadB.start() 来启动线程B,那么线程A对共享变量的修改在接下来线程b开始执行前对线程B可见。 注意: 线程B 启动之后,线程A在对变量修改线程B未必可见。
	
	6 join 规则
		解释: 如果线程A 执行操作ThreadB.join 并成功返回。那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成返回
				线程t1 写入的所有变量,在任意其他线程t2调用t1.join,或者t1.isAlive成功返回后,都对t2可见。

demo: https://download.csdn.net/download/IT_peng/12203228

参考:

https://www.bilibili.com/video/av81907443?p=18 (这个视频 质量挺高)
java并发编程的艺术
https://www.cnblogs.com/54chensongxia/p/11806836.html
https://blog.csdn.net/anjxue/article/details/51038466?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值