ReentrantLock可重入锁源码原理详解

ReentrantLock可重入锁源码原理详解

背景介绍

AbstractQueuedSynchronizer是Doug Lea在JDK1.5的时候加入的一个同步框架,也被简称为AQS,该框架主要维护了被竞争资源的状态,和获取到资源的线程(经过AbstractOwnableSynchronizer来维护)以及未获取到资源的线程的管理,AQS主要经过volatile的内存可见性和CAS来实现。具体的竞争资源的方式(公平、非公平)由子类实现,Doug Lea在引入该框架时提供了一系列已经实现好的子类,好比:ReentrantLock、ReentrantReadWriteLock,为了对使用者透明具体实现细节下降使用门槛,这两个锁自己并不继承AQS,而是由内部类Sync继承AQS并经过组合的方式实现锁。java

ReentrantLock介绍

AQS实现了共享方式和排它方式,而ReentrantLock只对外暴露出了AQS的排它方式,因此ReentrantLock也叫作排它锁,在这个基础上ReentrantLock又经过两个内部类(FairSync、NonfairSync)间接继承了AQS分别实现了公平锁、非公平锁node

在这里插入图片描述

ReentrantLock与synchronized对比

1. 竞争锁标识安全

​ synchronized: 线程经过获取某个对象的Monitor的全部权框架

​ ReentrantLock: 线程经过修改AQS中的被volatile修饰的int类型的state变量ide

2. 抢到锁标识的线程以及未抢到锁标识的线程维护函数

​ synchronized: 不清楚具体实现(跟Monitor Record有关),不了解JVM是如何维护的。性能

​ ReentrantLock: 经过AQS的基类AbstractOwnableSynchronizer中的一个成员变量来记录获取到资源的线程,并把为获取到锁标识的线程维护在一个FIFO的队列中。测试

Monitor Record内部结构:ui

Monitor Record
Owner 初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程惟一标识,当锁被释放时又设置为NULL;
EntryQ 关联一个系统互斥锁(semaphore),阻塞全部试图锁住monitor record失败的线程。
RcThis 表示blocked或waiting在该monitor record上的全部线程的个数。
Nest 用来实现重入锁的计数。
HashCode 保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate 用来避免没必要要的阻塞或等待线程唤醒,由于每一次只有一个线程可以成功拥有锁,若是每次前一个释放锁的线程唤醒全部正在阻塞或等待的线程,会引发没必要要的上下文切换(从阻塞到就绪而后由于竞争锁失败又被阻塞)从而致使性能严重降低。Candidate只有两种可能的值0表示没有须要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

3. 线程通讯this

synchronized: 经过某个对象的wait()、notify()、notifyAll()方法来实现。 注: 该方法须要在synchronized 语句块中调用不然会抛IllegalMonitorStateException异常。

ReentrantLock:经过await()、signal()、signalAll()方法来实现(区别于wait()、notify()、notifyAll())。 注: 该方法须要在ReentrantLock调用lock()方法后,unlock()方法前调用,不然会抛IllegalMonitorStateException异常。

4. 未抢到锁标识的线程状态

synchronized: 线程处于BLOCKED状态。

ReentrantLock: 线程处于WAITING状态。
下文证实

5.锁类型

synchronized: 只有非公平锁实现。

ReentrantLock: 既有非公平锁又有公平锁,默认为非公平锁。

ReentrantLock(非公平锁)实现源码

首先看一下ReentrantLock的构造函数:

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

1.获取锁(采用模板方法模式)

获取锁的过程主要采用模板方法模式,流程看起来感受会有点乱。

  1. 获取锁的入口是ReentrantLock类中的lock()方法,该方法会调用内部抽象类的lock()方法

    public void lock() { 
            sync.lock();    //sync为ReentrantLock的内部抽象类继承AQS
        }
  2. 内部抽象类Sync的lock()方法为抽象方法,该方法的具体实现由ReentrantLock的内部类NonfairSync实现

    abstract void lock();
  3. ReentrantLock的lock()方法最终实现是在NonfairSync的lock()方法,一进入该方法先去竞争锁标识,这里也是非公平的缘由之一。

    final void lock() { 
                if (compareAndSetState(0, 1))
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    acquire(1);
            }

    ① 获取锁的时候先经过CAS竞争锁标识,若是成功把AQS中的state成员变量从0修改成1就认为本身(当前线程)成功。(方法简单不展开)

    获取到锁标识并记录到AbstractOwnableSynchronizer中的成员变量exclusiveOwnerThread中,线程继续向下执行。(方法简单不展开)

    ③ 若是①失败表示该线程未获取到锁标识则进入AQS的acquire()方法。

  4. AQS的acquire()方法

    public final void acquire(int arg) {     //该方法采用模板方法模式
            if (!tryAcquire(arg) &&         
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))        
                selfInterrupt();
        }

    该方法采用模板方法模式,其中tryAcquire()方法尝试获取锁标识具体实现由子类实现,若是获取锁标识成功线程继续向下执行,若是获取失败,线程将会进入等待状态(不是阻塞状态跟synchronized不一样,以下图),而后将该线程构建成一个独占式的节点放到队列中进行维护。

    如图(分析两个线程经过ReentrantLock锁成功和失败的线程状态):

    示例代码:

    public static void main(String[] args) throws ExecutionException, InterruptedException { 
    
            ReentrantLock reentrantLock = new ReentrantLock();
    
            Thread t1 = new Thread(new Runnable() { 
                @Override
                public void run() { 
    
                    try { 
                        reentrantLock.lock();
                        while (true) { 
                        }
                    } finally { 
                        reentrantLock.unlock();
                    }
                }
            }, "T1");
            t1.start();
    
            Thread t2 = new Thread(new Runnable() { 
                @Override
                public void run() { 
    
                    try { 
                        reentrantLock.lock();
                        while (true) { 
                        }
                    } finally { 
                        reentrantLock.unlock();
                    }
                }
            }, "T2");
            t2.start();
    
            t1.join();
            t2.join();
    
        }

    上述事例代码很简单,两个线程进行争抢同一把锁,而后经过jstack分析T一、T2线程所处的状态:

在这里插入图片描述

能够清晰的看到T1线程获取到了锁标识处于RUNNABLE(JVM将操做系统的READY就绪状态和RUNNING运行中状态合并为RUNNABLE状态)状态,T2线程并未获取到锁标识处于WAITING状态(并非BLOCKED状态缘由是由于底层调用LockSupport.park()方法)。

  1. 若是竞争锁标识失败将会进入addWaiter(Node.EXCLUSIVE)方法:

    private Node addWaiter(Node mode) { 
            Node node = new Node(Thread.currentThread(), mode);
            // Try the fast path of enq; backup to full enq on failure
            Node pred = tail;
            if (pred != null) { 
                node.prev = pred;
                if (compareAndSetTail(pred, node)) { 
                    pred.next = node;
                    return node;
                }
            }
            enq(node);
            return node;
        }

    该方法把当前线程构建成一个独占式的Node节点放到FIFO队列中,该方法先判断队尾是否为空,若是不为空经过CAS将该线程的节点放到队尾而后返回若CAS失败则进入enq()方法,若是队尾为空则证实该队列还没有初始化,则进入enq()方法初始化队列并将该节点放入队列中,具体向下看enq()方法实现。

  2. enq()方法分析:

    private Node enq(final Node node) { 
            for (;;) { 
                Node t = tail;
                if (t == null) {  // Must initialize
                    if (compareAndSetHead(new Node()))
                        tail = head;
                } else { 
                    node.prev = t;
                    if (compareAndSetTail(t, node)) { 
                        t.next = node;
                        return t;
                    }
                }
            }
        }

    能够看到该方法是个死循环(CAS的失败重试),用来保证该线程节点放入队尾,第一个判断用来判断该队列是否已经初始化过,若还没有初始化则先进性初始化操做,而后在经过CAS失败重试将该节点放入队尾并返回。

  3. 再继续分析将该节点放入队列中的后续操做,将会执行acquireQueued()方法:

    final boolean acquireQueued(final Node node, int arg) { 
            boolean failed = true;
            try { 
                boolean interrupted = false;
                for (;;) { 
                    final Node p = node.predecessor();     //获取该节点的父节点
                    if (p == head && tryAcquire(arg)) { 
                        setHead(node);
                        p.next = null; // help GC
                        failed = false;
                        return interrupted;
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())      //主要调用LockSupport.park()方法阻塞当前线程,并在当前线程醒来时重 置该线程的中断标志位
                        interrupted = true;
                }
            } finally { 
                if (failed)
                    cancelAcquire(node);
            }
        }

① 首先该方法将会进入一个死循环,若是该节点的父节点是对头(只有对头的节点才持有锁标识),若是该节点的父节点是头结点则证实该节点可能(下文宏观获取锁流程讲解为何是可能) 立刻就能够获取到锁标识了,进行tryAcquire()尝试获取锁标识,若是获取成功,把该节点设置为头结点,并返回false(主要是返回一个暗号,不让当前线程设置中断标识[下文讲解线程中断])。

② 若是该节点的父节点不是头结点(说明下一个获取到锁的线程必定不是他),或者是头结点可是竞争锁标识失败了(下文宏观获取锁流程讲解为何会失败),将会进入shouldParkAfterFailedAcquire()方法,该方法主要判断该节点是否能够安全的进行阻塞,还有其余处理逻辑,若是能够安全阻塞将会(触发LockSupport.park()方法进入WAITING状态)阻塞该线程。

③ 能够发现上述流程发生在一个死循环中,通常状况会等到获取到锁标识后正常返回,不过肯能存在几种状况在为获取到锁以后就返回了,不如线程取消,等待超时,将会进入cancelAcquire()方法(该方法还没来得及好好看,好像主要是针对这些未正常获取到锁就返回的线程的处理以及对存放线程Node节点的队列的一些维护操做)。

④ 正常返回的状况下会返回true或者false,若是返回false证实该线程并无进入WAITING状态,若是返回true则说明该线程进入过WAITING状态并在苏醒时对线程的中断标志位进行置位。后续操做会根据这个返回结果对该线程的中断标志位进行相应的设置。

注:线程中断标志位好像在本流程中没用,好像在其余流程中用到了,好比tryAcquireNanos()方法,该方法好像会相应线程中断。

  1. 还有一个重要的方法就是tryAcquire()方法:

    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");
                    setState(nextc);
                    return true;
                }
                return false;
            }

    该方法很重要不少地方都用到了,可是也很简单。

    ① 首先获取当前线程,而且获取锁标识的值(0标识没有线程持有锁)。

    ② 当锁标识为0的时候标识没有线程持有锁,使用CAS竞争锁标识,若是竞争成功则把当前线程记录到AbstractOwnableSynchronizer的成员变量中。

    ③ 若是锁标识不为0,则说明有线程持有该锁,而后判断持有锁的线程是不是当前线程,若是是则将锁标识位+1(这就是为何说ReentrantLock是一把可重入锁)。

获取锁(非公平锁)的流程结束,释放锁的流程很简单,你们有兴趣能够本身去看源码(实在是不想写了)

线程中断

我的理解:线程中断是一种线程间进行通讯的方式之一,他自己是线程的一个属性,用来标识其余线程(也能够是他自己)给该线程(运行中)的中断标识位设置值(好像是一个boolean值),至关于其余线程给该线程发送的一个消息。

注:该线程必须处于运行中才能收到该线程的中断消息,好比该线程处于WAITING状态没法收到中断消息。

宏观获取锁流程

首先看一下公平锁和非公平锁表现的结果:

测试代码:

public class ReentrantLockTest { 

    //须要继承ReentrantLock把getQueuedThreads()方法暴露出来
    public class MyReentrantLock extends ReentrantLock{ 

        public MyReentrantLock(boolean fair) { 
            super(fair);
        }

        @Override
        public Collection<Thread> getQueuedThreads() { 
            List<Thread> list = new ArrayList<>(super.getQueuedThreads());
            //因为是逆序输出的因此进行翻转,不信能够看输入线程队列的源码
            Collections.reverse(list);
            return list;
        }
    }

    public static class Job extends Thread{ 

        public MyReentrantLock reentrantLock;
        public Job(MyReentrantLock reentrantLock){ 
            this.reentrantLock = reentrantLock;
        }
        @Override
        public void run() { 

            for (int i = 0; i < 2; i++){ 
                reentrantLock.lock();
                List<String> collect = reentrantLock.getQueuedThreads().stream().map(e -> { 
                    return e.getName();
                }).collect(Collectors.toList());

                System.out.println("当前线程:"+Thread.currentThread().getName()+",阻塞队列线程:"+collect);

                try { 
                    TimeUnit.MILLISECONDS.sleep(20);
                }catch (Exception e){ }
                reentrantLock.unlock();
            }
        }
    }

    @Test   //非公平锁测试
    public void testNotFair() throws InterruptedException { 
        MyReentrantLock myReentrantLock = new MyReentrantLock(false);
        for (int i=0;i<5;i++){ 
            Job job = new Job(myReentrantLock);
            job.setName(i+"");
            job.start();
        }
        Thread.currentThread().join(2000);
    }

    @Test   //公平锁测试
    public void testFair() throws InterruptedException { 
        MyReentrantLock myReentrantLock = new MyReentrantLock(true);
        for (int i=0;i<5;i++){ 
            Job job = new Job(myReentrantLock);
            job.setName(i+"");
            job.start();
        }
       Thread.currentThread().join(2000);
    }
}

结果:

非公平锁testNotFair():

在这里插入图片描述

公平锁testFair():

在这里插入图片描述

对比结果能够看到以下结果:

非公平锁:每当一个线程抢到锁以后,再来的其余线程将会进入到FIFO的队列中进行排队,当获取到锁的线程释放锁以后,本应该队列中的下一个节点线程获取锁,可是若是有新线程(没有在队列中的线程)来抢锁,那么新线程将会和队列中的头结点的下一个线程争抢锁标识,这就是表现出来的不公平的地方,可是仔细观察结果会发现不公平中也存在的公平,只要线程进入队列中以后,将会在队列中按照先进先出的顺序进行获取锁。

公平锁:每当一个线程抢到锁以后,再来的其余线程不会尝试抢锁,先回去判断线程队列中是否还有线程在进行排队获取锁,若是有本身将会去队列中排队获取锁,从结果来看,线程0释放锁以后将会去队列末尾排队,线程1释放锁后一样回去排队,以此类推。

这就解决了上述的问题,若是本文章有什么不对或者不合理的地方,但愿大佬指正。

相关文章
相关标签/搜索