Java并发编程 线程协作、控制并发流程

1.什么是控制并发流程

  • 控制并发流程的工具类,作用就是帮助我们程序员更容易的让线程之间合作
  • 让线程之间相互配合,来满足业务逻辑
  • 比如让线程A等待线程B执行完毕后再执行等合作策略
有哪些控制并发流程的工具类?
作用 说明
Semaphore 信号量,可以通过控制“许可证”的数量,来保证线程之间的配合 线程只有拿到“许可证”后才能继续运行。相比于其他同步器更灵活
CyclicBarrier 线程会等待,直到足够多线程达到了事先规定的数目。一旦达到触发条件,就可以进行下一步的动作。 适用于线程之间相互等待处理结果就绪的场景
Phaser 和CyclicBarrier类似,但是计数可变 Java7 加入的
CountDownLatch 和CyclicBarrier类似,数量递减到0时,触发工作 不可重复使用
Exchanger 让两个线程在合适时交换对象 适用场景:当两个线程工作在同一个类的不同实例上时,用于交换数据
Condition 可以控制线程的“等待”和“唤醒” 是Object.wait的升级版

2.CountDownLatch倒计时门闩

并发流程控制的工具

  • 倒数门闩
  • 例子:购物拼团;大巴(游乐园坐过山车排队),人满发车。
  • 流程:倒数结束之前,一直处于等待状态,直到倒计时结束了,此线程才继续工作。
2.1 类的主要方法
//参数count为计数值
public CountDownLatch(int count) {  };  

//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };   
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
//将count值减1,直到为0时,等待的线程会被唤起
public void countDown() { };  
两个典型用法

类的主要方法介绍
用法1:一个线程等待多个线程都执行完毕 ,再继续自己的工作

public class CountDownLatchDemo {

   public static void main(String[] args) throws InterruptedException {
       CountDownLatch latch = new CountDownLatch(5);
       ExecutorService service = Executors.newFixedThreadPool(5);
       for (int i = 0; i < 5; i++) {
           final int no = i + 1;
           Runnable runnable = new Runnable() {
               @Override
               public void run() {
                   try {
                       Thread.sleep((long) (Math.random() * 10000));
                       System.out.println("No." + no + "完成了检查。");
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   } finally {
                       latch.countDown();
                   }
               }
           };
           service.submit(runnable);
       }
       System.out.println("等待5个人检查完.....");
       latch.await();
       System.out.println("所有人都完成了工作,进入下一个环节。");
   }
}
等待5个人检查完.....
No.1完成了检查。
No.4完成了检查。
No.2完成了检查。
No.5完成了检查。
No.3完成了检查。
所有人都完成了工作,进入下一个环节。

用法2:多个线程等待某一个线程的信号,同时开始执行

public class CountDownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch begin = new CountDownLatch(1);
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            final int no = i + 1;
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("No." + no + "准备完毕,等待发令枪");
                    try {
                        begin.await();
                        System.out.println("No." + no + "开始跑步了");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            service.submit(runnable);
        }
        //裁判员检查发令枪...
        Thread.sleep(5000);
        System.out.println("发令枪响,比赛开始!");
        begin.countDown();
    }
}
No.3准备完毕,等待发令枪
No.1准备完毕,等待发令枪
No.2准备完毕,等待发令枪
No.4准备完毕,等待发令枪
No.5准备完毕,等待发令枪
发令枪响,比赛开始!
No.2开始跑步了
No.5开始跑步了
No.1开始跑步了
No.3开始跑步了
No.4开始跑步了

用法1和用法2结合

public class CountDownLatchDemo1And2 {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch begin = new CountDownLatch(1);

        CountDownLatch end = new CountDownLatch(5);
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            final int no = i + 1;
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("No." + no + "准备完毕,等待发令枪");
                    try {
                        begin.await();
                        System.out.println("No." + no + "开始跑步了");
                        Thread.sleep((long) (Math.random() * 10000));
                        System.out.println("No." + no + "跑到终点了");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        end.countDown();
                    }
                }
            };
            service.submit(runnable);
        }
        //裁判员检查发令枪...
        Thread.sleep(5000);
        System.out.println("发令枪响,比赛开始!");
        begin.countDown();

        end.await();
        System.out.println("所有人到达终点,比赛结束");
    }
}
No.3准备完毕,等待发令枪
No.1准备完毕,等待发令枪
No.2准备完毕,等待发令枪
No.4准备完毕,等待发令枪
No.5准备完毕,等待发令枪
发令枪响,比赛开始!
No.2开始跑步了
No.5开始跑步了
No.1开始跑步了
No.3开始跑步了
No.4开始跑步了
总结:多等1,CountDownLatch count为1,
1等多(N),CountDownLatch count为N

注意点:
扩展用法:多个线程等待多个线程完成执行后,再同时执行
CountDownLatch是不够重用的,如果需要重新计数,可以考虑CyclicBarrier或者创建新的CountDownLatch实例。

3.Semaphore信号量

Semaphore叫信号量,Semaphore有两个目的,第一个是多个共享资源互斥使用,第二个是并发线程数的控制。
1.初始化Semaphore并指定许可证的数量
2.在需要被现在的代码前面加acquire()或者acquireUninterruptibly()方法
3.在任务执行结束后,调用release()来释放许可证
信号量主要方法

1、类Semaphore的构造函数permits 是许可的意思,代表同一时间,最多允许permits执行acquire() 和release() 之间的代码。
例如:
Semaphore semaphore = new Semaphore(1);
表示同一时间内最多只允许1个线程执行 acquire()和release()之间的代码。
2、方法acquire(n) 的功能是每调用1次此方法,就消耗掉n个许可。
3、方法release(n) 的功能是每调用1次此方法,就动态添加n个许可。
4、方法acquireUnnterruptibly()作用是是等待进入acquire() 方法的线程不允许被中断。
5、方法availablePermits() 返回Semaphore对象中当前可以用的许可数。
6、方法drainPermits() 获取并返回所有的许可个数,并且将可用的许可重置为0
7、方法 getQueueLength() 的作用是取得等待的许可的线程个数
8、方法 hasQueueThreads() 的作用是判断有没有线程在等待这个许可
9、公平和非公平信号量:
有些时候获取许可的的顺序与线程启动的顺序有关,这是的信号量就要分为公平和非公平的。所谓的公平信号量是获得锁的顺序与线程启动的顺序有关,但不代表100%获得信号量,仅仅是在概率上能保证,而非公平信号量就是无关的。
例如:
Semaphore semaphore = new Semaphore(1,false);
False:表示非公平信号量,即线程启动的顺序与调用semaphore.acquire() 的顺序无关,也就是线程先启动了并不代表先获得 许可。
True:公平信号量,即线程启动的顺序与调用semaphore.acquire() 的顺序有关,也就是先启动的线程优先获得许可。
10、方法tryAcquire() 的作用是尝试获取1个许可。如果获取不到则返回false,通常与if语句结合使用,其具有无阻塞的特点。无阻塞的特点可以使不至于在同步处于一直持续等待的状态。
11、方法tryAcquire(n) 的作用是尝试获取n个许可,如果获取不到则返回false
12、方法tryAcquire(long timeout,TimeUnit unit)的作用是在指定的时间内尝试获取1个许可,如果获取不到则返回false
13、方法tryAcquire(int permits,long timeout,TimeUnit unit) 的作用是在指定的时间内尝试获取n 个许可,如果获取不到则返回false

基本用法

public class SemaphoreDemo {

    static Semaphore semaphore = new Semaphore(5, true);

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(50);
        for (int i = 0; i < 100; i++) {
            service.submit(new Task());
        }
        service.shutdown();
    }

    static class Task implements Runnable {

        @Override
        public void run() {
            try {
                semaphore.acquire(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "拿到了许可证");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "释放了许可证");
            semaphore.release(3);
        }
    }
}
public class SemaphoreDemo {

    static Semaphore semaphore = new Semaphore(5, true);

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(50);
        for (int i = 0; i < 100; i++) {
            service.submit(new Task());
        }
        service.shutdown();
    }

    static class Task implements Runnable {

        @Override
        public void run() {
            try {
                semaphore.acquire(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "拿到了许可证");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "释放了许可证");
            semaphore.release(3);
        }
    }
}
pool-1-thread-1拿到了许可证
pool-1-thread-1释放了许可证
pool-1-thread-3拿到了许可证
pool-1-thread-3释放了许可证
pool-1-thread-4拿到了许可证
...
注意事项

1.获取和释放许可证数量必须一致,否则会导致程序卡死。
2.注意在初始化Semaphore的时候设置公平性,一般设置为true会更合理。一般使用信用信号量的服务为慢服务,如果线程非公平会导致线程饥饿。
3.并不是必须由获取许可证的线程释放哪个许可证,事实上,获取和释放许可证对线程并无需求,也许是A获取了,然后由B释放,只要逻辑合理即可。
4.信号量的作用,除了控制临界区最多同时有N个线程访问外,另外一个作用是可以实现“条件等待”,例如线程1需要在线程2完成准备工作后才能开始工作,那么就线程1acquire(),而线程2完成任务后release(),这样的话,相当于轻量级的CountDownLatch。

4.Condition接口(又称条件对象)

41.作用
对比项 Object监视器方法 Condition
前置条件 获取对象的监视器锁 调用Lock.lock()获取锁
调用Lock.newCondition()获取Condition对象
调用方法 直接调用 如:object.wait() 直接调用 如:condition.await()
当前线程释放锁并进入等待队列 支持 支持
当前线程释放锁并进入等待队列,在等待状态中不响应中断 不支持 支持
当前线程释放锁并进入超时等待状态 支持 支持
当前线程释放锁并进入等待状态到将来的某个时间 不支持 支持
唤醒等待队列中的一个线程 支持 支持
唤醒等待队列中的全部线程 支持 支持

signalAll()和signal()区别

  • signalAll()会唤起所有的正在等待的线程
  • signal()是公平的,只会唤起哪个等待时间最长的线程
4.2.代码演示
public class ConditionDemo {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    void method1() throws InterruptedException {
        lock.lock();
        try{
            System.out.println("条件不满足,开始await");
            condition.await();
            System.out.println("条件满足了,开始执行后续的任务");
        }finally {
            lock.unlock();
        }
    }

    void method2() {
        lock.lock();
        try{
            System.out.println("准备工作完成,唤醒其他的线程");
            condition.signal();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ConditionDemo conditionDemo = new ConditionDemo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                    conditionDemo.method2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        conditionDemo.method1();
    }
}
条件不满足,开始await
准备工作完成,唤醒其他的线程
条件满足了,开始执行后续的任务
用Condition实现生产者消费者模式
public class ConditionDemo {

    private int queueSize = 10;
    private PriorityQueue queue = new PriorityQueue(queueSize);
    private Lock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmpty = lock.newCondition();

    public static void main(String[] args) {
        ConditionDemo conditionDemo2 = new ConditionDemo();
        Producer producer = conditionDemo2.new Producer();
        Consumer consumer = conditionDemo2.new Consumer();
        producer.start();
        consumer.start();
    }

    class Consumer extends Thread {

        @Override
        public void run() {
            consume();
        }

        private void consume() {
            while (true) {
                lock.lock();
                try {
                    while (queue.size() == 0) {
                        System.out.println("队列空,等待数据");
                        try {
                            notEmpty.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.poll();
                    notFull.signalAll();
                    System.out.println("从队列里取走了一个数据,队列剩余" + queue.size() + "个元素");
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    class Producer extends Thread {

        @Override
        public void run() {
            produce();
        }

        private void produce() {
            while (true) {
                lock.lock();
                try {
                    while (queue.size() == queueSize) {
                        System.out.println("队列满,等待有空余");
                        try {
                            notFull.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.offer(1);
                    notEmpty.signalAll();
                    System.out.println("向队列插入了一个元素,队列剩余空间" + (queueSize - queue.size()));
                } finally {
                    lock.unlock();
                }
            }
        }
    }

}
取走了一个数据,队列剩余9个元素
从队列里取走了一个数据,队列剩余8个元素
从队列里取走了一个数据,队列剩余7个元素
从队列里取走了一个数据,队列剩余6个元素
从队列里取走了一个数据,队列剩余5个元素
从队列里取走了一个数据,队列剩余4个元素
从队列里取走了一个数据,队列剩余3个元素
从队列里取走了一个数据,队列剩余2个元素
从队列里取走了一个数据,队列剩余1个元素
从队列里取走了一个数据,队列剩余0个元素
队列空,等待数据
向队列插入了一个元素,队列剩余空间9
向队列插入了一个元素,队列剩余空间8
...
4.3.Condition注意点
  • 如果说Lock用来代替synchronized,那么Condition就是用来代替相对应的Object.wait/notify的,所以在用法和性质上,几乎都一样。
  • await方法会自动释放持有的Lock锁,和Object.wait一样,不需要自己手动先释放锁
  • 调用await的时候,必须持有锁,否则会抛出异常,和Object.wait一样。

5.CyclicBarrier循环栅栏

从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障。
它的作用就是会让所有线程都等待完成后才会继续下一步行动。

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                System.out.println("所有人都到场了, 大家统一出发!");
            }
        });
        for (int i = 0; i < 10; i++) {
            new Thread(new Task(i, cyclicBarrier)).start();
        }
    }

    static class Task implements Runnable{
        private int id;
        private CyclicBarrier cyclicBarrier;

        public Task(int id, CyclicBarrier cyclicBarrier) {
            this.id = id;
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            System.out.println("线程" + id + "现在前往集合地点");
            try {
                Thread.sleep((long) (Math.random()*10000));
                System.out.println("线程"+id+"到了集合地点,开始等待其他人到达");
                cyclicBarrier.await();
                System.out.println("线程"+id+"出发了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
}

线程1现在前往集合地点
线程6现在前往集合地点
线程0现在前往集合地点
线程7现在前往集合地点
线程4现在前往集合地点
线程2现在前往集合地点
线程3现在前往集合地点
线程9现在前往集合地点
线程8现在前往集合地点
线程5现在前往集合地点
线程3到了集合地点,开始等待其他人到达
线程8到了集合地点,开始等待其他人到达
线程5到了集合地点,开始等待其他人到达
线程2到了集合地点,开始等待其他人到达
线程0到了集合地点,开始等待其他人到达
所有人都到场了, 大家统一出发!
线程0出发了
线程3出发了
线程8出发了
线程2出发了
线程5出发了
线程6到了集合地点,开始等待其他人到达
线程1到了集合地点,开始等待其他人到达
线程7到了集合地点,开始等待其他人到达
线程9到了集合地点,开始等待其他人到达
线程4到了集合地点,开始等待其他人到达
所有人都到场了, 大家统一出发!
线程4出发了
线程6出发了
线程9出发了
线程7出发了
线程1出发了
CyclicBarrier 使用场景

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

CyclicBarrier 与 CountDownLatch 区别
  • CyclicBarrier 要等固定数量的线程都达到了栅栏位置才能执行,而CountDownLatch 只需要等待数字为0,也就是说,CountDownLatch 用于事件,但是CyclicBarrier 适用于线程的。
  • CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的
  • CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。

你可能感兴趣的:(Java并发编程 线程协作、控制并发流程)