Class ReentrantLock
先看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; }
- 如果前一个节点的等待状态waitStatus<0,也就是前面的节点还没有获得到锁,那么返回true,表示当前节点(线程)就应该park()了。
- 如果前一个节点的等待状态waitStatus>0,也就是前一个节点被CANCELLED了,那么就将前一个节点去掉,递归此操作直到所有前一个节点的waitStatus<=0,
- 前一个节点等待状态waitStatus=0,修改前一个节点状态位为SINGAL,表示后面有节点等待你处理,需要根据它的等待状态来决定是否该park()。
- 返回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
相关推荐
然而,这也意味着Synchronized不具备显式的锁获取和释放控制,可能导致一些复杂情况下的锁管理不便。 相比之下,ReentrantLock(可重入锁)是Java并发包java.util.concurrent.locks中的一个类,提供了更细粒度的锁...
5. **锁的释放与资源清理**: 使用`finally`块确保了无论是否发生异常,`unlock()`方法总会被执行,从而释放锁。此外,`getName()`方法用于获取线程的名称,便于在控制台输出中识别是哪个线程在执行。 6. **性能...
ReentrantLock源码解析之释放锁unlock() ReentrantLock是一个可重入锁,它提供了一个unlock()方法来释放锁。在本章中,我们将深入探讨unlock()方法的源码,了解其释放锁的步骤和机制。 unlock()方法的主要作用是...
lock()方法用于获取锁,lockInterruptibly()方法用于可中断的获取锁,tryLock()方法用于尝试获取锁,tryLock(long time, TimeUnit unit)方法用于尝试获取锁,拥有超时机制,unlock()方法用于释放锁,newCondition()...
`ReentrantLock`类提供了一系列方法来控制锁的获取、释放以及查询锁的状态: - `ReentrantLock()`:创建一个非公平锁。 - `ReentrantLock(boolean fair)`:创建公平锁或非公平锁,取决于`fair`参数。 - `...
2. 显式锁支持可中断的锁获取,`Lock.tryLock()`方法允许尝试获取锁,如果没有获取到,可以立即返回。 3. 使用`Lock`的`tryLock()`方法配合`finally`块,可以确保即使在异常情况下也能正确释放锁,以维护_happens-...
与synchronized关键字相比,ReentrantLock提供了更高的灵活性,如尝试加锁、定时加锁和公平锁等功能。本文将深入探讨ReentrantLock的实现原理,主要涉及其内部类AbstractQueuedSynchronizer(AQS)和Unsafe工具类。 ...
这意味着一个线程可以多次获取同一锁,只要它在每次获取后都能正确释放。对于`ReentrantLock`,可以通过`lock()`和`unlock()`方法控制这一过程。线程在进入同步代码块之前调用`lock()`,退出时调用`unlock()`。由于...
ReentrantLock的`lock()`方法调用`Sync`的`lock()`,然后由`NonfairSync`或`FairSync`完成具体的锁获取逻辑。这个过程中涉及到的状态转换、线程阻塞与唤醒等细节,都需要通过阅读源码来详细了解。 总结,...
- 自动释放锁,当线程执行完同步代码块或方法后,会自动释放锁。 4. **局限性**: - 不支持公平锁,即线程获取锁的顺序并不总是按照它们请求锁的时间顺序进行。 - 无法中断正在等待获取锁的线程,除非抛出异常...
ReentrantLock 是 Java 中的一个同步工具类,它实现了 Lock 接口,提供了锁的获取和释放机制。ReentrantLock 的实现原理基于 AQS(AbstractQueuedSynchronizer),是一个重入锁,允许一个线程反复地获取锁而不会出现...
ReentrantLock提供了多种锁获取方式,包括但不限于: - `lock()`:无条件地尝试获取锁,如果锁已被其他线程占用,则当前线程会阻塞直至获得锁。 - `lockInterruptibly()`:与`lock()`类似,但支持响应中断。即在等待...
"锁的释放-获取建立的happens before关系"是指在并发执行的线程之间,当一个线程释放锁之后,另一个线程获取同一把锁时,它们之间存在一种特定的内存可见性保证。这种关系是由Java内存模型(Java Memory Model,JMM...
Java中的ReentrantLock是Java并发包(java.util.concurrent.locks)中的一个高级锁,它是可重入的,意味着一个线程可以多次获取同一锁。在深入ReentrantLock之前,我们首先需要了解Java并发编程的基础,特别是Java...
在本文中,我们将深入分析`ReentrantLock`的`lock()`方法,理解其内部机制,包括锁的获取、释放以及公平性和非公平性的实现。 首先,`ReentrantLock`的`lock()`方法很简单,它只是调用了内部类`Sync`的`lock()`方法...
- 锁的释放与获取必须在相同的堆栈帧中进行,这在某些场景下可能不够灵活。 - 缺乏高级特性,如定时等待、可中断等待等。 #### 三、ReentrantLock 类 `ReentrantLock`是`java.util.concurrent.locks`包下的一个...
- `ReentrantLock`需要显式地调用`lock()`和`unlock()`方法来获取和释放锁,且建议使用`try-finally`结构确保锁的释放,以防止异常导致的锁未释放问题。 总的来说,如果`synchronized`能满足需求,应优先使用,...
ReentrantLock的名字来源于它的可重入性,这意味着一个线程可以多次获取同一把锁,这在递归调用中特别有用。无论是ReentrantLock还是synchronized,它们都是可重入锁。 ReentrantLock在Java 5.0引入,最初因其性能...
- **可中断锁获取**: `lockInterruptibly()`方法使得在等待锁时,线程可以响应中断,这在某些情况下是必要的,比如当线程需要在等待锁时能够被外部中断。 - **读写锁**: 除了ReentrantLock,Java还提供了...
unlock()方法用于释放锁,当锁被释放后,其他线程可以获取锁。 ReentrantLock的使用示例代码如下所示: ```java public class ThreadDomain35 { private Lock lock = new ReentrantLock(); public void ...