目录
? ? ? ?ReentrantLock是Java中的一个并发工具类,可以实现可重入的互斥锁。与传统的synchronized关键字相比,ReentrantLock更加灵活,可以支持更复杂的并发控制。以下是ReentrantLock的一些特点以及常见使用方法:
1. 可重入性:与synchronized一样,ReentrantLock允许线程在已经拥有锁的情况下重复获取该锁,而不会造成死锁。
2. 公平性:ReentrantLock支持公平锁和非公平锁,默认情况下是非公平锁。在公平模式下,锁会按照请求的顺序分配给等待的线程,而在非公平模式下,锁可能分配给等待时间更短的线程。
3. 条件等待:ReentrantLock允许你使用newCondition
方法创建多个条件对象,用于在线程之间进行协调和通信。这些条件对象可以用于等待和唤醒线程。
4. 可中断性:ReentrantLock支持可中断的锁获取,如果线程在等待锁时被中断,可以通过捕获InterruptedException
来响应中断。
5. 超时等待:你可以使用tryLock(long timeout, TimeUnit unit)
方法来尝试获取锁,并在一定时间内等待,如果等待超时则返回结果,而不是一直阻塞等待。
1. 创建ReentrantLock实例
ReentrantLock lock = new ReentrantLock(); //非公平锁
ReentrantLock lock = new ReentrantLock(true); //公平锁
2. 获取锁
使用lock()
方法获取锁,如果锁已经被其他线程占用,则当前线程会阻塞,直到获取到锁。
lock.lock();
try {
// 业务代码
} finally {
lock.unlock(); // 必须在finally块中释放锁,以确保锁的释放
}
3. 条件等待/通知
class SharedQueue {
private Queue<Integer> queue = new LinkedList<>();
private int capacity = 5; // 队列容量
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
public void produce(int item) {
lock.lock();
try {
// 如果队列已满,等待
while (queue.size() == capacity) {
notFull.await();
}
// 生产者放入数据
queue.add(item);
System.out.println("Produced: " + item);
// 通知消费者队列非空
notEmpty.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void consume() {
lock.lock();
try {
// 如果队列为空,等待
while (queue.isEmpty()) {
notEmpty.await();
}
// 消费者取出数据
int item = queue.poll();
System.out.println("Consumed: " + item);
// 通知生产者队列非满
notFull.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
class ProducerConsumerExample {
public static void main(String[] args) throws InterruptedException {
SharedQueue sharedQueue = new SharedQueue();
// 创建生产者线程
Thread producerThread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
sharedQueue.produce(i);
try {
Thread.sleep(100); // 模拟生产耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Producer");
// 创建消费者线程
Thread consumerThread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
sharedQueue.consume();
try {
Thread.sleep(150); // 模拟消费耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Consumer");
// 启动线程
producerThread.start();
Thread.sleep(1000);
consumerThread.start();
}
}
? ?SharedQueue
类表示共享的队列。生产者使用produce
方法往队列中放入数据,如果队列已满,则调用notFull.await()
等待;消费者使用consume
方法从队列中取出数据,如果队列为空,则调用notEmpty.await()
等待。
4. 尝试获取锁
? ? ? ?使用tryLock()
方法可以尝试获取锁,如果锁已经被其他线程占用,则返回false。可以用于实现超时等待的逻辑。??
if (lock.tryLock()) {
try {
// 获取锁成功后的操作
} finally {
lock.unlock();
}
} else {
// 获取锁失败,处理其他逻辑
}
5. 可中断获取锁
? ? ? ?使用tryLock(long timeout, TimeUnit unit)
方法来尝试获取锁,如果在指定的时间内获取不到锁,返回false。当线程在等待获取锁的过程中被其他线程中断时,会抛出InterruptedException异常。
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 获取锁成功后的操作
} finally {
lock.unlock();
}
} else {
// 获取锁失败,处理其他逻辑
}
} catch (InterruptedException e) {
// 线程被中断,处理中断逻辑
}
6. 释放锁
? ? ?必须在finally
块中使用unlock()
来释放锁,以确保锁的正确释放,避免死锁。
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 必须在finally块中释放锁
}
1. 解决多线程竞争资源的问题,例如多个线程同时对同一个数据库进行写操作,可以使用ReentrantLock保证每次只有一个线程能够写入。
2. 实现多线程任务的顺序执行,例如在一个线程执行完某个任务后,再让另一个线程执行任务。
3. 实现多线程等待/通知机制,例如在某个线程执行完某个任务后,通知其他线程继续执行任务。
可重入锁保护资源举例如下:
class Example {
private final Lock lock = new ReentrantLock();
public void outerMethod() {
lock.lock(); // 第一次获取锁
try {
System.out.println("Entered outerMethod");
innerMethod(); // 在outerMethod内部调用另一个需要同一把锁的方法
} finally {
lock.unlock(); // 释放锁
}
}
private void innerMethod() {
lock.lock(); // 第二次获取锁,可重入
try {
System.out.println("Entered innerMethod");
// 执行一些操作
} finally {
lock.unlock(); // 释放锁
}
}
}
public class ReentrantLockExample {
public static void main(String[] args) {
Example example = new Example();
Thread thread = new Thread(() -> {
example.outerMethod();
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
? ? ? ?outerMethod()和innerMethod()都使用了同一把ReentrantLock来保护共享资源。在outerMethod()中获取锁后,它调用了innerMethod(),而innerMethod()再次获取了同一把锁,表示可重入锁。这种锁机制允许同一个线程在持有锁的情况下多次获取同一把锁,而不会造成死锁。
??Semaphore是Java并发包中的一个同步工具类,它可以用来控制同时访问某个资源的线程数量。可用于控制并发线程数,可以用作同步工具。以下是Semaphore的一些特点以及常见使用方法:
1. 许可证机制:Semaphore维护了一个许可证集合,线程在尝试获取许可证时会被阻塞,直到有可用的许可证。
2. 可用许可证数量:Semaphore有一个许可证计数器,表示可用的许可证数量。线程成功获取许可证后,计数器会减少;释放许可证后,计数器会增加。
1. 创建Semaphore实例
Semaphore semaphore = new Semaphore(3); // 创建一个包含3个许可证的Semaphore
2. 获取许可证
? ? ? 使用acquire()方法尝试获取许可证。如果没有可用的许可证,当前线程将被阻塞,直到有许可证可用。
try {
semaphore.acquire(); // 尝试获取一个许可证
// 访问共享资源或执行其他操作
} catch (InterruptedException e) {
// 处理中断异常
} finally {
// 释放许可证
semaphore.release();
}
3. 尝试获取许可证
使用tryAcquire()方法尝试获取许可证,如果成功返回true,否则返回false。
if (semaphore.tryAcquire()) {
try {
// 获取许可证成功后的操作
} finally {
semaphore.release(); // 释放许可证
}
} else {
// 获取许可证失败,执行其他逻辑
}
4. 获取多个许可证
semaphore.acquire(2); // 尝试获取2个许可证
// 执行需要2个许可证的操作
semaphore.release(2); // 释放2个许可证
5. 获取可用许可证数量
使用availablePermits()方法可以获取当前可用的许可证数量。
int permits = semaphore.availablePermits(); // 获取可用许可证数量
6. 动态增加许可证数量
使用release(int permits)方法可以动态地增加许可证的数量。
semaphore.release(3); // 增加3个许可证
7. 公平性和非公平性
Semaphore fairSemaphore = new Semaphore(3, true); // 创建一个公平的Semaphore
资源池:Semaphore可以用于实现资源池,以维护一组有限的共享资源。
限流:Semaphore可以用于限制对共享资源的并发访问数量,以控制系统的流量。
class SharedResource {
// 假设这是一个共享资源
private int sharedValue = 0;
// 使用Semaphore来控制对共享资源的访问数量
private Semaphore semaphore = new Semaphore(1); // 参数1表示只允许一个线程同时访问共享资源
public void accessSharedResource() {
try {
// 获取许可证,如果没有许可证可用,线程将被阻塞直到有许可证为止
semaphore.acquire();
// 访问共享资源
sharedValue++;
System.out.println("Thread " + Thread.currentThread().getId() + " is accessing shared resource. Value: " + sharedValue);
// 模拟一些工作时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放许可证
semaphore.release();
}
}
}
class MyThread extends Thread {
private SharedResource sharedResource;
public MyThread(SharedResource sharedResource) {
this.sharedResource = sharedResource;
}
@Override
public void run() {
// 在线程中访问共享资源
sharedResource.accessSharedResource();
}
}
public class SemaphoreExample {
public static void main(String[] args) {
// 创建一个共享资源实例
SharedResource sharedResource = new SharedResource();
// 创建多个线程,并让它们同时访问共享资源
for (int i = 1; i <= 5; i++) {
MyThread myThread = new MyThread(sharedResource);
myThread.start();
}
}
}
? ? ? ?在以上述例子中,SharedResource
类表示一个共享资源,Semaphore
用于限制对该资源的并发访问数量。每个线程在访问共享资源之前必须先获取许可证,如果没有可用的许可证,线程将被阻塞。在访问完成后,线程释放许可证,以便其他线程可以继续访问。这样可以确保同时只有一个线程在访问共享资源。
CountDownLatch是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集。
1. 计数器机制: CountDownLatch内部维护一个计数器,初始值由用户设置。该计数器可以通过countDown()方法递减,当计数器值变为零时,等待的线程可以继续执行。
2. 一次性: CountDownLatch是一次性的,一旦计数器减到零,就不能重置。如果需要类似的功能,并能重置计数器,可以考虑使用CyclicBarrier。
3. 等待阻塞: 方await()法会阻塞调用线程,直到计数器减至零。可以选择在等待过程中设置超时时间。
1. CountDownLatch(int count): 构造方法,初始化计数器的值。
2. void await(): 调用此方法的线程会阻塞,直到计数器减至零。
3. boolean await(long timeout, TimeUnit unit): 可以设置等待超时时间,超过指定时间仍未减至零则返回false。
4. void countDown(): 减少计数器的值,通常在任务完成时调用。
1. 并行任务同步:CountDownLatch可以用于协调多个并行任务的完成情况,确保所有任务都完成后再继续执行下一步操作。
2. 多任务汇总:CountDownLatch可以用于统计多个线程的完成情况,以确定所有线程都已完成工作。
3. 资源初始化:CountDownLatch可以用于等待资源的初始化完成,以便在资源初始化完成后开始使用。
以模拟百米赛跑举例如下:
public class CountDownLatchDemo {
// begin 代表裁判 初始为 1
private static CountDownLatch begin = new CountDownLatch(1);
// end 代表玩家 初始为 8
private static CountDownLatch end = new CountDownLatch(8);
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 8; i++) {
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
// 预备状态
System.out.println("参赛者"+Thread.currentThread().getName()+ "准备好了");
// 等待裁判吹哨
begin.await();
// 开始跑步
System.out.println("参赛者"+Thread.currentThread().getName() + "开始跑步");
Thread.sleep(1000);
// 跑步结束, 跑完了
System.out.println("参赛者"+Thread.currentThread().getName()+ "到达终点");
// 跑到终点, 计数器就减一
end.countDown();
}
}).start();
}
// 等待 5s 就开始吹哨
Thread.sleep(5000);
System.out.println("开始比赛");
// 裁判吹哨, 计数器减一
begin.countDown();
// 等待所有玩家到达终点
end.await();
System.out.println("比赛结束");
}