多线程编程学习——04线程的通讯机制



为什么要线程通讯

多个线程并发执行时,在默认情况下CPU是随机切换线程的,有时我们希望CPU按我们的规律执行线程,此时就需要线程之间协调通信。


线程通讯方式

线程间通讯常用方式如下:

  • 休眠唤醒方式: Object的wait、notify、notifyAll
  • Condition的await、signal、signalAll
  • CountDownLatch:用于某个线程A等待若干个其他线程执行完之后,它才执行
  • CyclicBarrier:一组线程等待至某个状态之后再全部同时执行
  • Semaphore:用于控制对某组资源的访问权限

休眠唤醒方式

Oject的wait、notify、notifyAll
  1. wait():当调用wait()方法时,该线程从运行时状态转为等待阻塞状态,jvm将该线程加入到该对象的等待队列中,该线程释放所持有的该对象的锁。
  2. notify():当调用notify()方法时,会随机唤醒一个线程使其从等待阻塞状态转变为就绪状态。
  3. notifyAll():唤醒所有的线程并将线程放入到锁池当中,只有竞争得到锁的那个线程才可以重回就绪状态。
public class WaitNotifyRunnable {
    private Object obj = new Object();
    private Integer i = 0;

    public void odd() {
        while (i < 10) {
            synchronized (obj) {
                if (i % 2 == 1) {
                    System.out.println("奇数:" + i);
                    i++;
                    //当调用该对象的notify()方法时,则随机唤醒一个该对象的等待阻塞队列中的线程。
                    obj.notify();
                } else {
                    try {
                    //当调用该对象的wait()方法时,jvm虚拟机将该线程放入该对象的等待队列中,并释放该线程所持有的对象的锁
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public void even() {
        while (i < 10) {
            synchronized (obj) {
                if (i % 2 == 0) {
                    System.out.println("偶数:" + i);
                    i++;
                    obj.notify();
                } else {
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        final WaitNotifyRunnable runnable = new WaitNotifyRunnable();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                runnable.even();
            }
        }, "奇数线程");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                runnable.odd();
            }
        }, "偶数线程");
        t1.start();
        t2.start();


    }
}

Condition的await、signal、signalAll

引用该博主内容:Condition详解

概述:Condition是AQS的内部类。每个Condition对象都包含一个队列(等待队列)。等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。
在这里插入图片描述
等待分为首节点和尾节点。当一个线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。新增节点就是将尾部节点指向新增的节点。节点引用更新本来就是在获取锁之后的操作,所以不需要CAS保证,同时也是线程安全的操作。

等待

当线程调用了await方法以后,线程就作为队列中的一个节点被加入到等待队列中去了。同时会释放所持有的锁。当从await方法返回的时候,一定会获取condition相关联的锁。当等待队列中的节点被唤醒的时候,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException异常信息。

通知

调用Condition的signal方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到同步队列中。当前线程加入到等待队列中如图所示:
在这里插入图片描述
在调用signal方法之前必须先判断是否获取到了锁,接着获取等待队列的首节点,将其移动到同步队列并且利用LockSupport唤醒节点中得线程。节点从等待队列移动到同步队列如下图所示:
在这里插入图片描述
被唤醒的线程从await方法中的while循环中退出。随后加入到同步状态的竞争中去。成功获取到竞争的线程则会返回到await方法之前的状态。

总结

调用await方法后,将当前线程加入Condition等待队列中,当前线程释放锁。否则别的线程就无法拿到锁而发生死锁。自选(while)挂起,不断检测节点是否在同步队列中,如果“”尝试获取锁,否则挂起。当线程被signal方法唤醒,被唤醒的线程将从await()方法中的while循环中退出来,然后调用acquireQueued()方法竞争同步状态。


Obejct的通信机制和Condition对象的通信机制的对比。

Condition接口提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。
在这里插入图片描述

Condition常用方法解析

  1. await():造成当前线程在接到信号或被中断之前一直处于等待状态。
  2. await(long time ,TimeUnit unit):造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
  3. awaitNanos(long nanosTimeout):造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
  4. awaitUninterruptibly():造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
  5. awaitUntil(Date deadline):造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被统治,则返回true,否则表示到了指定时间,返回false。
  6. signal():唤醒一个等待线程。该线程从等待方法返回前必须获得与condition相关的锁。
  7. signalAll():唤醒所有等待线程,能够从等待方法返回的线程必须获得与Condition相关的锁。
public class WaitNotifyRunnable {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private Integer i = 0;

    public void odd() {
        while (i < 10) {
            lock.lock();
            try {
                if (i % 2 == 1) {
                    System.out.println("奇数:" + i);
                    i++;
                    condition.signal();
                } else {
                    condition.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public void even() {
        while (i < 10) {
            lock.lock();
            try {
                if (i % 2 == 0) {
                    System.out.println("偶数:" + i);
                    i++;
                    condition.signal();
                } else {
                    condition.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        final WaitNotifyRunnable runnable = new WaitNotifyRunnable();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                runnable.odd();
            }
        }, "偶数线程");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                runnable.even();
            }
        }, "奇数线程");
        t1.start();
        t2.start();

    }
}

CountDownLatch方式

引用该博主内容:CountDownLatch简单介绍

概念

CountDownLatch是在java1.5被引入的,存在于java.util.concurrent包下。
CountDownLatch是一个同步工具类,能够使一个线程等待其他线程完成各自的工作后再执行,用来协调多个线程之间的同步。
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减1。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后再CountDownLatch上等待的线程就可以恢复执行接下来的任务。
CountDownLatch原理图
在这里插入图片描述

CountDownLatch的用法

  1. 某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为new CountDownLatch(n),每当一个任务执行完毕,就将计数器减1 countdownLatch.countDown(),当计数器的值变为0时,在CountDownLatch上await()的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
  2. 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。做法是初始化一个共享的CountDownLatch(1),将其计算器初始化为1,多个线程再开始执行任务前首先countdownlatch。await(),当主线程调用countDown()时,计数器变为0,多个线程同时被唤醒。

CountDownLatch的不足

CountDownLatch是一次性的,计算器的值只能在构造方法中初始化一次,之后没有热河机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。

CountDownLatch方法详解:

  1. public void countDown()
    递减锁存器的计数,如果计数到达零,则释放所有等待的线程。如果当前计数大于零,则将计数减少。
  2. public boolean await(long timeout,TimeUnit unit) throws InterruptedException
    使当前线程在锁存器倒计时数至零之前一直等待,除非线程被中断或超出了指定的等待时间,如果当前计数为零,则返回true值。
    如果当前计数大于零,则出于线程调度目的,将禁用当前线程,且在发生以下三种情况之一前,该线程将一直处于休眠状态
    (1) 由于调用countDown()方法,计数到达零。
    (2) 其他某个线程中断当前线程。
    (3) 已超出指定的等待时间。

**示例1:**使用CountDownLatch完成主线程等待子线程执行完成再执行的场景。

public class CountdownLatchTest01 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        final CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("子线程" + Thread.currentThread().getName() + "开始执行");
                        Thread.sleep((long) (Math.random() * 10000));
                        System.out.println("子线程" + Thread.currentThread().getName() + "执行完成");
                        latch.countDown();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            service.execute(runnable);
        }
        try {
            System.out.println("主线程" + Thread.currentThread().getName() + "等待子线程执行完成...");
            latch.await();//阻塞当前线程,直到计数器的值为0
            System.out.println("主线程" + Thread.currentThread().getName() + "开始执行...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

**示例2:**百米赛跑,4名运动员选手到达场地等待裁判口令,裁判一声口令,选手听到后同时起跑,当所有选手到达终点,裁判进行汇总排名

public class CountdownLatchTest02 {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        final CountDownLatch cdOrder = new CountDownLatch(1);
        final CountDownLatch cdAnswer = new CountDownLatch(4);
        for (int i = 0; i < 4; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("选手" + Thread.currentThread().getName() + "正在等待裁判发令");
                        cdOrder.await();
                        System.out.println("选手" + Thread.currentThread().getName() + "已接收到裁判下令");
                        Thread.sleep((long) Math.random() * 10000);
                        System.out.println("选手" + Thread.currentThread().getName() + "到达终点");
                        cdAnswer.countDown();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            service.execute(runnable);
        }
        try {
            Thread.sleep((long) (Math.random() * 10000));
            System.out.println("裁判" + Thread.currentThread().getName() + "即将发令");
            cdOrder.countDown();
            System.out.println("裁判" + Thread.currentThread().getName() + "已发送口令,正在等待所有选手到达终点");
            cdAnswer.await();
            System.out.println("所有选手都到达终点");
            System.out.println("裁判" + Thread.currentThread().getName() + "汇总成绩排名");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.shutdown();
    }
}

CyclicBarrier方式

引用该博主内容:CyclicBarrier简单介绍

概述

现实生活中我们经常会遇到这样的情景,在进行某个活动前需要等待人全部都齐了才开始。例如吃饭时要等全家人都上座了才动筷子,旅游时要等全部人都到齐了才出发,比赛时要等运动员都上场后才开始。
 
 
在JUC包中为我们提供了一个同步工具类能够很好的模拟这类场景,它就是CyclicBarrier类。利用>CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。下图演示了这一过程
 
 
CyclicBarrier字面意思是“可重复使用的栅栏”,相比CountDownLatch 简单得多,是 ReentrantLock 和 Condition 的组合使用。

在这里插入图片描述
原理

在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时内部计数器会减1,当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒。这就是实现一组线程相互等待的原理。

源码介绍

// CyclicBarrier的成员变量
// 同步操作锁
private final ReentrantLock lock = new ReentrantLock();
// 线程拦截器
// 通过条件队列trip对线程进行阻塞
private final Condition trip = lock.newCondition();
// 表示每次拦截的线程数,在构造时进行赋值
private final int parties;
// 换代前执行的任务
private final Runnable barrierCommand;
// CyclicBarrier有一个静态内部类Generation,该类的对象代表栅栏的当前代。
private Generation generation = new Generation();
// 计数器,初始值和parties相同,随着每次await方法的调用减1,直到减为0就将所有线程唤醒。
private int count;
 
// 静态内部类Generation
private static class Generation {
  boolean broken = false;
}

CyclicBarrier运行机制示例图
在这里插入图片描述

// CyclicBarrier类最主要的功能就是使先到达屏障点的线程阻塞并等待后面的线程,
//其中它提供了两种等待的方法,分别是定时等待和非定时等待。
//构造器1:核心构造器
public CyclicBarrier(int parties, Runnable barrierAction) {
  if (parties <= 0) throw new IllegalArgumentException();
  this.parties = parties;
  this.count = parties;
  this.barrierCommand = barrierAction;
}
 
//构造器2
public CyclicBarrier(int parties) {
  this(parties, null);

//非定时等待
public int await() throws InterruptedException, BrokenBarrierException {
  try {
    return dowait(false, 0L);
  } catch (TimeoutException toe) {
    throw new Error(toe);
  }
}
 
//定时等待
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException {
return dowait(true, unit.toNanos(timeout));
}

无论是非定时等待和定时等待都调用了dowait方法

//核心等待方法
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {
  final ReentrantLock lock = this.lock;
  lock.lock();
  try {
    final Generation g = generation;
    //检查当前栅栏是否被打翻
    if (g.broken) {
      throw new BrokenBarrierException();
    }
    //检查当前线程是否被中断
    if (Thread.interrupted()) {
      //如果当前线程被中断会做以下三件事
      //1.打翻当前栅栏
      //2.唤醒拦截的所有线程
      //3.抛出中断异常
      breakBarrier();
      throw new InterruptedException();
    }
    //每次都将计数器的值减1
    int index = --count;
    //计数器的值减为0则需唤醒所有线程并转换到下一代
    if (index == 0) {
      boolean ranAction = false;
      try {
        //唤醒所有线程前先执行指定的任务
        final Runnable command = barrierCommand;
        if (command != null) {
          command.run();
        }
        ranAction = true;
        //唤醒所有线程并转到下一代
        nextGeneration();
        return 0;
      } finally {
        //确保在任务未成功执行时能将所有线程唤醒
        if (!ranAction) {
          breakBarrier();
        }
      }
    }
 
    //如果计数器不为0则执行此循环
    for (;;) {
      try {
        //根据传入的参数来决定是定时等待还是非定时等待
        if (!timed) {
          trip.await();
        }else if (nanos > 0L) {
          nanos = trip.awaitNanos(nanos);
        }
      } catch (InterruptedException ie) {
        //若当前线程在等待期间被中断则打翻栅栏唤醒其他线程
        if (g == generation && ! g.broken) {
          breakBarrier();
          throw ie;
        } else {
          //若在捕获中断异常前已经完成在栅栏上的等待, 则直接调用中断操作
          Thread.currentThread().interrupt();
        }
      }
      //如果线程因为打翻栅栏操作而被唤醒则抛出异常
      if (g.broken) {
        throw new BrokenBarrierException();
      }
      //如果线程因为换代操作而被唤醒则返回计数器的值
      if (g != generation) {
        return index;
      }
      //如果线程因为时间到了而被唤醒则打翻栅栏并抛出异常
      if (timed && nanos <= 0L) {
        breakBarrier();
        throw new TimeoutException();
      }
    }
  } finally {
    lock.unlock();
  }
}

示例: 赛马比赛

public class Horse implements Runnable {
    private static int counter = 0;
    private final int id = counter++;
    private int strides = 0;
    private static Random rand = new Random(47);
    private static CyclicBarrier barrier;

    public Horse(CyclicBarrier b) {
        barrier = b;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                synchronized (this) {
                    strides += rand.nextInt(3);
                }
                barrier.await();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public String tracks() {
        StringBuilder s = new StringBuilder();
        for (int i = 0; i < getStrides(); i++) {
            s.append("*");
        }
        s.append(id);
        return s.toString();
    }

    public synchronized int getStrides() {
        return strides;
    }

    public String toString() {
        return "House" + id + " ";
    }
}
public class HorseRace implements Runnable {

    private static final int FINISH_LINE = 75;
    private static List<Horse> horses = new ArrayList<>();
    private static ExecutorService service = Executors.newCachedThreadPool();

    @Override
    public void run() {
        StringBuilder s = new StringBuilder();
        //打印赛道边界
        for (int i = 0; i < FINISH_LINE; i++) {
            s.append("=");
        }
        System.out.println(s);
        //打印赛马轨迹
        for (Horse horse : horses) {
            System.out.println(horse.tracks());
        }
        //判断是否结束
        for (Horse horse : horses) {
            if (horse.getStrides() >= FINISH_LINE) {
                System.out.println(horse + "won!");
                service.shutdownNow();
                return;
            }
        }
        //休息指定时间再到下一轮
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            System.out.println("barrier-action sleep interrupted");
        }
    }

    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(7, new HorseRace());
        for (int i = 0; i < 7; i++) {
            Horse horse = new Horse(barrier);
            horses.add(horse);
            service.execute(horse);
        }
    }

}

Semaphore方式

引用该博主内容:Semaphore简单介绍

概述

Semaphore,是JDK1.5的java.util.concurrent并发包中提供的一个并发工具类.所谓Semaphore即 信号量 的意思。

其作用在JDK注释中是这样描述的:

A counting semaphore.
Conceptually, a semaphore maintains a set of permits.
Each {@link #acquire} blocks if necessary until a permit is available, and then takes it.
Each {@link #release} adds a permit, potentially releasing a blocking acquirer.
However, no actual permit objects are used; the {@code Semaphore} just keeps a count of the number available and acts accordingly.

翻译过来,就是:

Semaphore是一个计数信号量。
从概念上将,Semaphore包含一组许可证。
如果有需要的话,每个acquire()方法都会阻塞,直到获取一个可用的许可证。
每个release()方法都会释放持有许可证的线程,并且归还Semaphore一个可用的许可证。
然而,实际上并没有真实的许可证对象供线程使用,Semaphore只是对可用的数量进行管理维护。

Sempaphore方法说明

  1. Semaphore(permits)
    初始化许可证数量的构造函数
  2. Semaphore(permits,fair)
    初始化许可证数量和是否公平模式的构造函数
  3. isFair()
    是否公平模式FIFO
  4. availablePermits()
    获取当前可用的许可证数量
  5. acquire()
    当前线程尝试去阻塞的获取1个许可证。
    此过程是阻塞的,它会一直等待许可证,直到发生以下任意一件事
  • 当前线程获取了1个可用的许可证,则会停止等待,继续执行。
  • 当前线程被中断,则会抛出interruptedException异常,并停止等待,继续执行。
  1. acquire(permits)
    当前线程尝试去阻塞的获取permits个许可证。
    此过程是阻塞的,它会一直等待许可证,直到发生以下任意一件事
  • 当前线程获取了n个可用的许可证,则会停止等待,继续执行。
  • 当前线程被中断,则会抛出InterruptedException异常,并停止等待,继续执行。
  1. acquierUninterruptibly()
    当前线程尝试去阻塞的获取1个许可证(不可中断的)。
    此过程是阻塞的,它会一直等待许可证,直到发生以下任意一件事
  • 当前线程获取了1个可用的许可证,则会停止等待,继续执行
  1. acquireUninterruptibly(permits)
    当前线程尝试去阻塞的获取permits个许可证。
    此过程是阻塞的,它会一直等待许可证,直到发生以下任意一件事
  • 当前线程获取了n个可用的许可证,则会停止等待,继续执行。
  1. tryAcquire()
    当前线程尝试去获取1个许可证。
    此过程是非阻塞的,它只是在方法调用时进行一次尝试。
    如果当前线程获取了1个可用的许可证,则会停止等待,继续执行,并返回true。
    如果当前线程没有获得这个许可证,也会停止等待,继续执行,并返回false。
  2. tryAcquire(permits)
    当前线程尝试去获取permits个许可证。
    此过程是非阻塞的,它只是在方法调用时进行一次尝试。
    如果当前线程获取了permits个可用的许可证,则会停止等待,继续执行,并返回true。
    如果当前线程没有获得permits个许可证,也会停止等待,继续执行,并返回false。
  3. tryAcquire(timeout,TimeUnit)
    当前线程在限定时间内,阻塞的尝试去获取1个许可证。
    此过程是阻塞的,它会一直等待许可证,直到发生以下任意一件事
  • 当前线程获取了可用的许可证,则会停止等待,继续执行,并返回true。
  • 当前线程等待时间timeout超时,则会停止等待,继续执行,并返回false。
  • 当前线程在timeout时间内被中断,则会抛出InterruptedException一次,并停止等待,继续执行。
  1. tryAcquire(permits,timeout,TimeUnit)
    当前线程在限定时间内,阻塞的尝试去获取permits个许可证。
    此过程是阻塞的,它会一直等待许可证,直到发生以下任意一件事
  • 当前线程获取了可用的permits个许可证,则会停止等待,继续执行,并返回true。
  • 当前线程等待时间timeout超时,则会停止等待,继续执行,并返回false。
  • 当前线程在timeout时间内被中断,则会抛出InterruptedException一次,并停止等待,继续执行。
  1. release()
    当前线程释放1个可用的许可证。
  2. release(permits)
    当前线程释放permits个可用的许可证。
  3. drainPermits()
    当前线程获得剩余的所有可用许可证。
  4. hasQueuedThreads()
    判断当前Semaphore对象上是否存在正在等待许可证的线程。
  5. getQueueLength()
    获取当前Semaphore对象上是正在等待许可证的线程数量。

代码示例1:

public class SemaphoreThread {
    public static void main(String[] args) throws InterruptedException {
        //初始化许可证数量的构造函数
        Semaphore semaphore = new Semaphore(5);
        //new Semaphore(permits,fair):初始化许可证数量和是否公平模式的构造函数
        semaphore = new Semaphore(5, true);
        //isFair();是否公平模式FIFO
        System.out.println("是否公平FIFO:" + semaphore.isFair());
        //availablePermits()获取当前可用的许可证数量
        System.out.println("获取当前可用的许可证数量:开始---" + semaphore.availablePermits());
        //acquire();获取1个许可证
        //----此线程会一直阻塞,直到获取这个许可证,或者被终端(抛出InterruptedException异常).
        semaphore.acquire();
        System.out.println("获取当前可用的许可证数量:acquire 1个---" + semaphore.availablePermits());
        //release():释放1个许可证
        semaphore.release();
        System.out.println("获取当前可用的许可证数量:release 1个---" + semaphore.availablePermits());
        semaphore.acquire(4);
        System.out.println("获取当前可用的许可证数量:acquire 4个---" + semaphore.availablePermits());
        semaphore.release(4);
        System.out.println("获取当前可用的许可证数量:release 4个---" + semaphore.availablePermits());
        //hasQueuedThreads():是否有正在等待许可证的线程
        System.out.println("是否有正在等待许可证的线程---" + semaphore.hasQueuedThreads());
        //getQueueLength():正在等待许可证的队列长度(线程数量)
        System.out.println("正在等待许可证的队列长度(线程数量):" + semaphore.getQueueLength());
        Thread.sleep(10);
        System.out.println();
        Semaphore finalSemaphore = semaphore;
        new Thread(() -> {
            //drainPermits():获取剩余的所有的许可证
            int permits = finalSemaphore.drainPermits();
            System.out.println(Thread.currentThread().getName() + "获取了剩余的全部" + permits + "个许可证.");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //释放所有的许可证
            finalSemaphore.release(permits);
            System.out.println(Thread.currentThread().getName() + "释放了" + permits + "个许可证.");
        }).start();
        Thread.sleep(10);
        new Thread(() -> {
            try {
                finalSemaphore.acquire();
                System.out.println(Thread.currentThread().getName() + "获取了1个许可证.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            finalSemaphore.release();
            System.out.println(Thread.currentThread().getName() + "释放了1个许可证.");
        }).start();
    }
}

代码示例2:
场景说明:

模拟学校食堂的窗口打饭过程
学校食堂有2个打饭窗口
学校中午有20个学生 按次序 排队打饭
每个人打饭时耗费时间不一样
有的学生耐心很好,他们会一直等待直到打到饭
有的学生耐心不好,他们等待时间超过了心里预期,就不再排队,而是回宿舍吃泡面了
有的学生耐心很好,但是突然接到通知,说是全班聚餐,所以也不用再排队,而是去吃大餐了

重点分析:

  • 食堂有2个打饭窗口:需要定义一个permits=2的Semaphore对象。
  • 学生 按次序 排队打饭:此Semaphore对象是公平的。
  • 有20个学生:定义20个学生线程。
  • 打到饭的学生:调用了acquireUninterruptibly()方法,无法被中断
  • 吃泡面的学生:调用了tryAcquire(timeout,TimeUnit)方法,并且等待时间超时了
  • 吃大餐的学生:调用了acquire()方法,并且被中断了

代码实现

public class Student implements Runnable {

    //学生姓名
    private String name;
    //打饭许可
    private Semaphore semaphore;

    /**
     * 打饭方式
     * 0 : 一直等待直到打到饭
     * 1 : 等了一会不耐烦了,回宿舍吃泡面了。
     * 2 : 打饭中途被其他同学叫走了,不再等待。
     */
    private int type;

    public Student(String name, Semaphore semaphore, int type) {
        this.name = name;
        this.semaphore = semaphore;
        this.type = type;
    }

    //打饭
    @Override
    public void run() {

        switch (type) {
            // 0 : 一直等待直到打到饭
            case 0:
                //一直等待获得许可证不被中断
                semaphore.acquireUninterruptibly();
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //打完饭后将打饭机会留给下一位同学
                semaphore.release();
                System.out.println(name + " 0 终于打到饭");
                break;
            // 1 : 等了一会不耐烦了,回宿舍吃泡面了。
            case 1:
                try {
                    if (semaphore.tryAcquire((long) Math.random() * 1000, TimeUnit.MILLISECONDS)) {
                        //进行打饭
                        Thread.sleep((long) (Math.random() * 1000));
                        //打完饭后将打饭机会留给下一位同学
                        semaphore.release();
                        System.out.println(name + " 1 终于打到饭");
                    } else {
                        System.out.println(name + " 1 回宿舍吃泡面");
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                break;
            //2 : 打饭中途被其他同学叫走了,不再等待。
            case 2:
                try {
                    semaphore.acquire();
                    //进行打饭
                    Thread.sleep((long) (Math.random() * 1000));
                    //打完饭后将打饭机会留给下一位同学
                    semaphore.release();
                    System.out.println(name + " 2 终于打到饭");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println(name + " 2 全部聚餐,不再打饭");
                }
                break;
            default:
                break;
        }

    }
}

食堂

public class Canteen {

    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2);
        Thread[] students = new Thread[20];
        for (int i = 0; i < 20; i++) {
            if (i < 10) {
                new Thread(new Student("打饭学生", semaphore, 0)).start();
            } else if (i >= 10 && i < 15) {
                new Thread(new Student("泡面学生", semaphore, 1)).start();
            } else {
                Thread thread = new Thread(new Student("聚餐学生", semaphore, 2));
                thread.start();
                thread.interrupt();
            }
        }
    }
}

相关面试题

sleep和wait区别

在这里插入图片描述

wait和notify区别
  • wait和notify都是Object中的方法
  • wait和notify执行前线程都必须获得对象锁
  • wait的作用是使当前线程进行等待
  • notify的作用是通知其他等待当前线程的对象锁的线程
Object和Condition休眠唤醒区别
  1. object wait()必须在synchronized(同步锁)下使用,
  2. object wait()必须要通过notify()方法进行唤醒
  3. condition await() 必须和Lock(互斥锁/共享锁)配合使用
  4. condition await() 必须通过 signal() 方法进行唤醒
CyclicBarrier与CountDownLatch的区别

共同点: 两者都可以实现一组线程在到达某个条件之前进行等待,它们内部都有一个计数器,当计数器的值不断的减为0的时候所有阻塞的线程将会被唤醒。
不同点:

  1. CyclicBarrier的计数器由自己控制,而CountDownLatch的计数器则由使用者来控制;
  2. 在CyclicBarrier中线程调用await方法不仅会将自己阻塞还会将计数器减1,而在CountDownLatch中线程调用await方法只是将自己阻塞而不会减少计数器的值。
  3. CountDownLatch只能拦截一轮,而CyclicBarrier可以实现循环拦截。一般来说用CyclicBarrier可以实现CountDownLatch的功能,而反之则不能,
  4. 除此之外,CyclicBarrier还提供了:resert()、getNumberWaiting()、isBroken()等比较有用的方法。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值