`
guzizai2007
  • 浏览: 354194 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

ReentrantLock锁的获取与释放

 
阅读更多
java.util.concurrent.locks

Class ReentrantLock

All Implemented Interfaces:
Serializable, Lock

先看ReentrantLock的构造方法:

ReentrantLock()
Creates an instance of ReentrantLock.
ReentrantLock(boolean fair)
Creates an instance of ReentrantLock with the given fairness policy.

这里引入两个概念:公平锁和非公平锁

      如果获取一个锁是按照请求的顺序得到的,那么就是公平锁,否则就是非公平锁。

在公平锁上,线程将按照它们发出的请求的顺序来获取锁,但在非公平锁上,则允许“插队”,当一个新线程请求非公平锁时的同时锁状态变为可用,则该线程跳过队列中所有等待线程获取该锁,但是如果锁不可用,则还是会被放入到等待队列中。

 

为什么会用到非公平锁:

       这是因为通常情况下挂起的线程重新唤醒和真正运行之间会出现严重的延时,比如线程A在使用锁,线程B来访问锁被占挂起进入等待队列,当A释放锁时,B将 被唤醒,与此同时如果线程C也请求该锁,那么C可能在B完全唤醒之前获得使用以及释放锁,所以此时线程B获取锁的时间没有被推迟,而线程C也及时的获取了 锁,性能上就会比公平锁高。

       如果持有锁的时间相对较长,或者请求锁的平均时间间隔较长,就应该使用公平锁,这种情况下,持有锁时间长,请求锁的间隔时间长,对于“插队”带来的性能提升并不明显。

 

ReentrantLock默认构造函数就是提供了一个非公平锁,而Synchronized内置锁也不保证锁的公平性。


一、锁的获取

先来看看公平锁Lock的实现:

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);  //这里
        }
    }
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

       这段代码的实现也是比较简洁,先尝试一次tryAcquire操作,如果失败,则把当前线程加入到同步队列中去,这个时候可能会反复的阻塞与唤醒这个线程,直到后续的tryAcquire(看acquireQueued的实现)操作成功。

再看看tryAcquire的实现:

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();  //获取当前锁被持有的次数
            if (c == 0) {  //未被持有
                if (!hasQueuedPredecessors() &&    //判断同步队列是否为空 是否存在别的等待线程
                    compareAndSetState(0, acquires)) {  //修改state值
                    setExclusiveOwnerThread(current);  //设置锁所属线程
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {  //被当前线程多次重入
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);  //修改state
                return true;
            }
            return false;
}
public final boolean hasQueuedPredecessors() {
        Node t = tail;   
        Node h = head;
        Node s;
        return h != t &&    //第一次执行的时候  head和tail还未初始化 h == t
            ((s = h.next) == null || s.thread != Thread.currentThread());  //判断头节点后的第一个节点是否存在 或者 是否是当前线程
}

       这段代码是尝试获取锁的过程,它先判断当前的AQS的state值,如果为0,则表示该锁没有被持有过,如果这个时候同步队列是空的或者当前线程就是在同步队列的头部,那么修改state的值,并且设置排他锁的持有线程为当前线程。

       如果大于0,则判断当前线程是否是排他锁的持有线程,如果是,那么把state值加1(注意state是int类型的,所以state的最大值是就是int的最大值)

       如果第一次tryAcquire()操作失败,那么就把当前线程加入到等待队列中去,看addWaiter()方法:

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);  //创建当前线程等待结点
        Node pred = tail;
        if (pred != null) {  //判断尾节点是否为空 
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);  //头尾节点 这里进行初始化
        return node;
}

        上面这段首先创建当前线程的等待结点,并判断当前等待队列是否初始化,如果没有则进入enq()方法进行初始化并新增,enq()方法如下:

private Node enq(final Node node) {
        for (;;) {   //循环
            Node t = tail;
            if (t == null) { // 这里进行头尾结点初始化 初始化为new Node()对象
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {  //第二次进入这里 把新增结点放在队列尾部
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}

        最后在当前线程被加入到等待队列中去以后,再调用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)) {  //如果前一个节点是头节点,则立即执行tryAcquire()尝试获取锁
                    setHead(node);  //设置头节点为当前结点
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&   //获取锁失败后 判断是否阻塞当前线程
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}

       这段代码中拿到当前线程在同步队列中的前面一个节点,如果这个节点是是头部,那么马上进行一次tryAcquire操作,如果操作成功,那么把当前线程弹出队列,整个操作就此结束。如果这个节点不是头部或者说tryAcquire操作失败的话,那么就判断是不是要将当前线程给阻塞掉 (shouldParkAfterFailedAcquire)方法:判断当前线程是否应该被阻塞掉,实际上判断的是当前线程的前一个节点的状态,如果前一个节点的状态小于0(condition或者signal),那么返回true,阻塞当前线程;如果前一个节点的状态大于0(cancelled),则向前遍历,直到找到一个节点状态不大于0的节点,并且将中间的cancelled状态的节点全部踢出队列;如果前一个节点的状态等于0,那么将其状态置为 -1(signal),并且返回false,等待下一次循环的时候再阻塞。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;  //前一个节点的等待状态
        if (ws == Node.SIGNAL)  //如果是singnal则返回true,阻塞当前线程
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {  //如果大于0,表示前一个结点被cancle了,则把这个节点依次去掉 直到一个小于等于0的结点
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {  //默认刚创建的是0 ,把他修改成singnal,表示后面有线程在等待
            /*
             * 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;
}

 

    1. 如果前一个节点的等待状态waitStatus<0,也就是前面的节点还没有获得到锁,那么返回true,表示当前节点(线程)就应该park()了。
    2. 如果前一个节点的等待状态waitStatus>0,也就是前一个节点被CANCELLED了,那么就将前一个节点去掉,递归此操作直到所有前一个节点的waitStatus<=0,
    3. 前一个节点等待状态waitStatus=0,修改前一个节点状态位为SINGAL,表示后面有节点等待你处理,需要根据它的等待状态来决定是否该park()。
    4. 返回false,表示线程不应该park()。
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);  //阻塞当前获取锁的线程
        return Thread.interrupted();  //返回线程是否中断
}
public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        unsafe.park(false, 0L);
        setBlocker(t, null);
}

       整个锁的获取过程就是这样,我们再来总结一下整个过程:acquire()方法会先调用一次tryAcquire方法获取一次锁,如果失败,则把当前线程加入到等待队列中去,然后再调用acquireQueued获取锁,acquireQueued在当前节点不在头部的时候会把当前线程的前一个结点的状态置为SIGNAL,然后阻塞当前线程。当当前线程到了队列的头部的时候,那么获取锁的操作就会成功返回。

 

二、锁的释放

       首先,我们知道在acquireQueued方法中,如果一个线程成功获取到了锁,那么它就应该是整个等待队列的head节点,然后,我们再来看一看 unlock()方法,和lock()方法一样,unlock()方法也是只有一行代码,直接调用release()方法,我们看看release()方法的实现:

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)//头节点等待状态不为0
                unparkSuccessor(h); //唤醒等待队列结点
            return true;
        }
        return false;
}

      这个过程首先调用tryRelease方法,如果锁已经完全释放,那么就唤醒下一个节点,先来看看tryRelease方法:

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;  //当前锁持有次数-1
            if (Thread.currentThread() != getExclusiveOwnerThread()) //判断当前释放锁线程是否拥有锁
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
}

       这段代码首先获取当前AQS的state状态并且将其值减一,如果结果等于0(锁已经被完全释放),那么将排他锁的持有线程置为null。将AQS的state状态置为减一后的结果。

       然后再看看唤醒继任节点的代码:

private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus; //头节点等待状态  默认为0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);  //重置为0

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;  //判断下一个节点状态是否可用
        if (s == null || s.waitStatus > 0) {  //如果为null或者等待状态》0 不可用 则依次遍历
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)  //从尾部开始遍历  直到一个小于等于0的结点
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null) //唤醒此结点线程
            LockSupport.unpark(s.thread);
}

       这段代码先清除当前节点的waitStatus为0,然后判断下一个节点是不是null或者cancelled的状态,如果是,则从队列的尾部往前开始找,找到一个非cancelled状态的节点,最后唤醒这个节点。

       最后,总结一下释放操作的整个过程:其实整个释放过程就做了两件事情,一个是将state值减1,然后就是判断锁是否被完全释放,如果被完全释放,则唤醒继任节点。

 

三、整体过程描述

    看了上面的锁的获取与释放操作以后,整体过程还是比较清晰的,在文章的最后,我们把获取与释放操作串在一起在简单看一下:

  • 获取锁的时候将当前线程放入同步队列,并且将前一个节点的状态置为signal状态,然后阻塞
  • 当这个节点的前一个节点成功获取到锁,前一个节点就成了整个同步队列的head。
  • 当前一个节点释放锁的时候,它就唤醒当前线程的这个节点,然后当前线程的节点就可以成功获取到锁了
  • 这个时候它就到整个队列的头部了,然后release操作的时候又可以唤醒下一个。

转自:http://www.goldendoc.org/2011/06/lock_acquire_release/

           http://www.blogjava.net/xylz/archive/2010/07/06/325390.html

           http://www.blogjava.net/xylz/archive/2010/07/07/325410.html

分享到:
评论
发表评论

文章已被作者锁定,不允许评论。

相关推荐

    java ReentrantLock详解.docx

    ReentrantLock与synchronized来的区别 1.synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。 2.synchronized...

    教你完全理解ReentrantLock重入锁

    ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。在java关键字synchronized隐式支持重...

    Java中的ReentrantLock类最全讲义

    2.2 获取锁和释放锁 公平性与非公平性 3.1 公平锁 3.2 非公平锁 中断响应 条件变量与Condition 5.1 创建Condition 5.2 await()和signal() 可重入性 ReentrantLock与synchronized的对比 最佳实践与注意事项

    设计模式简述

    以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程调用lock()方法时,会调用tryAcquire()独占锁并将state+1。...但是要注意,获取多少次就要释放多少次,这样才能保证state回到零状态。

    Lock锁的底层原理完整版

    Lock锁的灵活性相比synchronized更高,它支持手动获取和释放锁,能够中断的获取锁以及超时获取锁。 具体来说,Lock锁有以下主要方法:lock()用于上锁,unlock()用于解锁,tryLock()尝试非阻塞地获取锁,tryLock...

    JUC核心类AQS的底层原理

    详细阐述了ReentrantLock通过AQS获取锁到释放锁的过程,附有关键方法的源码及注释

    腾讯面试题 你了解ReentrantLock吗?

    腾讯面试题 你了解ReentrantLock吗?...公平锁:多个线程申请获取同一资源时,必须按照申请顺序,依次获取资源。 非公平锁:资源释放时,任何线程都有机会获得资源,而不管其申请顺序。 具体原理分析 在ne

    synchronized ReentrantLock volatile Atomic 原理分析.docx

    原理 synchronized关键字是通过字节码指令来实现的 synchronized关键字编译后会在同步块...如果获取失败,那么当前线程阻塞,直到锁被对另一个线程释放 执行monitorexit指令时,计数器减一,当为0的时候锁释放

    Lock接口与synchronized关键字

    此外,Lock接口还提供了更灵活的锁获取方式,如可以尝试获取锁(tryLock),可以设置锁的获取超时时间等。这些功能使得Lock接口在处理复杂的并发场景时更具优势。 在性能方面,synchronized通常比Lock接口开销更小...

    关于synchronized、Lock的深入理解

    我们先说一说synchronized,当一个方法或者代码块被synchronized修饰,并执行到此方法或者代码块时,获取到锁并执行,其他线程进来拿不到锁就会一直等待,等待获取到锁的线程释放锁。而这里获取到锁的线程释

    带你看看Java的锁(二)-Semaphore

    带你看看Java的锁-Semaphore前言简介使用源码分析类结构图SyncNonfairSyncFairSyncSemaphore 构造函数Semaphore 成员方法获取释放总结 前言 简介 Semaphore 中文称信号量,它和ReentrantLock 有所区别,...

    并发锁核心类AQS学习笔记

    一、概念 AQS 是 AbstractQueuedSynchronizer 的简称,AQS 是一个抽象的队列式同步器框架,提供了...等到占有线程释放锁后唤醒队列中的任务争抢锁,这个队列为 CLH 队列。 使用state成员变量表示当前的同步状态,提供

Global site tag (gtag.js) - Google Analytics