并发编程中的AQS(AbstractQueuedSynchronizer)详解
1. AQS是什么?有什么用?
AQS(AbstractQueuedSynchronizer)是Java中用于构建各种同步器的框架
。它提供了一种可扩展的、高效的同步机制,可以用于构建各种类型的同步器,比如独占锁、共享锁、信号量等。
AQS的主要作用在于提供了一种通用的机制,让开发人员能够相对容易地构建出符合特定需求的同步器
,并且隐藏了底层的实现细节,提供了更高层次的抽象。它通过FIFO队列(双向链表)
来管理等待线程,并提供了一些方法供子类去实现不同类型的同步器。AQS主要提供了两种方式来实现同步器:独占模式和共享模式
,在独占模式
下,只有一个线程可以获取到锁
,而在共享模式
下,多个线程
可以同时获取到锁。
具体来说,AQS的使用场景包括但不限于:
实现独占锁(ReentrantLock)
:通过AQS,可以实现可重入锁,允许同一个线程多次获得该锁;实现共享锁(Semaphore、CountDownLatch)
:AQS也可以用于实现一些共享资源的控制,例如信号量、倒计时器等;- 实现自定义同步器:开发者可以基于AQS自定义各种同步器,满足特定的同步需求。
2. 有哪些常见的AQS锁?
在Java中,使用AQS(AbstractQueuedSynchronizer)框架可以实现各种类型的锁。以下是一些常见的基于AQS的锁:
-
ReentrantLock(可重入锁):
- ReentrantLock 是基于AQS实现的独占锁,支持可重入特性,
同一个线程可以多次获取该锁而不会造成死锁
。
- ReentrantLock 是基于AQS实现的独占锁,支持可重入特性,
-
ReadWriteLock(读写锁):
- ReadWriteLock 接口提供了读锁和写锁两种类型的锁,
其中读锁是共享锁,写锁是独占锁
。ReentrantReadWriteLock 是基于AQS实现的 ReadWriteLock 接口的实现类。
- ReadWriteLock 接口提供了读锁和写锁两种类型的锁,
-
Semaphore(信号量):
- Semaphore 是一个计数信号量,
用于控制同时访问某个资源的线程个数
。AQS的共享模式被用来实现 Semaphore。
- Semaphore 是一个计数信号量,
-
CountDownLatch(倒计时器):
- CountDownLatch 是一种同步工具类,它允许一个或多个线程等待其他线程完成操作。AQS的共享模式也被用来实现 CountDownLatch。
-
CyclicBarrier(回环栅栏):
- CyclicBarrier 是一种同步辅助类,
它允许一组线程互相等待,直到所有线程都到达某个屏障点
。AQS的共享模式也被用来实现 CyclicBarrier。
- CyclicBarrier 是一种同步辅助类,
-
ReentrantReadWriteLock(可重入读写锁):
- ReentrantReadWriteLock 是 ReadWriteLock 接口的实现类,支持可重入特性,允许读锁和写锁之间的互斥。
这些是常见的基于AQS实现的锁,它们提供了不同的同步机制,可以根据实际需求选择合适的锁来实现线程安全和并发控制。
3. 模拟加锁
3.1 使用ReentrantLock来模拟锁的功能
在Java中,我们可以使用ReentrantLock来模拟锁的功能。下面是一个简单的示例代码,演示了如何使用ReentrantLock来实现对共享资源的线程安全访问:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
// 创建多个线程并发访问共享资源
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
example.increment();
}
}).start();
}
// 等待所有线程执行完毕
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出最终的计数结果
System.out.println("Final count: " + example.getCount());
}
}
在这个示例中,我们创建了一个ReentrantLock对象,并使用lock()和unlock()方法来手动控制临界区的访问。在increment()方法中,我们使用lock()来获取锁,在执行完临界区的操作后,使用unlock()来释放锁;在getCount()方法中同样使用lock()和unlock()来保证对count变量的访问是线程安全的。通过ReentrantLock,,我们可以实现对共享资源的线程安全访问,避免了多线程环境下的竞争和数据不一致的问题。
3.2 使用ReadWriteLock来模拟锁的功能
下面是一个简单的示例代码,演示了如何使用ReentrantReadWriteLock来实现对共享资源的读写操作:
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
private int sharedData = 0;
public void writeData(int data) {
writeLock.lock();
try {
sharedData = data;
System.out.println("Write: " + data);
} finally {
writeLock.unlock();
}
}
public int readData() {
readLock.lock();
try {
System.out.println("Read: " + sharedData);
return sharedData;
} finally {
readLock.unlock();
}
}
public static void main(String[] args) {
ReadWriteLockExample example = new ReadWriteLockExample();
// 创建多个读线程和写线程并发访问共享资源
for (int i = 0; i < 3; i++) {
new Thread(() -> {
example.readData();
}).start();
}
for (int i = 0; i < 2; i++) {
new Thread(() -> {
example.writeData((int) (Math.random() * 100));
}).start();
}
}
}
在这个示例中,我们创建了一个ReentrantReadWriteLock对象,并分别获取了读锁和写锁。writeData()方法使用写锁来写入数据到共享资源,并在操作完成后释放锁;readData()方法使用读锁来读取共享资源的数据,并在操作完成后释放锁。通过ReentrantReadWriteLock,读取操作可以并发进行,写操作会互斥进行,
从而提高了读多写少场景下的并发性能。
在main方法中,我们创建了多个读线程和写线程,并发地访问共享资源,演示了读写锁的功能。读线程可以同时进行读取操作,而写线程之间会互斥进行写入操作,确保数据的一致性和线程安全性。
3.3 使用Semaphore来模拟锁的功能
下面是一个简单的示例代码,演示了如何使用Semaphore来实现对共享资源的控制:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private static final Semaphore semaphore = new Semaphore(3); // 初始化信号量为3,表示最多允许3个线程同时访问共享资源
public static void main(String[] args) {
// 创建多个线程并发访问共享资源
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取信号量,如果信号量大于0,则可以继续执行,否则阻塞等待
System.out.println("Thread " + Thread.currentThread().getId() + " is accessing the shared resource");
Thread.sleep(2000); // 模拟线程访问共享资源的时间
semaphore.release(); // 释放信号量
System.out.println("Thread " + Thread.currentThread().getId() + " has released the shared resource");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
在这个示例中,我们创建了一个Semaphore对象,并初始化信号量为3,表示最多允许3个线程同时访问共享资源。在每个线程的执行过程中,通过semaphore.acquire()来获取信号量,如果信号量大于0,则表示可以继续执行;否则会被阻塞,直到有其他线程释放了信号量。在访问共享资源后,通过semaphore.release()来释放信号量,表示当前线程不再占用共享资源。
通过Semaphore,我们可以灵活控制对共享资源的访问权限,限制同时访问该资源的线程数量
,从而实现线程之间的协调与控制。
3.4 使用CountDownLatch来模拟锁的功能
下面是一个简单的示例代码,演示了如何使用CountDownLatch来实现线程间的协作:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3); // 初始化计数器为3
// 创建多个线程并发执行任务
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println("Thread " + Thread.currentThread().getId() + " is performing the task");
latch.countDown(); // 每个线程执行完毕后,调用countDown()来减少计数器
}).start();
}
latch.await(); // 主线程等待计数器归零
System.out.println("All threads have completed the task, continuing...");
}
}
在这个示例中,我们创建了一个CountDownLatch对象,并初始化计数器为3。在每个线程执行完任务后,调用latch.countDown()来递减计数器。主线程通过latch.await()来等待计数器归零,一旦计数器为0,主线程就会继续执行。
通过CountDownLatch,我们可以实现线程间的协作和同步,例如在主线程等待所有子线程执行完毕后再进行下一步操作
。这种情况适合于需要等待多个线程完成某个任务后再继续执行的场景。
3.5 使用CyclicBarrier来模拟锁的功能
下面是一个简单的示例代码,演示了如何使用CyclicBarrier来实现线程间的同步:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
private static final int THREAD_COUNT = 3;
private static final CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () -> {
System.out.println("All threads have reached the barrier, continuing...");
});
public static void main(String[] args) {
// 创建多个线程并发执行任务
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
try {
System.out.println("Thread " + Thread.currentThread().getId() + " is performing the task");
barrier.await(); // 等待所有线程到达屏障点
System.out.println("Thread " + Thread.currentThread().getId() + " continues after barrier");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
在这个示例中,我们创建了一个CyclicBarrier对象,并指定了线程数量为3。每个线程在执行完任务后,通过barrier.await()来等待其他线程到达屏障点。一旦所有线程都到达屏障点,屏障就会打开,所有线程可以继续执行。
通过CyclicBarrier,我们可以让多个线程在特定的屏障点处相互等待,然后继续执行后续操作。这种情况适合于需要多个线程协作完成某个阶段性任务后再进行下一步操作的场景。