在多
线
程并
发编
程中
synchronized
一直是元老
级
角色,很多人都会称呼它
为
重量
级锁
。但
是,随着
Java SE 1.6
对
synchronized
进
行了各种
优
化之后,有些情况下它就并不那么重了。
先来看下利用
synchronized
实现
同步的基
础
:
Java
中的每一个
对
象都可以作
为锁
。具体表
现
为
以下
3
种形式。
·
对
于普通同步方法,
锁
是当前
实
例
对
象。
·
对
于静
态
同步方法,
锁
是当前
类
的
Class
对
象。
·
对
于同步方法
块
,
锁
是
Synchonized
括号里配置的
对
象。
Synchonized
在
JVM
里的
实现
原理,
JVM
基于
进
入和退出
Monitor对象
{
//TODO
了解管程 ( )
每一个对象和类都与一个监视器相关联
要线程独占某块数据/代码(SpecialRoom) ,那么先进入Hallway等待,然后调度器基于某些规则(如先进先出)从Hallway中取一个线程,若线程处于被挂起状态 那么就把它送进等待房间,过一段时间再送进 SpecialRoom。
而Monitor (监视器)的作用就是
保证 仅一个线程访问受保护的数据/代码
在Java中 每个对象和类都有一个监视器与之关联。为了实现监视器的互斥功能,锁(有时候也称为互斥体)与每一个对象和类关联。在操作系统书中,这叫做信号量,互斥锁也被称为二元信号量。
如果一个线程拥有某些数据上的锁,其他线程想要获得锁只能等到这个线程释放锁。如果我们在进行多线程编程时总是需要编写一个信号量,那就不太方便了。幸运的是,我们不需要这样做,因为JVM会自动为我们做这件事。
JVM会实现一个信号量,来与每个类/对象的监视器关联。
为了声明一个同步区域(这里意味着数据不可能被超过一个线程访问),Java提供了synchronized块和synchronized方法。一旦代码被synchronized关键字绑定,它就是一个监视器区域。它的锁将会在后面被JVM实现。
}
来实现
方法同步和代
码块
同步,但两者的
实现细节
不一
样
。代
码块
同步是使用
monitorenter
和
monitorexit
指令
实现
的,而方法同步是使用另外一种方式
实现
的,
细节
在
JVM
规
范里并没有
详细说
明。但是,方法的同步同
样
可以使用
这
两个指令来
实现
。
monitorenter
指令是在
编译
后插入到同步代
码块
的开始位置,而
monitorexit
是插入到方法
结
束处和异常处
,
JVM
要保
证每个monitorenter必须有对应的monitorexit与之配对
。
Java对象头
在pom.xml中 加入
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
虽然看包名出处是openjdk,但在OpenJDK和Oracle JDK上都可以使用
用法:
System.out.println(ClassLayout.parseInstance(user).toPrintable());
32位虚拟机下的MarkWord
打印synchronize锁住的对象信息和没有被锁的对象信息
可以看到 对象被锁住时 头部第一行最后两位变成了01 即被重量级锁锁定。
64位虚拟机下的MarkWord
{
存疑 System.out.println(System.getProperty("sun.arch.data.model"));
通过这个方法可以得知我的jvm是64位 但是为什么markword按照32位来的??
}
锁的升级和对比
Java SE 1.6
为
了减少
获
得
锁
和
释
放
锁带
来的性能消耗,引入了
“
偏向
锁
”
和
“
轻
量
级锁
”
,在 Java SE 1.6中,
锁
一共有
4
种状
态
,
级别
从低到高依次是:无
锁状态、偏向锁状态、轻量级锁状态和重量级锁状态
,
这
几个状
态
会随着
竞
争情况逐
渐
升
级
。
锁可以升级但不能降
级 {
偏向锁
升
级
成
轻
量
级锁
后不能降
级
成偏向
锁 ,但是偏向锁可以降成无锁
}
这
种
锁
升
级
却不能降
级
的策略,目的是
为
了提高获得
锁
和
释
放
锁
的效率(后续介绍)
偏向锁:
HotSpot
[1]
的作者
经过
研究
发现
,大多数情况下,
锁
不
仅
不存在多
线
程
竞
争,而且
总
是由同 一线
程多次
获
得,
为
了
让线
程
获
得
锁
的代价更低而引入了偏向
锁
。
当一个
线
程
访问
同步
块
并获取
锁时
,会在
对
象
头
和
栈帧
中的
锁记录
里存
储锁
偏向的
线
程
ID
,以后
该线
程在
进
入和退出
同步
块时
不需要进行CAS操作来加锁和解锁
,
只需
简单
地
测试
一下
对
象
头
的
Mark Word
里是否存储
着指向当前线程的偏向锁
。
如果
测试
成功,表示
线
程已
经获
得了
锁
。
如果
测试
失
败
,
则
需要再测试
一下
Mark Word
中偏向
锁的标识是否设置成1
(表示当前是偏向
锁
):
如果没有
设
置,
则使用CAS
竞
争
锁
;(实际上就是用无锁方式中的CAS)
如果
设
置了,
则尝试
使用
CAS
将
对
象
头
的偏向
锁
指向当前
线
程。
偏向锁的撤销
偏向
锁
使用了一种等到
竞
争出
现
才
释
放
锁
的机制,所以当其他
线
程
尝试竞
争偏向
锁时
, 持有偏向锁
的
线
程才会
释
放
锁
。
偏向
锁
的撤
销
,需要
等待全局安全点{ 这个来保证线程安全 }
(在
这
个
时间
点上没有正在执
行的字
节码
)。
它会首先
暂停拥有偏向锁的线
程,然后
检查
持有偏向
锁
的
线
程是否活着,
如果线
程不
处
于活
动
状
态
,
则
将
对
象
头设
置成无
锁
状
态
;
如果
线
程仍然活着,
拥
有偏向
锁
的
栈会被执
行,遍
历
偏向
对
象的
锁记录
,
栈
中的
锁记录
和
对
象
头
的
Mark Word
要么重新偏向于其他线程,要么恢复到无
锁
或者
标记对
象不适合作
为
偏向
锁
,最后
唤
醒
暂
停的
线
程。
偏向锁的初始化流程
关闭偏向锁
偏向
锁
在
Java 6
和
Java 7
里是默
认
启用的,但是它在
应
用程序启
动
几秒
钟
之后才激活,
如有必要可以使用JVM
参数来关
闭
延
迟
:
-XX:BiasedLockingStartupDelay=0
。
如果你确定
应
用程 序里所有的锁
通常情况下
处
于
竞
争状
态
,可以通
过
JVM
参数关
闭
偏向
锁
:
-XX:UseBiasedLocking=false,那么程序默
认
会
进
入
轻
量
级锁
状
态
。
轻量级锁
轻量级锁加锁
线
程在
执
行同步
块
之前,
JVM
会先在当前
线
程的
栈桢
中
创
建用于存
储锁记录
的空
间
,并
将
对
象
头
中的
Mark Word
复制到
锁记录
中,官方称
为
Displaced Mark Word
。
然后
线
程
尝试
使用CAS将
对
象
头
中的
Mark Word
替
换为
指向
锁记录
的指
针
。如果成功,当前
线
程
获
得
锁
,如果失
败
,表示其他
线
程
竞
争
锁
,当前
线
程便
尝试
使用自旋来
获
取
锁
。
轻量级锁解锁
轻
量
级
解
锁时
,会使用原子的
CAS
操作将
Displaced Mark Word
替
换
回到
对
象
头
,如果成
功,
则
表示没有
竞
争
发
生。如果失
败
,表示当前
锁
存在
竞
争,
锁
就会膨
胀
成重量
级锁
。
争夺锁导致的锁膨胀流程图
(线程1 :我忙活了一圈白干了是吧 )
轻量级锁和偏向锁的区别 :
偏向锁没有复制对象头MarkWord到当前线程的栈帧,而是通过将对象头中的
线程id指向自己 ,
轻量级锁则是将MarkWord替换为指向锁记录的指针。长度是 30bit 最后i两位是00
偏向锁 在MarkWord倒数第三位会 用1 表示自己是偏向锁 最后两位是01
相同点
都使用了 CAS操作 偏向锁使用CAS操作替换MarkWord中的线程ID 而 轻量级锁 则是 比较 线程程中复制的DisplayMarkWord 和 对象的MarkWord 。
偏向锁 轻量级锁 重量级锁 优缺点