Java内存模型与线程知识点总结
首先讨论一下物理机对于并发的处理方案
运算任务不可能只靠处理器简单的计算就能完成,必须还要增加与内存的交互操作(如读取数据,存储数据), 由于计算机的存储设备与处理器的运算速度之间有着几个数量级的差距,所以现代计算机系统选择加入高速缓存(Cache)来进行内存与处理器之间的缓存来提高效率
由于高速缓存是与处理器绑定的,而主内存是所有处理共享的,所以会产生缓存一致性的问题,意思是当多个处理器都要处理同一块主内存区域时,可能会导致处理器的缓存中数据与主内存中的数据不一致,如何判断和选择正确的数据呢? 为了解决这个问题就要用到缓存一致性的协议
几个概念:
乱序执行优化 与 指令重排序:将不存在数据依赖关系的指令按照非线性顺序执行,比如泡茶这个操作会包括 洗杯子,烧开水,倒入茶叶,泡茶几个步骤,其中烧开水和洗杯子可以是同时进行的,那么就可以将整个流程划分为两个分隔的流程, 洗杯子,倒茶叶,泡茶交给一个处理器,烧开水交给另一个处理器来完成。 指令重排序就是Java编译器中存在的类似处理器进行的乱序重拍的优化。
数据依赖: 如果两个操作需要访问同一个变量并且两个操作之中有一个是写操作,那么就可成为两个操作之间存在数据依赖关系。
as-if-serial语义: 无论是处理器还是编译器,不管怎么重排都要保证(单线程)程序的执行结果不能被改变,这就是as-if-serial语义.比如烧水煮茶的最终结果永远是煮茶,而不能变成烧水,顺序可以不同,但是结果要相同。
Java内存模型是为了屏蔽掉各种硬件和操作系统的内存访问差异,从而实现Java程序在各种平台之下都能够达到一致的并发效果。
设计的主要对象是各个变量(包括实例字段,静态字段和构成数组对象的元素等共有元素)的访问规则(在工作内存和主内存之间的相互访问规则)。
Java内存模型与Java内存区域划分的关系: 这是两个不同层次上的划分, 只能勉强对应。
Java内存区域划分是虚拟机在执行Java程序的过程之中把它所管理的内存按照不同的用途划分为不同的数据区域: 主要分为堆(线程私有),方法区(线程私有),Native方法栈, 虚拟机栈,程序计数器。
- 内存间交互基本操作:
- 主内存相关:
- 锁定相关: lock(作用于主内存的变量) 将变量标记为线程独占状态, unlock(作用于主内存的变量) 解除变量线程独占的状态,
- 与工作内存交互相关:read(作用于主内存的变量) 将变量从主内存传到工作内存之中, write(与load反向)
- 工作内存相关:
- 与主内存交互相关: load(将read到的变量放入工作内存中的变量副本), store(与read相反操作)
- 工作内存变量相关: use(将工作内存的值给执行引擎), assign(将执行引擎的值给工作内存)
volatile关键字
volatile可以被看做为Java虚拟机提供的最轻量级的同步机制,只能保证特定场景下的线程安全:
1.运算结果不依赖变量的当前值,能够保证只有单一的线程修改变量的值
2.变量不需要与其他的状态变量共同参与不变约束
volatitle boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
两个主要特性:
- 保证被修饰的变量对所有线程的可见性(可见性含义是当一个线程修改了volatile变量的值,新值对于其它线程来说可以立即得知, 通过每次使用之前都需要刷新该变量来保证这一点 对比普通变量: 变量值在线程之间传递是通过主内存来完成的, A线程修改一个普通变量的值,然后向主内存进行回写,另外一个线程B在A回写完成之后再从主内存进行读取时,B线程才能够读取新变量的值)但不能够保证线程安全
- 禁止指令重排序优化: 与 普通变量仅能够保证在方法的执行过程之中所有依赖赋值记过的地方都能得到正确的结果, 而不能够保证赋值操作的顺序与程序代码中的执行顺序一致。
原子性: 直接保证原子性的变量操作为read, load, assign, use, store, write六个
可见性: 当一个线程修改了共享变量的值, 其他线程能够立即得知这个修改。 可以通过volatitle, synchronized, final关键字保证。
有序性: 本线程内观察,可以看出全部有序。 而从另一个线程内观察,可以看出全部无序. 通过volatile 和 synchronized关键字来保证线程之间的有序。 synchronized保证在同一个时刻只允许一个线程对其进行lock操作。
Java中的线程实现,三种方式:
- 使用内核线程
- 使用用户线程
- 混合使用
Java线程调度
分为两种方式: 协同式线程调度(Cooperative Threads-Scheduling) 和抢占式线程调度(Preemptive Threads-Scheduling)
协同式: 线程的执行时间由线程本身来控制, 当所有工作执行完了之后,通知系统切换到另一个线程上, 好处实现简单,不存在线程同步的问题. 问题是: 线程执行时间不可控, 会导致程序一直被某一个线程的执行阻塞, 可能会与系统崩溃的危险。
抢占式(Java采用): 每个线程的执行时间由系统来分配, 线程的切换不由线程本身决定。 可以通过优先级进行一些优化, 10个级别, 但是优先级与不同系统的底层线程优先级可能会存在冲突
Java线程转换:
新建(New): 创建之后还未启动的线程
运行(Runnable): 分为Running(正在执行) 和 Ready(正在等待CPU为它分配执行时间) 两种状态
等待(Waiting) 可以分为无限期等待 Waiting(不会被分配CPU执行时间, 必须要等待其它线程显式的唤醒) 和限期等待(Timed Waiting(不会被分配CPU执行时间,但不需要等待被其它线程显式唤醒,会在一定时间之后由系统自动进行唤醒) 对应方法Thread.sleep() 设置了Timeout参数的Object.wait() 设置了Timeout参数的Thread.join()
阻塞(Blocked): 等待着获取一个排它锁。
结束(Terminated): 终止线程的执行, 终止之前要完成所有线程任务的执行 (happen before原则)