JAVA多线程之——互斥锁ReentrantLock

来源:互联网 发布:软件测试干什么的 编辑:程序博客网 时间:2024/05/16 01:43

ReentrantLock简介
首先回顾一下synchronized关键字。
把代码声明为synchronized之后,那么就会保证,每次都只有一个线程获取对象的内部锁,进而产生互斥保证共享资源的安全。synchronized是获取对象的内部锁,所以是原生语法层面的互斥,需要JVM实现。
ReentrantLock是jdk1.5开始引入的JUC并发包中的一个类,ReentrantLock基于java代码实现,也就是API层面的互斥。ReentrantLock是锁的实现类,这就让它更具有灵活性,可以用多种算法来实现。而且在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上)
要理解ReentranLock.我们先从它的整体结构和阅读源码开始学习。
在学习前可以先看看ReentrantLock的基本工作流程。然后带着流程去理解源码。这个流程图是在学习之后总结出来的。这里在前面也放一份。
这里写图片描述
ReentrantLock类中的方法列表:

// 创建一个 ReentrantLock ,默认是“非公平锁”。ReentrantLock()// 创建策略是fair的 ReentrantLock。fair为true表示是公平锁,fair为false表示是非公平锁。ReentrantLock(boolean fair)// 查询当前线程保持此锁的次数。int getHoldCount()// 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。protected Thread getOwner()// 返回一个 collection,它包含可能正等待获取此锁的线程。protected Collection<Thread> getQueuedThreads()// 返回正等待获取此锁的线程估计数。int getQueueLength()// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。protected Collection<Thread> getWaitingThreads(Condition condition)// 返回等待与此锁相关的给定条件的线程估计数。int getWaitQueueLength(Condition condition)// 查询给定线程是否正在等待获取此锁。boolean hasQueuedThread(Thread thread)// 查询是否有些线程正在等待获取此锁。boolean hasQueuedThreads()// 查询是否有些线程正在等待与此锁有关的给定条件。boolean hasWaiters(Condition condition)// 如果是“公平锁”返回true,否则返回falseboolean isFair()// 查询当前线程是否保持此锁。boolean isHeldByCurrentThread()// 查询此锁是否由任意线程保持。boolean isLocked()// 获取锁。void lock()// 如果当前线程未被中断,则获取锁。void lockInterruptibly()// 返回用来与此 Lock 实例一起使用的 Condition 实例。Condition newCondition()// 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。boolean tryLock()// 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。boolean tryLock(long timeout, TimeUnit unit)// 试图释放此锁。void unlock()

在来看一下它的源码,如果贴上全部源码可能会看的有点头晕。所以可以自行在Eclipse中打开源码。这里做一个简单的归类,以及后面一点一点的读。如图:
这里写图片描述
类图,ReentrantLock是Lock类的实现类,它有一个自己的内部类Sync.ReentrantLock将Lock类的大部分实现,全部委托给了Sync来实现。现,AbstractQueuedSynchronizer中抽象了绝大多数Lock的功能,而只把tryAcquire方法延迟到子类中实现。Sync同时有两个子类,一个就是用于公平锁,一个用于非公平锁。下面就学习一下,是如何实现的。

锁的实现(加锁)
查看ReentrantLock API可以看到有一个方法lock()获取锁。源码如下:

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

前面说了,大部分实现都是委托给了Sync这个类。而Sync有两个子类,先学习公平锁,理解了公平锁,对于非公平锁也容易多了。因此查看Sync子类 FairSync中的 lock()方法:

 final void lock() {   acquire(1);  }

发现调用了一个acquire(1)方法,这个方法是干嘛的呢?从类图可以看到Sync继承了AbstractQueuedSynchronizer。而acquire(1)就是其AQS的一个方法。点击查看源码:

    /**     * Acquires in exclusive mode, ignoring interrupts.  Implemented     * by invoking at least once {@link #tryAcquire},     * returning on success.  Otherwise the thread is queued, possibly     * repeatedly blocking and unblocking, invoking {@link     * #tryAcquire} until success.  This method can be used     * to implement method {@link Lock#lock}.     *     * @param arg the acquire argument.  This value is conveyed to     *        {@link #tryAcquire} but is otherwise uninterpreted and     *        can represent anything you like.     */    public final void acquire(int arg) {        if (!tryAcquire(arg) &&            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))            selfInterrupt();    }

看其注释,这个方法是独占模式下的一个方法,并且忽略中断。如果获取到资源,就直接返回,否则就进入一个队列。直到获取到资源为止。我们获取资源第一个真方法可以说是这个才正式开始,那么这个方法也可以理解为独占模式下获取资源的顶层入口。然后分析方法:
方法中有一个判断,如果为真,就执行一个方法 selfInterrupt()。从方法名称来看,这个是自我中断。那么综合方法的注释——在获取资源时候,忽略中断。知道获取资源。那么方法就可以拆分2部分
1.获取资源,然后判断。
2.根据判断的真假决定是否执行自我中断(selfInterrupt())
先看第一步,if中包含两个方法。
1.tryAcquire(arg)
2.acquireQueued(addWaiter(Node.EXCLUSIVE), arg));
tryAcquire(arg)
tryAcquire是Sync的一个方法。源码:

   /**         * Fair version of tryAcquire.  Don't grant access unless         * recursive call or no waiters or is first.         */        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;        }    }

该方法表示试图获取锁,如果获取成功直接返回true否则返回false。这里又分为2步骤
1.判断c是否等于0. 如果c等于0,那么就判断中执行hasQueuedPredecessors() 与compareAndSetState(0, acquires)。如果条件为真,就执行 setExclusiveOwnerThread(current) 并返回ture。
hasQueuedPredecessors 方法

   public final boolean hasQueuedPredecessors() {        // The correctness of this depends on head being initialized        // before tail and on head.next being accurate if the current        // thread is first in queue.        Node t = tail; // Read fields in reverse initialization order        Node h = head;        Node s;        return h != t &&            ((s = h.next) == null || s.thread != Thread.currentThread());    }

这个方法就是判断当队列中是否有其它线程,如果没有,说明没有线程占有着锁。关于这个线程队列,下次会做个详细学习笔记。所以当hasQueuedPredecessors 返回false,说明当前队列没有线程,那么就执行compareAndSetState(0, acquires)方法。这个方法其实就是前面学习过类似的方法,CAS更新。把state更新为1.然后把独占线程设置为当前线程,就是说让当前线程获取到资源
那么为什么非得是c==0进来呢。那就看一下c不等于0是怎么执行的。
不等与0的时候执行current == getExclusiveOwnerThread()判断当前线程是否就是当前占锁的线程。那么这就可以理解,如果c==0就是说这个时候锁是空着的,没有任何线程占有,不等于0就是说明锁被占着。所以在上面,当更新完状态之后,我们就把锁给当前线程。那锁被占了为什么还要判断锁是不是当前线程占有,这是因为ReentrantLock是可重入锁,所以就要判断一次。如果是,就把c加上acquires然后更新状态值。返回。
这样我们就梳理一下:
在lock中调用的acquire(1),这个常量1就代表锁获取一次就需要更新的状态值,如果是第一次获取资源,则变为1,如果是重入那么就在原来的基础上加1.因此:ReentrantLock的锁的一个机制,是每当线程获取一次相同锁,就进行一次计数加1.同理,如果释放一次就减1.如果计数为0说明就是释放了锁
总结tryAcquire方法就是尝试的去获取一下资源,如果获取就返回true否则返回false.
因此在在尝试获取失败后再执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。
addWaiter 方法

   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)) { //进行CAS操作更新节点。如果失败代表有并发,进入enq方   法                pred.next = node;                return node;            }        }        enq(node);        return node;    }

嗯!在分析这个源码之前,先来看一下Node是什么。

 static final class Node {        /** Marker to indicate a node is waiting in shared mode */        static final Node SHARED = new Node();        /** Marker to indicate a node is waiting in exclusive mode */        static final Node EXCLUSIVE = null;        /** waitStatus value to indicate thread has cancelled */        static final int CANCELLED =  1;        /** waitStatus value to indicate successor's thread needs unparking */        static final int SIGNAL    = -1;        /** waitStatus value to indicate thread is waiting on condition */        static final int CONDITION = -2;        /**         * waitStatus value to indicate the next acquireShared should         * unconditionally propagate         */        static final int PROPAGATE = -3;        volatile int waitStatus;        volatile Node prev;        volatile Thread thread;        Node nextWaiter;        final boolean isShared() {            return nextWaiter == SHARED;        }        final Node predecessor() throws NullPointerException {            Node p = prev;            if (p == null)                throw new NullPointerException();            else                return p;        }        Node() {    // Used to establish initial head or SHARED marker        }        Node(Thread thread, Node mode) {     // Used by addWaiter            this.nextWaiter = mode;            this.thread = thread;        }        Node(Thread thread, int waitStatus) { // Used by Condition            this.waitStatus = waitStatus;            this.thread = thread;        }    }

首先他是AbstractQueuedSynchronizer的一个静态的内部类。这个跟LinkedList源码中的Node类似。我们刚才说的队列就是由这个Node组成。组成的队列就是AQS中的CLH队列。关于CLH队列后续会写一遍学习笔记来仔细记录。这里我们需要理解的就是这个Node会把在一个个获取资源的线程串联成一个FIFO(先进先出)的队列。这样就保证了公平性。因此addWaiter 就是如果这个队列不为空就直接把线程加入队列,加入失败就代表有并发竞争,那就进入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;                }            }        }    }

OK。这个时候线程已经被封装成一个节点,并且加入了队列中。接下来要做一件什么事情呢?线程加入队列不可能还要一直运行着。所以需要将它挂起来。所以这个任务就交给acquireQueued来实现。

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; //此处返回false.就是避免前面selfinterrupt方法执行。                }                if (shouldParkAfterFailedAcquire(p, node) && //如果不当前节点不是头节点或者头节点获取资源失败。那么就意味着要等待头节点的下一次获取,那么判断当前线程是否需要挂起。                    parkAndCheckInterrupt())   //如果当前线程需要挂起就调用LockSupport类中的park方法将线程挂起。然后进入下一次循环。                    interrupted = true;              }        } finally {            if (failed) //抛出异常就把节点从队列中移除                cancelAcquire(node);        }    }

shouldParkAfterFailedAcquire

   */    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {        int ws = pred.waitStatus;        if (ws == Node.SIGNAL)            /*             * This node has already set status asking a release             * to signal it, so it can safely park.             */            return true;        if (ws > 0) {            /*             * Predecessor was cancelled. Skip over predecessors and             * indicate retry.             */            do {                node.prev = pred = pred.prev;            } while (pred.waitStatus > 0);            pred.next = node;        } else {            /*             * waitStatus must be 0 or PROPAGATE.  Indicate that we             * need a signal, but don't park yet.  Caller will need to             * retry to make sure it cannot acquire before parking.             */            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);        }        return false;    }

判断当前是否需要挂起是通过 waitStatus值来判断。
Node节点中,除了存储当前线程,节点类型,队列中前后元素的变量,还有一个叫waitStatus的变量,改变量用于描述节点的状态,为什么需要这个状态呢?
原因是:AQS的队列中,在有并发时,肯定会存取一定数量的节点,每个节点 代表了一个线程的状态,有的线程可能可能“等不及”获取锁了,需要放弃竞争,退出队列,有点线程在等待一些条件满足,满足后才恢复执行(这里的描述很像某个J.U.C包下的工具类,ReentrankLock的Condition,事实上,Condition同样也是AQS的子类)等等,总之,各个线程有各个线程的状态,但总需要一个变量买描述它,这个变量就叫waitStatus,它有四种状态:

     /** waitStatus value to indicate thread has cancelled */        static final int CANCELLED =  1;         /** waitStatus value to indicate successor's thread needs unparking */        static final int SIGNAL    = -1;         /** waitStatus value to indicate thread is waiting on condition */  **重点内容**      static final int CONDITION = -2;         /**         * waitStatus value to indicate the next acquireShared should         * unconditionally propagate         */        static final int PROPAGATE = -3;  

CANCELLED:因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会被踢出队列,被GC回收;
SIGNAL:表示这个结点的继任结点被阻塞了,到时需要通知它;
CONDITION:表示这个结点在条件队列中,因为等待某个条件而被阻塞;
PROPAGATE:使用在共享模式头结点有可能牌处于这种状态,表示锁的下一次获取可以无条件传播;
当且仅当前一个节点处于SIGNAL时候,才需要挂起。

释放锁
获取锁之后,是必须要在最后释放锁的。理解了获取锁,释放锁就容易多了。
释放操作需要做哪些事情:
1. 因为获取锁的线程的节点,此时在AQS的头节点位置,所以,可能需要将头节点移除。
2. 而应该是直接释放锁,然后找到AQS的头节点,通知它可以来竞争锁了。

到此ReentrantLock的公平锁基本分析完毕。那么还有一个非公平锁。非公平锁其实就是抢占式的。先看源码:

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

源码中就是非公平锁的实现,就是在执行公平锁之前,先不管三七二十一,先去获取一次锁,如果获取成功,直接返回,否则就老老实实的排队进入公平锁。让我想到买火车票排队。人太多,有些人就懒得排队,就会跑到最前面问售票员,可不可以帮我先买一张,我很急。售票员如果给一个白眼,那么他就公平了,如果售票员同情心起来,卖给他,那就非公平性了。

总结
源码的学习就在于能知其然。所以可能学习的过程会比较困难。但是希望自己坚持这种学习方式。到这里ReentrantLock基本学习分析完毕。其中涉及到的CLH队列。会做一个新的篇章学习。最后画一个流程图。来表示ReentrantLock的基本流程:
这里写图片描述

0 0
原创粉丝点击