多线程基础-并发编程3大特性-可见性

本文通过一个程序实例分析了Java中线程可见性问题,解释了由于CPU缓存导致的线程间数据不同步。通过volatile关键字解决了这个问题,讨论了其作用以及三级缓存和缓存行的概念。同时,提到了伪共享现象,并介绍了@Contended注解来避免伪共享,以提高多线程环境下的程序性能。
摘要由CSDN通过智能技术生成

一、先看一个程序

public class VisibilityDemo {
    static boolean check = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while(!check) {
//                System.out.println("你咋不结束");
            }
        }).start();
        Thread.sleep(100);
        check=true;
    }

}

执行后发现,明明check改为true了,但是子线程依然在执行死循环。

二、问题产生的原因

每个线程都存有一份数据副本,可以理解为一份只有当前线程可读的缓存。如图
在这里插入图片描述

变量check本来存在内存中中,线程A读取了并缓存。
线程B也读取了并缓存。此时线程A把check修改成了true,但线程B依然只读取自己缓存的check,导致变量不可见。
有细心的朋友发现了,把 System.out.println(“你咋不结束”); 注释去掉程序就不会进入死循环了,这是因为有些语句会触发线程重新从内存中读取变量。

三、解决的办法

添加volatile关键字

public class VisibilityDemo {
    volatile static boolean check = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while(!check) {
//                System.out.println("你咋不结束");
            }
        }).start();
        Thread.sleep(100);
        check=true;
    }

}

四、volatile作用

// TODO

五、三级缓存

cpu缓存实际的模型:
在这里插入图片描述

在这里插入图片描述

L1,L2,L3,执行速度逐渐下降,空间逐渐变大,成本逐渐降低

六、缓存行概念

线程读取L3时并非是一个个变量的读的,而是以一块一块的读的,这一块数据被称为缓存行,一般是64个字节(因特尔CPU,啥你说为啥是64字节,因为少了就快又命中率低,多了就慢又命中率高,64是一个折中值)。
下面我们看一个程序:

	public static class T {
	//        protected long p1,p2,p3,p4,p5,p6,p7;
	
	        public volatile long x = 0L;
	//        protected long p8,p9,p10,p11,p12,p13,p14;
    }

    public static void main(String[] args) {
        int count = 1_0000_0000;
        Long sysTime = System.currentTimeMillis();
        CountDownLatch latch = new CountDownLatch(2);

        T[] ts = new T[2];
        ts[0] = new T();
        ts[1] = new T();

        Thread t1 = new Thread(()->{
            for(int i=0; i<count; i++) {
                ts[0].x = 1L;
            }
            latch.countDown();
        });

        Thread t2 = new Thread(()->{
            for(int i=0; i<count; i++) {
                ts[1].x = 1L;
            }
            latch.countDown();
        });

        t1.start();
        t2.start();
        try {
            latch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("耗时:"+(System.currentTimeMillis()-sysTime));
    }

程序功能是2个线程修改T数组里的变量x。
执行后耗时约2000ms。然后把2个protected注释去掉,执行耗时变成了800ms。
PS:笔者用的是macbook M1处理器,盲猜缓存行是128字节的,加到p28才演示出效果。

  • 为何会产生这样的效果?
    这是因为2个T对象的x原本存在同一个缓存行。线程A修改了T1的x,线程B修改T2时,跟T1说我改了这行,你也要再读一下。这种现象也称为伪共享

  • 为何去掉注释变快了?
    因为每个long占8个字节,x前后都塞了54个字节,2个x在同一缓存行,杜绝伪共享的发生。
    在这里插入图片描述

  • 太难看了,有无更好的写法?
    有,就是@Contended注解,用法如图:

@Contended
public volatile long x = 0L;

注:此注解只在jdk1.8,并且要添加启动参数:
-XX:-RestrictContended
才能生效

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值