并发编程-线程的并发工具

431 阅读8分钟

CountDownLatch

是一组线程等待其他的线程完成工作以后在执行;await 用来等待,countDown 负责计数器的减一;可以多次使用等待方法,将在扣减完毕后一起释放。简单来说就是增强版的 join;CountDownLatch 使用时将引用传入需要控制的线程即可。

作用:一个线程等待其他线程完成以后再工作,可以用于控制线程执行顺序

注意:计数器必须大于 0

示例:

static final CountDownLatch LATCH = new CountDownLatch(3);

 public static void main(String[] args) throws InterruptedException {

  new Worker().start();

  for (int i = 0; i < 3; i++) {
   new Main(i).start();
  }

  LATCH.await();  //可以多次使用等待方法,将在扣减完毕后一起释放
  System.out.println(System.currentTimeMillis() + " 主线程执行了..");
 }

 static class Main extends Thread {

  public Main(int i ){
   setName("Main-" + i);
  }

  @Override
  public void run() {
   try {
    TimeUnit.SECONDS.sleep(ThreadLocalRandom.current().nextLong(5));
    System.out.println(getName() + " Main..go");
    LATCH.countDown();
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
  }
 }

 static class Worker extends Thread {
  @Override
  public void run() {
   try {
    LATCH.await();
    System.out.println(System.currentTimeMillis() + " Worker..go");
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
  }
 }

执行结果可能是:

Main-2 Main..go
Main-1 Main..go
Main-0 Main..go
1554609118643 Worker..go
1554609118643 主线程执行了..

可以看到主线程和 Worker 线程一直在等待 3 个 Main 线程执行完毕。当LATCH.countDown()扣减 3 次完毕时LATCH.await()返回,程序继续执行。

CyclicBarrier

让一组线程达到某个屏障,被阻塞,一直到组内最后一个线程达到屏障时,屏障开放,所有被阻塞的线程会继续运行。

作用:1.测试并发;2.可以其他线程执行完成后在执行自定义的任务。

CyclicBarrier(int parties)
CyclicBarrier(int parties, Runnable barrierAction),屏障开放,barrierAction定义的任务会执行。

某个线程调用CyclicBarrier.await()代表我已经到达屏障,然后当前线程被阻塞。如果当前线程是最后一个到达屏障点的,那么所有等待的线程被激活。

CountDownLatch 和 CyclicBarrier 区别:

  1. countdownlatch 放行由外部线程控制,CyclicBarrier 放行由一组线程本身控制
  2. countdownlatch 放行条件>=线程数,CyclicBarrier 放行条件==线程数

示例:

static final CyclicBarrier BARRIER = new CyclicBarrier(3);

 static final CyclicBarrier BARRIER_WITH_TASK = new CyclicBarrier(2new Runnable() {
  @Override
  public void run() {
   System.out.println("barrier_with_task GO..");
  }
 });

 public static void main(String[] args) throws InterruptedException, BrokenBarrierException {

  // 普通用法可循环使用,注意不要再不同线程中用
  for (int i = 0; i < 3; i++) {
   new ExcuteThread("normal-" + i).start();
  }

  // 到达屏障前会执行BARRIER_WITH_TASK定义的内容
  for (int i = 0; i < 2; i++) {
   new ExcuteThread_2().start();
  }

 }

 static class ExcuteThread extends Thread {

  public ExcuteThread(String name) {
   super(name);
  }

  @Override
  public void run() {
   try {
    if (new Random().nextBoolean()) {
     TimeUnit.SECONDS.sleep(2);
    }
    System.out.println(getName() + " 到达屏障前");
    BARRIER.await();  //第一次使用
    TimeUnit.SECONDS.sleep(3);
    System.out.println(getName() + " over");

    BARRIER.await(); //可以再次使用,所以称为循环屏障
    TimeUnit.SECONDS.sleep(3);
    System.out.println(getName() + " over again");

   } catch (InterruptedException | BrokenBarrierException e) { //BrokenBarrierException代表已经破损,可能无法等待所有线程齐全了
    e.printStackTrace();
   }
  }
 }

 static class ExcuteThread_2 extends Thread {
  @Override
  public void run() {
   try {
    if (new Random().nextBoolean()) {
     TimeUnit.SECONDS.sleep(2);
    }
    System.out.println(getName() + " 到达屏障前");
    BARRIER_WITH_TASK.await();
    TimeUnit.SECONDS.sleep(3);
    System.out.println(getName() + " 到达位置");
   } catch (InterruptedException | BrokenBarrierException e) {
    e.printStackTrace();
   }
  }
 }

以上示例想演示什么?3 个 ExcuteThread 线程会随机休眠,只有等待最后一个线程休眠结束后才会执行CyclicBarrier.await()后的方法,当执行完毕后我们再次调用CyclicBarrier.await()会发现它依然生效,这也正是称为循环屏障的原因。

Semaphore

信号量是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

作用:流量控制,特别是公共资源有限的应用场景,比如数据库连接。

示例:

final Semaphore SA = new Semaphore(5);

 public static void main(String[] args) {

  SemaphoreTest semaphoreTest = new SemaphoreTest();
  for (int i = 0; i < 10; i++) {
   new Thread(semaphoreTest).start();
  }
 }

 @Override
 public void run() {
  try {
   SA.acquire(); // 获取1个许可,如果不能获取到许可则会一直阻塞
   TimeUnit.SECONDS.sleep(3);
   System.out.println(System.currentTimeMillis() + Thread.currentThread().getName() + " go.");
   SA.release(); // 归还1个许可
  } catch (InterruptedException e) {
   e.printStackTrace();
  }
 }

console output:

1554624115958Thread-0 go.
1554624115958Thread-4 go.
1554624115958Thread-1 go.
1554624115958Thread-3 go.
1554624115958Thread-2 go.
1554624118958Thread-6 go.
1554624118958Thread-7 go.
1554624118958Thread-9 go.
1554624118958Thread-8 go.
1554624118958Thread-5 go.

程序执行的结果是每次有 5 个线程并行执行,因为只有 5 个许可,也就是说前 5 个线程可以获取到许可,其他线程想要获取许可必须等待已获取许可的线程归还许可。

Exchanger

交换者用于交换两个线程之间的数据。直接看代码:

public static void main(String[] args) {
  Exchanger<String> exchange = new Exchanger<String>();

  new Thread(new Runnable() {
   @Override
   public void run() {
    String salary = "1";
    try {
     System.out.println("我的工资是" + salary + ",你呢?" + exchange.exchange(salary)); //会一直等待第二个线程也执行exchange方法
    } catch (InterruptedException e) {
     e.printStackTrace();
    }
   }
  }).start();

  new Thread(new Runnable() {
   @Override
   public void run() {
    String salary = "2";
    try {
     System.out.println("我的工资是" + salary + ",你呢?" + exchange.exchange(salary));
    } catch (InterruptedException e) {
     e.printStackTrace();
    }
   }
  }).start();
 }

console output:

我的工资是2,你呢?1
我的工资是1,你呢?2

这个类基本没什么可说的,如果担心交换的时间过久,可以使用带超时时间的方法。

LockSupport

强悍的线程阻塞工具类(并且是静态的),它可以在线程的任意位置让线程阻塞。与Thread.suspend()相比,它弥补了容易导致死锁的缺点。与Object.wait()相比,它不需要获取对象的锁。

示例:

public static void main(String[] args) throws InterruptedException {

  ChangeObjectThread t1 = new ChangeObjectThread("t1");
  ChangeObjectThread t2 = new ChangeObjectThread("t2");

  t1.start();
  TimeUnit.SECONDS.sleep(1); // 保证t1.park在t1.unpark前执行
  t2.start();

  if (ThreadLocalRandom.current().nextBoolean()) {
   t1.interrupt();
  } else {
   LockSupport.unpark(t1);
   System.out.println("t1" + "|" + System.currentTimeMillis() + " unpark");

  }

  LockSupport.unpark(t2);
  System.out.println("t2" + "|" + System.currentTimeMillis() + " unpark");

  t1.join();
  t2.join();
 }

 public static class ChangeObjectThread extends Thread {

  public ChangeObjectThread(String name) {
   super(name);
  }

  @Override
  public void run() {
   try {
    TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextLong(150));
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
   System.out.println(getName() + "|" + System.currentTimeMillis() + " park");
   // LockSupport.park();// 阻塞 如果被中断,不会抛出异常而是修改中断标志位
   LockSupport.park(this); //建议使用方便dump查看等待对象

   System.out.println(isInterrupted());
   if (Thread.interrupted()) {
    System.out.println("被中断了");
    System.out.println(isInterrupted());
   }
  }
 }

console output:

t1|1554627431151 park
false
t1|1554627432066 unpark
t2|1554627432066 unpark
t2|1554627432202 park
false

或者

t1|1554627498633 park
true
被中断了
false
t2|1554627499600 unpark
t2|1554627499654 park
false

可以看到t2.unpark()是在t2.park()之前执行的,但是程序也正常结束了。这里面的实现原理是,LockSupport 为每个线程准备了一个许可(只有一个),当许可不可用时会阻塞,当许可可用时park()会立即返回,而unpark()方法则是使许可可用。也就是说t2.unpark()执行后,当执行t2.park()时并不会阻塞了。

其他的定时阻塞方法:

LockSupport.parkNanos(nanos);
LockSupport.parkUntil(deadline);