【并发编程三大特性】:
可见性
有序性
原子性( 较复杂 )
【线程的可见性】:
【一个例子认识线程的可见性】:
import Utils.SleepHelper;
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;
}
}
【最终输出】:
//发现程序停下了,一直在运行并没有结束。
【 并不会每次都去读去堆内存 】:
running位于主存当中,T1线程只去读取一次;
在上面的代码中,running是存在于堆内存的t对象中
- 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
- 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
- 使用volatile,将会强制所有线程都去堆内存中读取running的值
- volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
【引入Volatile关键字】:
/**
* volatile 关键字,使一个变量在多个线程间可见
* A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
* 使用volatile关键字,会让所有线程都会读到变量的修改值
* <p>
* 在下面的代码中,running是存在于堆内存的t对象中
* 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
* 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
* <p>
* 使用volatile,将会强制所有线程都去堆内存中读取running的值
* volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
*/
package T02_Thread_Volatile;
import Utils.SleepHelper;
import java.io.IOException;
public class T01_HelloVolatile2 {
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) throws IOException {
new Thread(T01_HelloVolatile2::m, "t1").start();
SleepHelper.sleepSeconds(1);
running = false;
}
}
【最终输出】:
【理解】:
volatile修饰的那块内存,对于它的任何修改,其他的线程立马可见。——保持可见性。
【线程间好像又可见了】:
import Utils.SleepHelper;
import java.io.IOException;
public class T01_HelloVolatile3 {
private static 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) throws IOException {
new Thread(T01_HelloVolatile3::m, "t1").start();
SleepHelper.sleepSeconds(1);
running = false;
}
}
【最终输出】:
//在输出若干个hello之后 , 线程结束了。
【触发了可见性机制】:
//synchronized也是可以保持可见性的。
【简单理解】:
在某些语句执行的情况下,它是可以触发本地的缓存和我们主内存之间的数据进行一个刷新和同步。
【volatile修饰引用类型】:
volatile 引用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性。
//内存中有一个A对象 , 小a指向它 , 这个a是被volatile修饰的 , 两个线程本地的缓存里也都有一个 r , 一个线程修改自己内部的缓存r , 其他线程并不会可见 。
但是,如果小a指向的内容被修改了,其他线程就可见了.
[测试程序]:
public class T02_VolatileReference {
private static class A {
boolean running = true; //要加volatile的话加在这里 ~
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;
}
}
【输出】:
运行程序,发现一直在循环。
【改进程序】:
public class T02_VolatileReference2 {
private static class A {
volatile 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;
}
}
【最终输出】:
【三级缓存】:
Registers -> L1 -> L2 -> L3 -> 内存。
寄存器需要一个数据首先会去L1里找,然后依次去L2、L3、内存中找。回去的时候先往L3中存一份,再往L2中存2一份,再往L1中存一份,最后存到寄存器里。我们说的可见性并不是ThreadLocal , 而是各个CPU中L123的数据的可见性。
【缓存行的基本概念】:
【读数据时的效率】:
并非每次需要变量都是从内存中读取数据都是L3、2、1这种,而是一整块儿一整块儿地从内存中读出来。
【程序的局部性原理】:
按块读取;
程序局部性原理 , 可以提高效率;
充分发挥总线CPU针脚等一次性读取更多数据的能力。
【空间局部性原理】:
当我用到某一个值的时候,我很快就会用到和它相邻的值。用到这个值的时候,我一次性把它周边的值都给读取到缓存里,并非只读一个。
【时间局部性原理】:
当我用到一个指令的时候 , 很快我会用到和它相邻的指令,也是一次性地将很多指令相关的数据全给读取到内存里面;
【这一块儿数据到底有多大呢?】:
专业名词——CacheLine , 一行大小64bytes 。
【 通过程序认识缓存一致性 】:
public class T01_CacheLinePadding {
public static long COUNT = 10_0000_0000L;
private static class T {
public long x = 0L; //8 bytes ——————8个Long类型就能够把缓存行给填满。
}
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);
}
}
【最终输出】:
//运行了很多次——几乎都在600左右~~~。
【修改程序】:
在上述程序的基础上只修改一处:
private static class T {
private long p1, p2, p3, p4, p5, p6, p7;
public long x = 0L; //8bytes
private long p9, p10, p11, p12, p13, p14, p15;
}
【最终发现】:
程序运行变快了——到了300多秒!!!!!!
//跟volatile没有关系 , 你加不加volatile都能够体现出效率的区别来。
//这是由于缓存一致性协议所导致的。
//多线程修改的内容位于同一缓存行的情况时会互相干扰。CPU1中的缓存行修改了,会去通知CPU2 ,同样CPU2中缓存行修改了会去通知CPU1。
前面加7个变量也就是56字节,56+8=64 , 所以任意两个变量绝对不会位于同一个缓存行当中。————互相之间不用做通知了,互相之间也不用同步了。
【 Disruptor闪电框架源码 】:
//最后面堆了7个Long就是为了利用缓存行机制。
【 认识Contended 】:
认识一个注解——Contended 。
被这个注解所标注的数据不会和其他数据位于同一缓存行。
【先不加注解】:
import sun.misc.Contended;
//注意:运行这个小程序的时候,需要加参数:-XX:-RestrictContended
import java.util.concurrent.CountDownLatch;
public class T05_Contended {
public static long COUNT = 10_0000_0000L;
//@Contended //只有1.8起作用 , 保证x位于单独一行中
private static class T {
public long x = 0L;
}
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);
}
}
【再加上注解比较两者的速度】:
import sun.misc.Contended;
//注意:运行这个小程序的时候,需要加参数:-XX:-RestrictContended
import java.util.concurrent.CountDownLatch;
public class T05_Contended2 {
public static long COUNT = 10_0000_0000L;
//@Contended //只有1.8起作用 , 保证x位于单独一行中
private static class T {
@Contended
public long x = 0L;
}
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);
}
}
//可以发现效率会大幅度地提升。
需要加上如下参数:
//默认情况下Contended的注解是被限制住的。你必须把这个参数打开,否则加了也不会起作用。
【 硬件层面的缓存一致性 】:
缓存一致性不要和MESI概念混淆。
//不同类型的CPU所采用的缓存一致性协议是不一样的。
MESI只是缓存一致性协议中的一种,这个协议是Intel公司设计的。MESI指的是缓存行的四种状态。
Modified——-被修改了;
Exclusive——独享;
Shared———共享;
Invalid———-失效;
Volatile底层并不是由MESI协议实现的!!!
【为什么缓存行一行64字节?】:
//工业实践当中得出的最佳实践。
缓存行越大 , 局部性空间效率越高 , 但读取时间慢。(缓存行大了的话,读取的时间就慢了。)
缓存行越小, 局部性空间效率越低 , 但读取时间快。
取一个折中值,目前多用:————64字节。
【总结】:
- volatile保障线程可见性
- 缓存行
- 缓存一致性协议
【volatile的底层实现】:
volatile除了保障线程可见性之外 , 它还可以禁止重排序。了解完有序性之后才能真正理解它的底层实现原理。