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

原创 码农  2019-12-19 11:14:56  阅读 264 次 评论 0 条

在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 写过的值)。

探索Java并发编程中的内存可见性 编程代码 第1张

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

探索Java并发编程中的内存可见性 编程代码 第2张

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

本文地址:https://www.itcodeit.com/post/24.html
版权声明:本文为原创文章,版权归 码农 所有,欢迎分享本文,转载请保留出处!

发表评论


表情

还没有留言,还不快点抢沙发?