ReentrantLock简介
ReentrantLock是一种可重入的独占锁。ReentrantLock构造方法:
//默认构建非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//传入公平参数,构建公平锁/非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
从构造方法可知,ReentrantLock支持公平锁FairSync和非公平锁NonfairSync。默认情况下,创建ReentrantLock实例会得到一个非公平锁。其锁的功能由内部类Sync完成,Sync扩展自AQS,并依赖AQS的基础行为。Sync的抽象方法lock()由子类FairSync和NonfairSync各自实现。先以NonfairSync为例介绍非公平锁的加锁和解锁原理。
非公平锁
NonfairSync是ReentrantLock的非公平锁实现,它主要实现Sync中定义的抽象方法lock()和AQS中未实现的抽象方法tryAcquire。
加锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
//执行锁定
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//尝试获取锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
加锁时,首先通过CAS操作将状态从0修改为1,如果修改成功,则修改锁的持有者为当前线程,加锁成功。
什么是CAS?这里简单说明下:
CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。
如果修改状态失败,说明锁的状态不为0,仍被其他线程持有。调用acquire(1),acquire在AQS中提供了默认实现:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
调用tryAcquire()尝试获取锁,tryAcquire()调用nonfairTryAcquire方法,进入源码:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果锁状态为0,则尝试锁定,成功返回true
//这里是为了在并发条件下增加获取锁的可能性;若刚好之前持有锁的线程已经释放锁,则当前线程可再次尝试
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}//否则,如果当前线程为锁的持有者,则state+acquires,这里即体现了ReentrantLock的可重入性
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;
}
如果此次尝试成功获取锁,直接返回。否则获取锁失败,将当前线程加入等待队列。ReentrantLock是可重入的独占锁,因此以独占模式在链表中添加等待节点。
private Node addWaiter(Node mode) {
//创建当前结点
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//tail节点不空,则使用CAS操作将node添加到链表尾部,成功则返回;失败则进入enq
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//tail节点为空,调用enq初始化链表
//或节点加入链表尾部CAS操作失败,说明当前存在竞争
enq(node);
return node;
}
private Node enq(final Node node) {
//由于这里存在多线程并发问题,使用自旋保证node能够添加到链表中,因此enq本身是线程安全的
for (;;) {
Node t = tail;
//再次判断tail指向的节点是否为空,之所以再次判断,可能有其他线程已经初始化链表
if (t == null) {
//多线程并发时,只有一个线程执行compareAndSetHead返回成功,执行失败的线程将进入else代码块
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
//这里同样存在多线程并发,一次只会有一个线程成功,失败的线程将进入下一次循环,直至成功
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter保证将节点加入等待队列。若队列非空,首先会尝试一次将节点插入队列,若成功则无需进入自旋代码块。通常情况下,没有竞争时,无需自旋即可完成。进入自旋通常是因为CAS操作竞争比较激烈。当前节点加入等待队列之后,进入acquireQueued等待挂起。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取当前结点的前驱节点
final Node p = node.predecessor();
//前驱节点是head,表示当前节点之前已无节点在等待锁,再次尝试获取锁
//这里不会和等待队列中的其他线程发生锁竞争,但会和尝试获取锁且尚未进入等待队列的线程发生竞争
if (p == head && tryAcquire(arg)) {
//获取锁成功,则设置node为新的head
setHead(node);
//释放节点,帮助GC
p.next = null;
failed = false;
return interrupted;
}
//前驱节点不是head
//parkAndCheckInterrupt会挂起当前线程,直到调用release收到唤醒信号
if (shouldParkAfterFailedAcquire(p, node)&&parkAndCheckInterrupt())
interrupted = true;//如果线程被中断,则将interrupted设为true
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//将node设为头部节点,并将thread和prev置空
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
如果当前节点的前驱节点为头节点,当前线程会再次尝试获取锁。如果再次尝试失败或当前节点的前驱节点不是头节点,调用shouldParkAfterFailedAcquire判断是否应当挂起当前线程,看实现:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//表示pred的后继节点node可以安全的挂起
if (ws == Node.SIGNAL)
return true;
//表示pred处于取消状态
if (ws > 0) {
do {
//从链表中移除pred,一直循环直到pred前面的节点不是取消状态
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//修改next指针
pred.next = node;
} else {
//ws为0或者PROPAGATE,将pred状态设置为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//阻塞当前线程,并返回线程的中断情况
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);
}
private static void setBlocker(Thread t, Object arg) {
unsafe.putObject(t, parkBlockerOffset, arg);
}
只有当前节点的前驱节点的状态为SIGNAL时,线程可安全挂起。其他情况线程会再次进入自旋。
总体看来,shouldParkAfterFailedAcquire就是靠前驱节点判断当前线程是否应该被挂起,如果前驱节点为SIGNAL,则挂起当前线程;如果前继节点处于CANCELLED状态,移除取消节点,并更新当前节点pred指针;前驱节点状态为0(默认进入队列即为0)或PROPAGATE,设前驱节点的状态为SIGNAL,当前线程再次进入自旋,还是未获取锁就会被安全挂起。acquireQueued是一个自旋操作,线程在此挂起,直到被唤醒,满足p == head,执行tryAcquire(arg)尝试获取锁,成功则将当前节点设为新的head,该操作非常重要(想想为什么,后面解释)。
解锁
解锁操作实际调用AQS中的release方法:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
//head不空且waitStatus不为0,唤醒后继节点;waitStatus为0则说明无线程在等待队列中。
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release首先调用tryRelease尝试释放锁。
protected final boolean tryRelease(int releases) {
//如果线程被锁定多次,则需要进行多次释放
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
调用tryRelease返回true,说明锁已释放,判断head节点,若还有后继节点,调用unparkSuccessor进行唤醒。执行唤醒操作前,首先将当前节点(这里是head)的waitStatus置为0,再判断后继节点是否为取消状态,若为取消状态,需要使用pred指针从tail向前遍历,找到最靠近head的非CANCEL节点,使用LockSupport进行唤醒。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//获取后继节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {//s为空或node处于取消状态
s = null;
//从链表尾部向前遍历,直到找到位于链表最前端且waitStatus小于0的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒线程
}
被唤醒的线程将继续在acquireQueued的自旋操作中进行锁竞争,直到成功获取锁。
上面提到acquireQueued中,线程(假设为线程B)获取锁之后,会调用setHead将当前节点设为新的head。原因是,当持有锁的线程(假设为线程A)释放锁之后,是否需要唤醒后继节点是根据head的waitStatus是否为0来判定,当调用unparkSuccessor进行唤醒时,又会将head的waitStatus置为0。假设线程B之后仍然有线程C在阻塞,线程A唤醒B后将head的waitStatus置为0,若不执行setHead,线程B释放锁时,看到head的waitStatus为0,则将不会唤醒线程C,线程C及后面的节点将一直阻塞。当执行setHead之后,若线程B之后仍然有线程在阻塞,则新的head节点waitStatus必然是SIGNAL,唤醒操作将有序进行。好精妙,佩服!!!
公平锁
FairSync是ReentrantLock的公平锁实现,公平锁的加锁原理:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
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;
}
}
由FairSync源码可知,加锁方法直接调用acquire,然后调用tryAcquire方法。与NonfairSync最大的区别就在于tryAcquire实现不同,首先调用hasQueuedPredecessors确定队列中没有等待线程,或者队列中第一个等待者为当前线程,FairSync才会尝试获取锁。否则,直接进入等待队列。
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
//h != t,说明队列不空
//(s = h.next) == null,队列中无排队线程
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
公平锁实现完全遵循FIFO的原则,在进入等待队列之前不会存在锁竞争的问题。即所有线程按公平原则获取锁,如果锁未被占用且队列为空,获取锁;如果队列为空,自动去排队,不去争抢锁。通常情况下,公平锁的效率不如非公平锁。
相关推荐
3. **并发控制**:Java提供了多种并发控制工具,包括synchronized、wait()、notify()、notifyAll()、ReentrantLock(可重入锁)、Semaphore(信号量)和CountDownLatch(倒计时器)等。这些工具用于协调不同线程的...
此外,还介绍了高级的锁接口`java.util.concurrent.locks`,如`ReentrantLock`,它提供了更细粒度的控制,支持公平性和非公平性锁定,以及可中断和可重入的特性。 Java内存模型(JMM)是理解并发编程中数据一致性...
- **Lock接口与ReentrantLock** 提供了比`synchronized`更细粒度的锁控制,可以实现公平锁和非公平锁,以及可中断和可重入的特性。 4. **并发设计模式** - **生产者-消费者模式** 使用队列作为缓冲区,一个线程...
锁机制是Java并发编程中的另一大主题,包括内置锁(互斥锁)和显式锁(如`ReentrantLock`)。内置锁是`synchronized`关键字提供的,而显式锁提供了更细粒度的控制和更丰富的功能。书中可能还会讨论读写锁(`...
Lock接口提供了更细粒度的锁控制,可以实现公平锁、非公平锁、可重入锁等。Atomic类提供了原子操作,适用于简单的共享变量更新场景。 Java内存模型(JMM)是理解并发编程中内存可见性问题的基础。它定义了线程如何...
第四部分深入探讨了Java并发编程的高级主题,包括显式锁(如ReentrantLock)、原子变量(Atomic类)、非阻塞算法以及自定义同步组件的开发。这些高级主题帮助开发者解决复杂并发场景下的问题,实现更高层次的并发...
Java源码解析之可重入锁ReentrantLock ReentrantLock是一个可重入锁,在ConcurrentHashMap中使用了ReentrantLock。它是一个可重入的排他锁,它和synchronized的方法和代码有着相同的行为和语义,但有更多的功能。 ...
根据提供的信息,“Java 并发编程实战.pdf”这本书聚焦于Java并发编程的实践与应用,旨在帮助读者深入了解并掌握Java中的多线程技术及其在实际项目中的应用技巧。虽然部分内容未能提供具体章节或实例,但从标题及...
《Java并发编程的艺术》这本书是Java开发者深入理解并发编程的重要参考书籍。这本书全面地介绍了Java平台上的并发和多线程编程技术,旨在帮助开发者解决在实际工作中遇到的并发问题,提高程序的性能和可伸缩性。 ...
Java并发编程实践是Java开发中不可或缺的一个领域,它涉及到如何高效、正确地处理多线程环境中的任务。这本书的读书笔记涵盖了多个关键知识点,旨在帮助读者深入理解Java并发编程的核心概念。 1. **线程和进程的...
综上所述,《Java并发编程实战》不仅涵盖了Java并发编程的基础知识和技术细节,还包含了丰富的实践经验和前瞻性的思考,是任何一位从事Java开发工作的程序员不可或缺的学习资源。无论是初学者还是有经验的开发者都能...
《Java并发编程实践》是一本深入探讨Java多线程编程的经典著作,由Brian Goetz、Tim Peierls、Joshua Bloch、Joseph Bowles和David Holmes等专家共同编写。这本书全面介绍了Java平台上的并发编程技术,是Java开发...
《JAVA并发编程艺术》是Java开发者深入理解和掌握并发编程的一本重要著作,它涵盖了Java并发领域的核心概念和技术。这本书详细阐述了如何在多线程环境下有效地编写高效、可靠的代码,对于提升Java程序员的技能水平...
"Java并发编程与实践"文档深入剖析了这一主题,旨在帮助开发者理解和掌握如何在Java环境中有效地实现并发。 并发是指在单个执行单元(如CPU)中同时执行两个或更多任务的能力。在Java中,这主要通过线程来实现,...
5. **ReentrantLock**: 这是可重入的互斥锁,比`synchronized`关键字更灵活,支持公平锁和非公平锁,以及可中断和定时等待。 6. **Atomic* 类**: 如AtomicInteger、AtomicLong等,提供了原子操作,保证在并发环境下...
《Java 并发编程实战》是一本专注于Java并发编程的权威指南,对于任何希望深入了解Java多线程和并发控制机制的开发者来说,都是不可或缺的参考资料。这本书深入浅出地介绍了如何在Java环境中有效地管理和控制并发...
3. ReentrantLock:可重入锁,提供比synchronized更细粒度的控制,支持公平锁和非公平锁。 4. Condition:ReentrantLock的条件对象,可以实现更灵活的线程等待和唤醒。 五、并发工具类 1. CountDownLatch:一次性...
### Java并发编程实践 #### 一、并发编程基础 ##### 1.1 并发与并行的区别 在Java并发编程中,首先需要理解“并发”(Concurrency)和“并行”(Parallelism)的区别。“并发”指的是多个任务在同一时间段内交替...
Java并发编程涉及到许多关键概念,如线程、同步、锁、并发容器、原子变量以及并发工具类等。以下是一些主要的知识点: 1. **线程基础**:线程是程序执行的最小单位,Java中的`Thread`类提供了创建和管理线程的方法...