并发
为什么用并发
- 充分利用多核CPU的计算能力
- 方便进行业务拆分,提升应用性能
并发产生的问题
- 高并发场景下,导致频繁的上下文切换
- 临界区线程安全问题,容易出现死锁的,产生死锁就会造成系统功能不可用
JMM模型
什么是JMM
Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描 述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构 成数组对象的元素)的访问方式。
JMM的基本结构
- 工作内存
- 线程
- 主内存
JMM所围绕的问题
- 可见性
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。
JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。 - 有序性
有序性是指在单线程中执行代码,我们总认为代码的执行顺序是按照顺序依次执行的。 - 原子性
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
JMM同步八大操作
- lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
- unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的 变量才可以被其他线程锁定
- read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中, 以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作 内存的变量副本中
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存 的变量
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中, 以便随后的write的操作
- write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送 到主内存的变量中
指令重排序
只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
好处 :适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
as-if-serial
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因 为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被 编译器和处理器重排序。
数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。
happens-before
注意:两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!只要保证前一个操作的结果对后一个操作可见即可。
只靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序 可能会显得十分麻烦,幸运的是,从JDK 5开始,Java使用新的JSR-133内存模型,提供了 happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下:
- 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
- 监视器锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说, 如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
- volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单 的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当 该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能 够看到该变量的最新值。
- 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的 start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量 的修改对线程B可见
- 传递性 A先于B ,B先于C 那么A必然先于C
- 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前 执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法 成功返回后,线程B对共享变量的修改将对线程A可见。
- 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中 断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
- 对象终结规则 对象的构造函数执行,结束先于finalize()方法