1、synchronized
1.1、synchronized的实现
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性
1.2、synchronized实现同步的基础:
1、对
于普通同步方法,
锁
是当前
实
例
对
象。
2、对
于静
态
同步方法,
锁
是当前
类
的
Class
对
象。
3、对
于同步方法
块
,
锁
是
Synchonized
括号里配置的
对
象。
1.3、synchronized的底层是怎么实现的?
三个特性:原子性、可见性、有序性。
1)、 同步代码块底层是通过一个monitor的对象来完成monitor是由ObjectMonitor实现的, ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成 ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的 monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释 放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放 monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁MonitorEnter指令:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的 锁;MonitorExit指令:插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit;
2)、
同步方法:方法级的同步是隐式,即无需通过字节码指令来控制的,通过 ACC_synchronized 方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用 的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了 monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同 步方法所持有的monitor将在异常抛到同步方法之外时自动释放
2、Java对象头
synchronized
用的
锁
是存在
Java
对
象
头
里的。如果
对
象是数
组类
型,
则
虚
拟
机用
3
个字
宽 (Word
)存
储对
象
头
,如果
对
象是非数
组类
型,
则
用
2
字
宽
存
储对
象
头
。在
32
位虚
拟
机中,
1
字
宽 等于4
字
节
,即
32bit
,如表
长度 | 内容 | 说明 |
32/64bit | Mark Word | 存储对象的hashcode或锁信息等 |
32/64bit | Class Metadata Address | 存储到对象类型的数据指针 |
32/32bit | Array Length | 数组的长度(如果当前对象为数组) |
1、Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。
2、在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化
3、在64位虚拟机下,Mark Word是64bit大小的,
3、锁的升级与对比
为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
1.偏向锁
大多数情况下,
锁
不
仅
不存在多
线
程
竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
1、偏向锁的撤销
偏向
锁
使用了一种等到
竞
争出
现
才
释
放
锁
的机制,所以当其他
线
程
尝试竞
争偏向
锁时
, 持有偏向锁
的
线
程才会
释
放
锁
。偏向
锁
的撤
销
,需要等待全局安全点(在
这
个
时间
点上没有正 在执
行的字
节码
)。它会首先
暂
停
拥
有偏向
锁
的
线
程,然后
检查
持有偏向
锁
的
线
程是否活着, 如果线
程不
处
于活
动
状
态
,
则
将
对
象
头设
置成无
锁
状
态
;如果
线
程仍然活着,
拥
有偏向
锁
的
栈会被执
行,遍
历
偏向
对
象的
锁记录
,
栈
中的
锁记录
和
对
象
头
的
Mark Word
要么重新偏向于其他线程,要么恢复到无
锁
或者
标记对
象不适合作
为
偏向
锁
,最后
唤
醒
暂
停的
线
程。
2、
关
闭
偏向
锁
偏向
锁
在
Java 7
里是默
认
启用的,但是它在
应
用程序启
动
几秒
钟
之后才激活,如 有必要可以使用JVM参数来关
闭
延
迟
:
-XX:BiasedLockingStartupDelay=0
。如果你确定
应
用程 序里所有的锁
通常情况下
处
于
竞
争状
态
,可以通
过
JVM
参数关
闭
偏向
锁
:
-XX:- UseBiasedLocking=false,那么程序默
认
会
进
入
轻
量
级锁
状
态。
2.轻量级锁
(
1
)
轻
量
级锁
加
锁
线
程在
执
行同步
块
之前,
JVM
会先在当前
线
程的
栈桢
中
创
建用于存
储锁记录
的空
间
,并 将对
象
头
中的
Mark Word
复制到
锁记录
中,官方称
为
Displaced Mark Word
。然后
线
程
尝试
使用 CAS将
对
象
头
中的
Mark Word
替
换为
指向
锁记录
的指
针
。如果成功,当前
线
程
获
得
锁
,如果失败,表示其他
线
程
竞
争
锁
,当前
线
程便
尝试
使用自旋来
获
取
锁
。
(
2
)
轻
量
级锁
解
锁
轻
量
级
解
锁时
,会使用原子的
CAS
操作将
Displaced Mark Word
替
换
回到
对
象
头
,如果成功,则
表示没有
竞
争
发
生。如果失
败
,表示当前
锁
存在
竞
争,
锁
就会膨
胀
成重量
级锁
。
图
2-2
是 两个线
程同
时
争
夺锁
,
导
致
锁
膨
胀
的流程
图
。
3.锁的优缺点对比
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不需要额外的消耗 如果线程存在竞争 | 会带来额外锁撤销的消耗 | 只有一个线程访问同步块的场景 |
轻量级锁 | 竞争的线程不会阻塞 | 提高的程序的响应速度 如果始终得不到锁竞争线程会使用自旋消耗CPU | 追求响应时间 同步块执行非常快 |
重量级锁 | 线程竞争不使用自旋,不消耗CPU 线程阻塞 | 响应时间慢 | 追求吞吐量 执行时间较长 |