【并发编程】(学习笔记-共享模型之内存)-part4

  1. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值

在这里插入图片描述

解决办法

  • 使用 volatile(易变关键字)

  • 它可以用来修饰成员变量静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存

2-2 可见性vs原子性

前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况

注意:

  • synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性
  • 但缺点是 synchronized 是属于重量级操作,性能相对更低。
  • 如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?
进入`println`源码,可以看出加了`synchronized`,保证了每次`run`变量都会从主存中获取
public void println(int x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

3.有序性


3-1 诡异的结果

看下面一个栗子:

int num = 0;

boolean ready = false;

// 线程1 执行此方法

public void actor1(I_Result r) {

if(ready) {

r.r1 = num + num;

} else {

r.r1 = 1;

}

}

// 线程2 执行此方法

public void actor2(I_Result r) {

num = 2;

ready = true;

}

看到这里可能聪明的小伙伴会想到有下面三种情况:

情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1

情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1

情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)

但其实还有可能为0哦! 😲

有可能还是:线程 2 执行 ready=true ,切换到线程1 ,进入if分支,相加为0,在切回线程 2 执行 num=2

这种现象就是指令重排

3-2 解决方法

volatile 修饰的变量,可以禁用指令重排

@JCStressTest

@Outcome(id = {“1”, “4”}, expect = Expect.ACCEPTABLE, desc = “ok”)

@Outcome(id = “0”, expect = Expect.ACCEPTABLE_INTERESTING, desc = “!!!”)

@State

public class ConcurrencyTest {

int num = 0;

volatile boolean ready = false;//可以禁用指令重排

@Actor

public void actor1(I_Result r) {

if(ready) {

r.r1 = num + num;

} else {

r.r1 = 1;

}

}

@Actor

public void actor2(I_Result r) {

num = 2;

ready = true;

}

}

3-3 有序性理解

同一线程内,JVM会在不影响正确性的前提下,可以调整语句的执行顺序,看看下面的代码:

static int i;

static int j;

// 在某个线程内执行如下赋值操作

i = …; // 较为耗时的操作

j = …;

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时, 既可以是

i = …; // 较为耗时的操作

j = …;

也可以是

j = …;

i = …; // 较为耗时的操作

这种特性称之为指令重排多线程下指令重排会影响正确性

3-4 happens-before

happens-before 规定了对共享变量写操作对其它线程的读操作可见,它是可见性有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见

static int x;

static Object m = new Object();

new Thread(()->{

synchronized(m) {

x = 10;

}

},“t1”).start();

new Thread(()->{

synchronized(m) {

System.out.println(x);

}

},“t2”).start()

  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见

volatile static int x;

new Thread(()->{

x = 10;

},“t1”).start();

new Thread(()->{

System.out.println(x);

},“t2”).start();

  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见

static int x;

x = 10;

new Thread(()->{

System.out.println(x);

},“t2”).start();

  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive()t1.join()等待它结束)

static int x;

Thread t1 = new Thread(()->{

x = 10;

},“t1”);

t1.start();

t1.join();

System.out.println(x);

  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通 过t2.interruptedt2.isInterrupted

static int x;

public static void main(String[] args) {

Thread t2 = new Thread(()->{

while(true) {

if(Thread.currentThread().isInterrupted()) {

System.out.println(x);//10

break;

}

}

},“t2”);

t2.start();

new Thread(()->{

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

x = 10;

t2.interrupt();

},“t1”).start();

while(!t2.isInterrupted()) {

Thread.yield();

}

System.out.println(x);//10

}

  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见

  • 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z

volatile static int x;

static int y;

new Thread(()->{

y = 10;

x = 20;//写屏障,y也会同步到主存

},“t1”).start();

new Thread(()->{

// x=20 对 t2 可见, 同时 y=10 也对 t2 可见

System.out.println(x);

},“t2”).start();

以上变量都是指共享变量即成员变量或静态资源变量

4.volatile 原理


volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障

  • 对 volatile 变量的读指令前会加入读屏障

4-1 如何保证可见性

  • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

public void actor2(I_Result r) {

num = 2;

ready = true; // ready 是 volatile 赋值带写屏障

// 写屏障

}

最后

学习视频:

大厂面试真题:


volatile的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障

  • 对 volatile 变量的读指令前会加入读屏障

4-1 如何保证可见性

  • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

public void actor2(I_Result r) {

num = 2;

ready = true; // ready 是 volatile 赋值带写屏障

// 写屏障

}

最后

学习视频:

[外链图片转存中…(img-LFpBrXxj-1714158809495)]

大厂面试真题:

[外链图片转存中…(img-DSnIWUtd-1714158809495)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 24
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值