【JDK源码分析】通过源码彻底理解ReentrantLock显示锁

前言

ReentrantLock和synchronized一样是一个可重入的互斥锁,但ReentrantLock功能更强大,它提供了非公平和公平两种锁争用策略供使用者选择,而synchronized只有非公平一种。ReentrantLock提供了可中断的锁等待机制以及可用于多组线程需要分组唤醒的条件。

类图

下面是ReentrantLock的类图,内部抽象类Sync继承了AbstractQueuedSynchronizer(以下简称AQS),公平锁FairSync、非公平锁NonfairSync继承了抽象类Sync。 
ReentrantLock

源码

ReentrantLock类属性和构造器

先看ReentrantLock的属性,属性只有sync,可见由它实现了整个类的功能。公平锁是所有线程都按FIFO的方式进行,获取锁释放锁的顺序进行;而非公平锁则是在锁释放的时候,不会限制新来的线程进行锁的争用。

    private final Sync sync;

    //默认构造器,生成的是公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    // 有参构造器,形参fair为true则构造公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }   

ReentrantLock类主要方法

下面以ReentrantLock的主要方法的调用过程来一步步分析源码

1. lock 方法

lock方法调用了内部类Sync的lock方法

    public void lock() {
        sync.lock();
    }

lock方法为抽象方法

        abstract void lock();

我们先看lock方法的非公平实现

1.1 lock方法的非公平实现

lock方法大量使用了其祖父类AQS中的方法,这里补充几点: 

  • AQS有个state变量,用于表示锁状态,为0表示处理无锁状态,>0表示处理有锁状态; 
  • compareAndSetState为AQS提供操作state变量的CAS原子方法; 
  • 关于详细的AQS的源码理解,可以查看本人上一篇博客,本文将不再赘述AQS中的源码分析。
        final void lock() {
            // 原子操作state变量,当state为0时将其置为1
            if (compareAndSetState(0, 1))
                // 成功将其置为1表示
                // 将当前线程设置为独占状态
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // CAS操作state变量失败,表示出于有锁状态
                acquire(1);
        }

acquire方法为AQS的方法,但其调用的tryAcquire则由NonfairSync实现

    public final void acquire(int arg) {
        //当tryAcquire返回false时,执行acquireQueued方法,它是将当前线程加入到队列中等待锁释放
        // acquireQueued在AQS中实现,主要逻辑是将当前线程封装成一个对象,加入到同步队列等待锁释放,然后再争用锁
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

tryAcquire方法,获取锁的方法

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

        final boolean nonfairTryAcquire(int acquires) {
            // 获取当前锁状态
            int c = getState();
            // 为0表示出于无锁
            if (c == 0) {
                // 比较并交换锁的值
                if (compareAndSetState(0, acquires)) {
                    // 设置当前线程为独占状态
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果当前线程和独占锁的线程是同一个线程,也就是重入
            else if (current == getExclusiveOwnerThread()) {
                //重入时将锁状态量加1
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            // 出于有锁状态时,直接返回false,表示该线程获取锁失败
            return false;
        }
1.2 lock方法的公平实现

现在来看公平锁的lock方法

        final void lock() {
            // 还是调用的祖父类的acquire方法
            acquire(1);
        }

祖父类的acquire调用的公平锁的tryAcquire实现,细心的人可能一眼就会发现和非公平锁的获取锁实现的唯一区别就在获取锁之前判断之前是否已经线程在等待锁释放

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                 // hasQueuedPredecessors 用来判断是否有其它线程在等待锁释放
                 // hasQueuedPredecessors 为AQS 实现,就是判断同步队列里是否有其它线程在等待
                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;
        }

至此lock方法的公平和非公平实现已完毕。

2. unlock 方法

unlock方法是二种策略锁共用的,它也是通过调用的AQS的release方法完成锁释放的

    public void unlock() {
        sync.release(1);
    }

其它方法比较简单,这里就不细说了。

总结

  1. ReentrantLock一般的使用场景是synchronized不能满足我们的功能需求时才使用; 
  2. 一般在使用的时候我们使用非公平锁,公平锁比非公平锁的吞吐量明显要低很多,这是因为公平锁在新的线程过来的时候都要去检查同步队列是否有等待锁的线程而且最主要的是在锁释放时唤醒一个被挂起的线程到线程真正开始执行之间有很大的延迟。
原文地址:https://www.cnblogs.com/d-homme/p/9361057.html