1.并发编程之可见性
多线程提高效率,本地缓存数据,造成数据修改不可见,
要想保证可见,要么触发同步指令,要么加上volatile,被修饰的内存,只要有修改,马上同步涉及到的每个线程
/**
* volatile 关键字,使一个变量在多个线程间可见
* A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
* 使用volatile关键字,会让所有线程都会读到变量的修改值
*
* 在下面的代码中,running是存在于堆内存的t对象中
* 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
* 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
*
* 使用volatile,将会强制所有线程都去堆内存中读取running的值
*
* volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,
* 也就是说volatile不能替代synchronized
*
*/
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) throws IOException {
new Thread(T01_HelloVolatile::m, "t1").start();
SleepHelper.sleepSeconds(1);
running = false;
System.in.read();
}
}
缓存行
最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享
图中说明了伪共享的问题。在核心1上运行的线程想更新变量X,同时核心2上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果核心1获得了所有权,缓存子系统将会使核心2中对应的缓存行失效。当核心2获得了所有权然后执行更新操作,核心1就要使自己对应的缓存行失效。这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。
缓存行带来的锁竞争
处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完之后不知道何时会写到内存;如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
Java8实现字节填充避免伪共享
JVM参数 -XX:-RestrictContended
@Contended 位于 sun.misc 用于注解java 属性字段,自动填充字节,防止伪共享
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);
}
}
缓存一致性协议 MESI
M:代表该缓存行中的内容被修改了,并且该缓存行只被缓存在该CPU中。这个状态的缓存行中的数据和内存中的不一样,在未来的某个时刻它会被写入到内存中(当其他CPU要读取该缓存行的内容时。或者其他CPU要修改该缓存对应的内存中的内容时(个人理解CPU要修改该内存时先要读取到缓存中再进行修改),这样的话和读取缓存中的内容其实是一个道理)。
E:E代表该缓存行对应内存中的内容只被该CPU缓存,其他CPU没有缓存该缓存对应内存行中的内容。这个状态的缓存行中的内容和内存中的内容一致。该缓存可以在任何其他CPU读取该缓存对应内存中的内容时变成S状态。或者本地处理器写该缓存就会变成M状态。
S:该状态意味着数据不止存在本地CPU缓存中,还存在别的CPU的缓存中。这个状态的数据和内存中的数据是一致的。当有一个CPU修改该缓存行对应的内存的内容时会使该缓存行变成 I 状态。
I:代表该缓存行中的内容时无效的。
2.并发编程之有序性
CPU的乱序执行,为了提高效率
线程的as-if-serial
单个线程,两条语句,未必是按顺序执行
单线程的重排序,必须保证最终一致性
as-if-serial:看上去像是序列化(单线程)
会产生:多线程会产生不希望看到的结果
Object 0 =new Object();
1.对象创建过程(半初始化)
2.DCL(double check lock) volatile 指令重排序
3.对象在内存中的存储布局
4.对象头具体包括啥
5.对象怎么定位
6.对象怎么分配
7.Object 0 =new Object();占多少字节
hanppens-before原则(JVM规定重排序必须遵守的规则)不能重排序
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 用于在多处理器中执行指令时对共享内存的独占使用。 它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。 另外还提供了有序的指令无法越过这个内存屏障的作用。
3.并发编程之原子性
public class T00_00_IPlusPlus {
private static long n = 0L;
public static void main(String[] args) throws Exception {
//Lock lock = new ReentrantLock();
Thread[] threads = new Thread[100];
CountDownLatch latch = new CountDownLatch(threads.length);
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
synchronized (T00_00_IPlusPlus.class) {
//lock.lock();
n++;
//lock.unlock();
}
}
latch.countDown();
});
}
for (Thread t : threads) {
t.start();
}
latch.await();
System.out.println(n);
}
}
保障操作的原子性(Atomicity)
-
悲观的认为这个操作会被别的线程打断(悲观锁)synchronized(上一个小程序)
-
乐观的认为这个做不会被别的线程打断(乐观锁 自旋锁 无锁)cas操作 CAS = Compare And Set/Swap/Exchange
CAS的深度剖析
CAS的ABA问题解决方案 - Version
CAS操作本身的原子性保障
Unsafe:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
synchronized
当我们使用synchronized的时候HotSpot的实现是这样的:上来之后第一个去访问某把锁的线程 比如sync (Object) ,来了之后先在这个Object的头上面markword记录这个线程。(如果只有第一个线程访问的时候实际上是没有给这个Object加锁的,在内部实现的时候,只是记录这个线程的ID(偏向锁))。
偏向锁如果有线程争用的话,就升级为自旋锁,概念就是(有一个哥们儿在蹲马桶 ,另外来了一个哥们,他就在旁边儿等着,他不会跑到cpu的就绪队列里去,而就在这等着占用cpu,用一个while的循环在这儿转圈玩儿, 很多圈之后不行的话就再一次进行升级)。
自旋锁转圈十次之后,升级为重量级锁,重量级锁就是去操作系统那里去申请资源。这是一个锁升级的过程。
内存对象布局
可重入锁
模拟一个父类子类的概念,父类synchronized,子类调用super.m的时候必须得可重入,否则就会出问题(调用父类是同一把锁)。所谓的重入锁就是你拿到这把锁之后不停加锁加锁,加好几道,但锁定的还是同一个对象,去一道就减个1