监视器锁 synchronized

一、synchronized 的原理
JVM 基于进入和推出Monitor对象来实现方法和同步代码块,但两者的实现细节不同。
  • synchronize 修饰的同步代码块:使用monitorenter 和 monitorexit 指令实现;
  • synchronize 修饰的方法并没有 monitorenter 和 monitorexit 指令 ,而取代之的是 ACC_SYNCHRONIZED标识,该标志指明了该方法是一个同步方法,从而执行相应的同步调用。
 
Monitorenter 指令实现编译后到同步代码块开始的位置,而 monitorexit 是插入在方法结束出和异常处,JVM要保证每个monitorter 必须要有一个与之对应的monitorexit 。任何一个对象都可以和一个monitor 相关联,且当一个执行 monitorenter 指令的时候,会尝试获取synchronized 圆括号中对象中相关联的 monitor的所有权,即尝试获取这个对象的锁(这里的是重量级锁,为没有后面提到的偏向锁和轻量级锁)

(1)synchronized修饰的方法

public synchronized void method() {
    int i = 0;
}

javap反编译后:

  public synchronized void method();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=1, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: return
      LineNumberTable:
        line 6: 0
        line 7: 2
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   Lcom/lock/SyncTest;
            2       1     1     i   I

对于同步方法,JVM会讲方法设置 ACC_SYNCHRONIZED 标志,调用的时候 JVM 根据这个标志判断是否是同步方法。

(2)synchronized修改的代码块

public void method() {
    synchronized (this) {
        int i = 0;
    }
}

javap反编译后:

  public void method();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: iconst_0
         5: istore_2
         6: aload_1
         7: monitorexit
         8: goto          14
        11: aload_1
        12: monitorexit
        13: athrow
        14: return
      Exception table:
         from    to  target type
             4     8    11   any
            11    13    11   any
      LineNumberTable:
        line 6: 0
        line 7: 4
        line 6: 6
        line 9: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   Lcom/lock/SyncTest;
对于Synchronized的同步代码块,JVM会在进入代码块之前加上monitorenter ,如果进入monitor成功,线程便获取了锁,一个对象的monitor同一时刻只能被一个线程锁占有;
上面为什么有两个monitorexit呢?第一个是正常退出,第二个是异常退出,保证不会死锁。
 
 
二、Monitor监视器锁
其中轻量级锁和偏向锁是Java6对synchronized锁进行优化后增加的,我们稍后会进行分析。这里我们主要分析重量级锁,也就是通常所说的synchronized对象锁,锁标识为10,其中指针指向monitor对象(也称之为管程或者监视器锁)的起始地址。每个对象都存在一个monitor与之关联,对象与其monitor之间也存在着多种实现方式,如monitor可以与对象一起创建或者销毁或当前线程试图获取锁时自动生成,但一个monitor被某线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是有ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)
ObjectMonitor() {
    _header       = NULL;
    _count        = 0;        // 记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;    // 记录当前持有锁的线程ID
    _WaitSet      = NULL;    // 等待池:处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;    // 锁池:处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
}

ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList ,用来保存ObjectWaiter 对象列表,每个等待锁的线程都会被封装成ObjectWaiter 对象,_owner 指向持有ObjectMonitor 对象的线程,当多个线程同时访问同一同步代码块或者同步方法时,首先会进入 _EntryList 队列,当线程获取到monitor 后进入_Owner 区域并把 monitor中的 _Owner 变量设置为当前线程,同时monitor 中的计数器count 加1,若线程调用wait() 方法,将释放当前持有的monitor,_owner变量恢复为null,count 自减 1 ,同时该线程进入_WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示:

每个对象都有一个Monitor对象相关联,Monitor对象中记录了持有锁的线程信息、等待队列等。Monitor对象包含以下三个字段:
  • _owner 记录当前持有锁的线程ID
  • _EntryList 是一个队列,记录所有阻塞等待锁的线程(阻塞队列,锁池)
  • _WaitSet 也是一个队列,记录调用 wait() 方法并还未被通知的线程(等待池)
当线程持有锁的时候,线程id等信息会拷贝进owner字段,其余线程会进入阻塞队列entrylist,当持有锁的线程执行wait方法,会立即释放锁进入waitset,当线程释放锁的时候,owner会被置空,公平锁条件下,entrylist中的线程会竞争锁,竞争成功的线程id会写入owner,其余线程继续在entrylist中等待。
 
采用Synchronized给对象加锁会使线程阻塞,因而会造成线程状态的切换,而线程状态的切换必须要操作系统来执行,因此需要将用户态切换为内核态,这个切换的过程是十分耗时的都需要操作系统来帮忙,有可能比用户执行代码的时间还要长。
 
所以,monitor对象存在于每一个java对象的对象头(存储指针的指向),synchronized锁便是通过这种方式获取的,也是为什么java中任何对象都可以作为锁的原因,同时也是 notify/notifyAll/wait 方法等存在于顶级对象Object中的原因。
 
三、synchronized的可重入性
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,也叫递归锁,请求将会成功, 在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。如下:
public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    static int j=0;
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){

            //this,当前实例对象锁
            synchronized(this){
                i++;
                increase();//synchronized的可重入性
            }
        }
    }

    public synchronized void increase(){
        j++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

正如代码所演示的,在获取当前实例对象锁后进入synchronized代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个synchronized方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现,需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1。

扫描二维码关注公众号,回复: 11139013 查看本文章

猜你喜欢

转载自www.cnblogs.com/caoxb/p/12787179.html