7. synchronized关键字(重量级锁)
-
synchronized在JVM中的实现原理:
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的表现细节不同。**本质是对一个对象的监视器(monitor)的获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到有synchronized所保护对象的监视器。**任意一个对象都拥有自己的监视器。任意线程对对象的访问,首先要获得对象的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问对象的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
synchronized关键字和synchronized方法的字节码略有不同,可以用
javap -v
命令查看class文件对应的JVM字节码信息。源码:
public class SyncTest {
public void syncBlock(){
synchronized (this){
System.out.println("hello block");
}
}
public synchronized void syncMethod(){
System.out.println("hello method");
}
}
字节码:
{
public void syncBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter // monitorenter指令进入同步块
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String hello block
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit // monitorexit指令退出同步块
14: goto 22
17: astore_2
18: aload_1
19: monitorexit // monitorexit指令退出同步块
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
public synchronized void syncMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED //添加了ACC_SYNCHRONIZED标记
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String hello method
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
从上面的中文注释处可以看到,
-
对于synchronized关键字而言,javac在编译时,会生成对应的monitorenter和monitorexit指令分别对应synchronized同步块的进入和退出,有两个monitorexit指令的原因是:为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。
-
而对于synchronized方法而言,javac为其生成了一个ACC_SYNCHRONIZED关键字,在JVM进行方法调用时,发现调用的方法被ACC_SYNCHRONIZED修饰,则会先尝试获得锁。
在JVM底层,对于这两种synchronized语义的实现大致相同。
在JDK 1.6之前,synchronized只有传统的锁机制,因此给开发者留下了synchronized关键字相比于其他同步机制性能不好的印象。
在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
-
monitorenter和monitorexit工作原理
每个对象都与一个monitor相关联。当且仅当拥其被拥有时,monitor才会被锁定。执行到monitorenter指令的线程,会尝试去获得对应的monitor:
-
每个对象维护着一个记录着被锁次数的计数器,对象未被锁定时,该计数器为0。
-
线程进入monitor(执行monitorenter指令)时,会把计数器设置为1。
-
当同一个线程再次获得该对象的锁的时候,计数器再次自增。
-
当其他线程想获得该monitor的时候,就会阻塞,直到计数器为0才能成功。
-
-
ACC_SYNCHRONIZED工作原理
当调用一个设置了ACC_SYNCHRONIZED标志的方法:- 执行线程需要先获得monitor锁,然后开始执行方法,方法执行之后再释放monitor锁,当方法不管是正常return还是抛出异常都会释放对应的monitor锁。
- 在这期间,如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。
- 如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。
-
synchronized用的锁存在java对象头里
对象头三种数据:Mark Word(对象hashcode、分代年龄、锁标记位等)、Class元数据地址、数组长度(数组类型的对象才有), 这三种数据分别占据一个Word(4字节)。其中可以看到synchronized用的锁存在java对象头里:
- 当对象状态为偏向锁(biasable)时,mark word存储的是偏向的线程ID;
- 当状态为轻量级锁(lightweight locked)时,mark word存储的是指向线程栈中Lock Record的指针;
- 当状态为重量级锁(inflated)时,为指向堆中的monitor对象的指针。
-
无锁、偏向锁、轻量级锁、重量级锁
锁一共有4中状态,由低到高为:无锁、偏向锁、轻量级锁、重量级锁,这几个状态会随着竞争情况逐渐升级。 锁可以升级但不能降级(提高获得锁和释放锁的效率)
-
偏向锁:(适用于只有一个线程访问同步块的场景)
原理 : 第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只会执行几个简单的命令,当有另一个线程尝试获得偏向锁,则该偏向锁就会升级成轻量级锁 (不绝对,有批量重偏向)
优点:加锁解锁不需要额外的消耗
缺点:如果线程间存在锁竞争,会带来额外的锁撤销的消耗
-
轻量级锁:(追求响应时间,同步块执行速度非常快,同步块中的代码不存在竞争 )
原理:通过CAS获取锁,若失败则说明有竞争,膨胀为重量级锁
优点:竞争的线程不会阻塞,提高了程序的响应速度
缺点:如果始终得不到锁竞争的线程,使用自旋会消耗CPU
-
重量级锁:(追求吞吐量,同步块执行时间较长)
原理:利用操作系统底层的同步机制去实现Java中的线程同步
优点:线程竞争不使用自旋,不会消耗CPU
缺点:线程阻塞,响应时间慢
-
-
关键字synchronize拥有锁重入的功能,也就是在使用synchronize时,当一个线程的得到了一个对象的锁后,再次请求此对象是可以再次得到该对象的锁。当一个线程请求一个由其他线程持有的锁时,发出请求的线程就会被阻塞,然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由她自己持有的锁,那么这个请求就会成功,“重入” 意味着获取锁的 操作的粒度是“线程”,而不是调用。
-
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。当条件不满足时,锁会按偏向锁->轻量级锁->重量级锁的顺序升级。JVM种的锁也是能降级的,只不过条件很苛刻,不在我们讨论范围之内。
下一篇
JUC知识点总结(三)ReentrantLock与ReentrantReadWriteLock源码解析