并发程序的特性
程序是什么?–> QQ.exe PowerPoint.exe
进程是什么?–> 程序启动 进入内存 资源分配的基本单位
线程是什么?–> 程序执行的基本单位
程序如何开始运行?–> CPU 读指令 - PC(存储指令地址) ,读数据 Register ,计算, 回写, -> 下一条
线程如何进行调度?–> linux 线程调度器(OS)操作系统
线程切换的概念是什么?–> Context Switch CPU保存现场 执行新线程,恢复现场,继续执行原线程这样的一个过程
线程的底层知识(可见性 有序性 原子性)
- 线程的执行
- 线程的调度(Context Switch)
- 一个核同一时刻,只能运行一个线程
面试题:
- 是不是线程数越多,效率就越高?
- 单个CPU设定多线程是否有意义?
可见性
线程间的可见性
MESI
多线程提高效率,本地缓存数据,造成数据修改不可见,
要想保证可见,要么触发同步指令,要么加上volatile,被修饰的内存,只要有修改,马上同步涉及到的每个线程
用volatile保障可见性
/**
* volatile 关键字,使一个变量在多个线程间可见
* A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
* 使用volatile关键字,会让所有线程都会读到变量的修改值
*
* 在下面的代码中,running是存在于堆内存的t对象中
* 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
* 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
*
* 使用volatile,将会强制所有线程都去堆内存中读取running的值
*
* volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
*
* @author mashibing
*/
package com.mashibing.juc.c_001_00_Visibility;
import com.mashibing.util.SleepHelper;
public class T01_HelloVolatile {
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(T01_HelloVolatile::m, "t1").start();
SleepHelper.sleepSeconds(1);
running = false;
}
}
缓存行对齐
-
缓存行对齐
缓存行64个字节是CPU同步的基本单位,缓存行隔离会比伪共享效率要高
Disruptor -
认识缓存行对齐的编程技巧
package com.mashibing.juc.c_001_02_FalseSharing; 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); } }
-
需要注意,JDK8引入了@sun.misc.Contended注解,来保证缓存行隔离效果
要使用此注解,必须去掉限制参数:-XX:-RestrictContendedpackage com.mashibing.juc.c_001_02_FalseSharing; import sun.misc.Contended; //注意:运行这个小程序的时候,需要加参数:-XX:-RestrictContended import java.util.concurrent.CountDownLatch; public class T05_Contended { public static long COUNT = 10_0000_0000L; private static class T { @Contended //只有1.8起作用 , 保证x位于单独一行中 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); } }
-
伪共享
有序性
CPU的乱序执行
Disorder这个程序,证明乱序执行的确存在
为什么会乱序?主要是为了提高效率
线程的as-if-serial
单个线程,两条语句,未必是按顺序执行
单线程的重排序,必须保证最终一致性
as-if-serial:看上去像是序列化(单线程)
会产生的后果
多线程会产生不希望看到的结果
哪些指令可以互换顺序
hanppens-before原则(JVM规定重排序必须遵守的规则)
JLS17.4.5 (不需要记住)
•程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。
•管程锁定规则:一个unlock操作先行发生于后面(时间上)对同一个锁的lock操作。
•volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作。
•线程启动规则:Thread的start( )方法先行发生于这个线程的每一个操作。
•线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过Thread.join( )方法结束、Thread.isAlive( )的返回值等手段检测线程的终止。
•线程中断规则:对线程interrupt( )方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt( )方法检测线程是否中断
•对象终结规则:一个对象的初始化完成先行于发生它的finalize()方法的开始。
•传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C
使用内存屏障阻止乱序执行
内存屏障是特殊指令:看到这种指令,前面的必须执行完,后面的才能执行
intel : lfence sfence mfence(CPU特有指令)
JVM中的内存屏障
所有实现JVM规范的虚拟机,必须实现四个屏障
LoadLoadBarrier LoadStore SL SS
volatile的底层实现
volatile修饰的内存,不可以重排序,对volatile修饰变量的读写访问,都不可以换顺序
1: volatile i
2: ACC_VOLATILE
3: JVM的内存屏障
屏障两边的指令不可以重排!保障有序!
happends-before
as - if - serial
4:hotspot实现
bytecodeinterpreter.cpp
int field_offset = cache->f2_as_index();
if (cache->is_volatile()) {
if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
OrderAccess::fence();
}
orderaccess_linux_x86.inline.hpp
inline void OrderAccess::fence() {
if (os::is_MP()) {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}
LOCK 用于在多处理器中执行指令时对共享内存的独占使用。
它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。
另外还提供了有序的指令无法越过这个内存屏障的作用。