阅读 130

JDK并发包常用工具类(Semaphore、CountDownLatch、CyclicBarrier)

介绍

Semaphore、CountDownLatch、CyclicBarrier工具类提供了一种并发流程控制的手段,Exchager工具类则提供了在线程间交换数据的一种手段。

Semaphore

无论是内部锁synchronized还是重入锁ReentrantLock,一次只允许一个线程访问一个资源,而信号量却可以指定多个线程同时访问某一个资源,功能更为强大。

Semaphore可以用于流量控制,特别是公共资源有限的应用场景,例如数据库连接等。

Semaphore提供的构造函数:

//permits为指定信号量的准入数,即同时能申请多少个许可(同时允许多少个线程访问某个资源)
public Semaphore(int permits)
//第二个参数指定是否公平
public Semaphore(int permits, boolean fair)

复制代码

主要逻辑方法:

//尝试获取一个准入的许可,无法获得则进行等待直到有线程释放许可或者当前线程被中断
public void acquire() throws InterruptedException
//和acquire类似,但不响应中断
public void acquireUninterruptibly() 
//尝试获得一个许可,成功返回true,失败返回false,不进行等待,直接返回
public boolean tryAcquire()
//有响应时间限制的尝试获取许可
public boolean tryAcquire(long timeout, TimeUnit unit)
    throws InterruptedException
//释放一个许可
public void release() 

public void acquire(int permits)

public void acquireUninterruptibly(int permits)

public boolean tryAcquire(int permits)

public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
    throws InterruptedException
    
public void release(int permits)


复制代码

许可获取和释放的原理:

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        //获取剩余许可数量
        int available = getState();
        //当前剩余许可数量减去申请许可数量
        int remaining = available - acquires;
        //如果没有足够的许可则返回,还有许可数量则通过compareAndSetState以原子方式降低许可的计数,降低许可计数成功则返回,失败则重新尝试
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        //获取剩余许可数量
        int current = getState();
        //剩余许可数量加上此次需要释放的许可数量
        int next = current + releases;
        //超过了限定的最大许可数量
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        //compareAndSetState原子操作变更剩余许可数量,变更不成功则循环尝试,成功则返回
        if (compareAndSetState(current, next))
            return true;
    }
}
复制代码

我们可以看到,Semaphore将AQS的同步状态用于保存当前可用许可的数量。

Semaphore简单示例:


public class SemapDemo implements Runnable{
    //最多有五个许可
    final Semaphore semp = new Semaphore(5);

    @Override
    public void run() {
        try {
            //获取许可
            semp.acquire();
            //模拟业务操作
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getId() + ":done!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放许可
            semp.release();
        }
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newFixedThreadPool(20);
        final SemapDemo demo = new SemapDemo();
        for (int i = 0; i < 20; i++){
            exec.submit(demo);
        }
        exec.shutdown();
    }
}
复制代码

上面示例会发现每五个线程一组为单位进行输出,每次打印5个线程ID

CountDownLatch

简称为倒计数器,初始化一个计数,依次递减,计数为0的时候再执行。

CountDownLatch的构造函数:

//参数为计数器的计数个数
public CountDownLatch(int count)
复制代码

CountDownLatch简单示例:


public class CountDownLatchDemo implements Runnable{
    //计数器为10
    static final CountDownLatch end = new CountDownLatch(10);
    static final CountDownLatchDemo demo = new CountDownLatchDemo();

    @Override
    public void run() {
        try {
            //模拟检查任务
            Thread.sleep(new Random().nextInt(10) * 1000);
            System.out.println("check complete");
            //通知CountDownLatch,一个线程完成检查任务,倒计数器减1
            end.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++){
            exec.submit(demo);
        }
        //主线程在CountDownLatch上等待,待所有检查任务完成,主线程才能继续执行
        end.await();
        System.out.println("Fire");
        exec.shutdown();
    }
}
复制代码

CountDownLatch倒计数器原理:

CountDownLatch使用AQS的方式与Semaphore很相似,在同步状态中保存的是当前的计数值。countDown方法调用release,使计数器递减,并且当计数值为0的时候,解除所有等待线程的阻塞。await调用aquire,当计数器为0的时候,acquire立即返回,否则将阻塞。

//countDown方法会调用
protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        //获取当前计数器数量
        int c = getState();
        //如果当前计数器数量为0则执行失败
        if (c == 0)
            return false;
        //计数器数减1
        int nextc = c-1;
        //compareAndSetState原子操作更新计数器数量
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

//await方法会调用
protected int tryAcquireShared(int acquires) {
    //当前计数器数量为0的时候返回1,则会退出等待。如果返回-1则会循环调用此方法进行等待
    return (getState() == 0) ? 1 : -1;
}

复制代码

另外还有一个带有指定时间的await方法:

//等待一定时间,超时则不会阻塞当前线程
public boolean await(long timeout, TimeUnit unit)
复制代码

注意事项:

CountDownLatch不可重复使用。不可能重新初始化或者修改CountDownLatch对象的内部计数器的值。

CyclicBarrier

和CountDownLatch非常相似,也可以实现线程间的计数等待,但更加强大。CyclicBarrier的计数器是可以反复使用的。

CyclicBarrier构造函数:

//参数为计数总数,也就是线程数
public CyclicBarrier(int parties)
//第二个参数是当计数器数量线程全部到达“屏障”时,系统自动执行的动作(线程)
//线程到达屏障时,优先执行barrierAction
public CyclicBarrier(int parties, Runnable barrierAction)

复制代码

CyclicBarrier的简单示例:

public class CyclicBarrierDemo {

    public static class Soldier implements Runnable {
        private String soldier;
        private final CyclicBarrier cyclic;

        public Soldier(String soldier, CyclicBarrier cyclic) {
            this.soldier = soldier;
            this.cyclic = cyclic;
        }

        @Override
        public void run() {
            try {
                // 等待所有士兵到齐
                cyclic.await();
                doWork();
                // 等待所有士兵完成工作
                cyclic.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        void doWork() {
            try {
                Thread.sleep(Math.abs(new Random().nextInt() % 10000));
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(soldier + ": 完成任务");
        }
    }

    public static class BarrierRun implements Runnable {
        boolean flag;
        int N;

        public BarrierRun(boolean flag, int n) {
            this.flag = flag;
            N = n;
        }

        @Override
        public void run() {
            if (flag) {
                System.out.println( N + " 个士兵完成任务");
            } else {
                System.out.println( N + " 个士兵集合完毕");
                flag = true;
            }
        }
    }

    public static void main(String[] args) {
        final int N = 10;
        Thread[] allSoldier = new Thread[N];
        boolean flag = false;
        CyclicBarrier cyclic = new CyclicBarrier(N, new BarrierRun(flag, N));

        System.out.println("集合队伍");
        for (int i = 0; i < 9; i++) {
            System.out.println("士兵" + i + "报到");
            allSoldier[i] = new Thread(new Soldier("士兵" + i, cyclic));
            allSoldier[i].start();
        }
    }
}
复制代码

上述代码创建了计数器为10,执行动作为BarrierRun的CyclicBarrier实例。Soldier调用了await,每调用一次,计数器减1,当计数器为0时会触发BarrierRun。并且可以看到Soldier调用了两次await,可以看出CyclicBarrier是可以重复使用的。

另外需要注意的是如果上面代码中定义的CyclicBarrier对象的parties参数设置为11执行上述操作,则主线程和子线程会永远等待,因为没有第11个线程执行await方法(没有第11个线程到达屏障),所以之前到达屏障的10个线程都不会继续执行。

使用场景: 例如CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。

CyclicBarrier和CountDownLatch的区别

CountDownLatch的计数器只能使用一次,CyclicBarrier的计数器可以重复使用(使用reset()方法重置)

Exchanger

Exchanger用于进行线程间的数据交换。它提供一个交换点,当两个线程执行exchange()方法到达同步点时,两个线程可以将本线程生产数据传递给对方。

使用场景:Exchanger可以用于遗传算法或者校对工作。

下面看一个简单示例:


public class ExchangerTest {

    private static final Exchanger<String> exgr = new Exchanger<>();
    private static ExecutorService threadPool = Executors.newFixedThreadPool(2);

    public static void main(String[] args) {
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                String A = "银行流水A"; //A录入银行流水数据
                try {
                    String B = exgr.exchange(A);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                String B = "银行流水B";  //B录入银行流水数据
                try {
                    String A = exgr.exchange(B);
                    System.out.println("A和B数据是否一致:" + A.equals(B) + ",A录入是:" + A + ",B录入是:" + B);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        threadPool.shutdown();
    }
}
复制代码

如果两个线程有一个没有执行exchange()方法,则另外一个线程则会一直等待,为避免一直等待可以使用如下方法设置最大等待时长:

//x:需要交换的数据 timeout:最大等待时长 unit:超时参数的时间单位
public V exchange(V x, long timeout, TimeUnit unit)
    throws InterruptedException, TimeoutException 
复制代码

参考书籍:《Java高并发程序设计(第2版)》《Java并发编程实战》《Java并发编程的艺术》

文章分类
后端
文章标签