目标都是最大化CPU的使用率
指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的
也称为伪并行(paeudo parallel)
指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行
也是并发编程Bug的源头:可见性、原子性和有序性
当一个线程修改了共享变量的值,其他线程能够看到被修改的值
Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.Objects;
import java.util.concurrent.locks.LockSupport;
/**
* -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
* hsdis-amd64.dll
* 可见性案例
*/
public class VisibilityTest {
private volatile boolean flag = true;
private Integer count = 0;
public void refresh() {
flag = false;
System.out.println(Thread.currentThread().getName() + "修改flag:" + flag);
}
public void load() {
System.out.println(Thread.currentThread().getName() + "开始执行.....");
while (flag) {
count++;
// 能够跳出循环 内存屏障
Objects.requireNonNull(UnsafeFactory.getUnsafe()).storeFence();
// 能够跳出循环 释放时间片,上下文切换 加载上下文:flag=true
Thread.yield();
// 能够跳出循环 内存屏障
System.out.println(count);
LockSupport.unpark(Thread.currentThread());
shortWait(1000000); //1ms
shortWait(1000);
try {
Thread.sleep(1); //内存屏障 上下文切换
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "跳出循环: count=" + count);
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest test = new VisibilityTest();
// 线程threadA模拟数据加载场景
Thread threadA = new Thread(test::load, "threadA");
threadA.start();
// 让threadA执行一会儿
Thread.sleep(1000);
// 线程threadB通过flag控制threadA的执行时间
Thread threadB = new Thread(test::refresh, "threadB");
threadB.start();
}
public static void shortWait(long interval) {
long start = System.nanoTime();
long end;
do {
end = System.nanoTime();
} while (start + interval >= end);
}
public static class UnsafeFactory {
public static Unsafe getUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static long getFieldOffset(Unsafe unsafe, Class<?> clazz, String fieldName) {
try {
return unsafe.objectFieldOffset(clazz.getDeclaredField(fieldName));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
}
}
public class VolatileTest2 {
private static volatile boolean flag = true;
public static void main(String[] args) {
new Thread(() -> {
while (true){
if (flag){
System.out.println("trun on");
flag = false;
}
}
}).start();
new Thread(() -> {
while (true){
if (!flag){
System.out.println("trun off");
flag = true;
}
}
}).start();
}
}
即程序执行的顺序按照代码的先后顺序执行。JVM 存在指令重排,所以存在有序性问题
比较经典的有单例模式未初始化对象问题
代码示例:
public class ReOrderTest {
private static int x = 0, y = 0;
private volatile static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int i=0;
while (true) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
//x,y: 00, 10, 01, 11
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
shortWait(20000);
a = 1; // volatile写
// StoreLoad
//UnsafeFactory.getUnsafe().storeFence();
x = b; // volatile读
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
//UnsafeFactory.getUnsafe().storeFence();
y = a;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("第" + i + "次(" + x + "," + y + ")");
if (x==0&&y==0){
break;
}
}
}
public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do{
end = System.nanoTime();
}while(start + interval >= end);
}
}
一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。不采取任何的原子性保障措施的自增操作并不是原子性的。在32为处理器下,对于基本数据类型long和double的读取和赋值操作不是原子的
Java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序
JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能
在编译器与CPU处理器中都能执行指令重排优化操作,如下图:
在JSR规范中定义了4种内存屏障:
由于x86只有store load可能会重排序,所以只有JSR的StoreLoad屏障对应它的mfence或lock前缀指令,其他屏障对应空操作
硬件层提供了一系列的内存屏障 memory barrier / memory fence(Intel的提法)来提供一致性的能力。拿X86平台来说,有几种主要的内存屏障:
对Load Barrier来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据;对Store Barrier来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存
Lock前缀实现了类似的能力,它先对总线和缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的数据刷新回主内存。在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放
不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码
Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。JMM描述的是一种抽象的概念,一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性、有序性、可见性展开的
Java内存模型与硬件内存架构之间存在差异。硬件内存架构没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内存中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中,Java内存模型和计算机硬件内存架构是一个交叉关系
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
按程序类型,Java程序的内存可见性保证可以分为下列3类:
未同步程序在JMM中的执行时,整体上是无序的,其执行结果无法预知。未同步程序在两个模型中的执行特性有如下几个差异:
JVM在32位处理器上运行时,可能会把一个64位long/double型变量的写操作拆分为两个32位的写操作来执行。这两个32位的写操作可能会被分配到不同的总线事务中执行,此时对这个64位变量的写操作将不具有原子性。从JSR-133内存模型开始(即从JDK5开始),仅仅只允许把一个64位long/double型变量的写操作拆分为两个32位的写操作来执行,任意的读操作在JSR-133中都必须具有原子性
64位的long型和double型变量,只要它是volatile变量,对该变量的读/写就具有原子性
在JSR-133之前的旧Java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量重排序。为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133专家组决定增强volatile的内存语义:
严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义
volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的,即修改后必须立即同步回主内存,使用时必须从主内存刷新,由此保证volatile变量操作对多线程的可见性
通过lock前缀指令,会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器的缓存无效
JVM中的字节码解释器(bytecodeInterpreter),用C++实现了JVM指令,其优点是实现相对简单且容易理解,缺点是执行慢
详细代码在JVM源码中的bytecodeInterpreter.cpp文件
模板解释器(templateInterpreter),其对每个指令都写了一段对应的汇编代码,启动时将每个指令与对应汇编代码入口绑定,可以说是效率做到了极致
详细代码在JVM源码中的templateTable_x86_64.cpp文件
void TemplateTable::volatile_barrier(Assembler::Membar_mask_bits
order_constraint) {
// Helper function to insert a is‐volatile test and memory barrier
if (os::is_MP()) { // Not needed on single CPU
__ membar(order_constraint);
}
}
// 负责执行putfield或putstatic指令
void TemplateTable::putfield_or_static(int byte_no, bool is_static, RewriteControl rc){
// ...
// Check for volatile store
__ testl(rdx, rdx);
__ jcc(Assembler::zero, notVolatile);
putfield_or_static_helper(byte_no, is_static, rc, obj, off, flags);
volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad |
Assembler::StoreStore));
__ jmp(Done);
__ bind(notVolatile);21
putfield_or_static_helper(byte_no, is_static, rc, obj, off, flags);
__ bind(Done);
}
在assembler_x86.hpp文件中
// Serializes memory and blows flags
void membar(Membar_mask_bits order_constraint) {
// We only have to handle StoreLoad
// x86平台只需要处理StoreLoad
if (order_constraint & StoreLoad) {
int offset = ‐VM_Version::L1_line_size();
if (offset < ‐128) {
offset = ‐128;
}
// 下面这两句插入了一条lock前缀指令: lock addl $0, $0(%rsp)
lock(); // lock前缀指令
addl(Address(rsp, offset), 0); // addl $0, $0(%rsp)
}
}
在linux系统x86中的实现
orderAccess_linux_x86.inline.hpp
inline void OrderAccess::storeload() { fence(); }
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
}
}
x86处理器中利用lock实现类似内存屏障的效果
添加下面的jvm参数查看之前可见性Demo的汇编指令
‐XX:+UnlockDiagnosticVMOptions ‐XX:+PrintAssembly ‐Xcomp
验证了volatile使用了lock前缀指令
从硬件层面分析Lock前缀指令
《64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf》中有如下描述:
The 32-bit IA-32 processors support locked atomic operations on locations in system memory. These operations are typically used to manage shared data structures (such as semaphores, segment descriptors,system segments, or page tables) in which two or more processors may try simultaneously to modify the same field or flag. The processor uses three interdependent mechanisms for carrying out locked atomic operations:
• Guaranteed atomic operations
• Bus locking, using the LOCK# signal and the LOCK instruction prefix
• Cache coherency protocols that ensure that atomic operations can be carried out on cached data structures (cache lock); this mechanism is present in the Pentium 4, Intel Xeon, and P6 family processors
译文如下:
32位的IA-32处理器支持对系统内存中的位置进行锁定的原子操作。这些操作通常用于管理共享的数据结构(如信号量、段描述符、系统段或页表),在这些结构中,两个或多个处理器可能同时试图修改相同的字段或标志。处理器使用三种相互依赖的机制来执行锁定的原子操作: