本文通过分析Java对象头的变化引申出锁的状态变化!
关于对象头的知识查看博客:Java的对象布局和对象头以及证明
初识synchronized:
1、synchronized关键字提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止数据不一致问题的出现。
2、synchronized关键字包括monitor enter和monitor exit两个JVM指令。
3、synchronized的指令严格遵守Java happens-before规则,一个monitor exit指令之前必定要有一个monitor enter。
一、锁的状态
锁一共有四种状态(由低到高的次序):无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态
锁的等级只可以升级,不可以降级。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。(特殊情况批量偏向)
二、理解 偏向锁、轻量级锁、重量级锁的概念
- 1、偏向锁
一个线程获取某个对象的偏向锁的成本是很低的,只需把对象头的偏向线程id改为自己就好,如果偏向线程id已经为自己则直接获得锁。当偏向锁的获取出现竞争,则偏向锁可能会升级为轻量级锁。
根据这些,可以看出偏向锁适合无竞争、竞争小的场景,理想的情况为总是由同一个线程去访问同步块、获取某个对象的锁。实际应用中,很多时候情景也确实是这样的。
- 2、轻量锁
轻量级锁由偏向锁升级而来,特点是获取轻量级锁的是通过CAS原子操作进行的,失败的线程不会进入阻塞,而是自旋尝试再次CAS去获取锁。若失败的次数过多,则轻量级锁会膨胀为重量级锁。因为自旋也是要消耗cpu的,不能让线程一直自旋下去。
根据这些,可以看出 轻量级锁最适合场景是追求响应时间的情景,理想的情况是少量线程交替访问同步块、获取锁。若多个线程访问同步块的时间重合的比骄密集就会发生很多自旋造成cpu资源浪费。
- 3、重量锁
重量级锁是轻量级锁受到激烈竞争时,为防止cpu被自旋的线程浪费膨胀而来,因此重量级锁肯定是应付大量线程同时访问同步块的情景。让申请锁失败的线程阻塞后,cpu的负担会减小不少,因此数据的吞吐量也就上来了
偏向锁膨胀为轻量级锁时,首先要撤销偏向锁,变成无锁,进行CAS操作,然后将线程ID信息写入对象都,变成轻量级锁!退出同步块之后,还原对象头为无锁!
二、 偏向锁、轻量级锁、重量级锁 证明
添加JOL依赖:JOL来分析java的对象布局
<dependencies>
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
</dependencies>
/**
* 对象A
*/
public class A {
boolean flag =false;
}
- 1、偏向锁
public class JOLExample3 {
static A a;
public static void main(String[] args) throws Exception {
//关闭偏向锁延时
//-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
Thread.sleep(5000);
a= new A();
//a.hashCode();
System.out.println("befor lock");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
synchronized (a){
System.out.println("lock ing");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
System.out.println("after lock");
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
查看代码和运行结果,这里有几个要点需要说明:
1)虚拟机在启动的时候对于偏向锁有延迟;
启动JVM时,存在大量的synchronized同步方法运行,而且很多不是偏向锁,如果启动时开启偏向锁,会造成偏向锁升级的问题,有可能会因为线程竞争太激烈导致产生太多安全点挂起,故JVM延迟4后多才开启偏向锁。
方法上:
<1>睡眠5秒钟
<2>直接通过JVM的参数来禁用延迟
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
2)befor lock 之前每次获得对象锁时,都会判断使用那种对象锁
因为锁的膨胀是不可逆滴,且进行hashcode计算后不可使用偏向锁,那么是只有对象锁第一次被获得及没有进行hashcode运算时才能变成可偏向锁。
这里为第一次获得对象锁且没有进行hashcode,所以图中为可偏向状态,并无具体线程ID占有;
3)偏向锁退出时,mark word锁的状态还是偏向锁状态,而轻量级锁退出时,会变成无锁状态;而重量级锁退出时还是重量锁状态。
4)对象A进行hashcode计算之后,则并不能变成偏向锁!将上述代码的a.hashCode();运行查看结果即可证明
关于偏向锁:
同步方法也许会有多线程来执行,也许只有一个线程,故有个偏向锁,如果只有一个线程执行同步方法时,可以提升效率,若有其他线程来时,升级锁即可。所以不太确定是否为单线程或多线程,又为了安全和效率,则可以使用偏向锁。
- 2、轻量级锁
因为JVM还没开启偏向锁,故此时打印,获取对象锁前应该是轻量级锁,获取到对象锁时,这时获得的对象锁为轻量级锁,当释放锁后,对象头则变成无锁状态!
/**
* 轻量锁
*/
public class JOLExample5 {
static B b;
public static void main(String[] args) throws Exception {
b = new B();
System.out.println("befre lock");
System.out.println(ClassLayout.parseInstance(b).toPrintable());
sync();
System.out.println("after lock");
System.out.println(ClassLayout.parseInstance(b).toPrintable());
}
public static void sync() throws InterruptedException {
synchronized (b){
System.out.println("lock ing");
System.out.println(ClassLayout.parseInstance(b).toPrintable());
}
}
}
运行结果:
3、重量级锁
重量级锁退出时还是重量锁状态
public class JOLExample7 {
static C c;
public static void main(String[] args) throws Exception {
Thread.sleep(5000);
c = new C();
out.println("befre lock");
out.println(ClassLayout.parseInstance(c).toPrintable());//无锁
Thread t1= new Thread(){
public void run() {
synchronized (c){
try {
Thread.sleep(5000);
System.out.println("t1 release----------");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
Thread.sleep(1000);
out.println("t1 lock ing");
out.println(ClassLayout.parseInstance(c).toPrintable());//轻量锁
sync();
out.println("after lock");
out.println(ClassLayout.parseInstance(c).toPrintable());//重量锁
System.gc();
out.println("after gc()");
out.println(ClassLayout.parseInstance(c).toPrintable());//无锁---gc
}
public static void sync() throws InterruptedException {
synchronized (c){
System.out.println("t1 main lock");
out.println(ClassLayout.parseInstance(c).toPrintable());//重量锁
}
}
}
执行结果太长,只截取部分:
四、 偏向锁、轻量级锁、重量级锁 性能对比
偏向锁、轻量级锁、重量级锁 性能肯定越来越低咯,我们来证明一下吧!
实例:
/**
* 对象A
*/
public class A {
int i=0;
public synchronized void parse(){
i++;
}
}
/**
* 轻量级锁:22500ms
* 偏向锁:2408ms
* @param args
* @throws Exception
*/
//关闭偏向锁延时
//-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
public static void main(String[] args) throws Exception {
A b = new A();
long start = System.currentTimeMillis();
//调用同步方法1000000000L 来计算1000000000L的++,对比偏向锁和轻量级锁的性能
//如果不出意外,结果灰常明显
for(int i=0;i<1000000000L;i++){
b.parse();
}
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end - start));
}
}
轻量级锁的运行结果为:22500ms;
当接通过JVM的参数来禁用延迟-
XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
偏向锁的运行结果为:2408ms;
重量锁:51676ms
public class C {
int i=0;
// boolean flag =false;
public synchronized void parse(){
i++;
JOLExample6.countDownLatch.countDown();
}
}
/**
* 重量锁:51676ms
*
*/
public class JOLExample6 {
static CountDownLatch countDownLatch = new CountDownLatch(1000000000);
public static void main(String[] args) throws Exception {
final C c = new C();
long start = System.currentTimeMillis();
//调用同步方法1000000000L 来计算1000000000L的++,对比偏向锁和轻量级锁的性能
//如果不出意外,结果灰常明显
for(int i=0;i<2;i++){
new Thread(){
@Override
public void run() {
while (countDownLatch.getCount() > 0) {
c.parse();
}
}
}.start();
}
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end - start));
}
}
五、如果调用wait方法则立刻变成重量锁
案例:
public class JOLExample11 {
static A a;
public static void main(String[] args) throws Exception {
//Thread.sleep(5000);
a = new A();
out.println("befre lock");
//无锁
out.println(ClassLayout.parseInstance(a).toPrintable());
Thread t1= new Thread(){
public void run() {
synchronized (a){
try {
synchronized (a) {
System.out.println("before wait");
//轻量级锁
out.println(ClassLayout.parseInstance(a).toPrintable());
a.wait();
System.out.println(" after wait");
//重量级锁
out.println(ClassLayout.parseInstance(a).toPrintable());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
Thread.sleep(7000);
synchronized (a) {
a.notifyAll();
}
}
}
六、需要注意的是如果对象已经计算了hashcode就不能偏向了
public class JOLExample8 {
static C c;
public static void main(String[] args) throws Exception {
Thread.sleep(5000);
c= new C();
c.hashCode();
out.println("befor lock");
out.println(ClassLayout.parseInstance(c).toPrintable());
synchronized (c){
out.println("lock ing");
out.println(ClassLayout.parseInstance(c).toPrintable());
}
out.println("after lock");
out.println(ClassLayout.parseInstance(c).toPrintable());
}
}
运行结果:
七、批量偏向
批量偏向阀值为20.达到阀值时,epoch值将设为ec会变为01,
1、非同步块中t1持有的锁的epoch!=ec;故epoch失效,将消息头重的线程id信息更新为当前线程t2;
2、如果某个对象还在同步块当中 将其epoch=ec!避免后续锁的安全问题。
t1实例化多个对象(同一个类)并且tangible这些对象,t2也同步了这些对象,因为要升级,锁多次撤销偏向锁,jvm会认为接下来的对象需要批量重偏向,那么接下来的对象都是偏向锁不再是轻量锁了!
注意:
偏向锁时的线程tid为偏向pid+epoche;
轻量锁时的线程tid为lockrecord
运行时:添加指令 -XX:+PrintFlagsInitial
运行结果中有:
其中 intx BiasedLockingBulkRebiasThreshold = 20
说的就是批量重定向的阀值为20;
intx BiasedLockingBulkRevokeThreshold = 40
说的是批量撤销的阀值为40;
public class JOLExample12 {
static List<A> list = new ArrayList<A>();
public static void main(String[] args) throws Exception {
Thread.sleep(5000);
Thread t1 = new Thread() {
public void run() {
for (int i=0;i<40;i++){
A a = new A();
synchronized (a){
System.out.println("111111");
list.add(a);
}
}
}
};
t1.start();
t1.join();
out.println("befre t2");
//偏向
out.println(ClassLayout.parseInstance(list.get(1)).toPrintable());
Thread t2 = new Thread() {
int k=0;
public void run() {
for(A a:list){
synchronized (a){
System.out.println("22222");
if (k==18||k==19){
out.println("t2 ing----" +k);
//第19个对象轻量锁,第20个对象偏向锁
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
k++;
}
}
};
t2.start();
t2.join();
out.println("after t2");
//无锁
out.println(ClassLayout.parseInstance(list.get(18)).toPrintable());
//偏向锁
out.println(ClassLayout.parseInstance(list.get(19)).toPrintable());
}
}