Java 并发工具类使用

java.util.concurrent 包从 JDK1.5 开始引入,目的是解决并发编程的线程安全问题,提供非常有用的并发工具类,包括 CountDownLatch、CyclicBarrier 与 Semaphore 等。

在 concurrent 包下还有两个子包,一个是 atomic,包含一些原子类,可以解决原子性问题;另一个包是 locks,提供了并发包里线程安全的最为基础的工具——显示锁(ReentrantLock、ReadWriteLock)。


CountDownLatch使用

CountDownLatch 允许一个或多个线程等待其他线程完成操作,利用它可以实现类似计数器的功能。比如现在有一个线程,需要等待其它线程执行完之后才能执行,此时就可以使用 CountDownLatch 实现这种需求。

当然,也可以通过 join() 方法来实现,不过 CountDownLatch 实现的更为灵活、更为方便。

CountDownLatch 的主要方法如下:

  • public CountDownLatch(int count):构造器,指定 count 值
  • public void await():调用 await() 方法的线程会被挂起,直到 count 值为0继续执行
  • public boolean await(long timeout, TimeUnit unit):调用 await() 方法的线程会被挂起,直到 count 值为0或指定时间后继续执行
  • public void countDown():将计数值 count 减1
  • public long getCount():返回 count 值

示例如下:

public class CountDownLatchDemo {
    private static CountDownLatch latch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "正在执行");
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + "执行完毕");
                latch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "正在执行");
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName() + "执行完毕");
                latch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        System.out.println("等待2个子线程执行完毕...");
        latch.await();
        System.out.println("2个子线程执行完毕");
        System.out.println("count值:" + latch.getCount());
        System.out.println("继续执行主线程");
    }
}

输出结果:

等待2个子线程执行完毕...
Thread-0正在执行
Thread-1正在执行
Thread-0执行完毕
Thread-1执行完毕
2个子线程执行完毕
count值:0
继续执行主线程

CountDownLatch 并没有提供设置 count 值的方法,只能在初始化时设置。当 count 值变为0时,如果还有第二次等待,还需要创建一个新的 CountDownLatch。


CyclicBarrier用法

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier),或者说回环栅栏。通过它可以实现让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,所有被屏障拦截的线程才会继续执行。每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

CyclicBarrier 主要方法如下:

  • public CyclicBarrier(int parties):构造器,参数为屏障拦截的线程数量
  • public CyclicBarrier(int parties, Runnable barrierAction):构造器,参数为屏障拦截的线程数量、这些线程都到达同步点时会执行的内容
  • public int await():调用 await() 方法的线程会被挂起,直至所有线程都到达同步点
  • public int await(long timeout, TimeUnit unit):调用 await() 方法的线程会被挂起,直至所有线程都到达同步点或到达指定的时间
  • public int getParties():返回设置的 parties 值
  • public void reset():重置为初始状态
注意:
  1. 如果有线程已经处于等待状态,调用 reset() 方法会导致已经在等待的线程抛出异常,并且将会导致其它线程始终处于阻塞状态。
  2. 如果在等待的过程中,线程被中断(Interrupt),会抛出异常,并且会影响到其他所有的线程。
  3. 如果超出指定的等待时间,当前线程会抛出 TimeoutException 异常,其他线程会抛出 BrokenBarrierException 异常。
  4. 如果在执行屏障操作过程(barrierAction)中发生异常,会影响到其他所有的线程,抛出 BrokenBarrierException 异常,屏障被损坏。

示例如下:

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () ->
                System.out.println(Thread.currentThread().getName() + ":所有线程写入完毕"));

        for (int i = 0; i < cyclicBarrier.getParties(); i++)
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "正在写入数据...");
                try {
                    Thread.sleep(3000);
                    System.out.println(Thread.currentThread().getName() + "写入数据完毕");
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
    }
}

输出结果:

Thread-0正在写入数据...
Thread-1正在写入数据...
Thread-2正在写入数据...
Thread-0写入数据完毕
Thread-1写入数据完毕
Thread-2写入数据完毕
Thread-2:所有线程写入完毕

CountDownLatch 与 CyclicBarrier 的含义稍微有一点区别。

对于前者,重点是一个或多个线程等待,而其它的线程在完成“某件事情”之后,可以继续执行;对于后者,重点是在任意一个线程没有完成“某件事情”之前,所有的线程都必须等待。

除此之外,相比 CountDownLatch,CyclicBarrier 是可以重用的。


Semaphore用法

Semaphore,即计数信号量,通常用来限制可以同时访问某些资源的线程数量。

举一个例子来说,假如银行有4个服务窗口,现在有7个人同时想要办理业务,怎么办呢?只能是先进去4个人,另外3个人在大厅等候。只有前面的一个人办完业务,服务窗口有空闲,另外一个人才能进去办业务。这里服务窗口就相当于资源,4个人相当于被限制的线程数量。

Semaphore 的主要方法如下:

  • public Semaphore(int permits):构造器,指定允许同时访问的许可数量,不遵循FIFO原则
  • public Semaphore(int permits, boolean fair):构造器,指定允许同时访问的线程数量;如果fair为true,表示遵循FIFO原则
  • public void acquire() throws InterruptedException:获得访问资源的权利(许可)
  • public void release():释放访问资源的权利(许可)
  • public int availablePermits():返回剩余可获得的许可数量

示例如下:

public class SemaphoreDemo {

    private static Semaphore semaphore = new Semaphore(4);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 7; i++) {
            int order = i;
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(order + "号线程接入,目前剩余" + semaphore.availablePermits());
                    TimeUnit.SECONDS.sleep((long) (Math.random() * 10) + 1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                semaphore.release();
                System.out.println(order + "号线程释放");
            }).start();
        }
    }
}

输出结果:

0号线程接入,目前剩余3
2号线程接入,目前剩余2
1号线程接入,目前剩余1
3号线程接入,目前剩余0
2号线程释放
4号线程接入,目前剩余0
1号线程释放
5号线程接入,目前剩余0
3号线程释放
6号线程接入,目前剩余0
0号线程释放
4号线程释放
6号线程释放
5号线程释放

注:因为多线程执行顺序不确定,因此多次执行的结果可能并不一致。

参考链接:

猜你喜欢

转载自blog.csdn.net/weixin_43320847/article/details/83046008