第12章 java内存模型与线程
第一节:java内存模型
- jvm规范定义一种java内存模型(JMM)来屏蔽掉各种硬件和操作系统的内存访问差异。以实现让java程序在不同平台下都能达到一致的内存访问效果。
一、主内存和工作内存
概述:
- java内存模型的主要目标,是定义程序中各个变量的访问规则
- 即在jvm中将变量存储到内存和从内存中读取变量的底层细节
- 这里的变量不包括虚拟机栈和方法参数。
1、主内存:
- 所有线程的公共内存
- 所有变量都必须保存在主内存中。
2、工作内存
- 线程独有,可与处理器高速缓存类比
- 工作内存保存了该线程使用到的从主内存中变量的副本拷贝
- 线程对变量的所有操作必须在自己的工作内存中,不能在主内存中
- 不同线程中,不能直接访问对方的工作内存,变量值的传递只能通过主内存中转
- 所有线程间的通信,都要通过主内存中转完成。
- **Node:**内存模型和以前的运行时数据区(栈,堆,方法区等)并不是同一个层次的内存划分,没有必然关系
二、内存交互操作
- 概述:主内存和工作内存之间的交互协议,定义了两者之间数据同步的细节处理(8种操作都具有原子性)。
1、交互协议的八种操作
- lock(锁定):作用于主内存的变量,标识该变量被一个线程独占状态
- unlick(解锁):作用于主内存的变量,把处于锁定的主内存的变量释放出来,允许其他线程锁定。
- read(读取):作用于主内存的变量,把一个主内存的变量的值传输到线程的工作内存,以便后面的load使用(read的下一个操作必须是load,否则该操作不能是read,这两个操作成对出现)。
- load(载入):作用于工作内存的变量,把read从主内存中得到的变量值放入到工作内存
- use(使用):作用于工作内存的变量,它把工作内存的一个变量传递给执行引擎。当jvm遇到需要使用需要使用变量值的时候,就会执行这个操作(比如:操作数栈的内容弹栈进行数据计算)
- assign(赋值):作用域工作内存的变量,把从执行引擎获取到的值赋值给工作内存变量。每当jvm遇到一个给变量赋值的字节码指令时执行这个操作。(比如:计算完毕后,结果压入操作数栈)
- store(存储):作用于工作内存的变量,把工作内存的变量值传递到主内存中。(必然和后续的write成对出现,也就store的后一个操作必然是write)
- write(写入):作用于主内存的变量,把从store获取到的变量值,放入到主内存中。
三、volatile型变量的特殊规则(针对多核CPU)
概述:
volatile是jvm提供的最轻量级的同步机制,但是并不是线程安全的
volatile修饰过得变量,对所有线程可见。即一条线程修改该变量,其他线程理解得知
- 普通变量,线程A修改后回写入主内存,线程B主动去主内存读取才能得知该变量的新值。
volatile,线程A修改后回写入主内存,其余线程每次使用前,都必须主动去主内存读取新值(不加锁,一旦读取到操作数栈进行运算,并发情况下就不安全)
1、保证可见性:
- 必须符合以下规则:
- 运算结果并不依赖变量的当前值,或者能保证只有单一线程修改变量的值
- 变量不需要与其他状态变量共同参与不变约束。
2、禁止重排序
- A线程内部study()最后一行
flag=false。线程B根据flag判断是否执行。
- 因为重排序的原因,可能造成study()其他代码未执行完,flag被置为true,线程B开始执行
- 原理:
volatile修饰的变量,会增加一个内存屏障。当前修改变量的cpu把catche写入主内存的时候,会造成其他观测cpu的catche失效
3、选择volatile的意义:
- 同步机制性能要优于锁,由于锁的消除和优化,很难量化的说明synchronized快多少(大多数情况是比锁开销低)。
volatile变量读操作和普通变量差不多,写操作会慢一些(有内存屏障)。
4、volatile对8种交互协议影响(线程T,两个被volatile修饰的变量V、W)
- 线程T对变量V的执行动作必须是
read、load、use这三者依次顺序。(这条规则保证每次使用V前,都必须先刷新主内存中变量最新值,保证能获取到其他线程对变量V的修改的最新的值)。
- 线程T对变量V的执行动作必须是
assign、store、write这三者依次顺序(这条规则要求,在工作内存中,每次修改完变量必须同步回主内存,保证其他线程可以看到V的变量的修改)。
- 修改V和W两个变量,命令有交叉的情况,那么顺序执行(这条规则规定,被
volatile修饰的变量不会发生指令重排序,代码的执行顺序和程序的执行顺序相同)
四、原子性、可见性和有序性
概述:jmm就是围绕并发过程中的原子性、可见性和有序性三个特征展开的
1、原子性:
- 内存交互的6种(
read、load、use、assign、store以及write)操作(long、double的非原子性协定)保证了基本数据类型操作的原子性。
- 如果需要更大范围的原子性保证,jmm还提供了
lock、unlock反应到java代码就是synchronized关键字
2、可见性:
- 当一个线程修改了变量值,其他线程立即得知这个修改
volatile关键字保证了修改变量值立即同步回主内存,每次使用前读取主内存中变量的值。其他普通变量不保证可见性。
- final关键字可见性。被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this传递出去(this没有发生引用逃逸),那在其他线程中就能看到final字段的值。
- synchronized加锁,也能保证可见性
3、有序性:
- 如果在单线程内部观察操作全部有序,如果一个线程中观察另一个线程那么操作就全部是无序的
五、先行发生原则
概述:
规则:
- 程序次序规则:本线程内按照程序代码顺序执行。
- 管程锁定规则:lock之前,变量必须要是unlock(同一个锁)
volatile变量规则:对一个volatile修饰的变量写操作先发生于对这个变量的读操作
- 线程启动规则:Thread的start()现行发生于线程的其他动作
- 线程终止规则:线程所有的操作,都要在终止之前完成
- 线程中断规则:对线程的
intercept()调用,先行发生于被中断线程的代码检测到中断事件发生。(先中断)
- 对象终结规则:对象的初始化完成,先行发生于finalize()方法开始。
- 传递性:A比B先发生,B比C先发生,那么A就比C先发生。
第二节:java与线程
一、线程的实现
概述:
- 线程可以把一个进程的调度和资源分配分开。
- 线程既可以共享进程内存(内存地址、文件IO等),又可以独立调度(CPU的最小调度单元)
- 线程的实现有三种方式:内核线程实现、用户线程实现、用户线程和轻量级进程混合实现
1、内核线程实现
- 由操作系统内核实现的线程,线程切换通过内核调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。
- 每个内核线程可以看做是内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核叫多线程内核。
- 程序不会直接使用内核线程,而是通过使用内核的线程接口-轻量级进程。
- 这个轻量级进程也就是我们说的线程
- 每个轻量级进程都由一个内核线程支持,因此内核要支持内核线程才能支持轻量级进程,他们一对一关系
- 劣势:
- 基于内核线程实现,因此线程操作(创建、析构、同步)都需要系统调度。系统调度要在用户态和内核态来回切换,调度成本高
- 每个轻量级进程都要有一个内核线程支持,因此轻量级进程需要消耗一定的内核资源(内核线程栈空间)。系统支持的轻量级进程是有限的
2、用户线程实现
- 用户线程完全建立在用户空间的线程库上。内核空间不能感知线程存在的实现。
- 用户线程的建立、同步、调度和销毁完全在用户态下完成(快速低消耗),无需内核线程
- 无需在用户态和内核态下切换,性能更高。
- 支持更大规模的线程数量,部分高性能数据库采用一对多的模型设计
- 劣势:
- 线程的操作(创建、切换、调度)都要用户自己实现,且异常复杂
- 由于操作系统把处理器资源分配到进程,那
阻塞如何处理、多处理器如何将线程映射到其他处理器上等问题会变的异常困难,甚至无法完成
3、用户线程+轻量级进程
- 用户线程和轻量级进程,共同存在,支持多对多
- 用户线程任然全部建在用户空间
- 轻量级进程作为内核线程和用户线程之间的桥梁
4、java线程
- 根据不同操作系统,实现不同
- 在windows和Linux下,使用的轻量级进程,即轻量级进程和内核线程是一对一的关系。
二、java线程调度
概述:系统为线程分配处理器使用权的过程
1、协同式调度
- 线程的执行时间由线程本身控制
- 线程执行完毕后,通知处理器切换线程
- 实现简单,不存在同步问题
- 劣势:
- 执行时间不可控,如果线程不释放内核,就会造成程序阻塞
2、抢占式调度
- 线程执行时间由内核随机调度,线程切换不由线程本身决定
- java中的
Thread.yield(),可以让出线程执行,但是线程无法主动获取时间
- java使用的就是抢占式调度
- 不会因为一个线程阻塞,造成软件阻塞
- 可以通过设置线程优先级,建议系统给线程分配时间
- java线程映射到内线程实现,因此这种建议不一定生效
- 系统可能会修改优先级
三、线程状态切换
1、新建:
2、运行:
- Runable包含了内核线程的状态的running和ready,此状态的线程可能在执行也可能在等待CPU为其分配时间
3、无线等待(Waiting):
- 处于此种状态的线程不会被CPU分配时间,必须由其他线程显示的唤醒。以下方法会让线程进入无线等待:
- 没有设置Timeout参数的
Object.wait()
- 没有设置Timeout参数的
Thread.join()
LockSupport.park()
4、限期等待:
- 处于这种状态的线程CPU不会分配时间,但是也无需其他线程唤醒。一定时间后由系统自动唤醒。
Thread.sleep()
- 设置了Timeout参数的
Object.wait()
- 设置了Timeout参数的
Thread.join()
LockSupport.parkNanos()
LockSupport.parkUntil()
5、阻塞(Blocked):
- 线程被阻塞
- 线程阻塞:线程等待获取一个排他锁,这个事件在另外一个线程放弃这个锁的时候发生
- 等待状态:等待一段时间,或者被其他线程唤醒。(线程进入安全区)
6、结束: