Java并发的基本概念
硬件层次
由于CPU执行指令的速度是很快的,但是内存访问的速度就慢了很多,相差的不是一个数量级,于是为了不让内存成为计算机程序处理的瓶颈,通过在CPU和内存之间增加高速缓存的方式来解决
缓存一致性
在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致
解决缓存一致性的两种方案
- 通过在总线加LOCK#锁的方式(现代计算机都是多核CPU,总线加锁会导致其他CPU也无法访问内存,效率低下)
- 通过缓存一致性协议(Cache Coherence Protocol)
MESI缓存一致性协议
缓存一致性协议(Cache Coherence Protocol),最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的
MESI的核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取
在MESI协议中,每个缓存可能有有4个状态,它们分别是:
M(Modified)
:这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。E(Exclusive)
:这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中。S(Shared)
:这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中。I(Invalid)
:这行数据无效
MESI协议,可以保证缓存的一致性,但是无法保证实时性
处理器优化和指令重排
处理器优化
: 为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理指令重排
: 除了现在很多流行的处理器会对代码进行优化乱序处理,很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做指令重排- 联想记忆到:
- Spark中不存在依赖关系的task并发执行优化计算
- 不存在资源竞争的程序并发执行,比如某个程序抢占IO资源,那么可以先去执行其他不抢占IO资源的程序,省却等待时间
并发编程中的三个概念
原子性
原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行(联想记忆到数据库事务处理的原子性)
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
有序性
有序性即程序执行的顺序按照代码的先后顺序执行
其实,原子性问题,可见性问题和有序性问题。是人们抽象定义出来的。而这个抽象的底层问题就是前面提到的缓存一致性问题、处理器优化问题和指令重排问题等。
缓存一致性问题其实就是可见性问题,而处理器优化
可能会造成导致原子性问题的,指令重排
即会导致有序性问题
Java的内存模型
基本概念
Java的并发采用“共享内存”模型,线程之间通过读写内存的公共状态进行通讯。多个线程之间是不能通过直接传递数据交互的,它们之间交互只能通过共享变量实现。
JMM本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,规定所有变量都是存在主存中的,类似于
普通内存
,每个线程又包含自己的工作内存,类比高速缓存
。所以线程的操作都是以工作内存为主,它们只能访问自己的工作内存,且工作前后都要把值在同步回主内存
ps: 联想到有点类似于缓存+DB的系统架构,所有变量都是存在主存中的,类似于DB
, 每个线程又包含自己的工作内存,类比高速缓存
Java内存模型的实现
Java内存模型(JMM)规定所有变量都存储在主内存中,每个线程还有自己的工作内存:
- 线程的工作内存中保存了被该线程使用到的变量的拷贝(从主内存中拷贝过来),线程对变量的所有操作都必须在工作内存中执行,而不能直接访问主内存中的变量。
- 不同线程之间无法直接访问对方工作内存的变量,线程间变量值的传递都要通过主内存来完成。
Java线程之间的通信由内存模型JMM(Java Memory Model)控制:
- JMM决定一个线程对变量的写入何时对另一个线程可见。
- 线程之间共享变量存储在主内存中
- 每个线程有一个私有的本地内存,里面存储了读/写共享变量的副本。
- JMM通过控制每个线程的本地内存之间的交互,来为程序员提供内存可见性保证。
内存间交互操作:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把主内存的一个变量读取到工作内存中。
- load(载入):作用于工作内存,把read操作读取到工作内存的变量载入到工作内存的变量副本中
- use(使用):作用于工作内存的变量,把工作内存中的变量值传递给一个执行引擎。
- assign(赋值):作用于工作内存的变量。把执行引擎接收到的值赋值给工作内存的变量。
- store(存储):把工作内存的变量的值传递给主内存
- write(写入):把store操作的值入到主内存的变量中
注意:主内存和工作内存与JVM内存结构中的Java堆、栈、方法区等并不是同一个层次的内存划分
Java中并发的实现
原子性的实现
在Java中,为了保证原子性,提供了两个高级的字节码指令monitorenter和monitorexit,对应的关键字就是synchronized
Atomic类也可以实现原子性
基于CAS原理,参考为什么volatile不能保证原子性而Atomic可以
可见性的实现
通过volatile关键字标记内存屏障保证可见性。
通过synchronized关键字定义同步代码块或者同步方法保障可见性。
通过Lock接口保障可见性。
通过Atomic类型保障可见性
通过final关键字实现
被final修饰的字段一旦初始化完成(静态变量或者在构造函数中初始化),并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其它线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见final字段的值
有序性的实现
在Java中,可以使用synchronized和volatile来保证多线程之间操作的有序性
- volatile关键字会禁止指令重排
- synchronized关键字保证同一时刻只允许一条线程操作
happens-before原则
JMM具备一些先天的有序性,即不需要通过任何手段就可以保证的有序性,通常称为happens-before原则. 《JSR-133:Java Memory Model and Thread Specification》定义了如下happens-before规则:
程序顺序规则
: 即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行监视器锁规则
:对一个线程的解锁,happens-before于随后对这个线程的加锁volatile变量规则
: 对一个volatile域的写,happens-before于后续对这个volatile域的读传递性
:如果A happens-before B ,且 B happens-before C, 那么 A happens-before Cstart()规则
: 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见join()线程终止原则
: 线程的所有操作先于线程的终结,如果A执行ThreadB.join()并且成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。interrupt()线程中断原则
: 对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生finalize()对象终结原则
:一个对象的初始化完成先行发生于它的finalize()方法的开始