[Java源码][并发J.U.C]---用代码一步步实现ReentrantLock

前言

在前面的文章已经介绍了AQS, 接下来的几篇文章将会介绍各种锁, 而且这些锁都是基于AQS的, 所以需要对AQS有一定的了解将会帮助我们更容易理解这些锁. 本文分析的主题是重入锁ReentrantLock.

本文源代码 : 源代码下载

本文会以三步来进行ReentrantLock的分析.

1. 先以一个小例子来解释重入锁的基本概念
2. 用UML画出该类的结构
3. 分析源码并且用一个例子测试

例子1: 简单理解重入锁ReentrantLock

重入锁顾名思义就是说在某一个线程获得锁再次去获取锁, 是被允许的, 例如当递归调用一个synchronized修饰的方法, 说明synchronized是可以重入的. 那在[Java源码][并发J.U.C]---用代码一步步实现AQS(1)---独占锁的获取和释放 中的锁Mutex就不是重入锁(Mutex类的代码在代码下载处可以下载), 因此就用这两个锁比较看一下重入锁.

public class TestReentrantLock {
    static Lock mutexLock = new Mutex();
    static Lock reentrantLock = new ReentrantLock();
    
    public static void main(String[] args) {
        //new Runner(mutexLock, "thread-1").start();
        new Runner(reentrantLock, "thread-1").start();
    }
    
    static class Runner extends Thread {
        Lock lock;
        public Runner(Lock lock, String name) {
            super(name);
            this.lock = lock;
        }
        public void run() {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " get locks at the first time.");
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " get locks at the second time.");
            lock.unlock();
            System.out.println(Thread.currentThread().getName() + " release locks at the first time.");
            lock.unlock();
            System.out.println(Thread.currentThread().getName() + " release locks at the second time.");
        }
    }
}

当使用ReentrantLock的时候会输出如下内容. 表明该锁是可重入的.

thread-1 get locks at the first time.
thread-1 get locks at the second time.
thread-1 release locks at the first time.
thread-1 release locks at the second time.

当使用Mutex的时候会在输出第一句话的时候,就一直阻塞了,表明该线程在第一次获取锁成功了,当第二次再去获取锁的时候就一直阻塞了.

thread-1 get locks at the first time.

ReentrantLock 内部分析

如下是整个ReentrantLock类所包含的内容,关于Condition的部分我没有加进来,因为在后续的博客中会有专门的一篇来分析该类.

[Java源码][并发J.U.C]---用代码一步步实现ReentrantLock_第1张图片
ReentrantLock.png

如图所示, 先看右侧, ReentrantLock实现了Lock接口和Serializable接口. 因此就要实现Lock中的所有方法,也就是在图中ReentrantLock方法框中. 它的方法主要分三类:
1. 构造方法包括ReentrantLock()ReentrantLock(boolean fair)两个.
2. Lock接口中需要实现的方法, 从Lock()newCondition()方法.
3. 一些监控方法,包括取出等待队列中所有在等待的线程等等.从getHoldCount()开始到结尾.

另外Lock接口需要实现的方法基本上都是借助Sync类来实现的,因此ReentrantLock类中有一个sync的成员变量, 该成员变量可以是Sync的两个子类NonfairSync(非公平锁)和FairSync(公平锁)中的某一个.

公平性: 再看左侧, Sync继承了AQS,并且实现了一些共有方法,并且留有抽象方法lock()交由子类各自实现, 子类FairSync表示的是非公平锁, 意思是如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,否则该锁就是不公平的,也就是NonfairSync类.

其实ReentrantLock的核心就是Sync及其子类的实现, 因为实现Lock接口中的方法都是通过Sync类的实例来代理实现的. 所以接下来就具体看看Sync相关类的源码实现.

源码分析

Sync的实现
abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;
        // 留给子类实现
        abstract void lock();
        
        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;
        }
        
        // 判断当前线程是否持有该锁
        protected final boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }
        // condition相关博客会分析
        final ConditionObject newCondition() {
            return null;
        }
        // 获得持有该锁的线程
        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }
        // 获得持有该锁的个数 其实就是重入的次数
        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }
        // 判断锁有没有被占用 true表示被占用 false表示没有被占用
        final boolean isLocked() {
            return getState() != 0;
        }
        // 序列化的部分
        private void readObject(java.io.ObjectInputStream s)
                throws java.io.IOException, ClassNotFoundException {
                s.defaultReadObject();
                setState(0); // reset to unlocked state
        }
    }

从该类的实现可以看到当某一个线程获得了锁后可以无限制的重入, 如果达到了int变量的最大值后会抛出Error. 状态值为0的时候表明锁可以被获取, 状态值为n (n >= 1)的时候表明该线程重入了n-1次.

非公平锁NonfairSync的实现
static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        
        // 实现父类的方法
        final void lock() {
                /**
                 * 如果获取锁成功,则设置当前线程
                 * 否则去尝试获取锁
                 */
            if (compareAndSetState(0, 1)) 
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
            
        // 重写父类的父类AbstractQueuedSynchronizer的tryAcquire方法
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

其实没什么可说的, 需要实现从父类的lock()抽象方法和重写AQS中的tryAcquire方法即可.

公平锁FairSync的实现
static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }
        
        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;
        }
    }

与非公平锁类似实现locktryAcquire方法, 但是有一点不同的是在尝试获取锁的时候会先使用hasQueuedPredecessors判断等待队列中该节点是否有前驱节点, 如果有前驱节点必须要等到前驱节点所对应的线程获取并释放锁之后才能继续获取锁。

Lock接口方法的实现
    @Override
    public void lock() {
        sync.lock();
    }
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
      // 该方法不存在公平性的问题,所以直接调用nonfairTryAcquire方法
    @Override
    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }

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

    @Override
    public Condition newCondition() {
        return null;
    }
一些监控方法
// 获取锁被重入了多少次
    public int getHoldCount() {
        return sync.getHoldCount();
    }
    
    // 判断锁是不是被当前线程持有
    public boolean isHeldByCurrentThread() {
        return sync.isHeldExclusively();
    }
    
    // 判断锁有没有被任何一个线程占有
    public boolean isLocked() {
            return sync.isLocked();
    }
    // 判断该锁是不是公平锁
    public final boolean isFair() {
        return sync instanceof FairSync;
    }
    // 返回占有锁的那个线程
    protected Thread getOwner() {
        return sync.getOwner();
    }
    // 返回等待队列中是否还有节点 
    public final boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }
    // thread是否在等待队列中
    public final boolean hasQueuedThread(Thread thread) {
        return sync.isQueued(thread);
    }
    // 返回等待队列中的长度
    public final int getQueueLength() {
        return sync.getQueueLength();
    }
    // 返回等待队列中的所有线程
    protected Collection getQueuedThreads() {
        return sync.getQueuedThreads();
    }

注意: getQueuedThreads() 返回等待队列中的线程是按倒序的. 具体原因可以参考 [Java源码][并发J.U.C]---用代码一步步实现AQS(1)---独占锁的获取和释放

构造方法
    private final Sync sync;
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

默认是非公平锁, 可以通过传入参数来决定使用非公平锁还是公平锁.

例子2: 公平锁和非公平锁的对比

启动五个线程去夺取锁, 每个线程的作用就是打印当前等待队列中所有线程的顺序, 打印两次, 在第一次获取锁后打印一次后释放锁后再去尝试获取锁.

import java.util.concurrent.CountDownLatch;
public class TestFairAndNonFairLock {
    static ReentrantLock nonfair = new ReentrantLock(false);
    static ReentrantLock fair = new ReentrantLock(true);
    static CountDownLatch start = new CountDownLatch(1);
    // start 是为了保证5个线程同时运行 后续有专门博客会分析CountDownLatch
    public static void main(String[] args) {
        test(fair);
        // test(nonfair);
    }
    
    public static void test (ReentrantLock lock) {
        for (int i = 0; i < 5; i ++) {
            new Thread(new Runner2(lock), i+"").start();
        }
        start.countDown();
    }
    
    static class Runner2 implements Runnable {
        
        ReentrantLock lock;
        public Runner2(ReentrantLock lock) {
            this.lock = lock;
        }
        
        public void run() {
            try {
                start.await();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            lock.lock();
            System.out.print("lock by " + Thread.currentThread().getName() + " wait by [");
            String str = "";
            for (Thread t : lock.getQueuedThreads()) {
                str = t.getName() + "," + str;
            }
            System.out.println(str + "]");
            lock.unlock();
            
            lock.lock();
            System.out.print("lock by " + Thread.currentThread().getName() + " wait by [");
            str = "";
            for (Thread t : lock.getQueuedThreads()) {
                str = t.getName() + "," + str;
            }
            System.out.println(str + "]");
            lock.unlock();
        }
    }
}

对比如下:

公平锁 非公平锁
lock by 3 wait by [4,0,1,2,] lock by 4 wait by [1,3,2,]
lock by 4 wait by [0,1,2,3,] lock by 4 wait by [1,3,2,0,]
lock by 0 wait by [1,2,3,4,] lock by 1 wait by [3,2,0,]
lock by 1 wait by [2,3,4,0,] lock by 1 wait by [3,2,0,]
lock by 2 wait by [3,4,0,1,] lock by 3 wait by [2,0,]
lock by 3 wait by [4,0,1,2,] lock by 3 wait by [2,0,]
lock by 4 wait by [0,1,2,] lock by 2 wait by [0,]
lock by 0 wait by [1,2,] lock by 2 wait by [0,]
lock by 1 wait by [2,] lock by 0 wait by []
lock by 2 wait by [] lock by 0 wait by []

从上图中可以看到公平锁在每次释放锁后都会从等待队列中的第一个节点所对应的线程获得锁. 而非公平锁在该线程释放锁又基本上再次获得了锁, 这是因为在释放的过程中需要去唤醒等待队列中的节点,在唤醒后该线程与唤醒的线程竞争该锁, 然而从结果来看刚刚释放的锁竞争到锁的概率比较大.

参考

1. Java并发编程的艺术
2. Java1.8 java.util.concurrent.locks包的源代码

你可能感兴趣的:([Java源码][并发J.U.C]---用代码一步步实现ReentrantLock)