作者 | 井方哥
地址 | https://zhuanlan.zhihu.com/p/31582064
声明 | 本文是 井方哥 原创,已获授权发布,未经原作者允许请勿转载
前言
我们通过前面的学习,已经知道了各个内存区域的分配等等。我们首先应该知道:
服务性能重要指标:每秒事务处理数
线程并发问题:阻塞、死锁
本篇还解答了如下问题:
Java在进行执行运算时有哪些优化策略?
Java内存模型是怎样的?
工作内存和主内存时怎么交互操作的?
Java的线程是如何实现的?
Java的线程的状态切换是怎样的?
效率
1、高速缓存
由于在进行运算时如果直接操作内存效率会比较低,用CPU又比较消耗资源,所以在中间加了一层高速缓存区,作为内存和处理器之间的缓存。进行运算时可以先在高速缓存区进行读写操作,运算结束后将结果同步到内存中。
优势:解决了资源和效率的矛盾
劣势:增加了操作系统的复杂度,随之带来了“缓存一致性”的问题
2、乱序执行优化
计算机将乱序执行的结果进行重组,并保证和顺序执行的结果相同
有效的利用了处理器CPU
JVM对应有类似的指令重排序优化
Java 内存模型
内存模型:在特定的操作协议下,对特定的内存或者高速缓存进行读写访问的抽象过程
-
Java内存模型目标:定义各个变量的访问规则
变量包括:实例字段、静态字段、构成数组对象元素
变量不包括:局部变量与方法参数(线程私有,不会被共享)
主内存:所有变量的值都存在主内存当中(主要对应“Java堆”中对象实例数据部分)
工作内存:工作线程直接可以操作的内存区域(主要对应“栈”中部分区域)
线程间传递数据:必须要经过主内存
工作内存会优先于高速缓存和存储寄存器,因为程序运行时主要是在工作内存
内存间交互操作
虚拟机提供了8钟以下8种操作,都具有原子性:
lock 作用于主内存
unlock 作用于主内存
read 作用于主内存
write 作用于主内存
load 作用于工作内存
use 作用于工作内存
assign 作用工作主内存
store 作用工作主内存
可以这么去理解(假设在程序中我们需要将变量a赋值为2): 首先,我们进入到主内存,lock住a对象,接着从主内存中read到a对象,紧接着我们把a对象载入到工作内存(read和load必须顺序执行),然后再工作内存中use了a对象的,通过a对象的指令集取到a的值,然后给a对象assign一个值,紧接着store到工作内存中(assign和store必须顺序执行),最后把a对象write到主内存当中。最后最后就unlocka对象,方便其他的线程访问,
操作原则:
一个新的变量必须从主内存中诞生,也就是说工作内存中不能使用一个未初始化(load或者assign)的变量。
一个变量在同一时刻只允许被一个线程lock,被同一线程多次lock之后,必须要有相同次数的unlock,变量才会被解锁。
如果该对一个变量执行了lock,则会清空这个变量在工作线程中的值
对一个变量执行unlock操作之前,必须要把变量同步到主内存中(即执行完store和write之后)
Volatile 特点
可见性:一旦被修改,所有线程都可见(普通变量需要通过主内存才可知)
-
安全场景:
运算结果并不依赖当前的值
变量不需要与其他变量参与不变因素
-
禁止重排序优化
指令重排序:CPU采用了允许将多条指令不按规定的顺序分开发送给各相应的电路单元处理;
内存屏障:插入内存屏障指令,重排序时不能把后面的指令重排序到内存屏障之前的位置(实际上是保证了use或者assign操作按照代码顺序执行);
尤其在代码中进行标志位的设置时,可能因为“机器级”的优化,执行顺序发生了改变,加上volatile可以避免重排导致的问题;
性能:读操作与普通的差不多,写操作由于要插入许多内存屏障指令来保证不进行重排序,所以会慢一些。总开销比锁低。
-
操作原则:
read、load、use,三个动作必须连续出现;
每次使用变量之前,必须从主内存中刷新最新的值;
原子性、可见性、有序性
1、原子性
原子性操作:read、load、use、assign、store、write
synchronized:lock对应于monitorenter指令,unlock对应于monitorexit指令。这两个指令对应于synchronize关键字,操作对外,满足外部同步需求。
2、可见性
线程在工作内存操作后将数据同步到主内存,别的线程读取前先从主内存刷新变量值;
volatile:保证修改的值能立即同步到主内存,每次使用前需要从主内存刷新。
synchronized:对于一个变量执行unlock之前,必须先把变量同步回主内存(执行store、write);
final:所有线程可见,如果没有“逃逸”。
3、有序性
线程内观察,所有的操作有有序的,表现为串行
一个线程去观察另一个线程,所有的操作都是无序的,因为存在“指令重排序”现象和“工作内存同步到主内存延迟”现象
volatile:禁止“指令重排序”
synchronized:一个变量同一时刻只允许一条线程对其进行lock操作
4、先行发生原则
定义:如果操作A先行发生于操作B,则操作B能够观察到操作A的影响;
判断依据:先行发生原则是判断线程是否安全的依据
-
先天原则:
程序次序原则:一个线程内的操作遵循程序代码的控制流顺序
管程锁定原则:一个unlock操作先行发生于对于同一个锁的lock之前
volatile原则:写操作先行发生于后面对这个变量的读操作
线程启动原则:Thread对象start()先行发生该线程的任何动作
线程终止原则:所有操作都先行发生于该线程的终止检测,join()方法结束、isAlive()方法检测是否已经终止
线程中断原则:interrupt()方法先行发生于对中断线程的检测到中断事件发生
对象终结原则:对象初始化完成先行发生于finalize()
-
传递性:操作A先于操作B,操作B先于操作C,则得出操作A先于操作C
线程实现方式
1、内核线程实现
轻量级进程:通俗所说的线程;
-
性能:
用户态和内核态来回切换,消耗性能;
每个LWP都占用一个KLT,消耗资源;
2、用户线程实现
广义:LWP也属于用户线程;
-
狭义:
用户线程完全建立在用户空间之上,系统内核不能感知线程存在;
进程和线程1:N;
优点:快且低消耗
缺点:线程创建、切换、调度复杂,实现困难。
3、混合实现
许多UNIX系列操作系统采用
4、Java线程实现
操作系统支持的线程模型不同,JVM线程的映射也不同
-
调度方式
协同式调度:一个线程干完活通知系统切换线程
抢占式调度(Java使用):通过优先级设置控制
-
状态转换
新建(NEW):尚未启动
运行(Runable):可能正在执行,也可能正在等待CPU分配时间
无限等待(Wating):无限等待状态
限期等待(Timed Wating):过一定时间后自动被系统唤醒,如TheadSleep(1000)
阻塞(Blocked):等待一个排他锁
结束(Terminated):线程结束
小结
通过本篇的学习,终于加深了对内存模型、线程安全的理解。内存主要分为两大类,一个是工作内存,一个主内存。一个线程对应一个工作内存,在工作内存内完成运算后,再把数据同步到主内存。由此可以得出,线程间进行通信就必须要经过主内存。内存间的交互通过lock>read>load>use>assign>store>write>unlock这八大操作完成。其中比较特殊的是volatile关键字,首先他插入了内存屏障指令,阻止了JVM的重排序优化功能。同时要求在使用变量之前,必须重主内存中刷新最新的值。最后学习了线程实现方式,了解了Java线程的New\Blocked\Running\Waiting\TimedWaiting\Terminated六种状态的切换关系。
说明: 本系列多处摘抄《深入理解Java虚拟机》中内容,主要精简了本书的要点,并叙述自己对本书的理解。本人才疏学浅,文章中有不对的地方,还望批评指教。
往期