java 内存可见性_探索Java并发编程中的内存可见性

在JAVA程序员圈子,大家都知道掌握并发编程对于一个 Java 程序员是非常重要的。但相对于其他 Java 基础知识点来说,并发编程更加抽象,涉及到的知识点很多很零散,实际使用也更加麻烦。下面主要针对JAVA并发编程中的一个内存可见性问题进行探索。

问题:什么是内存的可见性?一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。

我们先来一个12306抢票的例子.public class Ticket {

private long sum = 20000;

public void sendTicket(){

for(int i = 0;i<10000;i++){

//卖出一张票

sum-=1;

}

}

public static void main(String[] args) throws Exception{

Ticket ticket = new Ticket();

//创建售票窗线程1

Thread thread1 = new Thread(new Work(ticket));

thread1.setName("售票窗线程-" + 1);

//创建售票窗口线程2

Thread thread2 = new Thread(new Work(ticket));

thread2.setName("售票窗线程-" + 2);

thread1.start();

thread2.start();

// 等待两个线程执行结束

thread1.join();

thread2.join();

System.out.println("sum="+ticket.sum);

}

}

class Work implements Runnable {

private final Ticket ticket;

public Work( Ticket ticket) {

this.ticket = ticket;

}

@Override

public void run() {

try {

ticket.sendTicket();

System.out.println(Thread.currentThread().getName() + "启动时间是" + System.currentTimeMillis());

} catch (Exception e1) {

e1.printStackTrace();

}

}

}第一次执行结果:

售票窗线程-2启动时间是1574601485313

售票窗线程-1启动时间是1574601485317

sum=2396再执行一个结果:

售票窗线程-2启动时间是1574601475353

售票窗线程-1启动时间是1574601475355

sum=8029

直觉告诉我们应该是0,因为两个售票窗都卖了10000张票,sum 的值就是0,但实际上的执行结果是个 0 到 10000 之间的随机数。啥情况啊这是?为啥和我们想象中的不太一样呢?

我们假设线程 A和线程B同时开始执行,那么第一次都会将 sum=0 读到各自的CPU缓存里,执行完sum-=1之后,各自 CPU缓存里的值都是1,同时写入内存后,我们会发内存中是 19999,而不是我们期望的19998。之后由于各自的 CPU 缓存里都有了 sum 的值,两个线程都是基于 CPU 缓存里的 sum 值来计算。这就是缓存的可见性问题。

这里可能又同学会提出疑问。那如果是都使用的上面的缓存中的值,那应该最后的结果也应该是10000才对呀。没错,理想状态下是这样的。但是我们知道两个线程不是同时启动的,有一个时差。

循环10000 次 sum-=1操作如果改为循环1亿张,总票数改为2亿张,并且使用CyclicBarrier让线程能在同一时刻触发,你会发现效果更明显,最终sum的值接近1亿,而不是 0。

改造之后的代码:public class Ticket {

private long sum = 200000000;

public void sendTicket(){

for(int i = 0;i<100000000;i++){

//卖出一张票

sum-=1;

}

}

public static void main(String[] args) throws Exception{

Ticket ticket = new Ticket();

//创建线程1抢票

CyclicBarrier cyclicBarrier = new CyclicBarrier(2);

//创建售票窗线程1

Thread thread1 = new Thread(new Work(cyclicBarrier,ticket));

thread1.setName("售票窗线程-" + 1);

//创建售票窗口线程2

Thread thread2 = new Thread(new Work(cyclicBarrier,ticket));

thread2.setName("售票窗线程-" + 2);

thread1.start();

thread2.start();

// 等待两个线程执行结束

thread1.join();

thread2.join();

System.out.println("sum="+ticket.sum);

}

}

class Work implements Runnable {

private final CyclicBarrier cyclicBarrier;

private final Ticket ticket;

public Work(CyclicBarrier cyclicBarrier, Ticket ticket) {

this.cyclicBarrier = cyclicBarrier;

this.ticket = ticket;

}

@Override

public void run() {

try {

/**

* CyclicBarrier类的await()方法对当前线程(运行cyclicBarrier.await()代码的线程)进行加锁,然后进入await状态;

* 当进入CyclicBarrier类的线程数(也就是调用cyclicBarrier.await()方法的线程)等于初始化CyclicBarrier类时配置的线程数时;

* 然后通过signalAll()方法唤醒所有的线程。

*/

cyclicBarrier.await();

ticket.sendTicket();

System.out.println(Thread.currentThread().getName() + "启动时间是" + System.currentTimeMillis());

} catch (InterruptedException | BrokenBarrierException e1) {

e1.printStackTrace();

}

}

}

执行结果:线程-2启动时间是1574601120948

线程-1启动时间是1574601120948

sum=99637289

在单核cpu的石器时代,我们所有的线程都是在一颗CPU上执行,CPU缓存与内存的数据一致性容易解决。因为所有线程都是操作同一个CPU的缓存,一个线程对缓存的写,对另外一个线程来说一定是可见的。

例如在下面的图中,线程A和线程B都是操作同一个CPU里面的缓存,所以线程A更新了变量a的值,那么线程B之后再访问变量 a,得到的一定是 a 的最新值(线程 A 写过的值)。b7a4cc888dab7e67eb388de55b7fc5e2.png

在多核CPU的时代,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的CPU上执行时,这些线程操作的是不同的CPU缓存。比如下图中,线程A操作的是CPU-1上的缓存,而线程B操作的是CPU-2上的缓存,很明显,这个时候线程A对变量a的操作对于线程B而言就不具备可见性了。这个就属于硬件程序员给软件程序员挖的“坑”。6c08531d6a9db8d7b0121abfb468150e.png

从上面的分析,我们可以知道,多核的CPU缓存会导致的可见性问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值