什么是并行与并发
并行
通常来说,指同一时间有多条命令在多个处理器上执行,是真实的物理架构。
并发
指同一时间只有一条命令在执行,把时间分成若干片,不同的任务交替执行,但是切换的速度非常快,在用户的感知角度就是在同时执行的。
并行与并发的目的:都是为了提供CPU的使用率
本质:解决多线程下同步、互斥、分工的问题
并发三大特性
并发编程Bug的源头:由原子性、可见性、有序性等产生的问题。
可见性
什么是可见性
当一个线程修改了共享变量值,当其他线程可以看见这个被修改的值。
如何保证线程的可见性
- 通过volatile关键字
- 通过内存屏障
- 通过synchronized关键字
- 通过Lock锁
- 通过final关键字
有序性
就是程序的执行顺序按照代码的书写顺序进行执行,Java存在指令重排序,所以存在有序性问题。
如何保证有序性
- 通过volatile关键字
- 通过内存屏障
- 通过Lock锁
- 通过synchronized关键字
原子性
什么是原子性
一个或多个操作,要么全部执行且在执行的过程中没有抛出异常,要么全部不执行。在Java中,对基本数据类型的读取和赋值是具备原自子性操作的,但是不做任何措施的进行自增或减是不具备原子性操作的。
如何保证原子性
- 通过synchronized关键字
- 通过Lock锁
- 通过CAS锁
三大特性分析
可见性问题深入分析
通过如下代码来分析可见性的问题
public class VisibilityTest {
/**
* 使用 volatile关键字进行修饰
*/
private Boolean flag = true;
private int count = 0;
/**
* Integer 中是被final修饰
*/
// private Integer count = 0;
public void refresh() {
flag = false;
System.out.println(Thread.currentThread().getName() + "进行修改flag");
}
public void load() {
System.out.println(Thread.currentThread().getName() + "开始执行");
while (flag) {
count++;
// 假设进行实际业务逻辑 操作
/**
* 跳出循环的原因会去调内存屏障
*/
// UnsafeFactory.getUnsafe().storeFence();
/**
* 是否时间片
*/
// Thread.yield();
/**
* 在底层调用了synchronized
*/
// System.out.println(count);
/**
* 发放一个许可,也是内存屏障
*/
// LockSupport.unpark(Thread.currentThread());
DateUtils.shortWait(1000000);
}
System.out.println(Thread.currentThread().getName() + "跳出循环:i=" + count);
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest test = new VisibilityTest();
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
Thread.sleep(1000);
Thread threadB = new Thread(() -> test.refresh(), "threadB");
threadB.start();
}
}
public class UnsafeFactory {
/**
* 通过反射的方式获取Unsafe对象
*
* @return
*/
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;
}
/**
* 获取字段的内存偏移量
*
* @param unsafe
* @param clazz
* @param fieldName
* @return
*/
public static Long getFieldOffset(Unsafe unsafe, Class clazz, String fieldName) {
try {
return unsafe.objectFieldOffset(clazz.getDeclaredField(fieldName));
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return null;
}
}
JMM的内存可见性保证
- 单线程程序。单线程程序不会出现内存可见性问题。
- 正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性,JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。
- 未同步/未正确同步的多线程程序。JMM为它们提供了最小安全保障;线程执行是读取到的值,整体上来说说是无序的,其执行结果也是无法预知的。
有序性问题深入分析
public class UnsafeFactory {
/**
* 通过反射的方式获取Unsafe对象
*
* @return
*/
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;
}
/**
* 获取字段的内存偏移量
*
* @param unsafe
* @param clazz
* @param fieldName
* @return
*/
public static Long getFieldOffset(Unsafe unsafe, Class clazz, String fieldName) {
try {
return unsafe.objectFieldOffset(clazz.getDeclaredField(fieldName));
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return null;
}
}
指令重排
Java在实际生成字节码文件的时候,在多处理器,CPU多级缓存的系统会适当的对机器指令进行重排序,是机器指令更符合CPU的执行特性,最大限度的发挥机器的性能。
volatile重排规则
是否能重排序 | 第二个关键字 | ||
第一个操作 | 普通读/写 | volatile 读 | volatile 写 |
普通读/写 | NO | ||
volatile 读 | NO | NO | NO |
volatile 写 | NO | NO |
**volatile禁止重排序场景** 1. 第二个操作是volatile写,不管第一个操作是什么都不会重排序 2. 第一个操作是volatile读,不管第二个操作是什么都不会重排序 3. 第一个操作是volatile写,第二个操作是volatile读,也不会发生重排序
JMM内存插入策略
- 在每个volatile写操作的前面插入一个StoreStore屏障
- 在每个volatile写操作的后面插入一个StoreLoad屏障
- 在每个volatile读操作的后面插入一个LoadLoad屏障
- 在每个volatile读操作的后面插入一个LoadStore屏障
原子性分析
原子性就是保证一段操作要么全部成功要么全部失败,即在后面的文章中会重点说道。
Java内存模型(JMM)
JMM定义
Java虚拟机规范中定义了Java内存模型(Java Memory Model)简称JMM,用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能到达一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的,规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须是如何同步的访问共享变量。JMM描述的是一种抽象的概念、一组规则,通过这组规则中的变量来进行对共享数据区域和私有数据区域的访问,JMM是围绕原子性、有序性、可见性进行的。
JMM与硬件内存架构的关系
Java内存模型与硬件内存之间存在差异。硬件内存架构没有区分线程栈和堆。对应硬件,所有的线程栈和堆都分别在主内存中,部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。
内存交互操作
内存交互即一个变量从共享(主)内存如何到工作内存,再如何从工作内存到共享(主)内存之间的实现步骤,内存操作如下图:
- lock(锁定)作用于主内存的变量,把一个变量标记为线程独占状态。
- read(读取)作用于主内存变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入)作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用)作用于内存的变量,把工作内存的一个变量值传递给执行引擎,当虚拟机遇到一个要使用变量的字节码命令的时候,就会执行该操作。
- assign(赋值)作用于工作内存的变量,它把一个从执行引擎接收到的赋值给工作内存的变量,当虚拟机遇到一个给变量赋值的字节码就会执行该操作。
- store(存储)作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入)作用于主内存的变量,它把store操作从工作内存中的一个变量值传送到主内存的变量中。
- unlock(解锁)作用于主内存的变量,把一个标记为线程独占状态的变量进行释放出来,这样其他线程就可以进行锁定。
Java内存模型还规定了在执行上述的过程中,对一个变量必须按照lock、read、load、use、assign、store、write、unlock这样的步骤进行执行。
演示代码库地址:https://github.com/fqzhangitem/learning-notes