并发编程的特性——可见性

线程间的可见性

多线程为了提高效率,在线程本地缓存数据,造成数据修改线程间不可见,而被volatile修饰的内存,只要有修改,马上同步涉及到的每个线程

public class HelloVolatile {
	// 对比一下有无volatile的情况下,整个程序运行结果的区别
    private static /*volatile*/ boolean running = true;
    private static void m() {
        System.out.println("m start");
        while (running) {
            //System.out.println("hello");
        }
        System.out.println("m end!");
    }

    public static void main(String[] args) {
        new Thread(HelloVolatile::m, "t1").start();
        SleepHelper.sleepSeconds(1);
        running = false;
    }
}

在上面的代码中,running是存在于堆内存的t对象中

  • 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行

  • 使用volatile,将会强制所有线程都去堆内存中读取running的值

  • volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized

缓存行对齐

为了提高线程缓存和内存之间数据交互的效率,所以要使用缓存行。cpu将数据加载到缓存中的最小数据单位是行,缓存中也是以缓存行为单位进行存储的

缓存行越大,局部性空间效率越高,但读取时间慢;缓存行越小,局部性空间效率越底低,但读取时间快。目前采用最多的是折中值,即64字节

缓存行的最低容量限制带来了一个问题,就是伪共享问题。位于同一缓存行的两个不同数据,被两个不同CPU锁定,产生互相影响的伪共享问题,比如xy位于同一缓存行,C1(CPU1)只用x,C2只用2,但是更新读取数据都是以整个缓存行为单位,所以即使仅仅只是改了缓存行中的一条数据,但更新的是整个缓存行,一个CPU更新缓存行之后另一个又要重新读取,这就是缓存行的伪共享

要解决这个问题,可以让x和y不在同一个缓存行里就行,这就是缓存行对齐

用一个demo来演示一下缓存行对齐和缓存一致性协议对效率的影响

public class CacheLinePadding {
    public static long COUNT = 10_0000_0000L;

    private static class T {
    	// 7个long类型56byte,加上x正好是一个缓存行的长度
        private long p1, p2, p3, p4, p5, p6, p7;
        public long x = 0L; //8bytes
        // 作用同上,补齐缓存行
        private long p9, p10, p11, p12, p13, p14, p15;
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws Exception {
        CountDownLatch latch = new CountDownLatch(2);

        Thread t1 = new Thread(() -> {
            for (long i = 0; i < COUNT; i++) {
                arr[0].x = i;
            }

            latch.countDown();
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < COUNT; i++) {
                arr[1].x = i;
            }

            latch.countDown();
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        latch.await();
        System.out.println((System.nanoTime() - start) / 100_0000);
    }
}

上述代码中,两个线程分别对数组元素T对象中的x变量进行修改,如果注掉上下两行long类型的变量,当两个线程执行完时打印耗时784,而如果放开这两行代码,则耗时236,这是为什么呢?

考虑一下,如果把这两行填充代码去掉,那么arr[0].x和arr[1].x,我们暂且叫x1和x2吧,他俩大概率是位于同一缓存行的。由于上面提到的伪共享问题,t1线程只修改了x1,t2线程只修改了x2,但他们都会以整个缓存行为单位进行更新和读取,既然同一缓存行的数据在两个线程中都存在,那么势必要有某种机制,当t1线程修改完缓存行时要通知t2线程,这种机制就是缓存一致性协议。

那为什么在x前后加了无意义填充效率会高呢,因为两个线程在初始化变量x时,不管在x前面还是后面的数据都会和x组成一个完整的缓存行,这样就能保证两个线程修改各自的x时,x所在的缓存行中没有其他线程需要的数据,修改不用通知其他线程,其他线程也不用同步,省去了缓存一致性协议的要求,所以效率提高了

实际上,这种对齐方式在一些框架源码中是有运用的,如LinkedBlockingQueue及Disruptor框架中

volatile的作用

保证线程可见性

大家知道java里面是有堆内存的,堆内存是所有线程共享里面的内存,除了共享的内存之外呢,每个线程都有自己的专属的区域,都有自己的工作内存,如果说在共享内存里有一个值的话,当我们线程,某一个线程都要去访问这个值的时候,会将这个值copy一份,copy到自己的这个工作空间里头,然后对这个值的任何改变,首先是在自己的空间里进行改变,什么时候写回去,就是改完之后会马上写回去。什么时候去检查有没有新的值,也不好控制。

在这个线程里面发生的改变,并没有及时的反应到另外一个线程里面,这就是线程之间的不可见 ,对这个变量值加了volatile之后就能够保证 一个线程的改变,另外一个线程马上就能看到。

禁止指令重排序

禁止指令重排序属于线程有序性特性中的内容,下篇再聊

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

紫荆之后-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值