线程间的可见性
多线程为了提高效率,在线程本地缓存数据,造成数据修改线程间不可见,而被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之后就能够保证 一个线程的改变,另外一个线程马上就能看到。
禁止指令重排序
禁止指令重排序属于线程有序性特性中的内容,下篇再聊