等待与通知
在java平台可以通过使用Object.wait()/Object.wait(long)和Object.notify()/Object.notifyAll()配合来实现线程的等待与通知。
Object.wait()能够使当前线程暂停(状态转为WAITING),该方法可以用来实现等待,其所在的线程称为等待线程。
Object.notify()可以唤醒一个等待线程,其所在线程被称为通知线程。
wait()和notify()方法都使Object类的方法,因为其是所有类的父类,所以,所有类都有这两个方法。
synchronized(lockObject){ while(等待条件){ lockObject.wait(); } ...... //后续操作 }
上面代码中,while的判断条件,我们暂且称为等待条件,当这个条件成立,这个线程就会进入等待条件,当其它线程重新将其唤醒,然后再次判断等待条件成不成立,若不成立表示线程可以继续往下执行做相应的操作,否则继续进入等待状态。
下面我们来思考一下,while的作用,等待条件为什么要和while配合使用,而不是和if配合使用。
/** * @ClassName WaitIfSample * @description: * @author: yong.yuan * @create: 2022-04-15 16:44 * @Version 1.0 **/ public class WaitIfExample { static AtomicInteger stock = new AtomicInteger(); static final Object LOCKER = new Object(); static class Consumer implements Runnable{ @Override public void run() { consume(); } void consume(){ synchronized (LOCKER){ while (stock.get() == 0) { try { LOCKER.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } stock.getAndDecrement(); System.out.println("consumer " + Thread.currentThread().getName() + "消费消息后库存为:"+stock.get()); LOCKER.notifyAll(); } } } static class Producer implements Runnable{ @Override public void run() { product(); } void product(){ synchronized (LOCKER) { while (stock.get() != 0) { try { LOCKER.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } stock.getAndAdd(2); System.out.println("producer 生产消息,生产后库存为:"+stock.get()); LOCKER.notifyAll(); } } } public static void main(String[] args) { Consumer consumer = new Consumer(); new Thread(consumer).start(); new Thread(consumer).start(); new Thread(consumer).start(); new Thread(new Producer()).start(); } }
上面我们实现一个简单的生产者-消费者的这样一个消息队列,对于消费者而言只有在库存大于0的时候才能进行消费,假设现在有3个消费线程正在执行,但是初始化的时候库存为0,所以这3个线程就都进入到了WAITING状态,然后有一个生产者在库存中增加了2,然后对所有等待线程进行唤醒,唤醒的线程会先通过while判断等待条件到底成不成立,成立→继续等待,不成立→向后执行消费动作,代码中的3个等待线程中有2个将会消费成功,有1个会继续进入等待状态。
producer 生产消息,生产后库存为:2 consumer Thread-2消费消息后库存为:1 consumer Thread-1消费消息后库存为:0
但是,如果将while换成if呢?当等待线程被唤醒后,即使,等待条件成立,线程也会继续执行,显然这不是我们想要的结果。
producer 生产消息,生产后库存为:2 consumer Thread-2消费消息后库存为:1 consumer Thread-1消费消息后库存为:0 consumer Thread-0消费消息后库存为:-1
当然while配合等待条件只是一种通用场景,有的特殊场景不使用while或者使用if也是可以的。
- Object.wait(long)允许我们指定一个超时时间(单位为毫秒),如果等待线程在这个时间内没有被其它线程唤醒,那么java虚拟机会自动唤醒这个线程,不过这样既不会抛出异常也没有返回值,所以线程是否自动唤醒需要一些额外操作。
wait/notify的开销及问题
- 过早唤醒:A、B、C三个线程中都使用同一个锁对象,而且都存在线程暂停的判断,但是A和B使用的线程暂停的判断条件和C是不同的,所以当A、B、C同时处于WAITING状态时,有一个线程为了唤醒A、B会使用notifyAll(),这样同时也会把C唤醒,但是C的通过while判断还是继续进入到了暂停状态,也就是说这个notify动作是与C没有太大关联的,这就被称作过早唤醒。
- 信号丢失:如果等待线程在执行Object.wait()前没有先判断保护条件是否已然成立,那么有可能出现这种情形——通知线程在该等待线程进入临界区之前就已经更新了相关共享变量,使得相应的保护条件成立并进行了通知,但是此时等待线程还没有被暂停,自然也就无所谓唤醒了。这就可能造成等待线程直接执行Object.wait()而被暂停的时候,该线程由于没有其他线程进行通知而一直处于等待状态。这种现象就相当于等待线程错过了一个本来“发送”给它的“信号”,因此被称为信号丢失。
- 欺骗性唤醒:线程可能在没有其他线程执行notify/notifyAll的情况下被唤醒,这就被称为欺骗性唤醒。在wait()方法外面加while()进行判断就可以解决这个问题。
- 上下文切换:wait/notify对应着是线程暂停/线程唤醒,所以会导致多次锁的申请与释放,锁的申请与释放可能会造成上下文切换。
- java虚拟机为每个对象维护一个被称为 等待集(wait set)的队列,该队列用于存储该对象上的等待线程,Object.wait()会让当前线程暂停并释放相应的锁,并将当前线程存入对象的等待集中。执行Object.notify()会使该对象的等待集中的任意一个线程被唤醒,唤醒的线程并不会立即被从这个等待集中移除,而是,等到这个线程在次持有对象锁的时候才会被移除。
Thread.join():某个线程执行完了,此线程才能执行
static void main(){ Thread t = new Thread(); t.start(); ...... t.join();//A ...... }
以上为例,只有t线程执行完毕后,主线程才能执行A后面的内容。
条件变量
- Condition可以作为wait/notify的替代品来实现等待/通知,它的await()、signal()/signalAll()分别对应wait()、notify()/notifyAll()并且解决了过早唤醒以及wait(long)是否超时无法判断等问题。
Object.wait()/Object.notify()要求执行线程持有该对象的内部锁。
Condition.await()/Condition.signal()要求执行线程持有该对象的显式锁。
- Condition.awaitUntil(Date),参数是等待的截至期限,当awaitUntil被其它线程signal时这个方法会返回true。
Condition使用样例
/** * @ClassName ConditionSimple * @description: * @author: yong.yuan * @create: 2022-04-18 10:40 * @Version 1.0 **/ public class ConditionExample { static Lock lock = new ReentrantLock(); static Condition conditionA = lock.newCondition(); static Condition conditionB = lock.newCondition(); static BlockingQueue
queue = new ArrayBlockingQueue (3); static class Consumer implements Runnable{ @Override public void run() { lock.lock(); try { while (queue.isEmpty()){ System.out.println("消费者暂停中...."); conditionA.await(); } System.out.println("消费线程"+Thread.currentThread().getName() +"消费消息:"+queue.take()); conditionB.signalAll(); }catch (InterruptedException e){ e.printStackTrace(); }finally { lock.unlock(); } } } static class Producer implements Runnable{ @Override public void run() { lock.lock(); try { while (queue.remainingCapacity() == 0){ System.out.println("生产者暂停中..."); conditionB.await(); } System.out.println("生产线程"+Thread.currentThread().getName() +"生产消息..."); queue.add("hello"); conditionA.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } }
倒计时协调器
CountDownLatch是一般用来实现一个其他线程等待其他线程完成一组特定的操作后再继续执行,这组特定的操作被称为先决操作。
CountDownLatch会有一个维护没有完成的先决操作的数量的计数器countDownLatch.countDown() 每被执行一次,计数器就会-1,CountDownLatch.await()相当于一个受保护方法,当它所在线程执行到这个方法后,线程就会暂停,直到它内部锁维护的计数器为0时,线程才会被唤醒,继续向下执行。
当CountDownLatch中的计数器为0时,再次执行countDown方法,计数器的值不会变,也不会抛出异常,再次执行await方法线程也不会停止,这意味着CountDownLatch的使用是一次性的。
使用CountDownLatch实现等待/通知的时候调用await、countDown方法都无须加锁。
使用场景:例如启动java服务,各服务之间有调用关系,一般先启动相应数量的被调用服务。例如A服务启动5个实例后才能启动B服务,那就可以有一个初始值为5的CountDownLatch,在B服务启动前设置await,每一个A服务启动就countDown,当A服务实例启动完毕后,B才开始启动。
public class CountDownLatchExample {
private static final CountDownLatch latch = new CountDownLatch(4);
private static int data;
public static void main(String[] args) throws InterruptedException {
Thread workerThread = new Thread() {
@Override
public void run() {
for (int i = 1; i < 10; i++) {
data = i;
latch.countDown();
// 使当前线程暂停(随机)一段时间
Tools.randomPause(1000);
}
};
};
workerThread.start();
latch.await();
Debug.info("It's done. data=%d", data);
}
}
栅栏
CyclicBarrier和CountDownLatch有一定相似之处,CountDownLatch是一个await的线程要等待其它线程完成一定数量的先决条件才能继续执行,CyclicBarrier会在多个线程中设置一个await点,到达这个点的线程数量达到了设置的数量要求才会继续执行。
/**
* @ClassName ClimbMountains
* @description:
* @author: yong.yuan
* @create: 2022-04-03 21:59
* @Version 1.0
**/
public class ClimbMountains {
static Logger logger = Logger.getLogger(ClimbMountains.class.getName());
static CyclicBarrier[] climbBarrier = new CyclicBarrier[2];
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
Company company = new Company(i);
company.start();
}
}
static class Company{
private final String name;
private final List staffList;
public Company(int c) {
this.name ="公司".concat(String.valueOf(c));
climbBarrier[c] = new CyclicBarrier(5);
staffList = new ArrayList<>();
for (int j = 0; j < 5; j++) {
staffList.add(new Staff(name,c,j));
}
}
synchronized void start() {
String log = String.format("%s 五位精英开始攀登....",name);
logger.info(log);
for (Staff staff:staffList) {
new Thread(staff::start).start();
}
}
}
static class Staff{
private final String name;
private final int c;
public Staff(String company,int c,int s) {
this.c = c;
this.name = company.concat("-员工").concat(String.valueOf(s));
}
void start() {
System.out.println(name + ":开始攀登!");
try {
int time = new Random().nextInt(20000);
Thread.sleep(time);
System.out.println(name+":用时"+time/1000+"秒");
climbBarrier[c].await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}finally {
System.out.println(name + ":完毕");
}
}
}
}
=================输出结果=================
四月 18, 2022 4:00:49 下午 io.github.viscent.mtia.ch5.ClimbMountains$Company start
信息: 公司0 五位精英开始攀登....
公司0-员工0:开始攀登!
公司0-员工1:开始攀登!
公司0-员工2:开始攀登!
公司0-员工3:开始攀登!
公司0-员工4:开始攀登!
公司1-员工0:开始攀登!
公司1-员工1:开始攀登!
公司1-员工2:开始攀登!
公司1-员工3:开始攀登!
公司1-员工4:开始攀登!
四月 18, 2022 4:00:49 下午 io.github.viscent.mtia.ch5.ClimbMountains$Company start
信息: 公司1 五位精英开始攀登....
公司1-员工3:用时4秒
公司1-员工4:用时4秒
公司0-员工0:用时5秒
公司0-员工3:用时7秒
公司0-员工2:用时9秒
公司1-员工2:用时11秒
公司1-员工1:用时12秒
公司1-员工0:用时13秒
公司1-员工0:完毕
公司1-员工1:完毕
公司1-员工2:完毕
公司1-员工4:完毕
公司1-员工3:完毕
公司0-员工4:用时13秒
公司0-员工1:用时16秒
公司0-员工0:完毕
公司0-员工4:完毕
公司0-员工2:完毕
公司0-员工3:完毕
![](https://eacape-1259159524.cos.ap-shanghai.myqcloud.com/images/clipboard.png)
公司0-员工1:完毕
阻塞队列
阻塞队列可以按照其存储空间是否受限制划分为有界队列和无界队列,有界队列的队列容量是由程序设定的,无界队列的容量是Integer.MAX\_VALUE也就是2^31
常用阻塞队列和常用方法
ArrayBlockingQueue
其底层的数据结构是一个数组,所以它在put和take的时候不会造成垃圾回收的负担,它在put和take的时候使用的是同一个显式锁,所以造成他在put和take的时候会伴随着锁的释放与申请,如果有大量线程在不断的put和take会造成锁竞争过高,从而不断导致上下文切换。
LinkedBlockingQueue
其底层的数据结构是一个链表,所以它在put个take的时候会伴随着空间的动态分配,也就是每此put或take操作都会伴随着节点的创建和移除,这样就会造成垃圾回收的负担。但是,它的put和take操作是使用的两个不一样的显式锁,这样相对会减缓锁竞争度。
此外其内部维护着一个Atomic变量用于维护队列长度,也可能存在被put线程和take线程不断的争用。
SychronousQueue
容量为0,主要时用来转发任务(阻塞作用),SychronousQueue.take(E)的时候没有线程执行SychronousQueue.put(E)那么消费线程就会暂停知道有生产线程执行SychronousQueue.put(E),
同样,SychronousQueue.put(E)的时候没有消费线程执行SychronousQueue.take(E),生产线程也会停止直到,有消费线程执行SychronousQueue.take(E)。
SychronousQueue和ArrayBlockingQueue/LinkedBlockingQueue前者就像送快递时快递员要把快递交到你的手上才能去送下一份快递,而后者就是直接把快递放到蜂巢储物柜中。
PriorityBlockingQueue
支持优先级的无界阻塞队列,可以通过自定义的类实现compareTo方法指定元素的 排序规则,它take时如果队列为空将会阻塞,但是它会无限扩容所以,put并不会 阻塞。其实现原理是堆排序
在最小堆[1, 5, 8, 6, 10, 11, 20]中再插入一个元素4,下面用图示分析插入的过程:
最大堆[20, 10, 15, 6, 9, 10, 12]中移除元素后,下面用图示分析重排的过程:
DelayWorkQueue
DelayWorkQueue是实现了延迟功能的PriorityBlockingQueue
内部采用的时堆结构(插入时会进行排序),特点是内部元素不会按照入队的顺序来出队,
而是会根据延时长短对内部元素进行排序。
ArrayBlockingQueue和SynchronousQueue都既支持非公平调度也支持公平调度,而LinkedBlockingQueue仅支持非公平调度。
如果生产者线程和消费者线程之间的并发程度比较大,那么这些线程对传输通道内部所使用的锁的争用可能性也随之增加。这时,有界队列的实现适合选用LinkedBlockingQueue,否则我们可以考虑ArrayBlockingQueue。
- LinkedBlockingQueue适合在生产者线程和消费者线程之间的并发程度比较大的情况下使用
- ArrayBlockingQueue适合在生产者线程和消费者线程之间的并发程度较低的情况下使用
- SynchronousQueue适合在消费者处理能力与生产者处理能力相差不大的情况下使用。
流量控制与信号量
SemaPhore通常叫做信号量,一般用来控制同时访问特定资源的的线程数量,通过协调各个线程,以保证合理使用资源。
通常使用的场景就是限流,比如说,限制数据库连接池的连接线程数量。
常用方法
acquire()
获取一个令牌,在获取到令牌、或者被其他线程调用中断之前线程一直处于阻塞状态。
acquire(int permits)
获取一个令牌,在获取到令牌、或者被其他线程调用中断、或超时之前线程一直处于阻塞状态。
acquireUninterruptibly()
获取一个令牌,在获取到令牌之前线程一直处于阻塞状态(忽略中断)。
tryAcquire()
尝试获得令牌,返回获取令牌成功或失败,不阻塞线程。
tryAcquire(long timeout, TimeUnit unit)
尝试获得令牌,在超时时间内循环尝试获取,直到尝试获取成功或超时返回,不阻塞线程。
release()
释放一个令牌,唤醒一个获取令牌不成功的阻塞线程。
hasQueuedThreads()
等待队列里是否还存在等待线程。
getQueueLength()
获取等待队列里阻塞的线程数。
drainPermits()
清空令牌把可用令牌数置为0,返回清空令牌的数量。
availablePermits()
返回可用的令牌数量。
实现一个简单的令牌获取的例子
/**
* @ClassName SemaphoreExample
* @description:
* @author: yong.yuan
* @create: 2022-04-18 18:59
* @Version 1.0
**/
public class SemaphoreExample {
static final Semaphore semaphore = new Semaphore(5);
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
static CountDownLatch countDownLatch = new CountDownLatch(10);
static class Worker implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000);
semaphore.acquire();
System.out.println(Thread.currentThread().getId()+
"号工人,从流水线上取货物一件,现有" + semaphore.availablePermits() + "件货物");
countDownLatch.countDown();
lock.lock();
try {
condition.signalAll();
}finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Machine implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000);
while(semaphore.availablePermits() >= 5) {
lock.lock();
try {
System.out.println("流水线上的货物满了");
condition.await();
} finally {
lock.unlock();
}
}
semaphore.release();
System.out.println("向流水线上送货物,现有"+semaphore.availablePermits()
+"件货物");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(new Worker()).start();
new Thread(new Machine()).start();
}
countDownLatch.await();
System.out.println("已经有10个工人搬走货物了");
while (semaphore.tryAcquire()){
System.out.println("剩余货物搬走.....");
}
}
}
==============执行结果==============
流水线上的货物满了
流水线上的货物满了
17号工人,从流水线上取货物一件,现有4件货物
向流水线上送货物,现有4件货物
流水线上的货物满了
向流水线上送货物,现有5件货物
19号工人,从流水线上取货物一件,现有5件货物
13号工人,从流水线上取货物一件,现有4件货物
向流水线上送货物,现有5件货物
流水线上的货物满了
向流水线上送货物,现有4件货物
27号工人,从流水线上取货物一件,现有3件货物
15号工人,从流水线上取货物一件,现有4件货物
向流水线上送货物,现有5件货物
向流水线上送货物,现有5件货物
25号工人,从流水线上取货物一件,现有4件货物
21号工人,从流水线上取货物一件,现有4件货物
向流水线上送货物,现有5件货物
23号工人,从流水线上取货物一件,现有4件货物
向流水线上送货物,现有5件货物
流水线上的货物满了
向流水线上送货物,现有5件货物
29号工人,从流水线上取货物一件,现有4件货物
向流水线上送货物,现有5件货物
31号工人,从流水线上取货物一件,现有5件货物
已经有10个工人搬走货物了
剩余货物搬走.....
剩余货物搬走.....
剩余货物搬走.....
剩余货物搬走.....
剩余货物搬走.....
Exchager
Exchanger类可用于两个线程之间交换信息。可简单地将Exchanger对象理解为一个包含两个格子的容器,通过exchanger方法可以向两个格子中填充信息。当两个格子中的均被填充时,该对象会自动将两个格子的信息交换,然后返回给线程,从而实现两个线程的信息交换。
当消费者线程消费一个已填充的缓冲区时,另外一个缓冲区可以由生产者线程进行填充,从而实现了数据生成与消费的并发。这种缓冲技术就被称为双缓冲
/**
* @ClassName ExchangerExample
* @description:
* @author: yong.yuan
* @create: 2022-04-18 23:10
* @Version 1.0
**/
public class ExchangerExample {
static Exchanger STRING_EXCHANGER = new Exchanger<>();
static class MyThread extends Thread{
String msg;
public MyThread(String threadName,String msg) {
Thread.currentThread().setName(threadName);
this.msg = msg;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+":"
+STRING_EXCHANGER.exchange(msg));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread t1 = new MyThread("thread-1","hello thread-1");
Thread t2 = new MyThread("thread-2","hello thread-2");
t1.start();
t2.start();
}
}
===============操作结果===============
Thread-1:hello thread-1
Thread-0:hello thread-2
如何正确的停止线程
使用interrupt终止线程
- 使用interrupt实际上是通过interrupt状态的变化来对线程实现停止,而不会立即终止这个线程。
- 当线程处于wait()或者sleep()状态时,使用interrupt可以将休眠的线程唤醒,但是会抛出异常,我们可以在Catch(InterruptedExcetion e){}中手动用interrupt()来终止这个线程
- 对比其它终止方式 - stop():会直接把线程停掉,不能处理停止之前想要处理的数据
public class StopThread{
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
int count = 0;
while (!Thread.currentThread().isInterrupted() && count < 1000){
System.out.println("count = " + count++);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//线程在休眠期间被中断,那么会自动清除中断信号,所以需要在catch中再次中断
Thread.currentThread().interrupt();
}
}
});
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}