Java多线程之并发控制工具

1,267 阅读8分钟

为什么需要并发控制

多个线程并发执行时候,在默认情况下CPU是随机切换线程的,不受我们程序员控制的。有时候我们希望CPU能按照我们想要的规律执行线程,此时就需要线程之间的协调控制。

如何实现并发控制

  • CountDownLatch
  • CyclicBarrier
  • Condition
  • Semaphore
  • Pharse

CountDownLatch倒计数器

CountDownLatch是一个非常实用的多线程控制工具类,通常用来控制线程等待,让某一个线程等待倒计数结束再开始执行。

主要方法

  • 构造方法 CountDownLatch(int count) 构造一个用给定计数初始化的 CountDownLatch
  • await() 调用该方法的线程会被挂起,等到count值减到0时才继续执行,除非线程被中断。
  • await(long timeout, TimeUnit unit)使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。
  • countDown():每调用一次countDown(),会将count值减1,直到减为0时,调用await的线程会被唤醒

用法场景

  • 用法一:一个线程等待多个线程都执行完后再继续执行。 比如:我们用他来模拟拼夕夕上的拼单过程。假如某个商品需要5个用户参与拼单购买才能生成订单,我们用CountDownLatch来实现。
public class CountDownLatchDemo {
	
	public static void main(String[] args) {
		CountDownLatch countDownLatch = new CountDownLatch(5);
		
		ExecutorService service = Executors.newFixedThreadPool(5);
		
		for(int i = 0; i < 5; i++){
			final int userId = i+1;
			service.submit(() -> {
				try {
					Thread.sleep((long) (Math.random() * 1000));
					System.out.println(String.format("[用户%s] 加入了拼单...", userId));
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					countDownLatch.countDown();
				}
			});
		}
		
		System.out.println("等待拼单.....");
		try {
			countDownLatch.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("完成拼单.....");
	}
}
等待拼单.....
[用户3] 加入了拼单...
[用户2] 加入了拼单...
[用户1] 加入了拼单...
[用户4] 加入了拼单...
[用户5] 加入了拼单...
完成拼单.....
  • 用法二:多个线程等待某一个线程的信号,同时开始执行
public class CountDownLatchDemo2 {
	
	public static void main(String[] args) {
		CountDownLatch begin = new CountDownLatch(1);
        ExecutorService service = Executors.newFixedThreadPool(5);
		
		for(int i = 0; i < 5; i++){
			final int userId = i+1;
			service.submit(() -> {
				try {
					System.out.println(String.format("[用户%s] 加入了秒杀抢购...", userId));
					begin.await();
					System.out.println(String.format("[用户%s] 开始秒杀...", userId));
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			});
		}
		service.shutdown();
		try {
			TimeUnit.SECONDS.sleep(5);
			System.out.println("秒杀开始!!!");
			begin.countDown();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
[用户5] 加入了秒杀抢购...
[用户4] 加入了秒杀抢购...
[用户3] 加入了秒杀抢购...
[用户1] 加入了秒杀抢购...
[用户2] 加入了秒杀抢购...
秒杀开始!!!
[用户3] 开始秒杀...
[用户5] 开始秒杀...
[用户4] 开始秒杀...
[用户1] 开始秒杀...
[用户2] 开始秒杀...

这种多等一的场景,可用于做压力测试。

使用总结:创建CountDownLatch的时候,需要传入倒数次数,哪个线程需要等待,就让那个线程中调用CountDownLatch的await()方法挂起,哪个线程倒数,就在那个线程中调用CountDownLatch的countDown()来减1。三个方法结合起来,当countDown到0时,之前调用await的线程就会被触发执行。从而控制并发。

  • CountDownLatch不能回滚重置

CyclicBarrier循环栅栏

CyclicBarrier翻译为循环栅栏,它允许一组线程互相等待,直到到达某个公共屏障点。这样看来CyclicBarrier和CountDownLatch很类似,都能阻塞一组线程。

主要方法

  • CyclicBarrier(int parties, Runnable barrierAction) 创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。

  • await() 在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。

代码示例

用CyclicBarrier模拟集五福

public class CyclicBarrierDemo {
	
	public static void main(String[] args) {
		
		CyclicBarrier cyclicBarrier = new CyclicBarrier(5, ()->{
			System.out.println(Thread.currentThread().getName());
			System.out.println("五福集齐,开始合成!");
		});
		String[] blessings = {"爱国福","友善福","敬业福","和谐福","富强福"};
		for (int i = 0; i < 5; i++) {
			final int index = i;
			new Thread(()->{
				System.out.println(
						String.format("%s送你%s",
						Thread.currentThread().getName(),
						blessings[index]));
				
				try {
					Thread.sleep(index * 100);
					
					//每个线程到达此处等待其他线程到达。五个都到达后,先由最后到达的线程执行构造函数中的线程,再继续往下执行
					cyclicBarrier.await();
					
					System.out.println("五福到!合成!!!"+blessings[index]+"消失");
					
					// 验证计数器是可以重置 
					cyclicBarrier.await();
					
				} catch (Exception e) {
					e.printStackTrace();
				}
				
			}).start();
		}
		
	}

}

==注意看控制台输出,先是五个线程分别到cyclicBarrier.await() 屏障点前,他们互相等待...收集到五福,即调用了五次await(),然后由最后一个线程Thread-4来执行构造函数中指定的屏障操作,不是main线程. 然后继续执行线程各自的操作。==

==而第二次我又调用了await方法。发现我们设置的cyclicBarrier(5, runnable)还能继续使用。==

Thread-2送你敬业福
Thread-4送你富强福
Thread-1送你友善福
Thread-0送你爱国福
Thread-3送你和谐福
Thread-4
五福集齐,开始合成!
五福到!合成!!!富强福消失
五福到!合成!!!敬业福消失
五福到!合成!!!爱国福消失
五福到!合成!!!友善福消失
五福到!合成!!!和谐福消失
Thread-3
五福集齐,开始合成!

与CountDownLatch的不同:CountDownLatch是做减法,在倒数到0,才唤醒await的线程,而且不能重复使用;而CyclicBarrier是加法,而且可以循环使用。

Semaphore信号量

Semaphore 翻译为信号量,是用来控制同时访问某个特定资源的操作数量,或者同时执行某个操作的数量。

主要方法

  • 初始化Semaphore并指定许可证数量
  • 得到许可证。在需要被限制访问次数的方法前调用 acquire()/acquireUninterruptibly()
  • 释放许可证release()
  • tryAcquire(),如果有空余的许可证,就获取。没有的话,也不会阻塞,可以去做别的事,过一会儿再来看是否有空余的。rtyAcquire(timeout)和tryAcquire()一样的,但是多了超时时间,指的是在timeout时间内或不到的话,就去做别的事。

简单使用

public class SemaphoreDemo {
	
	public static void main(String[] args) {
		Semaphore semaphore = new Semaphore(3);
		ExecutorService service = Executors.newFixedThreadPool(10);
		for (int i = 0; i < 10; i++) {
			service.submit(() ->{
				try {
					semaphore.acquire();
					System.out.println(Thread.currentThread().getName()+"获取到许可证!");
					//假设这是我们需要限流执行的方法	我们用线程睡眠代替
					Thread.sleep(1000);
					
				} catch (Exception e) {
					e.printStackTrace();
				} finally {
					semaphore.release();
					System.out.println(Thread.currentThread().getName()+"释放了许可证!");
				}
				
			});
		}
		service.shutdown();
	}

}

注意点

  • Semaphore的acquire(int permits)和release(int permits)可以看到,一次可以获取和释放==多个==许可证,但要注意获取和释放的数量要保持一致。

Condition 接口

Condition是一个接口,它用来替代传统的在synchronized的方法中调用Object的wait()、notify()实现线程间的协作。 Condition 实例实质上需要绑定到一个锁上,我们需要调用Lock.newCondition()来获取,必须在lock.lock()和lock.unlock之间使用condition的signal()与signalAll()。

主要方法

  • await() 当调用condition.await()方法后会使得当前获取lock的线程进入到等待队列。 直至被signal/signalAll唤醒,从等待队列移动到同步队列。
  • signal() 因为等待队列是一个FIFO的,只会唤起那个等待时间最长的线程。
  • signalAll()会唤起所有正在等待的线程。

用Condition来实现生产者消费者模式

public class ConditionDemo {

	private static int QUEUE_MAX_SIZE = 10;
	private Queue<Integer> queue = new ArrayDeque<Integer>(QUEUE_MAX_SIZE);
	private Lock lock = new ReentrantLock();
	private Condition full = lock.newCondition();
	private Condition empty = lock.newCondition();
	
	public static void main(String[] args) {
		ConditionDemo demo = new ConditionDemo();
		new Thread(demo.new Consumer()).start();
		new Thread(demo.new Producer()).start();
	}

	class Consumer implements Runnable {

		@Override
		public void run() {
			while (true) {
				try {
					lock.lock();
					while(queue.size()==0){
						System.out.println("队列空,等待生产者生产数据");
						empty.await();
					}

					queue.poll();
					full.signal();
					System.out.println(String.format("消费者消费了一个元素,剩余%s个元素",
							queue.size()));
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					lock.unlock();
				}
			}
		}
	}

	private class Producer implements Runnable {

		@Override
		public void run() {
			while (true) {
				try{
				lock.lock();
				while(queue.size() == QUEUE_MAX_SIZE){
					System.out.println("队列已满,等待消费者来消费");
					full.await();
				}

				queue.offer(1);
				empty.signal();
				System.out.println(String.format("生产者添加了一个元素,剩余%s个元素",
						queue.size()));
				} catch (InterruptedException e) {
					e.printStackTrace();
				}finally{
					lock.unlock();
				}
			}

		}

	}

}



Phaser 相位器

Phaser (我在工作中很少见到多使用的) 它是JDK1.7版本中新加的成员,它的功能与 CountDownLatch、CyclicBarrier类似,但是使用起来更加灵活强大。它可以实现====控制多线程分阶段共同完成任务====的情景问题。

主要方法

  • Phaser()/Phaser(int parties) 构建一个Phaser /创建一个指定屏障数量的Phaser,与CountDownLatch一样,传入同步的线程数。
  • register()/bulkRegister(int parties) 新注册一个party/批量注册多个party,这是比CountDownLatch强大之处,可以动态注册。
  • arrive() 到达此phaser的屏障点,完成该阶段,但不等待其他线程。
  • arriveAndAwaitAdvance() 到达此phaser的屏障点,完成该阶段,并且阻塞等待其他线程到达此屏障点。
  • arriveAndDeregister() 到达phaser的屏障点,使phaser的到达的线程数加一,取消一个屏障点的注册,不会阻塞等待其他线程。
  • 其他方法参见jdk文档

举个栗子

还是模拟拼单购物的场景,我们把拼购的过程假设为一个线程。这个线程分两个阶段,step1:加入拼单,step2:支付。假设五个用户参与拼购,即五个线程相互等待彼此完成step1,才能去完成step2。

public class PhaserDemo {

	public static void main(String[] args) {
		PhaserDemo demo = new PhaserDemo();
		Phaser phaser = new Phaser();
		phaser.bulkRegister(5);
		ExecutorService service = Executors.newFixedThreadPool(5);
		for (int i = 0; i < 5; i++) {
			final int userId = i + 1;
			service.submit(demo.new BuyTogeter(userId, phaser));
		}
		service.shutdown();
	}

	class BuyTogeter implements Runnable {

		private Phaser phaser;
		private int userId;

		public BuyTogeter(int userId, Phaser phaser) {
			this.userId = userId;
			this.phaser = phaser;
		}

		@Override
		public void run() {
			try {
				// 第0阶段— 屏障 -等待5 个用户加上拼单,只有五个用户拼单完成,才能支付
				Thread.sleep((long) (Math.random() * 1000));
				System.out.println(String.format("第%s阶段:[用户%s] 加入了拼单...",phaser.getPhase(), userId));
				phaser.arriveAndAwaitAdvance();
				// 第1阶段 -等待5 个用户支付
				Thread.sleep((long) (Math.random() * 1000));
				System.out.println(String.format("第%s阶段:[用户%s] 完成支付...",phaser.getPhase(), userId));
				phaser.arriveAndDeregister();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

	}

}

我们看到控制台输出

0阶段:[用户2] 加入了拼单...
第0阶段:[用户1] 加入了拼单...
第0阶段:[用户3] 加入了拼单...
第0阶段:[用户5] 加入了拼单...
第0阶段:[用户4] 加入了拼单...
第1阶段:[用户2] 完成支付...
第1阶段:[用户5] 完成支付...
第1阶段:[用户3] 完成支付...
第1阶段:[用户1] 完成支付...
第1阶段:[用户4] 完成支付...

总结

CountDownLatch/CyclicBarrier/Semaphore/Phaser 他们在用法上大同小异,我们在使用中注意他们的区别,当我们需要控制线程等待,相互等待,限流,分阶段等待执行时候,灵活运用这些工具类。

以上对线程并发控制工具类做了一些简单的入门介绍,并没有从源码层分析他们的实现原理,以后会去做源码的分析,欢迎关注。

欢迎扫描二维码关注公众号