深入理解JVM_线程_并发_锁
2016-11-07 18:01:11 0 举报
AI智能生成
...
作者其他创作
大纲/内容
java内存模型与线程
硬件效率和一致性
硬件使用了很多手段, 目的是提高硬件执行效率
为了解决处理器和内存速度的矛盾, 加多级缓存
为了使处理器内部运算单元尽量充分使用, 处理器对输入代码进行乱序执行(Out-Of-Order Execution)
实际jvm虚拟机中也是用了类似的技术手段来提升性能
线程的工作内存 - 硬件多级缓存
编译器指令重排序(Instruction Reorder) - 乱序执行
使用优化问题所带来的问题
多级缓存引入了缓存一致性问题
使用缓存一致性协议来解决, 如: MSI/ MESI/ MOSI/ Synapse/ Firefly/ Dragon Protocol 等
java内存模型(Jave Memory Model, JMM);
可参考JSR-133(Java Memory Model and Thread Specification Revision)
可参考JSR-133(Java Memory Model and Thread Specification Revision)
主要目标是定义程序中各个变量的访问规则, 这里的变量指的是实例字段/ 静态字段/ 数组对象的元素 等, 可以被线程共享的变量
@jvm没有直接像c/c++一样直接使用物理硬件和操作系统的内存模型, 增加了跨平台能力;
@内存模型的定义必须足够严谨, 保证并发内存操作不会产生奇异;
@但是定义也必须足够宽松, 使得虚拟机的实现有足够的自由空间去利用硬件的各种特性(寄存器/ 高速缓存/ 指令集中某些特有指令)/;
@内存模型的定义必须足够严谨, 保证并发内存操作不会产生奇异;
@但是定义也必须足够宽松, 使得虚拟机的实现有足够的自由空间去利用硬件的各种特性(寄存器/ 高速缓存/ 指令集中某些特有指令)/;
主内存和工作内存
主内存
jvm堆中的一部分
工作内存
每个线程都有自己的工作内存, 工作内存中保存被该内存使用的变量的主内存副本
线程的读写操作必须对工作内存操作, 无法对主内存直接进行读写操作
线程无法直接读写其他线程的工作内存;
线程间的变量值的传递需要通过主内存来完成
线程间的变量值的传递需要通过主内存来完成
jvm虚拟机栈的一部分
对应于硬件, 很可能对应了高速缓存甚至寄存器
内存间的交互操作
主要讲了java内存模型的原子性
read/load需要顺序执行;
store/write需要顺序执行;
但是在指令间可以插入其他指令, 如: read a; read b; load a; use a; load b;
store/write需要顺序执行;
但是在指令间可以插入其他指令, 如: read a; read b; load a; use a; load b;
8中原子操作还需要遵从以下规则
不允许read和load / store和write单独出现;
即不允许变量从主存读取了工作内存不接受 或者 从工作内存回写了主存不接受的情况
即不允许变量从主存读取了工作内存不接受 或者 从工作内存回写了主存不接受的情况
不允许一个线程丢弃它最近的assign操作;
即变量在工作内存中改变了之后必须把该变化同步到主存
即变量在工作内存中改变了之后必须把该变化同步到主存
不允许一个线程无原因地(没有发生过assign操作)把数据从工作内存同步到主存;
???防止对主存的无意义刷新更新其他线程已经修改的值(大雾);
即对一个变量使用store之前必须经过assign操作
???防止对主存的无意义刷新更新其他线程已经修改的值(大雾);
即对一个变量使用store之前必须经过assign操作
变量只能在主存中"诞生", 不允许在工作内存中使用一个未经初始化(load/assign)的变量;
即对一个变量使用use/ store之前必须经过 load/assign操作
即对一个变量使用use/ store之前必须经过 load/assign操作
一个变量在同一时刻只允许一个线程对其进行lock操作, 但lock可以被同一线程重复执行多次(可重入), 执行多次lock后必须执行相同次数的unlock操作, 变量才能被解锁
如果对一个变量执行lock操作, 将会清空工作内存中此变量的值, 在执行引擎使用该变量前, 需要重新执行load或者assign操作初始化变量的数值
如果一个变量事先没有被lock操作锁定, 那么就不允许对它执行unlock操作;
不允许一个线程去unlock一个被其他线程锁住的变量
不允许一个线程去unlock一个被其他线程锁住的变量
对一个变量执行unlock操作之前, 必须将该变量同步回主存中(执行store write操作)
这些规则(加上对volatile的一些特殊规定)确定了那些内存访问操作在并发下是安全的
volatile的特殊规定
保证volatile对所有线程的可见性;
即 当某线程改变变量值后, 新值对其他线程可立即得知;
即 当某线程改变变量值后, 新值对其他线程可立即得知;
可见性不等同于一致性;
volatile在符合以下所有情况时可以保证并发安全:
1. 运算结果并不依赖变量的当前值;
2. 确保只有一个线程可以修改变量;
3.变量不需要与其他的状态变量同时参与不变约束;
1. 运算结果并不依赖变量的当前值;
2. 确保只有一个线程可以修改变量;
3.变量不需要与其他的状态变量同时参与不变约束;
volatile变量可以禁止指令冲排序优化
利用了内存屏障, 指令重排序无法越过内存屏障
对long/double的特殊规定
long/double的非原子性协议:
允许虚拟机将非volatile修饰的64位数据的读写操作划为两次32位的操作来进行
允许虚拟机将非volatile修饰的64位数据的读写操作划为两次32位的操作来进行
在目前商用虚拟机中64位数据的读写也是原子性的
原子性/可见性/有序性
原子性:
由java内存模型直接保证的原子性操作包括lock/unlock/read/load/use/assign/store/write
由java内存模型直接保证的原子性操作包括lock/unlock/read/load/use/assign/store/write
可见性:
当一个线程修改了共享变量的值, 其他线程能够立即得知这个修改;
java内存模型通过在变量修改后将新值同步回主存, 在变量读取前从主存刷新变量值这种依赖主存为传递媒介的方式来实现可见性的; 普通变量和volatile变量都是如此;
不过volatile变量的特殊规定保证了新值能够立即同步到主存, 以及每次使用前立即从主存刷新; 因此可以说volatile保证了多线程操作时变量的可见性, 但普通变量无法保证这一点;
除了volatile变量外, sync同步代码块和final也可以保证可见性;
当一个线程修改了共享变量的值, 其他线程能够立即得知这个修改;
java内存模型通过在变量修改后将新值同步回主存, 在变量读取前从主存刷新变量值这种依赖主存为传递媒介的方式来实现可见性的; 普通变量和volatile变量都是如此;
不过volatile变量的特殊规定保证了新值能够立即同步到主存, 以及每次使用前立即从主存刷新; 因此可以说volatile保证了多线程操作时变量的可见性, 但普通变量无法保证这一点;
除了volatile变量外, sync同步代码块和final也可以保证可见性;
有序性:
如果在本线程内观察, 所有操作都是有序的; 如果在一个线程中观察另一个线程, 所有操作都是无序的;
前半句指线程内表现为串行的语义(Whthin-Thread As-If-Serial Semantics);
后半句指"指令重排序"现象和"工作内存与主内存同步延迟"现象;
volatile和sync同步代码块可以保证有序性;
如果在本线程内观察, 所有操作都是有序的; 如果在一个线程中观察另一个线程, 所有操作都是无序的;
前半句指线程内表现为串行的语义(Whthin-Thread As-If-Serial Semantics);
后半句指"指令重排序"现象和"工作内存与主内存同步延迟"现象;
volatile和sync同步代码块可以保证有序性;
先发性原则
先行发生是java内存模型中定义的两项操作间的偏序关系;
如果操作A先行发生于操作B, 实际指 在发生B之前, A产生的影响会被B观察到;
如果操作A先行发生于操作B, 实际指 在发生B之前, A产生的影响会被B观察到;
满足以下任意一规则, 则不可指令重拍
程序次序规则
管理锁定规则
volatile变量规则
线程启动规则
线程终止规则
线程中断规则
对象终结规则
传递性
时间先后顺序和先行发生原则之间基本没有太大关系, 所以衡量并发安全问题时只按先行发生原则为准即可;
Java与线程
线程的实现
使用内核线程实现
内核线程定义:
由操作系统内核(kernel)直接支持;
线程切换/调度/映射到处理器由线程调度器(Thread Scheduler)完成;
支持多线程的kernel叫做多线程内核;
由操作系统内核(kernel)直接支持;
线程切换/调度/映射到处理器由线程调度器(Thread Scheduler)完成;
支持多线程的kernel叫做多线程内核;
轻量级进程定义:
程序一般不会直接使用内核线程, 而是使用内核线程的一种高级接口: 轻量级进程(Light Weight Process);
轻量级进程即指我们通常意义所讲的线程;
轻量级线程和内核线程是的关系是 1 - 1;
程序一般不会直接使用内核线程, 而是使用内核线程的一种高级接口: 轻量级进程(Light Weight Process);
轻量级进程即指我们通常意义所讲的线程;
轻量级线程和内核线程是的关系是 1 - 1;
内核线程/轻量级进程的优点:
成为独立的调度单元, 某内核线程/轻量级进程阻塞不会影响整个进程(不影响其他线程);
成为独立的调度单元, 某内核线程/轻量级进程阻塞不会影响整个进程(不影响其他线程);
内核线程/轻量级进程的缺点:
线程创建/析构/同步都需要系统调用, 代价较高, 需要在用户态和内核态间切换;
会消耗内核资源(如 内核线程的栈空间), 因此一个系统支持内核线程/轻量级进程数是有限的;
线程创建/析构/同步都需要系统调用, 代价较高, 需要在用户态和内核态间切换;
会消耗内核资源(如 内核线程的栈空间), 因此一个系统支持内核线程/轻量级进程数是有限的;
使用用户线程实现
用户线程定义:
广义上 - 非内核线程, 如果这样看轻量级线程也属于用户线程;
狭义上 - 完全建立在用户空间的线程库上, kernel无法感知用户线程存在, 线程的创建/销毁/同步/调度也全部在用户态中完成;
和进程的关系是N-1;
广义上 - 非内核线程, 如果这样看轻量级线程也属于用户线程;
狭义上 - 完全建立在用户空间的线程库上, kernel无法感知用户线程存在, 线程的创建/销毁/同步/调度也全部在用户态中完成;
和进程的关系是N-1;
用户线程优点:
如果实现得当, 速度快且低消耗;
支持规模更大的线程数量(部分高性能数据库的多线程使用用户线程实现);
如果实现得当, 速度快且低消耗;
支持规模更大的线程数量(部分高性能数据库的多线程使用用户线程实现);
用户线程缺点:
实现极复杂;
某个线程阻塞会对整个进程造成影响;
实现极复杂;
某个线程阻塞会对整个进程造成影响;
使用用户线程加轻量级进程实现
结合使用两者优点, 用户线程和轻量级线程数量比不定, 为N-M;
Java线程的实现
1.2前, 用户线程
从1.2开始, 使用操作系统原生线程模型来实现, 所以现在使用那种线程和操作系统有关;
对于sun jdk来说:
windows和linux使用1 - 1线程模型;
Solaris因为支持N - M模型, 所以可以使用专有虚拟机参数设置;
windows和linux使用1 - 1线程模型;
Solaris因为支持N - M模型, 所以可以使用专有虚拟机参数设置;
Java线程调度
协同式调度
调度问题由线程自身控制
可以避免并发问题
如果某线程阻塞, 可能会造成整个无法运行
抢占式调度
调度由系统(内核线程/轻量级进程)或者其他线程调度程序(用户线程)控制
线程可主动让出调度时间, 但无法主动获取调度时间; (java使用Thread.yield()让出)
抢占式调度可以设置线程执行优先级
java中有10个优先级
操作系统中线程优先级并非10个, 所以有不对称的对应关系;
多的如Solaris中有2^32个优先级, 但是windows中只有7个;
多的如Solaris中有2^32个优先级, 但是windows中只有7个;
不应该太多java中的优先级, 操作系统对系统调度有优化, 如某个线程特别"勤奋", 会被越过优先级分配额外的执行时间;
线程状态和转换
新建
创建未启动
运行
java中的运行中, 对应kernel中的Runing和Ready, 即运行中或者等待kernel分配执行时间
无限期等待
需要被显示唤醒才能继续执行
切换到此状态的方法
Object.wait()
Thread.join()
LockSupport.park()
限期等待
切换到此状态的方法
Thread.sleep()
Object.wait(timeOut)
Thread.join(timeOut)
LockSupport.parkNanos()
LockSupport.parkUntil()
阻塞
在等待获取排它锁, 其他线程放弃排它锁时才可能结束阻塞状态
结束
线程安全和锁优化
线程安全
定义:
当多个线程访问一个对象时, 如果不用考虑这些线程在运行时环境下的调度和交替执行, 也不需要进行额外的同步, 或者在调用方法行进行任何其他的协调操作, 调用这个对象的行为都可以获得正确的结果, 那这个对象是线程安全的.
当多个线程访问一个对象时, 如果不用考虑这些线程在运行时环境下的调度和交替执行, 也不需要进行额外的同步, 或者在调用方法行进行任何其他的协调操作, 调用这个对象的行为都可以获得正确的结果, 那这个对象是线程安全的.
java语言中的线程安全
需要考虑线程安全的前提是: 多个线程间存在共享数据
按照线程的安全程度分类
不可变
final修饰的基础类型
属性全部为final修饰的对象
绝对线程安全
完全满足<上面对线程安全的定义>
java中大部分声明线程安全的对象并非"绝对"线程安全;
如: Vector两个线程分别进行增和删的操作, 会抛出ArrayIndexOutOfBoundsException
如: Vector两个线程分别进行增和删的操作, 会抛出ArrayIndexOutOfBoundsException
相对线程安全
保证对该对象单独的操作是线程安全的, 但是对特定顺序的连续调用, 就可能需要在调用时使用额外的同步手段来保证正确性
如: Vector/HashTable/Collections.synchronizedCollection()包装的集合 等
线程兼容
对象并非线程安全, 但可以通过在调用端正确地使用同步手段来保证安全
如: ArrayList/HashTable 等
线程对立
无论调用端是否采取同步措施, 都无法保证多鲜橙环境下的安全
在java中, 这种代码很少出现, 而且通常有害, 应避免
如: Thread的suspend()和resume();
如果两线程同时持有一个线程对象, 一个尝试去中断线程, 另一个尝试恢复线程, 即便使用同步手段, 目标线程都是存在死锁的风险;
如果两线程同时持有一个线程对象, 一个尝试去中断线程, 另一个尝试恢复线程, 即便使用同步手段, 目标线程都是存在死锁的风险;
线程安全的实现方法
互斥(阻塞)同步
属于悲观锁
synchronized关键字
需要reference类型参数来指明锁定/解锁的对象
编译后会生成monitorenter和monitorexit两个字节码指令, 来运行加锁/解锁操作;
monitorenter: 首先尝试获取锁, 如果对象未被锁定, 或者当前线程已经拥有该对象的锁(可重入特点), 把锁计数器+1;
monitorexit: 锁计数器-1, 当计数器为0时, 锁被释放;
monitorenter: 首先尝试获取锁, 如果对象未被锁定, 或者当前线程已经拥有该对象的锁(可重入特点), 把锁计数器+1;
monitorexit: 锁计数器-1, 当计数器为0时, 锁被释放;
如果没有明确指定哪个对象来加锁
static方法: 类的Class对象
普通方法: 当前实例, 即this
sync映射到了kernel中的轻量级进程, 加锁/解锁操作实际都需要在 内核态和用户态来回切换, 需要耗费较多的处理器时间, 因此也叫做重量级锁;
虚拟机本身对重量级锁会进行一些优化, 比如: 在通知操作系统前加入一段自旋等待过程, 避免频繁切入内核态;
虚拟机本身对重量级锁会进行一些优化, 比如: 在通知操作系统前加入一段自旋等待过程, 避免频繁切入内核态;
ReentrantLock
属于轻量级锁, 在api层面进行互斥, 相比sycn的优点:
1. 等待可中断;
2. 公平锁: 会根据申请锁的时间顺序来依次得到锁, 默认构造是非公平的;
3. 绑定多条件: 可以绑定多个Condition对象;
4. 1.6之前效率比sync高, 从1.6之后效率相差不大;
1. 等待可中断;
2. 公平锁: 会根据申请锁的时间顺序来依次得到锁, 默认构造是非公平的;
3. 绑定多条件: 可以绑定多个Condition对象;
4. 1.6之前效率比sync高, 从1.6之后效率相差不大;
非阻塞同步
属于乐观锁:
基于冲突检测的乐观并发策略, 即:
先进行操作, 如果没有其他线程争夺资源, 修改成功;
如果有资源争夺, 产生了冲突, 进行补偿措施(最常见的是不断重试, 直到成功为止);
基于冲突检测的乐观并发策略, 即:
先进行操作, 如果没有其他线程争夺资源, 修改成功;
如果有资源争夺, 产生了冲突, 进行补偿措施(最常见的是不断重试, 直到成功为止);
这种并非策略不需要将线程挂起, 因此也被称为 非阻塞同步;
使用乐观锁的必须条件: 操作和冲突检测 是原子性的(IA64和x86指令集通过CAS指令来实现);
java中原子性操作都是通过sun.misc.Unsafe的compareAndSwap来实现的
CAS指令会引入ABA问题, 比如: X线程修改前探测变量值为A, 后其他线程将变量先改为B, 再改为A, 这样X进行修改时无法探测到变量实际已经被修改过了;
但是并不影响并发的正确性;
但是并不影响并发的正确性;
无同步方案
可重入代码:
不依赖存储在堆上的数据和公共的系统资源;
用到的状态都通过参数传入;
不调用非可重入方法;
不依赖存储在堆上的数据和公共的系统资源;
用到的状态都通过参数传入;
不调用非可重入方法;
线程本地存储???
锁优化
自旋锁/自适应自旋
作为阻塞锁的补充, 避免频繁在用户态和内核态之间切换;
在进入系统阻塞前, 进行一段时间的自旋, 自旋一般为次数;
自旋次数也可以通过前一次在同一个锁上的自旋时间和锁的拥有者的状态来决定, 这种自旋锁叫自适应自旋锁;
自旋次数也可以通过前一次在同一个锁上的自旋时间和锁的拥有者的状态来决定, 这种自旋锁叫自适应自旋锁;
锁消除
即时编译器(JIT)在运行时会对不可能发生共享资源争夺的锁进行消除
JIT对资源是否可能产生争夺的判断 是 通过逃逸分析技术来进行的:
如果堆上的所有数据都不会逃逸出去而被其他线程访问到, 那么就可以把他们当做栈上的数据对待, 认为是线程私有的;
程序员一般可很清楚的明白数据是否可以逃逸, 但是在jdk的api中默认存在很多锁, 比如字符串"+"拼接在编译后变成使用StringBuffer的append(), 而StringBuffer是带锁的, 锁消除主要针对这些锁进行消除;
如果堆上的所有数据都不会逃逸出去而被其他线程访问到, 那么就可以把他们当做栈上的数据对待, 认为是线程私有的;
程序员一般可很清楚的明白数据是否可以逃逸, 但是在jdk的api中默认存在很多锁, 比如字符串"+"拼接在编译后变成使用StringBuffer的append(), 而StringBuffer是带锁的, 锁消除主要针对这些锁进行消除;
锁消除
前提:
在编写代码时的原则是尽量减小同步代码块的范围: 使锁占用时间尽量小, 其他阻塞线程尽快拿到锁, 以增加效率;
在编写代码时的原则是尽量减小同步代码块的范围: 使锁占用时间尽量小, 其他阻塞线程尽快拿到锁, 以增加效率;
但如果一系列的连续操作都对同一对象反复加锁/解锁, 甚至加锁操作出现在循环体内, 即使没有线程竞争, 频繁加锁/解锁也会造成不必要的性能损耗;
jvm在探测到这种一连串零碎操作使用同一个对象作锁的情况时, 会将锁进行粗化到整个操作序列外部;
如: StringBuffer的连续append()操作;
jvm在探测到这种一连串零碎操作使用同一个对象作锁的情况时, 会将锁进行粗化到整个操作序列外部;
如: StringBuffer的连续append()操作;
轻量级锁
在没有锁竞争时, 将通知kernel进入互斥状态的操作替换为了对于对象头锁标示位的CAS操作;
如果存在锁竞争, 轻量级锁会膨胀为重量锁;
在这种情况(存在锁竞争)下, 轻量级锁因为增加了额外的CAS操作, 会比重量级锁更慢;
在这种情况(存在锁竞争)下, 轻量级锁因为增加了额外的CAS操作, 会比重量级锁更慢;
偏向锁
将对象头标示位设置为偏向锁, 在没有锁竞争时, 再次执行时将同步操作消除掉, 连CAS也不做了
java内存模型定义了8种原子性的操作
lock
作用于主内存;
把一个对象标识为一条线程独占的状态
把一个对象标识为一条线程独占的状态
unlock
作用于主内存;
把处于锁定状态的对象释放
把处于锁定状态的对象释放
read
作用于主内存;
将主内存中的变量传输到工作内存
将主内存中的变量传输到工作内存
load
作用于工作内存;
把read操作从主内存得到的变量值放入工作内存的变量副本中
把read操作从主内存得到的变量值放入工作内存的变量副本中
use
作用于工作内存;
把工作内存中的一个变量值传递给执行引擎;
每当虚拟机遇到一个需要使用变量的值的字节码指令时执行该操作
把工作内存中的一个变量值传递给执行引擎;
每当虚拟机遇到一个需要使用变量的值的字节码指令时执行该操作
assign
作用于工作内存;
把从执行引擎接收到的值赋给工作内存的变量;
每当虚拟机遇到一个给变量赋值的字节码指令时执行该操作
把从执行引擎接收到的值赋给工作内存的变量;
每当虚拟机遇到一个给变量赋值的字节码指令时执行该操作
store
作用于工作内存;
把工作内存中的一个变量传出到主内存
把工作内存中的一个变量传出到主内存
write
作用于主内存;
把store操作从工作内存中得到的变量值放入主内存的变量中
把store操作从工作内存中得到的变量值放入主内存的变量中

收藏
0 条评论
下一页