从源码分析可重入锁(ReentrantLock)

392 阅读4分钟

从源码分析可重入锁(ReentrantLock)

重入锁(ReentrantLock),从它的名字就可以看得出来,它是指任意线程在获取到锁之后能够再次获取该锁而不被阻塞。在非重入锁的情况下一个线程获取到的锁,再次去获取锁的时候,那么这个线程就会因为之前获取到的锁而阻塞。

我们所熟知的synchronized关键字隐式地支持重进入,执行线程在获取了锁之后仍能多次获取锁,而ReentrantLock 是显式地支持重入,获得锁的状态下,再次调用Lock()方法获取锁而不被阻塞。隐式锁和显式锁的区别在 显示锁Lock有介绍。

如何实现重进入?

为实现重进入锁我们需要解决两个问题:

  1. 线程是否可以再次获取锁:线程需要去识别获取锁的线程是否是当前占据锁的线程,如果是可以再次获取,如果不是,那肯定不能(是我的锁我怎么锁都没问题,不是我的锁一次也不能用!)
  2. 锁的释放:获取了n次锁,那肯定要释放n次锁才能让其他线程获取锁。这就要求对于锁的获取进行计数自增,锁被释放时要求计数自减,当计数为0时表示锁被成功释放。

ReentrantLock通过组合自定义同步器来实现锁的获取与释放,获取同步状态的源代码如下:

    final boolean nonfairTryAcquire(int acquires) {
           final Thread current = Thread.currentThread();
           int c = getState();
           if (c == 0) {
               if (compareAndSetState(0, acquires)) {
                   setExclusiveOwnerThread(current);
                   return true;
              }
          }
           else if (current == getExclusiveOwnerThread()) {
               int nextc = c + acquires;
               if (nextc < 0) // overflow
                   throw new Error("Maximum lock count exceeded");
               setState(nextc);
               return true;
          }
           return false;
      }

该方法是通过判断当前线程是否为获取锁的线程来决定操作是否成功。

成功获取锁的线程再次获取锁就是增加同步状态值,那么在释放锁时就要减少同步状态值。释放同步状态方法源代码如下:

 protected final boolean tryRelease(int releases) {
           int c = getState() - releases;
           if (Thread.currentThread() != getExclusiveOwnerThread())
               throw new IllegalMonitorStateException();
           boolean free = false;
           if (c == 0) {
               free = true;
               setExclusiveOwnerThread(null);
          }
           setState(c);
           return free;
      }

从tryRelease方法可以看出,只有c==0 时,即同步状态为0时将占有线程设置为null,并返回true,表示释放成功。

这里还涉及到了锁获取的公平性问题,就像我们平时去食堂排队买饭,肯定是先排队的人先买到饭,不然干饭人可要掀桌子了。可我们看看上面的nonfairTryAcquire方法,只要设置同步状态成功就算该线程获取到了锁,并不保证先请求先获得锁,这可一点不公平。

公平锁与非公平锁的区别:

公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是先进先出

与默认的nonfairTryAcquire方法不同,ReentrantLock的tryAcquire方法则可实现公平锁。该方法源代码如下:

       protected final boolean tryAcquire(int acquires) {
           final Thread current = Thread.currentThread();
           int c = getState();
           if (c == 0) {
               if (!hasQueuedPredecessors() &&
                   compareAndSetState(0, acquires)) {
                   setExclusiveOwnerThread(current);
                   return true;
              }
          }
           else if (current == getExclusiveOwnerThread()) {
               int nextc = c + acquires;
               if (nextc < 0)
                   throw new Error("Maximum lock count exceeded");
               setState(nextc);
               return true;
          }
           return false;
      }
  }

可以发现tryAcquire与之前的nonfairTryAcquire代码大致是一样的,唯一不同的点就是该方法在判断条件上多了一个hasQueuedPredecessors,意思是判断加入同步队列中当前节点是否有前驱节点,如果有则表示有比当前线程更早请求锁的线程,只能等待前面的线程获取到并释放锁之后才能继续获取锁,可不能插队哦,咱们可是要讲文明的嘞。

但公平性锁一定就比非公平锁好吗?其实不然,公平性锁保证了锁的获取按照first in first out 的特征,但代价就是需要进行大量的线程切换,非公平性锁可能导致一个线程请求了老长时间,可能运气差就是抽不到它去获取锁,但是非公平性锁极少的线程切换,保证了更好的性能和吞吐量。