ReentrantLock
ReentrantLock是一种可重入的独占锁,它允许同一个线程多次获取同一个锁而不被阻塞。它的功能类似于synchronized是一种互斥锁,可以保证线程安全。相对于synchronized,ReentrangLock具备以下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 与synchronized一样,都可支持重入。
ReentrantLock 主要应用于需要更精细控制并发访问、支持可中断锁等待、有超时获取锁需求以及条件变量协调的多线程场景中,以提高程序的并发效率和灵活性。
常用api
Lock接口
ReentrantLock实现了Lock接口规范,常见API如下:
| 方法名 | 描述 |
|---|---|
| void lock() | 获取锁,如果锁不可用,线程将阻塞到锁可用为止 |
| void lockInterruptibly() | 获取锁,但允许线程在等待锁的过程中被中断,抛出InterrupedException |
| boolean tryLock() | 尝试非阻塞的获取锁,如果可用返回 true,否则返回false |
| boolean tryLock(long time, TimeUnit unit) | 尝试获取锁,如果在指定时间内未获取到则返回false,可相应中断。 |
| void unlock() | 释放当前线程持有的锁。必须在持有锁的同一线程中调用。 |
| Condition newCondition() | 创建与锁绑定的条件变量,用于线程间的进一步同步。 |
| int getHoldCount() | 返回当前线程保持此锁的次数。 |
| boolean isHeldByCurrentThread() | 查询当前线程是否保持此锁。 |
| boolean isLocked() | 查询锁是否被任意线程持有。 |
使用时要注意的问题
- 默认情况下ReentrantLock为非公平锁而非公平锁。
- 加锁次数和释放锁的次数一定要保持一致,否则会导致现成阻塞或异常。
- 加锁操作一定要放在try代码之前,这样可以避免未加锁成功而又释放锁的异常。
- 释放锁一定要放在finally中,否则会导致线程阻塞。
ReentrantLock的使用
方法示例
lock()
private static Lock lock = new ReentrantLock();
private static int sum = 0;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
//获取锁
lock.lock();
try {
//执行临界区代码
sum++;
} catch (Exception e) {
} finally {
//释放锁
lock.unlock();
}
});
thread.start();
}
lock.lockInterruptibly();
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Thread t = new Thread(() -> {
try {
lock.lockInterruptibly();
try {
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等锁的过程中被中断");
}
});
t.start();
}
lock.tryLock()
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Thread t = new Thread(() -> {
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
return;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
log.debug("t1获得了锁");
} finally {
lock.unlock();
}
},"t1");
t.start();
}
公平锁和非公平锁
ReentrantLock支持公平锁和非公平锁两种模式:
- 公平锁:线程在获取锁时,按照等待的先后顺序获取锁。
- 非公平锁:线程在获取锁时,不按等待的先后顺序获取锁,而是随机获取锁。ReentrantLock
默认是非公平锁。
//ReentrantLock lock = new ReentrantLock(true); //公平锁
ReentrantLock lock = new ReentrantLock(); //非公平锁
可重入锁
可重入锁又名递归锁,是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。Java中的ReentrantLock和synchronized都是可重入锁。可重入锁的一个优点是可一定程度避免死锁,在该锁的保护区内再次请求这个锁时,会成功获取并增加锁的计数器,在实际开发中,可重入锁常常用于递归操作、调用同一个类中的其他方法、锁嵌套等场景中
Condition
java.util.concurrent类库中提供的Condition类来实现线程之间的协调。调用Condition.await()方法使线程等待,其他线程调用Condition.signal()或Condition.signalAll()方法唤醒等待的线程。调用Condition的await()方法和signal()方法,都必须在lock保护之内。
public class ReentrantLockDemo6 {
private static ReentrantLock lock = new ReentrantLock();
private static Condition cigCon = lock.newCondition();
private static Condition takeCon = lock.newCondition();
private static boolean hashcig = false;
private static boolean hastakeout = false;
//工作
public void cigratee(){
lock.lock();
try {
while(!hashcig){
try {
log.debug("没有工作,歇一会");
//当一个线程调用 condition.await() 方法时,它会首先释放当前持有的锁,然后使该线程进入等待状态,
// 直到其他线程调用同一条件变量的 signal() 或 signalAll() 方法来唤醒它。
// 此时,被唤醒的线程将尝试重新获取锁(如果可用的话),一旦成功获取锁,将继续执行 await() 方法之后的代码。
cigCon.await();
}catch (Exception e){
e.printStackTrace();
}
}
log.debug("干活");
}finally {
lock.unlock();
}
}
//送饭
public void takeout(){
lock.lock();
try {
while(!hastakeout){
try {
log.debug("没有饭,等一会");
takeCon.await();
}catch (Exception e){
e.printStackTrace();
}
}
log.debug("有饭了,吃饭");
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockDemo6 test = new ReentrantLockDemo6();
new Thread(() ->{
test.cigratee();
}).start();
new Thread(() -> {
test.takeout();
}).start();
new Thread(() ->{
lock.lock();
try {
hashcig = true;
log.debug("唤醒工作的等待线程");
cigCon.signal();
}finally {
lock.unlock();
}
},"t1").start();
new Thread(() ->{
lock.lock();
try {
hastakeout = true;
log.debug("唤醒送饭的等待线程");
takeCon.signal();
}finally {
lock.unlock();
}
},"t2").start();
}
}
应用场景
- 解决多线程竞争资源的问题,例如多个线程同时对一数据进行读写操作,可以使用ReentrantLock保证每次只有一个线程写入。
- 实现多线程任务的顺序执行,例如在一个线程执行完某个任务后,再让另一个线程执行任务
- 实现多线程等待/通知机制,例如在某个线程执行完某个任务后,通知其他线程继续执行。
既然有了synchronized为什么还要有ReentrangLock
尽管 synchronized 关键字为Java提供了基本的线程同步机制,但 ReentrantLock 类作为 java.util.concurrent.locks 包的一部分,提供了更高级和灵活的功能,以下是几点原因说明为何需要 ReentrantLock:
- 可中断的等待:
synchronized块中的等待是不可中断的,而ReentrantLock提供了lockInterruptibly()方法,允许等待锁的线程被其他线程中断,提高了响应性和程序的灵活性。 - 尝试锁定与超时:
ReentrantLock提供了tryLock()方法尝试非阻塞地获取锁,以及tryLock(long time, TimeUnit unit)方法尝试在一定时间内获取锁,如果无法获取则超时返回,这在某些需要避免长时间阻塞的场景下非常有用。 - 公平性选择:
ReentrantLock允许开发者选择锁的公平性策略。公平锁会按照线程等待的先后顺序分配锁,而非公平锁则允许插队,可能提高吞吐量但牺牲了一定的公平性。synchronized默认采用非公平锁,无法调整。 - 条件变量:
ReentrantLock配合Condition对象可以提供更细粒度的线程同步控制,比synchronized中的wait()和notify/notifyAll()更加灵活和强大,允许为不同的条件维护独立的等待集。 - 锁的持有计数:
ReentrantLock是可重入的,和synchronized一样,但它还提供了getHoldCount()方法来检查锁被当前线程持有的次数,这对于调试和复杂的同步逻辑是有帮助的。 - 更好的性能: 在某些场景下,尤其是在高竞争环境下,
ReentrantLock可能提供比synchronized更好的性能,因为JVM对锁的优化和实现细节不断进步,尤其是在Java 6及以后的版本中。
总的来说,虽然 synchronized 更简洁易用,但在需要更高级特性时,ReentrantLock 提供了更多选择和控制权,以适应更复杂多变的并发需求。
既然有了wait()notify()为什么还要有Condition
尽管 wait(), notify(), 和 notifyAll() 方法可以实现线程之间的基本通信,但是它们与特定的对象监视器(锁)紧密绑定,使用时相对局限,且不易于管理复杂的线程同步逻辑。相比之下,Condition 接口(通过 ReentrantLock 或其它锁类的 newCondition() 方法获得)提供了更加灵活和强大的线程协调机制,以下是几个关键点说明为什么需要 Condition:
- 多个等待条件: 在复杂应用中,一个对象可能需要基于不同条件来通知不同的等待线程。
Condition允许多个等待集,每个条件有自己的等待和通知机制,而wait()和notify()只能为对象维护单一的等待集。 - 精确通知:
notify()方法随机唤醒一个等待线程,而notifyAll()唤醒所有等待线程,它们都不够精确。使用Condition,你可以精确控制哪些线程被唤醒,通过signal()唤醒单个线程或signalAll()唤醒所有等待该条件的线程。 - 避免虚假唤醒: 使用
wait()和notify()时,需要在循环中检查条件以防止虚假唤醒(spurious wakeups)。而Condition的await()方法在内部已经处理了虚假唤醒的问题,使得代码更简洁、易于理解和维护。 - 与锁分离:
Condition对象与锁(如ReentrantLock)解耦,可以在不同的锁上创建多个条件变量,增加了设计的灵活性和模块化程度,使得锁和条件逻辑可以独立管理。 - 更清晰的API设计:
Condition的 API 设计(如await(),signal(),signalAll())更明确地表达了等待和通知的意图,相比wait()和notify()更符合面向对象的设计原则,提高了代码的可读性和可维护性。
综上所述,虽然 wait()、notify() 和 notifyAll() 在简单的同步场景下足够使用,但当涉及到更复杂的线程间协调和通信时,Condition 接口提供的精细控制和高级功能使其成为更优的选择。
Semaphore
Semaphore 是 Java 并发编程中一个重要的同步工具类,位于 java.util.concurrent 包中,它基于计数信号量的概念,用于控制同时访问特定资源或执行某个操作的线程数量。Semaphore 可以看作是一种资源计数器,它维护了一个许可集合,线程通过调用 acquire() 方法获取许可(减小许可数量),执行完毕后通过 release() 方法归还许可(增加许可数量)。这种机制有效地实现了对并发访问的控制,特别是在资源有限的场景下,比如数据库连接池、线程池的大小限制、文件读写等。
常用api
| 方法名 | 描述 |
|---|---|
| Semaphore(int permits) | 构造函数,创建具有指定许可数量的 Semaphore,默认非公平策略。 |
| Semaphore(int permits, boolean fair) | 构造函数,创建具有指定许可数量和公平性的 Semaphore。true 表示公平策略。 |
| void acquire() | 获取一个许可,如果无可用许可则阻塞当前线程。 |
| boolean tryAcquire() | 尝试非阻塞地获取一个许可,立即返回是否成功。 |
| boolean tryAcquire(long timeout, TimeUnit unit) | 尝试获取许可,等待指定时间后返回是否成功。 |
| int availablePermits() | 返回当前可用的许可数量。 |
| int drainPermits() | 立即减少并返回当前可用的所有许可数量。 |
Semaphore的使用
模拟停车场
public class ParkingLotDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(5); // 停车场有5个车位
for (int i = 0; i < 10; i++) { // 10辆车尝试停车
new Thread(() -> {
try {
semaphore.acquire(); // 尝试获取车位
System.out.println(Thread.currentThread().getName() + " 停车成功");
TimeUnit.SECONDS.sleep(2); // 模拟停车时间
System.out.println(Thread.currentThread().getName() + " 离开");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放车位
}
}, "Car " + i).start();
}
}
}
模拟数据库连接池
public class DatabaseConnectionPool {
/**
* 控制并发访问的信号量
*/
private final Semaphore semaphore;
/**
* 连接池中初始连接数
*/
private int connectionCount;
public DatabaseConnectionPool(int maxConnections) {
this.semaphore = new Semaphore(maxConnections);
this.connectionCount = maxConnections;
}
public void getConnection() {
try {
//尝试获取许可
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "获取数据库连接");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取连接时被中断", e);
}
}
public void releaseConnection() {
//释放许可
semaphore.release();
System.out.println(Thread.currentThread().getName() + "释放数据库连接");
}
public static void main(String[] args) {
//包含5个连接的池
DatabaseConnectionPool databaseConnectionPool = new DatabaseConnectionPool(5);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
int taskId = i;
executorService.execute(() -> {
//请求连接
databaseConnectionPool.getConnection();
try {
//模拟数据库耗时
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + " 完成数据库操作" + "TaskId" + taskId);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
databaseConnectionPool.releaseConnection();
System.out.println(Thread.currentThread().getName() + " 释放连接" + "TaskId" + taskId);
}
});
}
}
}
应用场景
- 资源池管理:如数据库连接池、线程池,限制同时访问资源的最大数量。
- 限流:在高并发系统中,作为限流器,控制同时访问服务的请求量,防止系统过载。
- 生产者消费者模型:控制生产和消费的速度,保证队列的稳定。
- 互斥访问控制:虽然
synchronized和ReentrantLock更常用作互斥锁,但在某些特定场景下,Semaphore 也可以用于控制对共享资源的并发访问,尤其是当需要控制并发度而不是简单的互斥时。
CountDownLatch
CountDownLatch(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集。 CountDownLatch使用给定的计数值(count)初始化。await方法会阻塞直到当前的计数值(count),由于countDown方法的调用达到0,count为0之后所有等待的线程都会被释放,并且随后对await方法的调用都会立即返回。count不会被重置。
常用api
| 方法名 | 描述 |
|---|---|
| CountDownLatch(int count) | 构造方法,初始化一个CountDownLatch实例,计数器设为count。 |
| void await() | 使当前线程等待,直到计数器达到零。若当前计数不为零,则阻塞。 |
| boolean await(long timeout, TimeUnit unit) | 类似于await(),但增加超时限制,超时后返回false。 |
| void countDown() | 递减计数器的值。如果新的计数为零,则释放所有等待的线程。 |
| long getCount() | 返回当前计数器的值。 |
CountDownLatch的使用
模拟多任务完成后合并汇总
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
final int index = i;
new Thread(() -> {
try {
Thread.sleep(1000+ ThreadLocalRandom.current().nextInt(2000));
System.out.println("任务"+index+"执行完成");
countDownLatch.countDown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
//主线程阻塞,当计数器为0,唤醒主线程往下执行
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("主线程执行完毕");
}
应用场景
- 并行任务同步:CountDownLatch可以用于协调多个并行任务的完成情况,确保所有任务都完成后再执行下一步操作。
- 多任务汇总:CountDownLatch可以用于统计多个线程的完成情况,以确定所有线程都已完成工作。
- 资源初始化:CountDownLatch可以用于等待资源的初始化完成,以便在资源初始化完成后开始使用
CycliBarrier
CycliBarrier(回环栅栏或循环屏障),是java并发库中的一种同步工具,通过它可以实现让一组线程等待至某个状态(屏障点)之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,Cyclibarrier可以被重用
常用api
| 方法名 | 描述 |
|---|---|
| CyclicBarrier(int parties) | 构造方法,创建一个新的 CyclicBarrier 实例,parties 表示需要等待的线程数。 |
| CyclicBarrier(int parties, Runnable barrierAction) | 构造方法,额外接受一个 Runnable,在每次所有线程到达屏障后执行。 |
| int getParties() | 返回需要等待的参与者数目。 |
| int getNumberWaiting() | 返回当前在屏障处等待的参与者数目。 |
| boolean isBroken() | 查询屏障是否处于损坏状态。 |
| void reset() | 重置屏障至初始状态。如果当前有线程在等待,则抛出异常。 |
| void await() | 使当前线程在屏障处等待,直到所有参与者都到达屏障或超时/被中断。 |
| long await(long timeout, TimeUnit unit) | 类似于 await(),但增加超时限制。如果在指定时间内未达到屏障,则返回false。 |
CyclicBarrier的使用
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
for (int i = 0; i < 5; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()
+ "开始等待其他线程");
// 阻塞直到指定的线程都调用此方法,继续执行
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + "开始执行");
//TODO 模拟业务处理
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "执行完毕");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
应用场景
- 多线程任务:Cyclibarrier可以用于将复杂的任务分配给多个线程执行,并在所有线程完成工作后触发后续操作。
- 数据处理:Cyclibarrier可以用于协调多个线程间的数据处理,在所有线程处理完数据后触发后续操作。
Cyclibarrier与CountDownLatch区别
- CountDownLatch是一次性的,Cyclibarrier是循环可用的
- CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。
Exchanger
Exchanger 是 Java 并发工具类之一,用于两个线程之间交换数据。它提供了一个同步点,在这个点上两个线程可以相遇并交换它们携带的对象。这对于需要两个线程协作完成任务,尤其是在交替处理数据或结果的场景中非常有用。
常用api
| 方法名 | 描述 |
|---|---|
| Exchanger | 构造方法,创建一个新的 Exchanger 实例。 |
| V exchange(V x) | 交换数据的方法。线程调用此方法时会被阻塞,直到另一个线程也调用了此方法,然后两个线程交换它们提供的对象(x)。 |
| V exchange(V x, long timeout, TimeUnit unit) | 带超时的交换数据方法。如果在指定时间内没有其他线程来进行交换,则该调用将返回 null(如果当前线程被中断,则抛出 InterruptedException)。 |
Exchanger的使用
private static final Exchanger<String> exgr = new Exchanger<String>();
private static ExecutorService threadPool = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
String A = "银行流水A";// A录入银行流水数据
exgr.exchange(A);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
String B = "银行流水B";// B录入银行流水数据
String A = exgr.exchange(B);
System.out.println("A和B数据是否一致:" + A.equals(B) +
",A录入的是:"+ A + ",B录入是:" + B);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadPool.shutdown();
}
应用场景
Exchanger可以用于各种应用场景,具体取决于Exchanger的实现。常见的场景包括:
1. 数据交换:在多线程环境中,两个线程可以通过Exchanger进行数据交换。
2. 数据采集:在数据采集系统中,可以使用Exchanger在采集线程和处理线程间进行数据交换。
Phaser
Phaser(阶段协同器)是一个Java实现的并发工具类,用于协调多个线程的执行。它提供了一些方便的方法来管理多个阶段的执行,可以让程序员灵活地控制线程的执行顺序和阶段性的执行。Phaser可以被视为CyclicBarrier和CountDownLatch的进化版,它能够自适应地调整并发线程数,可以动态地增加或减少参与线程的数量。所以Phaser特别适合使用在重复执行或者重用的情况.
常用api
| 方法名 | 描述 |
|---|---|
| Phaser() | 构造一个新的 Phaser 实例,没有注册任何参与者,没有父节点,初始化阶段值为 0。 |
| Phaser(int parties) | 构造一个新的 Phaser 实例,初始注册指定数量的参与者。 |
| Phaser(Phaser parent) | 构造一个新的 Phaser 实例,并指定其父节点,形成层次化同步结构。 |
| int register() | 注册一个新参与者到此 Phaser,返回此参与者注册后的阶段编号。 |
| int bulkRegister(int parties) | 批量注册多个参与者,返回最后注册的参与者的阶段编号。 |
| boolean arriveAndAwaitAdvance() | 表示当前参与者到达屏障,如果所有参与者都到达,则推进到下一个阶段并返回 true;否则返回 false。 |
| boolean arrive() | 表示当前参与者到达屏障,但不等待其他参与者,仅返回当前阶段号。 |
| int arriveAndDeregister() | 表示当前参与者到达屏障,等待其他参与者,阶段推进后注销当前参与者,并返回新阶段号。 |
| int getPhase() | 返回当前阶段号。 |
| int getRegisteredParties() | 返回当前注册的参与者数量。 |
| boolean isTerminated() | 检查此 Phaser 是否终止,即没有活动参与者且阶段已结束。 |
| void advance() | 强制推进到下一个阶段,即使没有参与者到达。 |
Phaser的使用
// 创建一个Phaser实例,初始parties为3,表示初始时有3个参与者
Phaser phaser = new Phaser(3);
// 注册额外的参与者
phaser.register();
// 使用Lambda表达式创建并启动线程
IntStream.range(0, 4).forEach(i -> {
new Thread(() -> {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "到达阶段1");
// 到达阶段1,等待其他线程
phaser.arriveAndAwaitAdvance();
// 模拟执行任务
int taskDuration = ThreadLocalRandom.current().nextInt(1000);
try {
Thread.sleep(taskDuration);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadName + "完成任务,到达阶段2");
// 完成任务后,到达阶段2,再次等待其他线程
phaser.arriveAndAwaitAdvance();
// 最后阶段,到达阶段3
System.out.println(threadName + "到达最终阶段");
phaser.arriveAndDeregister(); // 到达并注销自己
}, "Thread-" + i).start();
});
// 主线程等待所有参与者完成最后阶段
while (phaser.getRegisteredParties() > 0) {
phaser.arriveAndAwaitAdvance(); // 主线程也需要推进阶段
}
System.out.println("所有任务完成,程序结束。");
}
应用场景
- 多线程任务分配:用于将复杂的任务分配给多个线程执行,并协调线程间的合作。
- 多级任务分配:用于实现多级任务流程,在每一级任务完成后触发下一级任务的开始
- 模拟并行计算:用于模拟并行运算,协调多个线程间的工作。
- 可以用于实现阶段性任务,在每一阶段任务完成后触发下一阶段任务的开始。