Java并发编程-锁(一)
文章目录
- Lock 接口
- API
- 使用示例
- 超时锁获取(避免死锁,响应中断):
- 条件等待/通知(基于 `Condition`):
- AQS 队列同步器
- 同步状态
- 同步队列
- 同步器将节点加入到同步队列
- 首节点移出同步队列
- 节点
- 未完待续...
这一章节将会给大家介绍Java并发包中与锁相关的API和组件,包括它们的使用方式和实现 细节。
- 使用:通过示例演示这些组件的使用方法以及详细介绍与锁相关的API;
- 实现:通过分析源码来剖析实现细节,因为理解实现的细节我们才能更加得心应手且正 确地使用这些组件。
希望通过以上两个方面的解读让大家对锁的使用和实现两个层面有一定的了解。
Lock 接口
锁是用来控制多个线程访问共享资源的方式,一般来说,锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。
在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java 5之后,新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的功能,只是在使用时需要显式地获取和释放锁。
虽然它不能像使用synchronized 那样隐式获取释放锁,但是却能灵活获取与释放锁、可中断的获取锁以及超时获取锁,这些都是synchronized所不具备的同步特性。
Lock的使用也很简单,来看下代码
Lock lock = new ReentrantLock();
lock.lock(); // 锁获取在try块外
try {// 临界区操作
} finally {lock.unlock(); // 确保仅当锁被成功获取时才释放
}
在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。
注意这里不要把获取锁的过程写在try块中,因为如果在获取锁的时候发生了异常, 没有获取到锁,最后还是会调用unlock方法,导致unlock 异常。
锁的释放条件:在
ReentrantLock
中,unlock()
必须由持有锁的线程调用。若线程未持有锁调用unlock()
,会触发IllegalMonitorStateException
。释放逻辑:
tryRelease()
方法内部会检查当前线程是否持有锁,否则直接抛出异常
protected final boolean tryRelease(int releases) {if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();// ...其他逻辑
}
Lock是一个接口,它定义了锁获取和释放的基本操作, 看下 Lock提供的API。
API
方法签名 | 功能说明 | 适用场景 |
---|---|---|
void lock() | 获取锁。若锁已被占用,则当前线程进入等待状态,直到锁被释放。 | 基础锁获取,无需响应中断或超时。 |
void unlock() | 释放锁。必须在 finally 块中调用以确保锁的释放。 | 与 lock() 配对使用,防止资源泄漏。 |
boolean tryLock() | 非阻塞尝试获取锁。立即返回获取结果(成功为 true ,失败为 false )。 | 需即时判断锁状态的场景(如避免死锁)。 |
boolean tryLock(long time, TimeUnit unit) | 可超时尝试获取锁。在指定时间内尝试获取锁,超时则返回 false ,被中断则抛出InterruptedException 。 | 需要限制等待时间的同步操作。 |
void lockInterruptibly() | 可中断获取锁。若锁被其他线程占用,当前线程进入等待状态,但可响应中断(抛出 InterruptedException )。 | 需要支持中断的线程协作场景。 |
Condition newCondition() | 创建与该锁绑定的 Condition 对象,用于线程间的等待/通知机制。 | 实现复杂的线程协调(如多条件等待)。 |
这里只是简单介绍一下Lock接口的API,随后会详细介绍同步器 AbstractQueuedSynchronizer以及常用Lock接口的实现ReentrantLock。
Lock接口的实现基本都是 通过聚合了一个同步器的子类来完成线程访问控制的。
使用示例
超时锁获取(避免死锁,响应中断):
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockInterruptDemo {private static final Lock lock = new ReentrantLock();public static void main(String[] args) throws InterruptedException {// 线程1:长时间持有锁Thread thread1 = new Thread(() -> {lock.lock();try {System.out.println("Thread-1 获取锁,将长期持有");TimeUnit.MINUTES.sleep(1); // 模拟长时间任务} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock();System.out.println("Thread-1 释放锁");}});thread1.start();// 线程2:尝试超时获取锁,并在等待中被中断Thread thread2 = new Thread(() -> {System.out.println("Thread-2 尝试获取锁...");try {boolean acquired = lock.tryLock(5, TimeUnit.SECONDS);if (acquired) {try {System.out.println("Thread-2 成功获取锁");} finally {lock.unlock();}} else {System.out.println("Thread-2 超时未获取锁");}} catch (InterruptedException e) {System.out.println("Thread-2 在等待锁时被中断!");Thread.currentThread().interrupt(); // 恢复中断标志}});thread2.start();// 主线程稍后中断thread2TimeUnit.SECONDS.sleep(1);thread2.interrupt(); // 触发中断}
}
条件等待/通知(基于 Condition
):
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition(); // 创建条件对象// 等待方
public void await() throws InterruptedException {lock.lock();try {condition.await(); // 释放锁并等待信号} finally {lock.unlock();}
}
// 通知方
public void signal() {lock.lock();try {condition.signal(); // 发送唤醒信号} finally {lock.unlock();}
}
AQS 队列同步器
AQS 比较关键, 可以说只有掌握了同步器的工作原理才能更加深入地理解并发包中其他的并发组件。
我们先看下定义,队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成线程的排队工作。
同步器是实现锁的关键,当然不只是锁,也可以是任意同步组件。在锁的实现中聚合同步器(静态内部类),利用同步器实现锁的语义。可以这样理解二者之间的关系:
-
锁(同步组件)是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;
-
同步器面向的是锁的实现者, 它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。
锁和同步器很好地隔离了使用者和实现者所需关注的领域。
我们先从宏观上来看 AQS 的两个核心数据结构, 即同步状态和同步队列(Node节点)。
同步状态
通过 volatile int state
表示资源状态(如锁的持有次数、信号量计数)。
private volatile int state; // volatile 保证多线程的可见性和防止重排序
使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来对同步状态进行操作,因为它们能够保证状态的改变是安全的。
-
getState():获取当前同步状态。
-
setState(int newState):设置当前同步状态。
-
compareAndSetState(int expect,int update):使用CAS设置当前状态,这个方法能够保证状态设置的原子性。CAS 操作包含两个操作数:预期数值和新值。CAS 的实现逻辑是将实际数值与预期数值想比较,若相等,则将数值替换为新值。若不相等,则不做任何操作。
State 用来控制能不能获得锁资源,如果获取不到锁,则会让线程在同步队列中等待。而Node就是用来保存等待的线程。
同步队列
当前线程获取同步状态失败时,同步器会将当前线程 以及 等待状态等信息 构造成为一个节点(Node),并加入同步队列,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,
Node 是AQS 里定义的内部类,看下代码:
static final class Node {//...// 前驱节点(prev)与后继节点(next)的引用,构成双向链表volatile Node prev;volatile Node next;volatile Thread thread; // 关联的线程对象,表示在同步队列中等待资源的线程//...
}
同步器拥有首节点(head) 和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入这个队列的尾部。
同步器将节点加入到同步队列
在图中,同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。 试想一下,当一个线程已经获取了同步状态(或者锁),其他线程将无法获取到同步状态,会被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全。因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update)
,它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式 与之前的尾节点建立关联。
首节点移出同步队列
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态 时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点:
在图中,设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点,然后断开原首节点的next引用即可。
节点
来看下节点的属性类型和名称 (waitStatus 可以暂时跳过)
节点是构成同步队列和等待队列的基础,等待队列我们在后面提到Condition的时候会详细讲解。
到这里,大家应该能大概了解 AQS 的基本原理,接下来,我们就来从源码实现角度,分析AQS是怎么完成独占式同步状态的获取与释放的。