多线程并发协同工具

539 阅读9分钟

前言

之前分享了好几篇关于线程并发安全问题的文章,相信大家对多线程安全问题已经深有感触了

但是关于多线程不仅要知道如何保证数据的安全,还要知道怎么让多线程协同去工作

场景举例

1.某个接口执行比较复杂的业务时占用内存和CPU都比较高,如果请求量超过10个将会导致系统的性能整体下降

2.某个接口处理时间比较长,为了提高接口的响应速度决定将一件事分给多个线程一起去做,当多线程做完了再由主线程合并计算

....................

实际工作中还会有很多这样的应用场景,限流、协同、线程栅栏这些常见的多线程应对手段都值得我们去学习和探讨

在了解并发工具类之前还要先知道aqs机制,接下来我们一起理解aqs

AbstractQueuedSynchronizer

嗯!既然是一个抽象类就意味着这是之前理解的一个模板,我们接着往下看

static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;

        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;

        volatile int waitStatus;

    	// 前驱结点
        volatile Node prev;

        // 后驱结点
        volatile Node next;

        // 当前线程
        volatile Thread thread;


        Node nextWaiter;

        final boolean isShared() {
            return nextWaiter == SHARED;
        }


        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    
        }

        Node(Thread thread, Node mode) {     
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { 
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

看到前驱结点和后驱节点之后就可以断定它就是一个链表,从这个类描述中可以看到这是一个双端双向循环链表

/**
* <pre>
*      +------+  prev +-----+       +-----+
* head |      | <---- |     | <---- |     |  tail
*      +------+       +-----+       +-----+
* </pre>
*/

因为head指向头部节点所以它知道头部是哪个,接下来通过画一个图来理解一下Node链表的关系

除了链表以外它还维护了一个状态,这个状态由volatile修饰那就必然具备了可见性,也提供了这个状态的cas操作

对于aqs里面的所有操作都是在维护这个状态,通过对这个状态的操作来完成我们期望的加锁/解锁

它把我们之前想要实现的可重入锁/读写锁中的模板代码封装了起来

如果日后要实现关于锁方面的需求,可以直接继承aqs实现它里面的一些特定方法即可完成相对应的功能

需要重写的特定方法如下

方法描述
isHeldExclusively()该线程是否正在独占资源.只有用到Condition才需要实现它
tryAcquire(int)独占方式尝试获取锁,成功返回true失败返回false
tryRelease(int)独占方式尝试释放锁,成功返回true失败返回false
tryAcquireShared(int)共享方式尝试获取锁,成功返回0,失败返回负数,正数表示成功且有剩余资源
tryReleaseShared(int)共享方式尝试释放锁,如果释放后允许唤醒后续等待节点返回true否则返回false

Semaphore

Semaphore是一个计数信号量,常用于限制可以访问某些资源(物理或逻辑的)线程数目,简单来说它就是一种用来控制并发量的共享锁

使用

/**
 * <p>
 * 计数信号量使用
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/24 0024 17:16
 */
public class SemaphoreDemo {

    private static final Semaphore semaphore = new Semaphore(2);

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                try {
                    // 获取信号量,当达到限制范围会阻塞该线程
                    semaphore.acquire();
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + "执行结束" + System.currentTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放信号量
                    semaphore.release();
                }
            });
        }
        executorService.shutdown();
    }
}

从结果中可以看到每次只会有两个线程去执行,其余的都在等待直到有线程释放后才能获取,它能够起到限流的作用

实现

从它的表现可以猜得出来像是一把限制了数量的读锁,每次只能给限定数量的线程通过超过则阻塞等待

既然如此我们可以继承aqs重写共享锁对应的两个特定方法(tryAcquireShared、tryReleaseShared)来实现Semaphore

/**
 * <p>
 * 自定义Semaphore
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/24 0024 17:31
 */
public class MySemaphore {

    private final Sync sync;

    class Sync extends AbstractQueuedSynchronizer {

        private volatile int permits;

        private Sync(int permits) {
            this.permits = permits;
        }

        @Override
        protected int tryAcquireShared(int arg) {
            int state = getState();
            int updateState = getState() + arg;
            if (updateState <= permits) {
                if (compareAndSetState(state, updateState)) {
                    return permits - updateState;
                }
            }
            return -1;
        }

        @Override
        protected boolean tryReleaseShared(int arg) {
            int state = getState();
            int updateState = getState() - arg;
            if (updateState >= 0) {
                return compareAndSetState(state, updateState);
            }
            return false;
        }
    }

    public MySemaphore(int permits) {
        sync = new Sync(permits);
    }

    public void acquire() {
        sync.acquireShared(1);
    }

    public void release() {
        sync.releaseShared(1);
    }
}

用了aqs以后发现代码量是很少的,因为很多重复的代码aqs都封装好了,改成用自己写的试一把

/**
 * <p>
 * 计数信号量使用
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/24 0024 17:16
 */
public class SemaphoreDemo {

    private static final MySemaphore semaphore = new MySemaphore(2);
    
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                try {
                    // 获取信号量,当达到限制范围会阻塞该线程
                    semaphore.acquire();
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放信号量
                    semaphore.release();
                }
            });
        }
        executorService.shutdown();
    }
}

通过使用我们实现MySemaphore同样可以达到semaphore的效果

CountDownLatch

CountDownLatch是一个线程计数器,能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行

使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一

当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务

使用

它有两种用法:1.希望多个线程在某一刻并发执行 2.主线程等待多线程执行完后执行

希望多个线程在某一刻并发执行

/**
 * <p>
 * 线程计数器使用
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/24 0024 17:50
 */
public class CountDownLatchDemo {

    private static CountDownLatch countDownLatch = new CountDownLatch(9);


    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 9; i++) {
            executorService.execute(() -> {
                // 计数器减一
                countDownLatch.countDown();
                try {
                    // 等待计数器的值为0时再执行
                    countDownLatch.await();
                    System.out.println(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}

主线程等待多线程执行完后执行

/**
 * <p>
 * 线程计数器使用
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/24 0024 17:50
 */
public class CountDownLatchDemo {

    private static CountDownLatch countDownLatch = new CountDownLatch(9);


    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 9; i++) {
            executorService.execute(() -> {
                list.add(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
                // 执行完毕计数器减一
                countDownLatch.countDown();
            });
        }
        executorService.shutdown();

        try {
            countDownLatch.await();
            list.forEach(System.out::println);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

实现

/**
 * <p>
 * 自定义CountDownLatch
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/24 0024 18:01
 */
public class MyCountDownLatch {

    private Sync sync;

    class Sync extends AbstractQueuedSynchronizer {

        private Sync(int count) {
            setState(count);
        }

        @Override
        protected int tryAcquireShared(int arg) {
            return getState() == 0 ? 0 : -1;
        }

        @Override
        protected boolean tryReleaseShared(int arg) {
            // 循环操作cas直到成功为止
            while (true) {
                int state = getState();
                int updateState = getState() - arg;
                if (updateState < 0) {
                    return false;
                } else {
                    if (compareAndSetState(state, updateState)) {
                        // 当最后一个线程执行完毕时再唤醒后续等待节点
                        return updateState == 0;
                    }
                }
            }
        }
    }

    public MyCountDownLatch(int count) {
        this.sync = new Sync(count);
    }

    public void countDown() {
        sync.releaseShared(1);
    }

    public void await() {
        sync.acquireShared(1);
    }
}
/**
 * <p>
 * 线程计数器使用
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/24 0024 17:50
 */
public class CountDownLatchDemo {

    private static MyCountDownLatch countDownLatch = new MyCountDownLatch(9);


    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();

        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 9; i++) {
            executorService.execute(() -> {
                list.add(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
                // 执行完毕计数器减一
                countDownLatch.countDown();
            });
        }
        executorService.shutdown();

        countDownLatch.await();
        list.forEach(System.out::println);
    }
}

通过使用我们实现MyCountDownLatch同样可以达到CountDownLatch的效果

CyclicBarrier

CyclicBarrier是一个循环栅栏,它必须要达到指定的线程数量才能往下执行

使用

/**
 * <p>
 * CyclicBarrier使用
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/24 0024 18:32
 */
public class CyclicBarrierDemo {

    private static int i = 0;
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> {
        i++;
        System.out.println("第" + i + "组执行");
    });


    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 4; i++) {
            executorService.execute(() -> {
                try {
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}

可以看得到它是按照指定的数量分组进行执行,但是假设某一组并未达到指定的线程数量将会进入阻塞状态

/**
 * <p>
 * CyclicBarrier使用
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/24 0024 18:32
 */
public class CyclicBarrierDemo {

    private static int i = 0;
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> {
        i++;
        System.out.println("第" + i + "组执行");
    });


    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 3; i++) {
            executorService.execute(() -> {
                try {
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}

可以看到程序没能结束,因为有一个线程进入了阻塞状态它在等待足够的数量才会被唤醒

实现

CyclicBarrier用cas的机制不好实现,而它的实现原理也并非是利用aqs

而是利用了可重入锁ReentrantLock里面的Condition机制

/**
 * <p>
 * 自定义CyclicBarrier
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/24 0024 18:51
 */
public class MyCyclicBarrier {

    /**
     * 可重入锁
     */
    private Lock lock = new ReentrantLock();

    /**
     * 等待/唤醒机制
     */
    private Condition condition = lock.newCondition();

    /**
     * 版本号(被唤醒时需要判断是否属于这一组的)
     */
    private Object generation = new Object();

    /**
     * 累加栅栏数
     */
    private volatile int count = 0;

    /**
     * 固定栅栏数
     */
    private final int parties;

    /**
     * 调用者自定义的Runnable方法,每当达到固定栅栏数时调用
     */
    private final Runnable runnable;

    public MyCyclicBarrier(int parties, Runnable runnable) {
        this.parties = parties;
        this.runnable = runnable;
    }

    public void await() throws InterruptedException {
        Lock lock = this.lock;
        lock.lock();
        try {
            count++;
            final Object obj = generation;
            if (count != parties) {
                // 防止伪唤醒,循环处理
                while (true) {
                    condition.await();
                    // condition.signalAll() 可能会唤醒到不同版本的线程,通过版本号进行区分
                    if (obj != generation) {
                        return;
                    }
                }
            } else {
                count = 0;
                generation = new Object();
                runnable.run();
                condition.signalAll();
            }
        } finally {
            lock.unlock();
        }
    }
}
/**
 * <p>
 * CyclicBarrier使用
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/24 0024 18:32
 */
public class CyclicBarrierDemo {

    private static int i = 0;
    private static MyCyclicBarrier cyclicBarrier = new MyCyclicBarrier(2, () -> {
        i++;
        System.out.println("第" + i + "组执行");
    });


    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 4; i++) {
            executorService.execute(() -> {
                try {
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}

故意将执行数量设置为拼凑不成一组的看看会不会被阻塞

/**
 * <p>
 * CyclicBarrier使用
 * </p>
 *
 * @author 千手修罗
 * @date 2020/12/24 0024 18:32
 */
public class CyclicBarrierDemo {

    private static int i = 0;
    private static MyCyclicBarrier cyclicBarrier = new MyCyclicBarrier(2, () -> {
        i++;
        System.out.println("第" + i + "组执行");
    });


    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 3; i++) {
            executorService.execute(() -> {
                try {
                    cyclicBarrier.await();
                    System.out.println(Thread.currentThread().getName() + "执行结束" + LocalDateTime.now());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        executorService.shutdown();
    }
}

通过使用我们实现MyCyclicBarrier同样可以达到CyclicBarrier的效果

总结

并发协同工具的使用和原理到这就结束了,现在回过头来再看aqs会发现已经不再那么神秘而陌生了

无非也就是将**(独占/共享)锁的实现进行封装起来**,然后再将上锁/解锁的方式交由到子类自己实现

从而达到子类不费吹灰之力就能够实现一把锁的特性,这也是为什么aqs常常会伴随着锁一起出现的原因