并发控制工具在实现多线程编程中是非常重要的一环。在Java的并发包java.util.concurrent中,有许多并发控制工具可以帮助我们更好地编写多线程代码。这些并发控制工具具有多样的特性,可以满足不同的并发需求。在本篇文章中,我们将详细介绍其中的几种并发控制工具,包括Semaphore、CountDownLatch、CyclicBarrier、Phaser和Exchanger。通过对它们的使用方法、工作原理及源码分析,我希望能帮助读者更深入地理解并发控制工具,从而更好地利用它们来编写高效并且安全的多线程代码。
Semaphore是一个用于控制并发线程数的并发控制工具。Semaphore内部维护了一组许可证,线程可以申请许可证,如果许可证不足,则线程阻塞,直到有线程释放许可证。
以下是一个简单的使用例子,它演示了如何使用Semaphore来限制并发线程数:
public class SemaphoreExample {
public static void main(String[] args) {
int threadNum = 10;
Semaphore semaphore = new Semaphore(5); // 最多允许5个并发线程
for (int i = 0; i < threadNum; i++) {
final int index = i;
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可证
System.out.println("Thread " + index + " is running.");
Thread.sleep((long) (Math.random() * 10000));
System.out.println("Thread " + index + " has finished.");
semaphore.release(); // 释放许可证
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
这个例子中,我们创建了10个线程,但是通过Semaphore限制了最多只有5个线程能够并发执行。每个线程在运行前先尝试获取许可证,如果许可证不足则阻塞;在运行后释放许可证。
Semaphore内部也是基于AQS(AbstractQueuedSynchronizer)实现的。Semaphore使用AQS的state变量作为许可证的数量。当线程调用acquire()方法时,如果state值大于0,则state值减
1,线程继续执行;否则,线程将被加入到等待队列并阻塞。当线程调用release()方法时,state值加1,如果等待队列中有线程正在等待许可证,那么唤醒一个线程。
CountDownLatch是一个非常实用的并发控制工具,它允许一个或多个线程等待其他线程完成操作。CountDownLatch以一个给定的数量初始化,countDown()方法递减计数器,await()方法阻塞等待计数器归零。
以下是一个简单的使用例子,它演示了如何使用CountDownLatch来实现一个简单的并行计算框架:
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int threadNum = 3;
final CountDownLatch latch = new CountDownLatch(threadNum);
for (int i = 0; i < threadNum; i++) {
final int index = i;
new Thread(() -> {
System.out.println("Thread " + index + " has started.");
try {
Thread.sleep((long) (Math.random() * 10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread " + index + " has finished.");
latch.countDown();
}).start();
}
System.out.println("Main thread is waiting.");
latch.await();
System.out.println("All threads have finished. Main thread continues.");
}
}
这个例子中,我们创建了3个工作线程,每个线程完成工作后调用latch.countDown()
方法,将计数器减1。主线程调用latch.await()
方法等待计数器归零,然后继续执行。因此,无论工作线程执行的快慢如何,主线程都会等待所有工作线程完成后才继续执行。
CountDownLatch的工作原理是基于AQS(AbstractQueuedSynchronizer)实现的。它内部维护了一个状态值state,初始化时state值为count的值。当线程调用countDown()方法时,state值减1;当线程调用await()方法时,如果state值不为0,那么该线程将被加入到AQS的等待队列并阻塞,直到state值为0。当state值为0时,所有在等待队列中等待的线程都将被唤醒。
接下来,我们会继续介绍其他的并发控制工具,如CyclicBarrier、Phaser和Exchanger。
CyclicBarrier是另一种多线程并发控制工具,它允许一组线程互相等待,直到所有线程都准备就绪后才能继续执行。CyclicBarrier是循环的,它可以被重复使用。
以下是一个简单的使用例子,它演示了如何使用CyclicBarrier来同步一组线程的执行:
public class CyclicBarrierExample {
public static void main(String[] args) {
int threadNum = 3;
CyclicBarrier barrier = new CyclicBarrier(threadNum, () -> System.out.println("All threads are ready. Continue."));
for (int i = 0; i < threadNum; i++) {
final int index = i;
new Thread(() -> {
System.out.println("Thread " + index + " is ready.");
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("Thread " + index + " continues.");
}).start();
}
}
}
这个例子中,我们创建了3个线程和一个CyclicBarrier,每个线程准备就绪后调用barrier.await()
方法等待其他线程。当所有线程都准备就绪后,CyclicBarrier会执行我们提供的Runnable任务(打印一条消息),然后所有线程继续执行。
CyclicBarrier内部也是基于AQS(AbstractQueuedSynchronizer)实现的。当线程调用await()方法
时,CyclicBarrier内部的计数器减1,然后检查计数器是否为0。如果计数器不为0,那么该线程将被加入到等待队列并阻塞;如果计数器为0,那么所有在等待队列中等待的线程都将被唤醒,并执行我们提供的Runnable任务(如果有的话)。此外,CyclicBarrier还会重置计数器,以便下一次使用。
接下来,我们会继续介绍其他的并发控制工具,如Phaser和Exchanger。
Phaser是一个更加灵活的并发控制工具,它允许开发者动态地注册和注销参与者,以及动态地改变参与者的数量。Phaser可以用来替代CountDownLatch和CyclicBarrier,因为它结合了它们的功能。
以下是一个简单的使用例子,它演示了如何使用Phaser来同步一组线程的执行:
public class PhaserExample {
public static void main(String[] args) {
int phaseNum = 3;
Phaser phaser = new Phaser(phaseNum);
for (int i = 0; i < phaseNum; i++) {
final int index = i;
new Thread(() -> {
System.out.println("Thread " + index + " is ready.");
phaser.arriveAndAwaitAdvance(); // 等待所有线程准备就绪
System.out.println("Thread " + index + " continues.");
}).start();
}
}
}
这个例子中,我们创建了3个线程和一个Phaser,每个线程准备就绪后调用phaser.arriveAndAwaitAdvance()
方法等待其他线程。当所有线程都准备就绪后,所有线程继续执行。
Phaser内部也是基于AQS(AbstractQueuedSynchronizer)实现的。Phaser使用一个32位的int类型变量来存储两个信息:阶段数(高16位)和参与者数量(低16位)。当线程调用arriveAndAwaitAdvance()方法时,参与者数量减1,然后检查参与者数量是否为0。如果参与者数量不为0,那么该线程将被加入到等待队列并阻塞;如果参与者数量为0,那么所有在等待队列中等待的线程都将被唤醒,并且阶段数增1。
接下来,我们会继续介绍其他的并发控制工具,如Semaphore和Exchanger。
Exchanger是一个用于两个线程交换数据的并发工具。Exchanger只能用于两个线程之间的数据交换,线程会在交换点阻塞,直到两个线程都到达后才进行数据的交换。
以下是一个简单的使用例子,它演示了如何使用Exchanger来进行数据交换:
public class ExchangerExample {
public static void main(String[] args) {
Exchanger<Integer> exchanger = new Exchanger<>();
new Thread(() -> {
int data = 1; // 线程1的数据
try {
int receivedData = exchanger.exchange(data);
System.out.println("Thread 1 received data: " + receivedData);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
int data = 2; // 线程2的数据
try {
int receivedData = exchanger.exchange(data);
System.out.println("Thread 2 received data: " + receivedData);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
这个例子中,我们创建了两个线程和一个Exchanger,线程1和线程2分别持有数据1和2,它们通过Exchanger交换数据,交换后,线程1得到数据2,线程2得到数据1。
Exchanger内部主要是通过一个等待队列来实现的。当一个线程调用exchange()方法时,如果等待队列中没有等待的线程,那么当前线程就会被加入到等待队列并阻塞;如果等待队列中有等待的线程,那么就与队列中的线程进行数据交换,并唤醒队列中的线程。
在并发编程中,我们需要根据具体的需求和场景来选择合适的并发控制工具。例如,如果我们需要控制并发线程数,那么可以使用Semaphore;如果我们需要进行多线程的数据交换,那么可以使用Exchanger;如果我们需要同步一组线程的执行,那么可以使用CyclicBarrier或者Phaser。
并发编程虽然可以提高程序的运行效率,但也带来了许多问题,如线程安全问题、死锁问题等。因此,在使用并发控制工具时,我们还需要注意这些并发问题。
并发控制工具是并发编程的重要组成部分,它们提供了丰富的功能,帮助我们更好地进行并发编程。在使用并发控制工具时,我们需要根据需求和场景选择合适的工具,并注意线程安全和死锁等并发问题。
重申并发编程的核心要点:明确并发编程的目标(正确性、性能),理解并正确使用并发工具,避免并发的常见问题(线程安全、死锁)。