并发编程 | 并发工具类 - 并发控制工具

引言

并发控制工具在实现多线程编程中是非常重要的一环。在Java的并发包java.util.concurrent中,有许多并发控制工具可以帮助我们更好地编写多线程代码。这些并发控制工具具有多样的特性,可以满足不同的并发需求。在本篇文章中,我们将详细介绍其中的几种并发控制工具,包括Semaphore、CountDownLatch、CyclicBarrier、Phaser和Exchanger。通过对它们的使用方法、工作原理及源码分析,我希望能帮助读者更深入地理解并发控制工具,从而更好地利用它们来编写高效并且安全的多线程代码。


Semaphore

Semaphore是一个用于控制并发线程数的并发控制工具。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的工作原理与源码分析

Semaphore内部也是基于AQS(AbstractQueuedSynchronizer)实现的。Semaphore使用AQS的state变量作为许可证的数量。当线程调用acquire()方法时,如果state值大于0,则state值减

1,线程继续执行;否则,线程将被加入到等待队列并阻塞。当线程调用release()方法时,state值加1,如果等待队列中有线程正在等待许可证,那么唤醒一个线程。

Semaphore相关面试题

  • 什么是Semaphore,它的工作原理是什么?
  • Semaphore如何用于控制并发线程数?
  • 如何使用Semaphore实现多线程的同步?

CountDownLatch

CountDownLatch是一个非常实用的并发控制工具,它允许一个或多个线程等待其他线程完成操作。CountDownLatch以一个给定的数量初始化,countDown()方法递减计数器,await()方法阻塞等待计数器归零。

CountDownLatch 的使用

以下是一个简单的使用例子,它演示了如何使用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 的工作原理与源码分析

CountDownLatch的工作原理是基于AQS(AbstractQueuedSynchronizer)实现的。它内部维护了一个状态值state,初始化时state值为count的值。当线程调用countDown()方法时,state值减1;当线程调用await()方法时,如果state值不为0,那么该线程将被加入到AQS的等待队列并阻塞,直到state值为0。当state值为0时,所有在等待队列中等待的线程都将被唤醒。

CountDownLatch相关面试题

  • 什么是CountDownLatch,它的工作原理是什么?
  • 为什么需要CountDownLatch?
  • 如何使用CountDownLatch实现多线程的同步?

接下来,我们会继续介绍其他的并发控制工具,如CyclicBarrier、Phaser和Exchanger。

CyclicBarrier

CyclicBarrier是另一种多线程并发控制工具,它允许一组线程互相等待,直到所有线程都准备就绪后才能继续执行。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的工作原理与源码分析

CyclicBarrier内部也是基于AQS(AbstractQueuedSynchronizer)实现的。当线程调用await()方法

时,CyclicBarrier内部的计数器减1,然后检查计数器是否为0。如果计数器不为0,那么该线程将被加入到等待队列并阻塞;如果计数器为0,那么所有在等待队列中等待的线程都将被唤醒,并执行我们提供的Runnable任务(如果有的话)。此外,CyclicBarrier还会重置计数器,以便下一次使用。

CyclicBarrier相关面试题

  • 什么是CyclicBarrier,它的工作原理是什么?
  • 为什么需要CyclicBarrier?
  • 如何使用CyclicBarrier实现多线程的同步?

接下来,我们会继续介绍其他的并发控制工具,如Phaser和Exchanger。


Phaser

Phaser是一个更加灵活的并发控制工具,它允许开发者动态地注册和注销参与者,以及动态地改变参与者的数量。Phaser可以用来替代CountDownLatch和CyclicBarrier,因为它结合了它们的功能。

Phaser 的使用

以下是一个简单的使用例子,它演示了如何使用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 的工作原理与源码分析

Phaser内部也是基于AQS(AbstractQueuedSynchronizer)实现的。Phaser使用一个32位的int类型变量来存储两个信息:阶段数(高16位)和参与者数量(低16位)。当线程调用arriveAndAwaitAdvance()方法时,参与者数量减1,然后检查参与者数量是否为0。如果参与者数量不为0,那么该线程将被加入到等待队列并阻塞;如果参与者数量为0,那么所有在等待队列中等待的线程都将被唤醒,并且阶段数增1。

Phaser相关面试题

  • 什么是Phaser,它的工作原理是什么?
  • Phaser与CyclicBarrier和CountDownLatch有什么不同?
  • 如何使用Phaser实现多线程的同步?

接下来,我们会继续介绍其他的并发控制工具,如Semaphore和Exchanger。


Exchanger

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 的工作原理与源码分析

Exchanger内部主要是通过一个等待队列来实现的。当一个线程调用exchange()方法时,如果等待队列中没有等待的线程,那么当前线程就会被加入到等待队列并阻塞;如果等待队列中有等待的线程,那么就与队列中的线程进行数据交换,并唤醒队列中的线程。

Exchanger相关面试题

  • 什么是Exchanger,它的工作原理是什么?
  • Exchanger与其他并发工具有什么不同?
  • 如何使用Exchanger进行线程间的数据交换?

使用并发控制工具的注意事项

选择合适的并发控制工具

在并发编程中,我们需要根据具体的需求和场景来选择合适的并发控制工具。例如,如果我们需要控制并发线程数,那么可以使用Semaphore;如果我们需要进行多线程的数据交换,那么可以使用Exchanger;如果我们需要同步一组线程的执行,那么可以使用CyclicBarrier或者Phaser。

并发问题的注意事项

并发编程虽然可以提高程序的运行效率,但也带来了许多问题,如线程安全问题、死锁问题等。因此,在使用并发控制工具时,我们还需要注意这些并发问题。

总结

并发控制工具是并发编程的重要组成部分,它们提供了丰富的功能,帮助我们更好地进行并发编程。在使用并发控制工具时,我们需要根据需求和场景选择合适的工具,并注意线程安全和死锁等并发问题。
重申并发编程的核心要点:明确并发编程的目标(正确性、性能),理解并正确使用并发工具,避免并发的常见问题(线程安全、死锁)。

你可能感兴趣的:(并发编程,面试,java,职场和发展)