Synchronized用法
Synchronized 是 Java 中的一个重要关键字,主要是用来加锁的。在使用Synchronized的时候需要指定一个对象,所以synchornized也被称为对象锁。
synchronized 的使用方法比较简单,主要可以用来修饰方法和代码块。根据其锁定的对象不同,可以用来定义同步方法和同步代码块。总的来说有三种用法:
1. 作用在实例方法上
修饰实例方法,相当于对当前实例对象this加锁,this作为对象监视器
public class main{
public synchronized void hello(){
System.out.println("hello world");
}
}
2. 作用在静态方法上
修饰静态方法,相当于对main类的Class对象加锁,main类的Class对象作为对象监视器。
public class main{
public synchronized static void helloStatic(){
System.out.println("hello world static");
}
}
3. 作用在代码块上
指定加锁对象,对给定对象加锁,括号括起来的test对象就是对象监视器。
public class main{
Object test = new Object();
synchronized (test){
System.out.println("hello world");
}
}
Synchronized的原理
先说结论:Synchronized的底层原理是通过Monitor对象来完成的。
Monitor是什么?
Monitor 对象被称为管程或者监视器锁,是由jvm提供,c++语言实现。在Java中,每一个对象实例都会关联一个Monitor对象。这个Monitor对象既可以与对象一起创建销毁,也可以在线程试图获取对象锁时自动生成。当这个Monitor对象被线程持有后,它便处于锁定状态。
Monitor 大致结构如下图所示
Monitor内部具体的存储结构:
Owner:存储当前获取锁的线程,只能有一个线程可以获取
EntryList:存储没有抢到锁的线程,这些都是处于Blocked状态的线程
WaitSet:存储调用了wait方法的线程,这些都是处于Waiting状态的线程
Count:用来记录该对象被线程获取锁的次数,这也说明了synchronized是可重入的
其中在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,ObjectMonitor.hpp文件如下所示:
ObjectMonitor() {
_header = NULL;
_count = 0; // 用来记录该对象被线程获取锁的次数,这也说明了synchronized是可重入的
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; // 指向持有ObjectMonitor对象的线程
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet,调用了wait方法之后会进入这里
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 没没有抢到锁,处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
同步代码块
我们通过借助javap命令查看synchronizedTest.clsss的字节码
public class synchronizedTest {
static Object test=new Object();
public static void main(String[] args) {
synchronized (test){
System.out.println("test synchronized");
}
}
}
找到这个类的class文件,在class文件目录下执行 javap -v synchronizedTest.class ,
反汇编效果如下:
可以看到,字节码是通过 monitorenter 和monitorexit 两个指令进行控制的。
monitorenter 是上锁开始的地方,monitorexit 是解锁的地方,其中被monitorenter和monitorexit包围住的指令就是上锁的代码 。
- monitorenter,当执行到monitorenter指令时,线程就会去尝试获取该对象对应的Monitor的所有权,即尝试获得该对象的锁。如果当前monitor的计数器为0时,线程就会获得monitor对象锁,并且计数器Count自增1,那么该线程就是monitor的拥有者(owner)。
- 如果该线程已经是monitor的拥有者,又重新进入,就会把计数器Count再次自增1。也就是可重入的。
- monitorexit,执行monitorexit的线程必须是monitor的拥有者,指令执行后,monitor的计数器Count减1,如果减1后计数器Count为0,则该线程会释放monitor对象锁。其他被阻塞的线程就可以尝试去获取monitor的所有权。
- 倘若其他线程已经拥有monitor 的所有权,那么当前线程获取monitor对象锁失败将被阻塞并进入到EntryList中,直到锁被释放为止。
monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异常退出释放锁
详细流程:
- 加锁时,即遇到Synchronized关键字时,线程会先进入monitor的_EntryList队列阻塞等待。
- 如果monitor的_owner为空,则从队列中移出并赋值与_owner。
- 如果在程序里调用了wait()方法,则该线程进入_WaitSet队列。我们都知道wait方法会释放monitor锁,即将_owner赋值为null并进入_WaitSet队列阻塞等待。这时其他在_EntryList中的线程就可以获取锁了。
- 当程序里其他线程调用了notify/notifyAll方法时,就会唤醒_WaitSet中的某个线程,这个线程就会再次尝试获取monitor锁。如果成功,则就会成为monitor的owner。
- 当程序里遇到Synchronized关键字的作用范围结束时,就会将monitor的owner设为null,退出。
同步方法的底层实现
我们通过借助javap命令查看SyncTest.clsss的字节码
public class SyncTest {
public static void main(String[] args) {
hello();
}
public static synchronized void hello(){
System.out.println("hello synchronized!");
}
}
找到这个类的class文件,在class文件目录下执行 javap -v SyncTest.class ,
反汇编效果如下:
可以看到,在字节码的体现上,这里并没有monitorenter和moniterexit两条指令,而是只给方法加了一个 flag:ACC_SYNCHRONIZED ,这其实也容易理解,因为整个方法都是同步代码,因此就不需要标记同步代码的入口和出口。当线程线程执行到这个方法时会判断是否有ACC_SYNCHRONIZED标志,如果有的话则会尝试获取monitor对象锁。当该对象的 monitor 的计数器count为0时,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。当同步方法执行完后,该对象的 monitor 的计数器减1,计数器的值为0时,执行线程将释放 monitor(锁),其他线程才有机会持有 monitor。
总的来说,synchronized的底层原理是通过monitor对象来完成的,对于同步代码块是使用monitorenter和monitorexit指令来完成加锁和解锁。对于同步方法则是通过判断方法上是否有ACC_SYNCHRONIZED标志来尝试获取monitor对象锁。
方法级的同步是隐式的(同步方法)。同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。当某个线程要访问某个方法的时候,会检查是否有 ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。
同步代码块使用 monitorenter 和 monitorexit 两个指令实现。 可以把执行 monitorenter 指令理解为加锁,执行 monitorexit 理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0,当一个线程获得锁(执行 monitorenter )后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行 monitorexit 指令)的时候,计数器再自减。当计数器为 0 的时候。锁将被释放,其他线程便可以获得锁。