提升--03---并发编程之---可见性
Posted 高高for 循环
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了提升--03---并发编程之---可见性相关的知识,希望对你有一定的参考价值。
并发编程三大特性
可见性
先看一个小案例:
import java.io.IOException;
public class T01_HelloVolatile {
private static boolean running = true;
private static void m() {
System.out.println("m start");
while (running) {
}
System.out.println("m end!");
}
public static void main(String[] args) throws IOException {
new Thread(T01_HelloVolatile::m, "t1").start();
SleepHelper.sleepSeconds(1);
running = false;
System.in.read();
}
}
一直运行,没有结束…
分析:
- 在下面的代码中,running是存在于堆内存的t对象中
- 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy(缓存),并不会每次都去读取堆内存.
- 这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行.
使用 volatile 关键字:
volatile 关键字,使一个变量在多个线程间可见
- 使用volatile,将会强制所有线程都去堆内存中读取running的值,会让所有线程都会读到变量的修改值.
- volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
注意:
volatile 引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性
public class T02_VolatileReference {
private static class A {
boolean running = true;
void m() {
System.out.println("m start");
while (running) {
}
System.out.println("m end!");
}
}
private volatile static A a = new A();
public static void main(String[] args) {
new Thread(a::m, "t1").start();
SleepHelper.sleepSeconds(1);
a.running = false;
}
}
- 以上volatile 只能保证a对象修改时,各线程可见.但不能保证内部属性running 的可见性.
- 因为a对象是引用类型,地址值并未改变,只是内部属性running 被主线程改变,所以对t1线程来说,还是存在a对象内部属性running ,修改的不可见性.
三级缓存
- 寄存器Register读取数据是先从L1找,L1找不到 ,再L2,L3,再找不到,再从内存中去寻找
缓存行
程序局部性原理
缓存是按块,一行一行局部读取的
缓存行对齐
缓存行: 64个字节是CPU同步的基本单位
缓存行隔离会比伪共享效率要高Disruptor
认识缓存行对齐的编程技巧----案例1
需求:
- 对一个数组内的元素,多线程的进行10亿次数据赋值.并计算这个共需要花费多少时间.
方式一:
数组中的两个元素arr[0],arr[1],大概率在同一个缓存行里
多线程操作数据时,因为为了保证多个cpu中的数据一致性,操作系统底层会通知各个cpu,当数据被修改时,需要从内存中读取跟新后的数据.从而达到各个cpu线程中的数据一致.
import java.util.concurrent.CountDownLatch;
public class T01_CacheLinePadding {
public static long COUNT = 10_0000_0000L;
private static class T {
// private long p1, p2, p3, p4, p5, p6, p7;
public long x = 0L;
// 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中加入属性,使得整个T对象的大小,大于64个字节.
所以数组中的两个元素arr[0],arr[1]一定不在同一个缓存行里.
import java.util.concurrent.CountDownLatch;
public class T01_CacheLinePadding {
public static long COUNT = 10_0000_0000L;
private static class T {
private long p1, p2, p3, p4, p5, p6, p7;
public long x = 0L;
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);
}
}
因为此时各cpu中的缓存行里的数据,不会被不同的线程修改,从而出现各cpu数据不一致的情况.
所以操作系统底层不需要频繁的通知各cpu线程,去从内存读取数据去跟新.
省略了通知和同步的时间,所以耗时更小,效率更高
@Contended注解
- JDK8引入了@sun.misc.Contended注解,来保证缓存行隔离效果
- 要使用此注解,必须去掉限制参数:-XX:-RestrictContended
只有1.8起作用 , 保证x位于单独一行中
缓存的一致性协议:
MESI是硬件层面的缓存一致性协议,Inter设计的
Disruptor 源码
以上是关于提升--03---并发编程之---可见性的主要内容,如果未能解决你的问题,请参考以下文章
JUC并发编程 共享模型之内存 -- Java 内存模型 & 原子性 & 可见性(可见性问题及解决 & 可见性 VS 原子性 & volatile(易变关键字))(代码