2

前言

上一次我们已经讲了AQS,如果对其不熟悉的话建议先去看看其实现原理,看完再来看ReentrantLock就很简单了。

啃碎JDK源码(一):String
啃碎JDK源码(二):Integer
啃碎JDK源码(三):ArrayList
啃碎JDK源码(四):HashMap
啃碎JDK源码(五):ConcurrentHashMap
啃碎JDK源码(六):LinkedList
啃碎JDK源码(七):AbstractQueuedSynchronizer(AQS)

ReentrantLockSynchornized 在面试中经常被用来比较,如果想了解Synchronized的话可以看我另外一篇文章:死磕Synchronized

正文

先来了解一下一些核心属性:

public class ReentrantLock implements Lock, java.io.Serializable {
// 实现AQS的内部类
private final Sync sync;
  ......
}

没错,ReentrantLock没有什么值得注意的属性,因为已经在AQS中定义好了,我们只需要继承它然后进行简单的实现即可。

先看下 ReentrantLock 的用法:

  public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            // 执行业务
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

只要调用 lock 方法就可以进行加锁操作,表示接下来的这段代码已经被当前线程锁住,其他线程需要执行时需要拿到这个锁才能执行,而当前线程在执行完之后要显式的调用 unlock 释放锁。

注意:看源码之前你必须要对AQS比较熟悉才行,可以参考我上一篇博客:
啃碎JDK源码(七):AbstractQueuedSynchronizer(AQS)

我们来跟进源码看一下,先来看我们的加锁lock方法:

  public void lock() {
      sync.lock();
  }
  
  // Sync继承了AQS
  abstract static class Sync extends AbstractQueuedSynchronizer {
  
     abstract void lock();
     ......
  }
  

可以看到是调用内部类的lock方法,而它是一个抽象方法,我们看下谁继承了这个抽象接口:

image.png

FairSyncNonfairSyncReentrantLock 的另外两个内部类。顾名思义一个是公平锁,一个是非公平锁。(公平锁就是永远都是队列的第一位才能得到锁

AQS有一个同步队列(CLH),是一种先进先出队列。公平锁的意思就是严格按照这个队列的顺序来获取锁,非公平锁的意思就是不一定按照这个队列的顺序来。

在new对象的时候便会对sync初始化,如下:

    public ReentrantLock() {
        sync = new NonfairSync();
    }
    
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

可以看出默认是非公平锁,如果传true则初始化为公平锁。

那我们首先来看看非公平锁:

  static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        final void lock() {
            // CAS修改状态
            if (compareAndSetState(0, 1))
                // 设置独占线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 进入队列等待
                acquire(1);
        }
        // tryAcquire是AQS的抽象方法,我们这里对其实现
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }    

首先用 compareAndSetState 方法使用CAS修改state状态变量的值,如果修改成功的话使用 setExclusiveOwnerThread(Thread.currentThread()) 方法将当前线程设置为独占锁的持有线程,否则调用AQS的 acquire 方法进去队列等待处理。

接下来看一下acquire方法:

   public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

该方法是AQS里的方法,我们上次已经介绍过了,这里直接截过来看下:

image.png

这次我们主要关注由子类ReentrantLock实现的tryAcquire方法:

image.png

    protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
     }
     
     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");
                // 直接修改state遍历,因为已经持有锁,不需要用CAS去修改
                setState(nextc);
                return true;
            }
            return false;
        }

上面代码和我们在上次手动实现一个可重入锁的代码差不多,这里就不再展开。

那接下来看一下 unlock 方法:

   public void unlock() {
       sync.release(1);
   }
   
   public final boolean release(int arg) {
        // 尝试释放锁
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

release 方法在AQS类中定义好了,我们子类主要实现 tryRelease 方法:

    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;
        }

这段代码上篇文章我们也已经讲过了,如果忘记的同学可以回头看看。

看完非公平锁的最后来看看公平锁的加锁方法:

     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;
        }
    }

其实代码基本和前面一样,只是多了hasQueuedPredecessors方法用来判断是否存在比等待更久的线程,因为要按照等待时间顺序获取资源,其它的这里就不再细说了。

其它疑问

以下问题来自 从源码角度理解ReentrantLock

为什么基于FIFO的同步队列可以实现非公平锁?

由FIFO队列的特性知,先加入同步队列等待的线程会比后加入的线程更靠近队列的头部,那么它将比后者更早的被唤醒,它也就能更早的得到锁。从这个意义上,对于在同步队列中等待的线程而言,它们获得锁的顺序和加入同步队列的顺序一致,这显然是一种公平模式。然而,线程并非只有在加入队列后才有机会获得锁,哪怕同步队列中已有线程在等待,非公平锁的不公平之处就在于此。回看下非公平锁的加锁流程,线程在进入同步队列等待之前有两次抢占锁的机会:

  • 第一次是使用compareAndSetState方法尝试修改state变量,只有在当前锁未被任何线程占有(包括自身)时才能成功。
  • 第二次是在进入同步队列前使用tryAcquire(arg)尝试获取锁。

只有这两次获取锁都失败后,线程才会构造结点并加入同步队列等待。而线程释放锁时是先释放锁(修改state值),然后才唤醒后继结点的线程的。试想下这种情况,线程A已经释放锁,但还没来得及唤醒后继线程C,而这时另一个线程B刚好尝试获取锁,此时锁恰好不被任何线程持有,它将成功获取锁而不用加入队列等待。线程C被唤醒尝试获取锁,而此时锁已经被线程B抢占,故而其获取失败并继续在队列中等待。
那我们在开发中为什么大多使用非公平锁?很简单,因为它性能好啊。

为什么非公平锁性能好

  1. 线程不必加入等待队列就可以获得锁,不仅免去了构造结点并加入队列的繁琐操作,同时也节省了线程阻塞唤醒的开销,线程阻塞和唤醒涉及到线程上下文的切换和操作系统的系统调用,是非常耗时的。。
  2. 减少CAS竞争。如果线程必须要加入阻塞队列才能获取锁,那入队时CAS竞争将变得异常激烈,CAS操作虽然不会导致失败线程挂起,但不断失败重试导致的对CPU的浪费也不能忽视。

总结

有关 ReentrantLock的知识就介绍到这里了,有什么不对的地方请多多指教。

image.png


超大只乌龟
882 声望1.4k 粉丝

区区码农