ReentrantLock
ReentrantLock是一种基于AQS(Abstract Queued Synchronizer)的应用实现,是JDK中一种线程并发的同步手段。
它的功能类似于sychronized,是一种互斥锁,可以保证线程安全。
sychronized是关键字,是JVM级别实现的悲观锁。
ReentrantLock是类级别实现的锁。
基本使用
ReentrantLock()
构造方法
- 传入参数 false/true 不传默认为false 使用的是非公平锁,true为公平锁实现
lock
当前线程获取锁方法
unlock
当前线程释放锁方法,如果当前线程不持有锁,则抛出IllegalMonitorStateException异常
为了确保线程不会因为异常而未解锁而导致的死锁问题,必须在finally语句块中解锁
同时为了保证线程能获取到锁,lock方法与try语句块之间不应该有其它代码。
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
private static ReentrantLock fairLock = new ReentrantLock(true);
private static ReentrantLock nonFairLock = new ReentrantLock();
public static void main(String[] args) {
System.out.println("非公平锁竞争:");
for (int i = 0; i < 10; i++) {
new Thread(()->{
nonFairLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"获取到锁");
}finally {
nonFairLock.unlock();
}
}).start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("公平锁竞争:");
for (int i = 0; i < 10; i++) {
new Thread(()->{
fairLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"获取到锁");
}finally {
fairLock.unlock();
}
}).start();
}
}
}
非公平锁竞争:
Thread-0获取到锁
Thread-1获取到锁
Thread-2获取到锁
Thread-3获取到锁
Thread-4获取到锁
Thread-5获取到锁
Thread-6获取到锁
Thread-7获取到锁
Thread-8获取到锁
Thread-9获取到锁
公平锁竞争:
Thread-10获取到锁
Thread-11获取到锁
Thread-12获取到锁
Thread-13获取到锁
Thread-14获取到锁
Thread-15获取到锁
Thread-16获取到锁
Thread-17获取到锁
Thread-18获取到锁
Thread-19获取到锁可见两种锁的结果是不同的,公平锁的获取锁顺序是有序的,而非公平锁则是无序的,后面将解释为什么两种锁的竞争结果不同。
AQS
由于ReentrantLock同步器底层是由AQS构建的,因此我们需要简单了解一下AQS
AQS 抽象队列式同步器
场景引入
有四个线程由于业务需求要同时占用某资源,但该资源在同一时刻只能被其中一个线程占用。也就是我们的共享资源场景。
AQS是一个集同步状态管理,线程阻塞,线程释放及队列管理功能于一身的同步框架。
核心思想:
提供了一个基于FIFO队列的框架,用于构建锁和其它同步器,这个框架用一个单一的整型变量state来表示同步状态。被放入队列的线程会保持阻塞直到被前驱结点唤醒。(确保了有序性,减少了不必要的竞争)
该队列中只有队首结点有资格竞争锁
核心组成部分
同步状态(State):AQS使用一个volatile变量来表示同步状态,这个状态可以表示锁的获取次数或者资源的可用性。
- **getState()**:获取当前的同步状态。
- **setState(int newState)**:设置当前的同步状态。
- **compareAndSetState(int expect, int update)**:如果当前状态值等于预期值
节点(Node)和队列:当线程尝试获取同步状态失败时,AQS会将这些线程封装成节点(Node)并加入到一个FIFO队列中。这个队列是AQS中管理线程等待的主要结构。
独占模式和共享模式:AQS支持两种同步模式。独占模式下,每次只有一个线程能够执行(ReentrantLock就是这种);共享模式下,多个线程可以同时执行。
获取和释放方法:AQS定义了
acquire
和release
等方法,允许线程以独占或共享的方式来获取和释放同步状态。等待和通知机制:AQS还提供了条件变量(Condition)的支持,允许线程在特定条件下等待,或者在条件满足时被通知唤醒。
AQS执行逻辑
- 线程1进入
- 如果锁可用(同步状态允许),直接获取锁
- 锁不可用(被占用),封装为Node放入FIFO队列并阻塞
- 非公平锁:线程2进入,尝试获取锁(自旋CAS),如果竞争失败,放入FIFO队尾,阻塞等待
- 公平锁:线程2进入,检查等待队列,确保无线程等待,若存在线程等待,加入队尾等待
- 线程1释放锁,更新同步状态,唤醒队列中下一个等待线程
- 线程2被唤醒,尝试获取锁
AQS特点
AQS利用CAS操作来管理同步状态,提高了并发效率(自旋锁+传统阻塞)
CAS操作在不放弃CPU时间片的情况下尝试重复获取锁(减少线程上下文切换)
同时AQS是许多同步器的基础,如ReentrantLock,Semaphore,CountDownLatch等
AQS使用模板方法模式,允许开发者通过继承AQS并重写指定方法来实现自定义的同步器
AQS支持条件变量,即允许线程在特定条件下等待,在特定条件满足时被唤醒
公平锁和非公平锁
公平锁:先来的线程先执行
在申请锁时如果有其它线程占用了锁,则进行排队等待
- 优点:线程资源都能得到分配,不会出现长时间得不到分配的情况
- 缺点:吞吐量会下降许多,队列里除了第一个线程,其它线程都会阻塞,CPU唤醒阻塞线程开销较大
非公平锁: 后来的线程有可能插队执行
- 优点: 减少CPU唤醒线程开销,整体吞吐效率会高一些,减少了线程上下文切换的开销
- 缺点: 被阻塞的线程可能会因为被插队而长时间获取不到资源
由此可见,我们之前的代码由于每个线程执行时间很短,竞争并不激烈。因此非公平锁模式下会经常发生插队,所以线程执行并不是FIFO的。
而公平锁模式下,线程的执行是遵循FIFO的。
可重入锁
可重入锁,也被称为递归锁,ReentrantLock就是典型的可重入锁。
核心:可重入锁的设计使得当同一个线程再次进入由它持有锁的代码块时,能再次获取锁并不被阻塞。
ReentrantLock对于AQS的state状态的定义是线程获取锁的次数,基于这种设计,ReentrantLock具备记忆锁次数的特性,因此当重复获取锁时可以通过state的值来判断当前线程是否持有锁。
ReentrantLock是什么?
基于上面的概念介绍,我们再来认识一下reentrantLock是什么。
- 一个基于AQS实现的同步器
- 支持公平锁和非公平锁两种模式
- 互斥锁(只能执行一个线程)
- 可重入锁
方法介绍
方法 | 注释 |
---|---|
lock() | 获取锁的方法,尝试获取锁,如果锁被占用,则阻塞等待。 |
unlock() | 一个线程在完成其临界区的代码后,应该调用这个方法来释放锁。 |
tryLock() | 尝试立即获取到锁,如果锁可用,返回true,如果锁被占用,返回false。 |
tryLock(long timeout, TimeUnit unit) | 这个方法尝试在给定的等待时间内获取锁,如果在指定的时间内锁变得可用,并且当前线程未被中断,它将返回true。 |
lockInterruptibly() | 这个方法类似于lock() ,但它允许在等待锁的过程中响应中断。如果当前线程在进入此方法时已经设置了中断状态,或者在等待锁的过程中被中断,它将抛出InterruptedException。 |
newCondition() | 这个方法返回一个与该锁绑定的新的Condition实例。Condition提供了一种在某个条件成立之前挂起线程的方法,并且在条件成立时能够唤醒一个或多个线程。(类似生产者消费者wait notify) |
getHoldCount() | 当前线程调用 lock() 方法的次数。 |
getQueueLength() | 当前正在等待获取 Lock 锁的线程的估计数。 |
getWaitQueueLength(Condition condition) | 当前正在等待状态的线程的估计数,需要传入 Condition 对象。 |
hasWaiters(Condition condition) | 查询是否有线程正在等待与 Lock 锁有关的 Condition 条件。 |
hasQueuedThread(Thread thread) | 查询指定的线程是否正在等待获取 Lock 锁。 |
hasQueuedThreads() | 查询是否有线程正在等待获取此锁定。 |
isFair() | 判断当前 Lock 锁是不是公平锁。 |
isHeldByCurrentThread() | 查询当前线程是否持有锁。 |
简单使用案例
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantExample {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
// 使用ReentrantLock增加计数
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantExample example = new ReentrantExample();
// 创建两个线程,模拟并发环境
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
t1.start();
t2.start();
// 等待两个线程完成
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出最终的计数值
System.out.println("Count is: " + example.count);
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以邮件至 1300452403@qq.com