通信工具类

概述

本次我们将讲述JUC包下的一些工具类

作用
Semaphore 限制线程的数量
Exchanger 两个线程交换数据
CountDownLatch 线程等待直到计数器减为0时开始工作
CyclicBarrier 作用跟CountDownLatch类似,但是可以重复使用
Phaser 增强的CyclicBarrier

 


Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,因此他可以用于流量控制,特别是公用资源有限的应用场景,比如数据库连接假 如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程 并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这 时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。
我们可以在其构造函数中传入初始资源总数,以及是否使用公平的同步器,默认是非公平的。

// 默认情况下使用非公平
public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

最主要的方法是acquire方法和release方法。

Semaphore内部有一个继承了AQS的同步器Sync,每次acquire()方法会申请一个permit,state就减1一次,直到许可证数量小于0则阻塞等待;

    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    
     public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())  //调用前先检测该线程中断标志位,检测该线程在之前是否被中断过
            throw new InterruptedException();
// 尝试获取共享资源锁,小于0则获取失败,tryAcquireShared方法由自定义同步器sync实现
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
// FairSync 公平锁的 tryAcquireShared 方法
        protected int tryAcquireShared(int acquires) {
        for (;;) { // 自旋的死循环操作方式
// 检查线程是否有阻塞队列:若有阻塞队列,说明共享资源的许可数量已经用完,返回-1进行入队操作
            if (hasQueuedPredecessors()) 
                return -1; 
            int available = getState(); // 获取锁资源的最新内存值
            int remaining = available - acquires; // 计算得到剩下的许可数量remaining
            if (remaining < 0 || // 若剩下的许可数量小于0,返回负数进入入队操作
                compareAndSetState(available, remaining)) 
// 若共享资源大于或等于0,通过compareAndSetState操作占据最后一个共享资源
                return remaining; 
// 不管得到remaining后进入了何种逻辑,操作了之后再将remaining返回,上层会根据remaining的值进行判断是否需要入队操作
        }
    }


 // NonfairSync 非公平锁的 tryAcquireShared 方法
    protected int tryAcquireShared(int acquires) {
        return nonfairTryAcquireShared(acquires); 
    }

    // NonfairSync 非公平锁父类 Sync 类的 nonfairTryAcquireShared 方法    
    final int nonfairTryAcquireShared(int acquires) {
        for (;;) { // 自旋的死循环操作方式
            int available = getState(); // 获取最新许可证
            int remaining = available - acquires; // 剩下的许可数量
            if (remaining < 0 || // 若剩下的许可数量小于0,返回负数然后进入入队操作
                compareAndSetState(available, remaining)) // 若共享资源大于或等于0,
//通过CAS操作占据最后一个共享资源

                return remaining; 
// 不管得到remaining后进入了何种逻辑,
//操作后再将remaining返回,上层会根据remaining的值进行判断是否需要入队操作
        }
    }

release()方法如下。释放许可的时候要保证唤醒后继结点,以此来保证线程释放他们所持有的信号量;

    public void release() {
        sync.releaseShared(1); // 释放一个许可资源 
    }

     public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) { // 尝试释放共享锁资源,此方法由AQS的具体子类sync实现
            doReleaseShared(); // 自旋操作,唤醒后继结点
            return true;
        }
        return false;
    }

    
     // NonfairSync 和 FairSync 的父类 Sync 类的 tryReleaseShared 方法   
    protected final boolean tryReleaseShared(int releases) {
        for (;;) { // 自旋的死循环操作方式
            int current = getState(); // 获取最新的共享锁资源值
            int next = current + releases; // 对许可数量进行加法操作
            // int类型的state状态值可能溢出了,
            if (next < current) // overflow
                throw new Error("Maximum permit count exceeded");
            if (compareAndSetState(current, next)) // 
                return true; // 返回成功标志,告诉上层该线程已经释放了共享锁资源
        }
    }

 

Semaphore案例

本例我们希望在10个线程的情况下,最多有3个线程在工作。

public class Test {
    static class MyThread extends Thread {
        private int value;
        private Semaphore semaphore;

        public MyThread(int value, Semaphore semaphore) {
            this.value = value;
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
            try {
                semaphore.acquire();
                System.out.println("线程" + value + "拿到许可证,还剩" + semaphore.availablePermits() +
                                    "个许可证,还有" + semaphore.getQueueLength() + "个线程在等待");
                Random random =new Random();
                Thread.sleep(random.nextInt(1000));
                semaphore.release();
                System.out.println("线程" + value + "释放了许可证");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
    public static void main(String[] args)  throws InterruptedException{
        Semaphore semaphore = new Semaphore(3);
        for(int i = 0; i < 10; i++) {
            new Thread(new MyThread(i, semaphore)).start();
        }
    }
}

输出:

通信工具类_第1张图片

Semaphore默认的acquire方法是会让线程进入等待队列,且会抛出中断异常。但它还有一些方法可以忽略中断或不进入阻塞队列:

// 忽略中断
public void acquireUninterruptibly()
public void acquireUninterruptibly(int permits)

// 不进入等待队列,底层使用CAS
public boolean tryAcquire
public boolean tryAcquire(int permits)
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
        throws InterruptedException
public boolean tryAcquire(long timeout, TimeUnit unit)

 


CountDownLatch

CountDownLatch是一种同步帮助,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。CountDownLatch内部也定义了一个继承AQS的同步器Sync。

总结CountDownLatch的流程即为:

(1)管理一个大于0的计数器值

(2)每当一个线程执行一次countDown方法,计数器值减1,直到计数器值为0,那就释放队列中的索引等待线程。

 

CountDownLatch使用示例

举例来说:当我们进入一个游戏前,一般会有一些前置任务完成,比如说“加载地图数据”,“加载人物模型”,“加载背景音乐”。只有等这些任务都完成了,才能进入一个游戏。现在我们用CountDownLatch来模拟这个场景。

public class Test {
    static class MyThread extends Thread {
        private String taskName;
        private CountDownLatch countDownLatch;

        public MyThread(String taskName, CountDownLatch countDownLatch) {
            this.taskName = taskName;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
                Random random =new Random();
                Thread.sleep(random.nextInt(1000));
                System.out.println(taskName + "任务完成");
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
    public static void main(String[] args)  throws InterruptedException{
        CountDownLatch countDownLatch = new CountDownLatch(3);
        //主任务
        new Thread(new Runnable() {
            @Override

            public void run() {
                try{
                    System.out.println("等待其他任务进行中");
                    System.out.println("还有" + countDownLatch.getCount() + "个待完成任务");
                    countDownLatch.await();
                    System.out.println("所有前置任务完成,开始主任务");
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
        }}).start();
//        前置任务
        new Thread(new MyThread("加载地图数据", countDownLatch)).start();
        new Thread(new MyThread("加载人物模型", countDownLatch)).start();
        new Thread(new MyThread("加载背景音乐", countDownLatch)).start();
    }
}

输出:

等待其他任务进行中
还有3个待完成任务
加载地图数据任务完成
加载人物模型任务完成
加载背景音乐任务完成
所有前置任务完成,开始主任务

 

构造方法

// 构造方法:
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);   //count赋值给了state
    }

await()

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
 // 调用之前先检测该线程中断标志位,检测该线程在之前是否被中断过
        if (Thread.interrupted())
            throw new InterruptedException(); // 若被中断过,则抛出中断异常
// 尝试获取共享资源锁,小于0则获取失败,此方法由CountDownLatch的具体子类Sync实现
        if (tryAcquireShared(arg) < 0) 
            doAcquireSharedInterruptibly(arg); // 将尝试获取锁资源的线程进行入队操作
}
//该方法判断计数器值是否为0
protected int tryAcquireShared(int acquires) {
// 计数器值与零比较判断,等于零则获取锁成功,否则获取锁失败
   return (getState() == 0) ? 1 : -1; 
}

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        // 按照给定的mode模式创建新的结点,模式有Node.EXCLUSIVE独占模式、Node.SHARED共享模式;
        final Node node = addWaiter(Node.SHARED); // 创建共享模式的结点
        boolean failed = true;
        try {
            for (;;) { 
                final Node p = node.predecessor(); // 获取结点的前驱结点
                if (p == head) { //若前驱结点为head,则尝试获取锁,这是因为头结点可能会释放锁
                    int r = tryAcquireShared(arg); 
                /*
                   若r>=0,说明成功获取共享锁资源. 
                   把当前node结点设置为头结点,该方法会调用doReleaseShared释放一下无用的结点
                */
                    if (r >= 0) { 
                        setHeadAndPropagate(node, r); 
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
    
                /*
                   若第二个节点没有成功获取锁(r < 0),此时在第一次循环中,
                   该节点的waitStatus=0,因此被修改一次为SIGNAL状态。然后在第二次循环中。
                   shouldParkAfterFailedAcquire方法时,返回ture就是需要休眠,然后调用
                    park方法阻塞等待。
                */
                if (shouldParkAfterFailedAcquire(p, node) && //根据前驱结点判断是否需要休息
                // 阻塞操作,正常情况下,获取不到共享锁,代码就在该方法停止了,直到被唤醒
                    parkAndCheckInterrupt()) 
                    throw new InterruptedException();
// 被唤醒后,若发现parkAndCheckInterrupt()里面检测了被中断了的话,则抛出异常
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

countDown()

public void countDown() {
   sync.releaseShared(1); // 释放一个许可资源 
}

//释放一个许可的方法 
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) { // 尝试释放共享锁资源,此方法由Sync实现
            doReleaseShared(); // 自旋操作,唤醒后继结点
            return true; // 返回true表明所有线程已释放
    }
   return false; // 返回false表明目前还没释放完全,只要计数器值不为零的话,那么都会返回false
}

//该方法尝试将计数器减1,该方法由CountDownLatch 的静态内部类 Sync 类实现
protected boolean tryReleaseShared(int releases) {
        for (;;) {
// 获取最新的计数器值,若为0,表示已通过CAS操作减至零,无需做什么,返回false
            int c = getState(); 
            if (c == 0) 
                return false;
            int nextc = c-1; // 计数器值减1操作
            if (compareAndSetState(c, nextc)) // 通过CAS比较,顺利情况下设置成功返回true
                return nextc == 0; 
//若CAS操作成功且计数值state被减成了0,则需要释放所有等待的线程队列
// 若CAS失败,则在下一次循环查看是否已经被其他线程处理了
        }
}

//该方法尝试将头结点置为空,并唤醒后继节点
private void doReleaseShared() {
        for (;;) {
            Node h = head; // 每次都是取出队列的头结点
            if (h != null && h != tail) { // 若头结点不为空且也不是队尾结点
                int ws = h.waitStatus; //获取头结点的waitStatus状态值
                if (ws == Node.SIGNAL) { // 若头结点是SIGNAL状态则意味着头结点的后继结点需要被唤醒了
//通过CAS尝试设置头结点的状态为空状态,失败的话,则继续循环,这是因为可能有其他线程也在释放。
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            
                    unparkSuccessor(h); // 唤醒头结点的后继结点
                }
//若头结点为空状态,则把其改为PROPAGATE状态,失败是因为其他线程改动过,因此再次循环处理
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;               
            }
// 若头结点没有发生什么变化,则说明上述设置已经完成
// 若发生了变化,可能是操作过程中头结点有了新增,那么则必须进行重试,以保证唤醒动作可以延续传递
            if (h == head)                   
                break;
        }
}

 


CyclicBarrier

CyclicBarrier允许多个线程相互等待,即多个线程到达同步点时被阻塞,直到最后一个线程到达同步点时栅栏才会被打开。之前介绍的CountDownLatch一旦计数值等于0后,就不能再重新设置,即只有1次屏障的作用;而CyclicBarrier不仅拥有CountDownLatch的所有功能,还可以使用reset()方法重置屏障。

 

CyclicBarrier使用示例

同样是CountDownLatch中的例子,不过我们等待需要完成的前置任务换成了三个关卡,每个关卡的前置任务都完成了,就可以进入游戏。

public class Test {
    static class MyThread extends Thread {
        private String taskName;
        private CyclicBarrier cyclicBarrier;

        public MyThread(String taskName, CyclicBarrier cyclicBarrier) {
            this.taskName = taskName;
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            //总共3个关卡
            for(int i = 1; i < 4; i++) {
                try {
                    Random random =new Random();
                    Thread.sleep(random.nextInt(1000));
                    System.out.print(Thread.currentThread().getName() + ":");
                    System.out.println("关卡" + i + "的" + taskName + "任务完成");
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                cyclicBarrier.reset();  //重置屏障
            }

        }
    }
    public static void main(String[] args)  throws InterruptedException{
        Thread anoTask = new Thread(new Runnable() {
            //主任务
            @Override
            public void run() {
                System.out.print(Thread.currentThread().getName());
                System.out.println("本卡关所有任务均已完成,开始进入游戏");
            }
        });
        anoTask.setName("gg");
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, anoTask);
//        前置任务
        Thread t1 = new Thread(new MyThread("加载地图数据", cyclicBarrier));
        t1.setName("t1");
        t1.start();

        Thread t2 = new Thread(new MyThread("加载人物模型", cyclicBarrier));
        t2.setName("t2");
        t2.start();

        Thread t3 = new Thread(new MyThread("加载背景音乐", cyclicBarrier));
        t3.setName("t3");
        t3.start();
    }
}

输出:

t1:关卡1的加载地图数据任务完成
t3:关卡1的加载背景音乐任务完成
t2:关卡1的加载人物模型任务完成
t2本卡关所有任务均已完成,开始进入游戏
t1:关卡2的加载地图数据任务完成
t2:关卡2的加载人物模型任务完成
t3:关卡2的加载背景音乐任务完成
t3本卡关所有任务均已完成,开始进入游戏
t3:关卡3的加载背景音乐任务完成
t2:关卡3的加载人物模型任务完成
t1:关卡3的加载地图数据任务完成
t1本卡关所有任务均已完成,开始进入游戏

CyclicBarrier没有分为await()countDown(),它只有单独的一个await()方法。当调用await()方法的线程数量等于构造方法中传入的任务总量3,就代表达到屏障了。CyclicBarrier允许我们在达到屏障时执行一个任务,可以在构造方法传入一个Runnable类型的对象。上例中就是在达到屏障(完成三个前置任务)后,选择一个前置任务线程来输出“本卡关所有任务均已完成,开始进入游戏”

 

CyclicBarrier构造器

// 构造函数一
public CyclicBarrier(int parties) {
        this(parties, null);
}

// 构造函数二
//parties参数为支持参与的最多线程,当最后一个阻塞线程被释放后,它有机会执行任务barrierAction
public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
}   

 

await()

public int await() throws InterruptedException, BrokenBarrierException {
       try {
            return dowait(false, 0L);
       } catch (TimeoutException toe) {
            throw new Error(toe); 
       }
}

/*
   dowait方法是CyclicBarrier实现阻塞等待的核心方法,当await方法被调用时阻塞等待被Condition
的一个队列trip维护着.
线程从await跳出来时,正常情况下一般都是由于发送了信号量,阻塞被解除,那么Condition的等待队列
将会被转移至AQS的等待队列; 然后一个逐渐锁释放,最后CyclicBarrier也处于了初始值状态,供下次调用使用;
因此CyclicBarrier每用完一套整个流程,又会回到初始状态值,又可以被其他地方当做新创建的对象一样
来使用,所以才成为循环栅栏;
*/
private int dowait(boolean timed, long nanos)throws InterruptedException,BrokenBarrierException, TimeoutException {

        final ReentrantLock lock = this.lock; // 获取独占锁
        lock.lock(); // 通过lock其父类AQS的CLH队列阻塞在此,但是为啥又会继续往下进入临界区执行try方法,其原因就是trip.await()这句代码
        try {
            final Generation g = generation;

            if (g.broken) //平衡被打破,则其他所有的线程都会抛出异常,
                throw new BrokenBarrierException();

//检测线程是否在其他地方被中断过,若任何一个线程被中断过,则打破平衡,并设置打破平衡的标志,
//还原初始状态值,然后再唤醒所有被阻塞的线程,
            if (Thread.interrupted()) { 
                breakBarrier(); 
                throw new InterruptedException();
            }
//count表示还有多少个线程还在lock阻塞队列中
            int index = --count; 
            if (index == 0) {  // 当count值降为0后,则表明所有线程都执行完了。
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand; // 构造方法传入的接口回调对象
                    if (command != null) // 当接口不为空时,最后一个执行的线程有机会执行该任务
                        command.run();
                    ranAction = true;
                    nextGeneration(); // 还原为初始状态值,以便下次可以重复再次使用
                    return 0;
                } finally {
                    if (!ranAction) // 若最后一个线程出现了任何异常的话,打破整体平衡
                        breakBarrier();
                }
            }

            for (;;) { // 自旋的死循环操作方式
                try {
// 若不需要使用超时等待信号量的话,那么下面就直接调用trip.await()进入阻塞等待
                    if (!timed) 
// 正常情况下,代码执行到此就不动了,该方法内部已经调用了park方法导致线程阻塞等待
                        trip.await(); 
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos); // 在指定时间内等待信号量
                } catch (InterruptedException ie) { // 若在阻塞等待期间由于被中断了
// 如果还没更换标识位,
//并且平衡标志位还为false的话,则继续打破平衡并且抛出中断异常
                    if (g == generation && ! g.broken) { 
                        breakBarrier();
                        throw ie;
                    } else {
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken) // ,若平衡被打破,则其他所有的线程都会抛出异常
                    throw new BrokenBarrierException();

                if (g != generation) // 若已经被改朝换代了,那么则直接返回index值
                    return index;
// 若设置了超时标志,并且不管是传入的nanos值也好还是通过等待后返回的nanos也好,
//只要小于或等于零都会打破平衡
                if (timed && nanos <= 0L) { 
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock(); 
        }
}

//打破平衡,并设置打破平衡的标志,然后再唤醒所有被阻塞的线程;
private void breakBarrier() {
      generation.broken = true; // 设置打破平衡的标志
      count = parties; // 重新还原count为初始值
      trip.signalAll(); // 发送信号量,唤醒所有Condition中的等待队列
}


//唤醒所有在Condition中等待的队列,然后还原初始状态值,
//并且重新换掉generation的引用,改朝换代,为下一轮操作做准备;
private void nextGeneration() {
        // signal completion of last generation
        trip.signalAll();
        // set up next generation
        count = parties;
        generation = new Generation();
}


 

 

 


参考资料

https://blog.csdn.net/YLIMH_HMILY/article/details/79521610

 

 

 

 

你可能感兴趣的:(多线程)