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

内核线程同步之signal Linux内核线程同步机制之Signal使用及编程。 阅读详情



为什么要线程通讯

多个线程并发执行时,在默认情况下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()等比较有用的方法。
打造Android即时通讯应用:代码与架构解析 即时通讯(Instant Messaging,简称IM)是现代移动应用的核心功能之一。它允许用户之间进行实时的文字、语音、视频对话和文件传输。在Android平台上,开发者需要通过网络编程、消息处理、用户界面设计等多个方面的技术来实现这一功能。 阅读详情

相关推荐

AI计算,为什么要用GPU?

几年后,2012年,神经网络之父杰弗里·辛顿(Geoffrey Hinton)的两个学生——亚历克斯·克里切夫斯基(Alex Krizhevsky)、伊利亚·苏茨克沃(Ilya Sutskever),利用“深度学习+GPU”的方案,提出了深度神经网络AlexNet,将识别成功率从74%提升到85%,一举赢得Image Net挑战赛的冠军。根据形态,GPU可分为独立GPU(dGPU,discrete/dedicated GPU)和集成GPU(iGPU,integrated GPU),也就是常说的独显、集显。

经营范围: CTA、SRRC、CCC、移动、电信、联通入库 1675

多线程之间是如何通信的

多线程之间是如何通信的

qq_45443475的博客 1520

MySQL根据idb文件恢复数据

恢复前提 MySQL 5.* 和 8.* 的恢复方式大同小异,最基本的区别是8.* 中,不再有表结构frm文件,所以要恢复 8.* 的数据,就必须要有完整的建表语句。 而恢复 5.* 的数据,就需要idb文件和 frm文件,当然,如果有完整的建表语句的话,不用frm文件也行。 恢复步骤 需要注意的是,以下操作需要选定数据库后执行。 5.* 恢复表结构 确定表列数 首先创建一张 与需要恢复数据的表 的名字相同的新表,如果已知原表的字段数量的话,就按照原表字段数建表,字段名和类型都不用考虑,如果不知道数量的话,

weixin_45121446的博客 5449

【进程与线程】信号(Signal)

/ 注册 SIGUSR1 的处理函数// 阻塞等待信号pause();// 暂停进程,直到收到信号return 0;# 编译运行# 输出 PID 后,在另一个终端发送信号:五. 信号的优缺点与适用场景用户触发中断如Ctrl+C发送SIGINT终止前台进程,Ctrl+Z发送SIGTSTP暂停进程。内核异常处理内存错误(SIGSEGV)、算术错误(SIGFPE)、非法指令(SIGILL)等异常事件。进程间通信通过kill()系统调用发送SIGUSR1SIGUSR2触发自定义逻辑。

猫猫的小茶馆 1277

【Python爬虫】股票信息定向爬取

【Python爬虫】股票信息定向爬取:通过requests-bs4-re技术路线爬虫获取上交所和深交所所有股票的名称和交易信息

qq_39321802的博客 1503

Zotero on Mac: 高效文献管理与引用指南

本文为Mac用户提供了一份全面的Zotero高效文献管理与引用指南。从安装配置、文献收集管理、PDF深度阅读与笔记,到在Word中无缝插入引用和生成参考文献,详细讲解了核心工作流。同时推荐了ZotFile、Better BibTeX等必备插件,并介绍了利用同步与WebDAV实现跨设备无缝衔接的方法,助力研究者构建个人数字知识库,极大提升学术研究效率。

weixin_29281941的博客 1178

多线程(六)线程间通信机制

线程间通信、wait/notify/notifyAll、Condition、await/signal/signalAll

HK的博客 1685

时序基础模型Timer-S1:十亿参数如何革新时间序列预测

时间序列预测是数据分析与机器学习中的核心任务,旨在基于历史数据预测未来趋势。其原理在于捕捉数据中的趋势、周期性和季节性等时序依赖关系。这项技术的价值在于赋能业务决策,广泛应用于金融风控、能源管理、物联网监控和供应链优化等关键场景。传统方法依赖针对特定任务的专用模型,面临泛化能力弱和开发成本高的挑战。本文聚焦的时序基础模型,通过在海量跨领域数据上进行预训练,学习时序通用模式,并结合十亿级参数带来的强大容量,实现了性能的显著突破。这种范式转变,使得模型能够像“全科医生”一样,通过少量数据微调即可快速适应新任务,

weixin_29164843的博客 269

线程间通讯(几种常见的通讯机制,以及实例)

介绍线程间通讯的几种方式,以及一些实例

bigger_belief的博客 1万+

E2000 RGMII0通讯异常问题总结

飞腾E2000 RGMII接YT8521,RGMII0路不通问题。

乌拉大喵喵 1630

华为OD机试 - 直捣黄龙 - 广度优先搜索BFS(Python/JS/C/C++ 新系统 200分)

从样例 1 可知:5 / 2,2 -> 输出 2 9如果按“移动步数”计算应该是 8,但题目输出是 9,说明这里的“路径长度”统计的是 路径上经过的节点数量。最短路径条数:ways[end]路径长度:dist[end] + 1。

学Java,找哪吒 125

c#多线程彻底学习

什么是进程? 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。 而一个进程又是由多个线程所组成的。 什么是线程? 线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。 什么是多线程? 多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执

ghevinn欢迎您光临 1605

Android 10.0 kenel中修改rom系统内部存储的大小

在10.0的系统rom产品开发定制中,在对一些产品开发中的配置需求方面,由于在产品后续订单中,有些产品是出口的,但是硬件方面已经定板,时间比较仓促,所以 就需要软件方面在rom内部存储的大小方面作假,修改rom真实的大小容量,所以就需要在kenel驱动部分来修改这部分的值最好了,接下来分析下计算rom容量的 相关代码,然后做出修改

安卓兼职framework和app工程师的博客 1404

一起学习多线程

多线程 目录 P1高并发编程第一阶段01讲、课程大纲及主要内容介绍 P2高并发编程第一阶段02讲、简单介绍什么是线程 P3高并发编程第一阶段03讲、创建并启动线程 P4高并发编程第一阶段04讲、线程生命周期以及start方法源码剖析 P5高并发编程第一阶段05讲、采用多线程方式模拟银行排队叫号 P6高并发编程第一阶段06讲、用Runnable接口将线程的逻辑执行单元从控制中抽取出来 P7高并发编程...

Jamesaonier的博客 314

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

多线程学习04-NSThread创建

学习多线程04

Leemin_ios的博客 755

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

C#多线程学习(一) 多线程的相关概念

C#多线程学习(一) 多线程的相关概念 什么是进程?当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程所组成的。什么是线程?线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。什么是多线程?多线程是指程序中包含多个执行流,即在一个程序中...

anmangchen6041的博客 122

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

c#多线程学习

转自:http://www.cnblogs.com/xugang/archive/2008/04/06/1138856.html 什么是进程? 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。 而一个进程又是由多个线程所组成的。 什么是线程? 线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的

zhang400的专栏 376

C#多线程学习(一) 多线程的相关概念(转)

什么是进程?当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程所组成的。什么是线程?线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。什么是多线程?多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就...

dgnwy6921的博客 76

C# 多线程

点击打开链接http://www.cnblogs.com/xugang/archive/2008/04/06/1138856.html

yzl20092856的专栏 625

c#多线程编程

什么是进程?当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程所组成的。什么是线程?线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。什么是多线程?多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说...

weixin_30691871的博客 75

C#多线程

转载自:http://www.cnblogs.com/xugang/archive/2008/04/06/1138856.html   什么是进程? 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。 而一个进程又是由多个线程所组成的。 什么是线程? 线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共

nate的专栏 1622

详解Python中的多线程编程

详解Python中的多线程编程 作者:taiyang1987912 字体:[增加 减小] 类型:转载 时间:2015-04-09 我要评论 这篇文章主要介绍了详解Python中的多线程编程,Python中的多线程一直是Python学习中的重点和难点,要反复巩固!需要的朋友可以参考下 一、简介        多线程编程技术可以实现代码并行性,优化处

u010302494的博客 559

C#多线程学习

源地址:http://www.cnblogs.com/xugang/archive/2008/04/06/1138856.html C#多线程学习(一) 多线程的相关概念 什么是进程? 当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。 而一个进程又是由多个线程所组成的。 什么是线程? 线程是程序中的一个执行流,每个线程都

anhkgg的专栏 413
上一篇: 设计模式学习——享元模式
下一篇: 设计模式学习——代理模式
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值