synchronized关键字的使用
synchronized 关键字是 Java 中一个独占式的悲观锁,可以用来修饰方法块和方法,根据锁定对象的类型进行分类,可以分为对象锁和类锁。
对象锁
修饰同步代码块:锁定对象为 this 或者实例对象;
public class Sync{
private int a = 0;
public void add(){
// 锁定对象为 this
synchronized(this){
System.out.println("a values " + ++a);
}
}
public void del(){
Sync s = new Sync();
// 锁定对象为实例对象
synchronized(s){
System.out.println("a values " + ++a);
}
}
}
修饰方法:同步非静态方法;
public class Sync{
private int a = 0;
// 同步非静态方法
public synchronized void add(){
}
}
类锁
修饰同步代码块:锁定对象为当前类;
public class Sync{
private int a = 0;
public void add(){
// 锁定对象为当前类
synchronized(Sync.class){
System.out.println("a values " + ++a);
}
}
}
修饰方法:同步静态方法;
public class Sync{
private int a = 0;
// 同步静态方法,锁定当前类
public synchronized static void add(){
}
}
了解了用法还不够,还要知道背后的实现原理,接下来我们分别从 synchronized 同步语句块和 synchronized 同步方法两方面来分析实现原理。
synchronized关键字的原理
synchronized 同步语句块
将同步代码块反编译结果如下:
可以看出,synchronized 同步语句块的实现,使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
每个 Java 对象的对象头中都存储了 monitor 对象,synchronized 就是通过对该对象的锁定和释放来实现加解锁的。而又因为每个对象头中都有对应的 monitor 对象,所以 Java 中任意对象都可以作为锁。
当 monitor 对象被占用时就会处于锁定状态,线程执行 monitorenter 指令时会尝试获取 monitor 的所有权,过程如下:
- 如果 monitor 的进入数为 0,则该线程进入 monitor,然后将进入数设置为 1,该线程即为monitor 的所有者;
- 如果线程已经占有该 monitor,只是重新进入,则进入 monitor 的进入数加 1;
- 如果其他线程已经占用了 monitor,则该线程进入阻塞状态,直到 monitor 的进入数为 0,再重新尝试获取 monitor 的所有权。
从以上过程可以看出,synchronized 关键字和 ReentrantLock 一样,都是可重入锁。
也就是说,当一个线程已经获取一个锁时,它可以再获取无数次,从代码的角度上将就是有无数个相同的 synchronized 语句块嵌套在一起。
在进入时,monitor 的进入数加一;退出时就减一,直到为 0 的时候才可以被其他线程竞争获取。
synchronized 同步方法
将同步方法反编译结果如下:
我们发现,同步方法里面没有了 monitorenter 和 monitorexit 指令,但是常量池中多了ACC_SYNCHRONIZED 标识符,JVM 就是根据该标识符实现方法的同步的。
当方法调用时,会检查方法的 ACC_SYNCHRONIZED 访问标识是否被设置,如果设置了,执行线程将先获取 monitor 对象,获取成功之后才能执行方法体,执行完后会释放 monitor 对象。
在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象。
这种方式与语句块没什么本质区别,都是通过竞争 monitor 对象的方式来实现的,只不过这种方式是一种隐式的实现。
通过上面的描述可以发现,synchronized 的关键实现主要是依靠 monitor 对象来完成的,在 Java 虚拟机 (HotSpot) 中,monitor 对象是由 ObjectMonitor 实现的,其数据结构如下:
ObjectMonitor() {
_count = 0; //记录个数
_owner = NULL; // 运行的线程
//两个队列
_WaitSet = NULL; //调用 wait 方法会被加入到_WaitSet
_EntryList = NULL ; //锁竞争失败,会被加入到该列表
}
可以看到,ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,每个等待锁的线程都会被封装成 ObjectWaiter 对象被加入队列中。
当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的 monitor 锁后,会把 ObjectMonitor 中的 _owner 变量设置为当前线程,同时会给计数器 _count 加 1。
若调用 wait() 方法,线程就会释放当前持有的 monitor 对象,同时把 _owner 变量恢复为 null,计数器自减 1,之后该线程就会进入 _WaitSet 集合中等待被唤醒。
除了调用 wait() 方法之外,当前线程执行完毕后也将释放 monitor 锁并复位变量的值,以便其他线程可以重新获取 monitor 锁。
JDK 1.6 中对 synchronized 关键字的优化
在JDK 1.6中,为了减少获得和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,锁的状态变成了四种,如下图所示:
偏向锁
偏向锁是 Java 为了提高程序的性能而设计的一个比较优雅的加锁方式。
它的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时对象头中的 Mark Word 会变为偏向锁结构,也就是使用 CAS 操作把获取到这个锁的线程的 ID 记录在对象的 Mark Word之中的偏向线程 ID 中,当这个线程再次请求锁时,无需再重新获取锁。
对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,会膨胀为轻量级锁。
轻量级锁
引入轻量级锁的目的:在多线程交替执行同步代码块时(未发生竞争),避免使用互斥量(重量锁)带来的性能消耗。但多个线程同时进入临界区时(发生竞争),则会使得轻量级锁膨胀为重量级锁。
当面对线程竞争时,轻量级锁主要采用自旋锁机制来进行解决,当自旋超过一定次数,轻量级锁就会升级为重量级锁。
重量级锁
轻量级锁膨胀之后,就会升级为重量级锁。
重量级锁是依赖对象内部的 monitor 锁来实现的,而 monitor 又依赖了操作系统的 MutexLock(互斥锁),所以重量级锁也被称为互斥锁。
我们本篇博客分析的 synchronized 主要是针对 JDK 1.6 之前的实现进行探讨的,对应到 1.6 之后的 synchronized ,就是重量级锁的底层实现。
本文参考资料如下,非常感谢:
Synchronized的实现原理(汇总)