并发工具类
1. CountDownLatch原理
背景:
在日常开发中会遇到这样的场景:需要在主线程中开启多个线程去执行任务,并且主线程需要等待所有子线程执行完后再进行汇总的场景。在CountDownLatch出现之前都是使用线程的join()方法来实现的,但是join()方法不够灵活,不能满足不同场景的需求,所以JDK提供了CountDownLatch这个类。
小结
- 相比于join()方法实现线程同步,更灵活、更方便;
- CountDownLatch是使用AQS实现的,使用AQS的状态值state来存放计数器的值,首先在初始化时设置状态值(计数器值),当多个线程调用countdown方法时实际时原子性递减AQS的状态值;
- 当线程调用await方法后当前线程会被放入AQS的阻塞队列等待计数器为0再返回。其他线程调用countdown方法来让计数器值递减1,当计数器值变为0时,当前线程还要调用AQS的doReleaseShared方法激活由于调用await()方法而被阻塞的线程。
public class CountDownLatchTest {
private static CountDownLatch cdl = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(1);
cdl.countDown();
System.out.println(2);
cdl.countDown();
}
}).start();
cdl.await();
System.out.println(3);
}
}
//N传入的是线程数
private static CountDownLatch cdl = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(1);
cdl.countDown();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(2);
cdl.countDown();
}
});
t1.start();
t2.start();
cdl.await();
System.out.println(3);
}
- CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。
- 当我们调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变成零。
- 由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。
- 用在多个线程时,只需要把这个CountDownLatch的引用传递到线程里即可。
【注意】:计数器必须大于等于0,只是等于0时候,计数器就是零,调用await方法时不会阻塞当前线程CountDownLatch不可能重新初始化或者修改CountDownLatch对象的内部计数器的值。
2. 同步屏障CyclicBarrier
作用:让一个线程到达一个屏障(也可以叫做同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
- 参数是几就最少有几个线程执行cycliBarrier.await()方法;
- 如果执行await()方法的线程数小于
parties
,那么这些执行await()
方法的线程就会被阻塞。
CountDownLatch与CyclicBarries的区别
- CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景。例如,如果计算发生错误,可以重置计数器,并让线程重新执行一次。
- CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得Cyclic-Barrier阻塞的线程数量。isBroken()方法用来了解阻塞的线程是否被中断。
3. 信号量Semaphore
作用
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
应用场景
Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。
使用:
- Semaphore(int permits)接受一个整型的数字,表示可用的许可证数量。
- Semaphore(10)表示允许10个线程获取许可证,也就是最大并发数是10。
- Semaphore的用法也很简单,首先线程使用Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证(锁)。还可以用tryAcquire()方法尝试获取许可证。
方法:
- int availablePermits():返回此信号量中当前可用的许可证数。
- int getQueueLength():返回正在等待获取许可证的线程数。
- boolean hasQueuedThreads():是否有线程正在等待获取许可证。
- void reducePermits(int reduction):减少reduction个许可证,是个protected方法。
- Collection getQueuedThreads():返回所有等待获取许可证的线程集合,是个protected方法。
4. 交换数据Exchanger
- Exchanger(交换者)是一个用于线程间协作的工具类。
- Exchanger用于进行线程间的数据交换。
- 它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
应用场景
-
用于遗传算法:遗传算法里需要选出两个人作为交配对象,这时候会交换两人的数据,并使用交叉规则得出2个交配结果。
-
用于校对工作;
public class ExchagerTest { private static final Exchanger<String> expr = new Exchanger<>(); private static ExecutorService pool = Executors.newFixedThreadPool(2); public static void main(String[] args) { pool.execute(new Runnable() { @Override public void run() { String emp1 = "流水账A"; try { expr.exchange(emp1); } catch (InterruptedException e) { e.printStackTrace(); } } }); pool.execute(new Runnable() { @Override public void run() { String emp2 = "流水账B"; try { String emp1 = expr.exchange(emp2); System.out.println("A和B数据是否一直:"+emp1.equals(emp2)); System.out.println("emp1录入的是:"+ emp1); System.out.println("emp2录入的是:"+ emp2); } catch (InterruptedException e) { e.printStackTrace(); } } }); // 关闭线程池 pool.shutdown(); } } /*返回结果: A和B数据是否一直:false emp1录入的是:流水账A emp2录入的是:流水账B */
如果两个线程有一个没有执行exchange()方法,则会一直等待,如果担心有特殊情况发生,避免一直等待,可以使用
exchange(V x,longtimeout,TimeUnit unit)
设置最大等待时长。
5. LockSupport工具类
LockSupport的主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础。
LockSupport类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport类的方法的线程是不持有许可证的。需要使用unpark(Thread t)方法添加许可证(使用这个方法,参数为一个线程,表示该线程具有一个许可证)
方法
- void park():如果调用该线程的方法已经获取到了许可证,那么就可以立即返回,否则调用线程会被禁止参与线程的调度(被阻塞挂起)
- void park(Object blocker):(推荐使用)当线程被阻塞挂起时,blocker对象会被记录到该线程内部。使用诊断工具可以观察线程被阻塞的原因,诊断工具通过调用getBlocker(Thread t)方法来获取blocker对象。参数一般为this
- void unspark(Thread t)
- 当一个线程调用unpark()时,如果参数 t 没有持有 t与LockSpark类关联的许可证,则让线程持有。
- 若线程 t之前因为调用LockSupport.park()方法而被挂起,则调用unspark()后,该线程会立马被唤醒。
- 如果线程 t 之前没有调用park,则调用unpark后,再调用park()方法,线程 t 不会被阻塞,会接着执行代码。
import java.util.concurrent.locks.LockSupport;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1线程执行spark()方法");
//调用park()方法,挂起自己
LockSupport.park();
System.out.println("t1线程获得了许可证");
}
});
//启动t1线程
t1.start();
//主线程休眠 1sec
Thread.sleep(1000);
System.out.println("主线程调用unspark()方法");
LockSupport.unpark(t1); //表示t1线程获得了许可证,可以接着从spark()方法后执行代码
}
/* 执行结果
t1线程执行spark()方法
主线程调用unspark()方法
t1线程获得了许可证 */
- void parkNanos(long nanos):如果调用parkNanos方法的线程还没有拿到许可证,则调用线程会被挂起 nanos时间后自动返回(被唤醒,接着执行)