JAVA多线程之——互斥锁ReentrantLock

30 篇文章 0 订阅

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的基本流程:
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值