Java中的5个并发工具类,你真的了解清楚了吗?

本文深入解析了JDK并发包中的CountDownLatch、CyclicBarrier、Semaphore和Exchanger四个工具类的使用方法及应用场景,帮助读者理解如何有效控制多线程流程与数据交换。

###引言

在JDK的并发包里提供了很多有意思的并发工具类。CountDownLatch、CyclicBarrier和Semaphore 工具类提供了一种并发流程控制的手段,Exchanger 工具类则提供了在线程间交换数据的一种手段。

###CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作。

其实最简单的做噶是使用join()方法,join用于让当前执行线程等待join线程执行结束。其实现原理是不停检查join线程是否存活,如果join线程存活则让当前线程永远等待。其中,wait(0) 表示永远等待下去,代码片段如下:

while (isAlive()) {     wait(0);}

知道线程中止后,线程的 this.notifyAll() 方法被调用,调用 notifyAll() 方法是在 JVM里实现的,所以在JDK里看不到,大家可以查看JVM源码。

在JDK1.5之后的并发包CountDownLatch也可以实现join的功能,并且功能更多,更强大。

示例代码:

public static void main(String[] args) throws InterruptedException {    CountDownLatch c = new CountDownLatch(2);    new Thread(new Runnable() {        @Override        public void run() {             System.out.println(1);             c.countDown();             System.out.println(2);             c.countDown();//注释这行        }     }).start();     c.await();     System.out.println("3");}

运行结果:

123

CountDownLatch的构造方法接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。

当我们调用CountDownLatch的countDown()方法时,N就会减1,CountDownLatch的 await() 方法会阻塞当前线程,直到N变成零。由于countDown()方法可以用在任何地方,所以这里说的N个点,也可以是N个线程。用在多个线程时,你只需要把这个CountDownLatch的引用传递到线程里。

如果有某个线程处理的比较慢,我们不可能让主线程一直等待,所以我们可以使用另外一个带指定时间的await方法,await(long time, TimeUnit unit), 这个方法等待特定时间后,就会不再阻塞当前线程。join也有类似的方法。

**注意:**计数器必须大于等于0,只是等于0时候,计数器就是零,调用await方法时不会阻塞当前线程。CountDownLatch不可能重新初始化或者修改CountDownLatch对象的内部计数器的值。一个线程调用countDown方法 happen-before 另外一个线程调用await方法。

###CyclicBarrier

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

1 构造方法

CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

示例代码:

    public static void main(String[] args) throws BrokenBarrierException, InterruptedException {        CyclicBarrier c = new CyclicBarrier(2);        new Thread(new Runnable() {            @Override            public void run() {                try {                    c.await();                } catch (InterruptedException e) {                    e.printStackTrace();                } catch (BrokenBarrierException e) {                    e.printStackTrace();                }                System.out.println("1");            }        }).start();        c.await();        System.out.println(2);    }

运行结果:

12

或者

21

如果把new CyclicBarrier(2)修改成new CyclicBarrier(3)则主线程和子线程会永远等待,因为没有第三个线程执行await方法,即没有第三个线程到达屏障,所以之前到达屏障的两个线程都不会继续执行


CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties, Runnable barrierAction),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。

示例代码:

    public static void main(String[] args) throws BrokenBarrierException, InterruptedException {        CyclicBarrier c = new CyclicBarrier(2, new Runnable() {            @Override            public void run() {                System.out.println(3);            }        });        new Thread(new Runnable() {            @Override            public void run() {                try {                    c.await();                } catch (InterruptedException e) {                    e.printStackTrace();                } catch (BrokenBarrierException e) {                    e.printStackTrace();                }                System.out.println("1");            }        }).start();        c.await();        System.out.println(2);    }

运行结果:

312
2 应用场景

CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。

2.3 CyclicBarrier和CountDownLatch的区别

CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,例如,如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。

示例代码:

    public static void main(String[] args){        CyclicBarrier c = new CyclicBarrier(2);        Thread t = new Thread(new Runnable() {            @Override            public void run() {                try {                    c.await();                } catch (InterruptedException e) {                    e.printStackTrace();                } catch (BrokenBarrierException e) {                    e.printStackTrace();                }            }        });        t.start();        t.interrupt();        try {            c.await();        } catch (InterruptedException e) {//            e.printStackTrace();        } catch (BrokenBarrierException e) {//            e.printStackTrace();        }finally {            System.out.println(c.isBroken());        }    }

运行结果:

true

###Semaphore

#####1 作用

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

#####2 简介

Semaphore也是一个线程同步的辅助类,可以维护当前访问自身的线程个数,并提供了同步机制。使用Semaphore可以控制同时访问资源的线程个数,例如,实现一个文件允许的并发访问数。

3 应用场景

Semaphore可以用于做流量控制,特别公用资源有限的应用场景,比如数据库连接。假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发的读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有十个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,我们就可以使用Semaphore来做流控,代码如下:

public static void main(String[] args) {    final int THREAD_NUM = 30;    ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_NUM);    Semaphore s = new Semaphore(10);//只允许10个线程并发执行    for (int i = 0; i < THREAD_NUM; i++) {        threadPool.execute(new Runnable() {            @Override            public void run() {                try {                    s.acquire();                    System.out.println("play");                    s.release();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });    }    threadPool.shutdown();}

在代码中,虽然有30个线程在执行,但是只允许10个并发的执行。Semaphore的构造方法Semaphore(int permits) 接受一个整型的数字,表示可用的许可证数量。Semaphore(10)表示允许10个线程获取许可证,也就是最大并发数是10。Semaphore的用法也很简单,首先线程使用Semaphore的acquire()获取一个许可证,使用完之后调用release()归还许可证。还可以用tryAcquire()方法尝试获取许可证。

4 其他方法

Semaphore还提供一些其他方法:

  • int availablePermits() :返回此信号量中当前可用的许可证数。

  • int getQueueLength():返回正在等待获取许可证的线程数。

  • boolean hasQueuedThreads() :是否有线程正在等待获取许可证。

  • void reducePermits(int reduction) :减少reduction个许可证。是个protected方法。

  • Collection getQueuedThreads() :返回所有等待获取许可证的线程集合。是个protected方法。

###Exchanger

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

应用场景

**1、**Exchanger可以用于遗传算法,遗传算法里需要选出两个人作为交配对象,这时候会交换两人的数据,并使用交叉规则得出2个交配结果。
**2、**Exchanger也可以用于校对工作。比如我们需要将纸制银流通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行录入,录入到Excel之后,系统需要加载这两个Excel,并对这两个Excel数据进行校对,看看是否录入的一致。代码如下:

    public static void main(String[] args) {        Exchanger<String> exchanger = new Exchanger<>();        new Thread(new Runnable() {            @Override            public void run() {                try {                    String a = "银行流水A";                    exchanger.exchange(a);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }).start();        new Thread(new Runnable() {            @Override            public void run() {                try {                    String b = "银行流水B";                    String a = exchanger.exchange(b);                    System.out.println("在B中获取到录入的A是:"+a);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }).start();    }

运行结果:

在B中获取到录入的A是:银行流水A

如果两个线程有一个没有到达exchange方法,则会一直等待,如果担心有特殊情况发生,避免一直等待,可以使用exchange(V data, long time, TimeUnit unit)设置最大等待时长。

想学习Java的小伙伴注意啦!我整理了一套从最基础的Java入门级学习到Java框架内容,以及开发工具;送给每一位想要学习Java的小伙伴,想要获取资料,关注微信公众号“速学Java”,后台回复“优快云”,即可获得学习资料,这里是小白聚集地,欢迎初学和进阶中的小伙伴~

关注微信公众号:速学Java

<think>好的,用户问的是Java中如何解决多线程并发问题。这个问题挺常见的,但需要详细分析不同的解决方案。首先得考虑用户可能的背景,可能是刚学多线程的新手,或者在实际开发中遇到了并发问题需要解决。他们可能需要全面的方法,而不仅仅是几个关键字。 首先,我应该从基础开始,比如同步代码块,这是最直接的。但需要解释清楚synchronized关键字的用法,包括对象锁和类锁的区别。可能用户对锁的概念不太清楚,所以要举例子,比如卖票系统的例子,这样比较直观。 接下来是Lock接口,比如ReentrantLock。这里要对比synchronized和Lock的区别,比如Lock更灵活,支持尝试获取锁、超时中断等。可能需要提到Condition的使用,这样用户知道如何更细粒度地控制线程通信。 然后是原子类,比如AtomicInteger。这部分要强调原子操作的原理,CAS机制,以及适用场景,比如计数器。用户可能不了解CAS,所以需要简单解释一下,避免他们误用。 线程安全容器也是重要的一点,比如ConcurrentHashMap和CopyOnWriteArrayList。需要说明它们是如何实现线程安全的,以及各自适用的场景。比如读多写少的情况用CopyOnWriteArrayList比较合适。 ThreadLocal也是一个关键点,它解决的是线程间数据隔离的问题。可能用户会遇到需要每个线程有自己独立变量的情况,比如SimpleDateFormat,这时候ThreadLocal就很有用。要提醒他们正确使用,避免内存泄漏。 接下来是volatile关键字,这里需要明确它的作用,保证可见性和禁止指令重排,但并不能保证原子性。可能用户会误解volatile能解决所有并发问题,需要举例说明适用的场景,比如状态标志位。 线程池部分要强调通过管理线程资源来减少并发问题。比如使用Executors工具类创建不同类型的线程池,但也要提到阿里巴巴规范不推荐直接使用Executors,而是通过ThreadPoolExecutor自定义参数,避免资源耗尽的风险。 并发工具类比如CountDownLatch、CyclicBarrier和Semaphore,这些在高并发场景下很常用。需要分别说明它们的用途和示例,比如CountDownLatch用于等待多个线程完成,CyclicBarrier用于线程同步,Semaphore控制资源访问数量。 最后是避免共享状态,推荐使用不可变对象和线程封闭。这里可能需要提到设计模式,比如使用不可变的类或者将对象限制在特定线程内。 总结部分要简明扼要,回顾所有方法,并建议根据具体场景选择合适方案。同时提醒用户深入理解原理,避免死锁和资源竞争问题。 需要检查是否有遗漏的重要方法,比如CAS、Fork/Join框架,或者CompletableFuture,但可能用户的问题更偏向基础,所以先集中在常用的解决方案上。另外,要确保例子简单易懂,避免过于复杂让用户困惑。语法和格式方面,按照系统指令使用正确的LaTeX,比如变量用$符号包裹,独立公式单独成段。最后用中文自然流畅地组织内容,结构清晰,分点说明。</think>在Java中解决多线程并发问题,需要根据具体场景选择合适的同步机制。以下是逐步解决方案: --- ### 一、同步代码块(Synchronized Block) 通过`synchronized`关键字实现对共享资源的互斥访问: ```java private final Object lock = new Object(); public void safeMethod() { synchronized(lock) { // 获取对象锁 // 临界区代码(如共享变量操作) } } ``` - **原理**:基于$monitor$机制,同一时间只允许一个线程进入临界区 - **适用场景**:简单的同步需求(如卖票系统:$ticketCount = n$) --- ### 二、Lock接口(显式锁) 使用`ReentrantLock`提供更灵活的锁控制: ```java private final Lock lock = new ReentrantLock(); public void safeMethod() { lock.lock(); try { // 临界区代码 } finally { lock.unlock(); // 必须保证释放锁 } } ``` - **优势**: 1. 支持尝试获取锁(`tryLock()`) 2. 可设置超时时间(`tryLock(long time, TimeUnit unit)`) 3. 支持公平锁(`new ReentrantLock(true)`) --- ### 三、原子类(Atomic Classes) 通过CAS(Compare-And-Swap)实现无锁线程安全: ```java AtomicInteger counter = new AtomicInteger(0); public void increment() { counter.getAndIncrement(); // 原子操作 } ``` - **常用类**: - `AtomicInteger` - `AtomicLong` - `AtomicReference<T>` - **适用场景**:计数器、状态标志等简单操作 --- ### 四、线程安全容器 使用JUC(`java.util.concurrent`)包中的线程安全集合: ```java ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); ``` - **实现原理**: - `ConcurrentHashMap`:分段锁(JDK8后改为CAS+synchronized) - `CopyOnWriteArrayList`:写时复制(适合读多写少场景) --- ### 五、ThreadLocal 为每个线程创建独立变量副本: ```java private static ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); ``` - **应用场景**: - 数据库连接管理 - 日期格式化工具(避免`SimpleDateFormat`线程不安全问题) --- ### 六、volatile关键字 保证变量可见性和禁止指令重排序: ```java private volatile boolean flag = false; ``` - **特性**: 1. 写操作立即刷新到主内存 2. 读操作直接从主内存读取 - **限制**:不保证原子性(如`i++`仍需同步) --- ### 七、线程池管理 通过`ExecutorService`规范线程使用: ```java ExecutorService executor = Executors.newFixedThreadPool(4); executor.submit(() -> { // 线程任务 }); ``` - **优点**:避免线程频繁创建/销毁,控制并发规模 --- ### 八、并发工具类 使用JUC中的高级同步工具: 1. **CountDownLatch**(倒计时门闩): ```java CountDownLatch latch = new CountDownLatch(3); latch.await(); // 等待计数器归零 latch.countDown(); // 减少计数器 ``` 2. **CyclicBarrier**(循环栅栏): ```java CyclicBarrier barrier = new CyclicBarrier(3, ()->System.out.println("All threads reached")); barrier.await(); // 线程等待集合点 ``` 3. **Semaphore**(信号量): ```java Semaphore semaphore = new Semaphore(5); // 允许5个线程同时访问 semaphore.acquire(); // 获取许可 semaphore.release(); // 释放许可 ``` --- ### 九、避免共享状态 1. **不可变对象**: ```java public final class ImmutableObject { private final int value; // 构造函数初始化后不可修改 } ``` 2. **线程封闭**:通过栈封闭或ThreadLocal将对象限制在单个线程内 --- ### 总结 | 方法 | 适用场景 | 特点 | |--------------------|---------------------------------|-------------------------| | 同步代码块 | 简单同步需求 | 简单但粒度粗 | | Lock接口 | 需要高级锁功能 | 灵活但需手动释放 | | 原子类 | 简单原子操作 | 无锁高效 | | 线程安全容器 | 集合类并发访问 | 优化过的并发实现 | | volatile | 状态标志可见性保证 | 不保证复合操作原子性 | **最佳实践建议**: 1. 优先使用线程安全容器和原子类 2. 需要精细控制时选择Lock接口 3. 高并发场景优先考虑无锁编程 4. 避免过早优化,先保证正确性再考虑性能 需要根据具体业务场景(如读/写比例、性能要求、数据一致性级别)选择最合适的解决方案。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值