Java并发编程
一、基本概念
1.1 概念
串行、并行、并发
串行:多个程序按序执行。上一个做完,接着做下一个。
并行:多个程序同时执行,一般需要多核处理器的支持。
并发:多个程序交错执行,就像看上去是同时执行的一样。
同步异步、阻塞非阻塞
同步和异步:当前线程是否需要等待方法调用执行完毕。
1 | // 同步 |
在异步调用下的代码逻辑相对而言不太直观,需要借助回调或事件通知,这在复杂逻辑下对编码能力的要求较高。而同步调用就是直来直去,等待执行完毕然后拿到结果紧接着执行下面的逻辑,对编码能力的要求较低,也更不容易出错。
所以你会发现有很多方法它是异步调用的方式,但是最终的使用还是异步转同步。
比如你向线程池提交一个任务,得到一个future
,此时是异步的,然后你在紧接着在代码里调用 future.get()
,那就变成等待这个任务执行完成,这就是所谓的异步转同步,像 Dubbo RPC 调用同步得到返回结果就是这样实现的。
阻塞和非阻塞:当前接口数据还未准备就绪时,线程是否被阻塞挂起。
何为阻塞挂起?就是当前线程还处于 CPU 时间片当中,调用了阻塞的方法,由于数据未准备就绪,则时间片还未到就让出 CPU。所以阻塞和同步看起来都是等,但是本质上它们不一样,同步的时候可没有让出 CPU。一旦同步结束,CPU可立马执行接下去的代码。
而非阻塞就是当前接口数据还未准备就绪时,线程不会被阻塞挂起,可以不断轮询请求接口,看看数据是否已经准备就绪。
1.2 线程的状态
- NEW:Thread对象被创建出来了,但是还没有执行start方法。
- RUNNABLE:Thread对象调用了start方法,就为RUNNABLE状态(CPU调度/没有调度)
- BLOCKED、WAITING、TIME_WAITING:都可以理解为是阻塞、等待状态,因为处在这三种状态下,CPU不会调度当前线程
- BLOCKED:synchronized没有拿到同步锁,被阻塞的情况
- WAITING:调用wait方法就会处于WAITING状态,需要被手动唤醒(notify或notifyAll)
- TIME_WAITING:调用sleep方法、join方法、wait(long timeout),会被自动唤醒,无需手动唤醒
- TERMINATED:run方法执行完毕,线程生命周期到头了
二、线程的创建
2.1 继承Thread类
1 | package com.leggasai.concurrent; |
2.2 实现Runnable
1 | package com.leggasai.concurrent; |
2.3 实现Callable
实现Callable接口,可以创建具有返回值的线程。需要配合FutureTask
使用。
1 | import java.util.concurrent.Callable; |
2.4 线程池
在大型应用中,一般都是使用线程池去并发执行多个任务,因为线程池实际上已经创建好了若干个活跃的线程,省去了创建线程的时间,具有更高的效率。
1 | public static void threadPoolDemo(){ |
三、线程的使用
3.1 获取线程信息
获取当前线程Thread.currentThread()
1 | System.out.println(Thread.currentThread()); |
获取线程相关信息
1 | System.out.println(thread.getId()); |
3.2 设置线程相关信息
1 | thread.setName("my thread"); |
线程的优先级:范围1-10。数值越高意味着优先级越高
1 | public static void main(String[] args) throws Exception { |
线程让步Thread.yield()
:当前线程向处理器表明自己可以让步,放弃CPU使用权让给其他相同优先级线程。这并不是一个强制性的指令,而是一个建议,具体实现依赖于底层操作系统的线程调度器。该方法一般很少使用。
1 | public static void main(String[] args) throws Exception { |
下面这种情况,yield
无法生效,因为yield()
不会释放锁
1 | public static void main(String[] args) throws Exception { |
线程休眠Thread.sleep()
:使得线程暂停执行一段时间进入时间等待状态,会让出CPU,但不会让出锁。
1 | public static void main(String[] args) throws Exception { |
线程join()
方法:使得当前线程等待被调用join()
方法的线程执行结束。
下面代码中,由于设置t1
为守护线程,main
线程不会等待t1
执行结束才结束,因此会直接结束运行。
1 | public static void main(String[] args) throws Exception { |
执行t1.join()
方法后,main
线程会等待t1
执行结束后,在继续运行。因此控制台可输出1
1 | public static void main(String[] args) throws Exception { |
守护线程 vs 非守护线程
- 默认情况下,线程都是非守护线程。
- 守护线程是一种在程序运行时,在后台提供服务的线程,随着非守护线程的结束而自动退出。
- 主线程默认是非守护线程,如果主线程执行结束,需要查看当前JVM内是否还有非守护线程,如果没有JVM直接停止
- 可以通过
setDaemon和isDaemon
来设置守护线程和查看是否是守护线程
1 | public static void main(String[] args) throws Exception { |
3.3 线程的等待和唤醒(重点)
wait()/wait(timeout)
:使得当前线程释放锁资源,进入等待状态,并加入等待队列。notify()
:唤醒在同一个对象上调用wait()
而进入等待队列中的某个线程,进入到锁队列。notifyAll()
:唤醒在同一个对象上调用wait()
而进入等待队列中的所有线程,进入到锁队列。
上述关键词必须在synchronized
代码块内或方法体内才能调用,因为他们是基于某个对象锁的。
- 线程A调用
wait()
方法,释放对象的锁,进入等待队列。 - 线程B执行一些操作,然后调用
notify()
方法,唤醒等待队列中的线程A。 - 线程A被唤醒后,它将进入锁队列,等待获取对象的锁。
- 当线程B释放对象的锁(例如,退出同步块或同步方法)时,线程A将有机会竞争锁,成功获取锁后继续执行。
3.4 线程结束
方法一:stop()
方法,但由于stop()
方法会产生不可预测行为使得程序进入不一致状态,已被弃用。
方法二:自然结束
方法三:修改共享变量。务必使用volatile
修饰共享变量,如下面的flag。原因见4.2可见性
1 | public static void main(String[] args) throws Exception{ |
方法四:interrupt()
方法(推荐使用)。
通过打断WAITING或者TIMED_WAITING状态的线程,从而抛出异常自行处理
这种停止线程方式是最常用的一种,在框架和JUC中也是最常见的
1 | public static void main(String[] args) throws Exception{ |
标记法
1 | public static void main(String[] args) throws Exception{ |
四、并发编程
三大特性:原子性、可见性、有序性。这三大特性是并发编程中需要特别关注和处理的问题,保证它们的正确性有助于避免并发引起的一系列问题,如竞态条件、死锁、活锁等。
4.1 原子性
原子性指一个操作是不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到他。
JMM(Java Memory Model)。不同的硬件和不同的操作系统在内存上的操作有一定差异的。Java为了解决相同代码在不同操作系统上出现的各种问题,用JMM屏蔽掉各种硬件和操作系统带来的差异。让Java的并发编程可以做到跨平台。
JMM规定所有变量都会存储在主内存中,在操作的时候,需要从主内存中复制一份到线程内存(CPU内存),在线程内部做计算。然后再写回主内存中。
1 | public class CounterDemo { |
上述程序按理应该输出200(两个线程各调用了increment()
方法一百次),但实际最终结果往往小于200。其原因在于count++
不满足原子性。
1 | count++不满足原子性,其可分为三步 |
考虑多线程并发执行的情况:可能会发生如下情景
线程#1在执行count++
但是在写回内存之前,有另外一个线程#2也开始执行count++
。于是对于线程#2读取到的实际可以被认为脏值,因为线程#1还没将更新值写回内存。最后线程#2将count
+1后的新值a+1
写回内存。明显不符合预期count=a+2
。因为丢失了更新。
如果加上synchronized
关键词,则同一时间最多只能有一个线程访问increment()
方法,这时流程图就变为了如下所示:这时最终值能够符合预期,因为锁的机制,使得count++
变为了原子操作。
解决方法二:原子类
目前Java中提供的原子类大部分底层使用了CAS锁(CompareAndSet自旋锁),如AtomicInteger、AtomicLong等;也有使用了分段锁+CAS锁的原子类,如LongAdder等。
解决方法三:Lock锁
如ReentrantLock
、ReentrantReadWriteLock
。详见锁章节
解决方法四:ThreadLocal类
这个类提供线程局部变量。这些变量与普通变量的不同之处在于,访问一个变量的每个线程都有自己的、独立初始化的变量副本。
Thread
、ThreadLocalMap
、ThreadLocal
的关系如上图所示。
- 每个
Thread
实例对象都有其各自的ThreadLocalMap
映射,该Map存放了ThreadLocal
到Object
的映射 - 可以创建多个
ThreadLocal
来为每个线程存储多个线程独立的变量。 - 每个
Thread
的变量是独立的,因为ThreadLocalMap
是每个线程独有的。而ThreadLocal
只是作为Key
java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class ThreadLocalDemo {
static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();
public static void main(String[] args) {
// belong to main thread
tl1.set("123");
tl2.set("456");
Thread t1 = new Thread(() -> {
System.out.println("t1:" + tl1.get());
System.out.println("t1:" + tl2.get());
});
t1.start();
System.out.println("main:" + tl1.get());
System.out.println("main:" + tl2.get());
}
}
main:123
main:456
t1:null
t1:null
ThreadLocal
:
set()
:在当前线程上绑定一个变量。也就是在当前线程的ThreadLocalMap
中添加了一个映射,ThreadLocal
->Object
get()
:获取此ThreadLocal
在当前线程上绑定的值。remove()
:删除此ThreadLocal
在当前线程上绑定的值,避免内存泄漏。
内存泄漏问题
内存泄漏:JVM创建的对象永远都无法访问到,但是GC又不能回收对象所占用的内存。
Java中的使用引用类型分别是强,软,弱,虚。
在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。强引用不会被JVM的GC回收。因此是造成内存泄漏的主要原因之一。
其次是软引用,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中,作为缓存使用。
然后是弱引用,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。可以解决内存泄漏问题,ThreadLocal就是基于弱引用解决内存泄漏的问题。
最后是虚引用,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。不过在开发中,我们用的更多的还是强引用。
ThreadLocalMap
中的Entry
的key
被设计成弱引用。
如果当我们不再使用ThreadLocal
变量时,依旧有一条强引用路径指向entry
为了解决这个问题,
ThreadLocal
的作者就把entry
中的key
设计成弱引用的形式,一旦栈中的ref-threadlocal
强引用消失,在下一次GC时就会清除堆中的threadlocal
对象。此时,该entry
的key
值为null
。此外,ThreadLocal
还有些额外的逻辑清除整个entry
的指针。其通过判断entry
的key
值是否为null
来判断该entry
是否过时。
4.2 可见性
可见性是指当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。在多线程环境中,由于线程之间的缓存机制,每个线程的工作内存(CPU三级缓存)都是独立的,会导致每个线程中做修改时,只改自己的工作内存,没有及时的同步到主内存,导致数据不一致问题。
1 | public class TestDemo { |
上述代码线程t1
永远不会结束,因为它无法观察到flag
已被修改。不同线程可能在各自的缓存中保存了相同变量的不同副本。如果一个线程修改了共享变量,其他线程可能不会立即看到这个修改。在上述代码中main
线程修改了自己缓冲区中的flag = false
,并写回主内存。但t1
线程的while(flag)
读取的依旧是自己缓冲区中的flag(true)
因此无法退出循环。
解决方法一:(不推荐)在while
循环中,使用某些语句读取一次flag
。
1 | Thread t1 = new Thread(() -> { |
在这个具体的语句中,由于涉及字符串拼接和 System.out.println
的操作,Java 的虚拟机可能会进行一些特殊处理,例如强制从主内存中读取 flag
的最新值,而不使用线程的本地缓存。因此可以通过重新读取flag
的方式刷新缓存。
解决方法二:使用volatile
关键词volatile
是一个关键字,用来修饰成员变量。
如果属性被volatile修饰,相当于会告诉CPU,对当前属性的操作,不允许使用CPU的缓存,必须去和主内存操作。
- volatile属性被写:当写一个volatile变量,JMM会将当前线程对应的CPU缓存及时的刷新到主内存中
- volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量java
1
static volatile boolean flag = true;
解决方法三:使用synchronized
。
如果涉及到了synchronized
的同步代码块或者是同步方法,获取锁资源之后,将内部涉及到的变量从CPU缓存中移除,必须去主内存中重新拿数据,而且在释放锁之后,会立即将CPU缓存中的数据同步到主内存。
1 | public class TestDemo { |
解决方法四:使用锁
Lock锁保证可见性的方式和synchronized完全不同,synchronized基于他的内存语义,在获取锁和释放锁时,对CPU缓存做一个同步到主内存的操作。
Lock锁是基于volatile实现的。Lock锁内部再进行加锁和释放锁时,会对一个由volatile修饰的state属性进行加减操作。
如果对volatile修饰的属性进行写操作,CPU会执行带有lock前缀的指令,CPU会将修改的数据,从CPU缓存立即同步到主内存,同时也会将其他的属性也立即同步到主内存中。还会将其他CPU缓存行中的这个数据设置为无效,必须重新从主内存中拉取。
1 | public class TestDemo { |
4.3 有序性
有序性是指程序执行的顺序按照代码的先后顺序执行。在并发编程中,由于编译器的优化、指令重排等原因,程序的执行顺序可能被改变,导致程序的实际执行顺序与代码的顺序不一致。
一个指令重排的例子:
1 | public class OrderDemo { |
上述代码在正常情况下,x
和y
不同时为0。只有发生指令重排,即先执行了x=b和y=a,才会导致x
和y
同时为0。
happens-before
规则:在如下规则下不允许指令重排
- 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
- 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
- volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操作。
- happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
- 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
- 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
- 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
- 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。
volatile
修饰:被该关键词修饰的属性不会被指令重排。通过写前StoreStore 屏障和读后LoadLoad 屏障实现。
五、锁
重要!!!
5.1 锁的分类
可重入锁、不可重入锁
重入:当前线程获取到A锁,在获取之后尝试再次获取A锁是可以直接拿到的。
不可重入:当前线程获取到A锁,在获取之后尝试再次获取A锁,无法获取到的,因为A锁被当前线程占用着,需要等待自己释放锁再获取锁。
Java中提供的synchronized,ReentrantLock,ReentrantReadWriteLock都是可重入锁。
乐观锁、悲观锁
悲观锁:悲观锁的基本思想是在整个操作过程中,将数据处于锁定状态,以确保在这个期间数据不会被其他线程修改。获取不到锁资源时,会将当前线程挂起(进入BLOCKED、WAITING),线程挂起会涉及到用户态和内核的太的切换,而这种切换是比较消耗资源的。
乐观锁:乐观锁的基本思想是假设在并发访问的情况下,数据不会发生冲突,因此不对数据加锁,而是在更新时检查数据是否被其他线程修改。获取不到锁资源,可以再次让CPU调度,重新尝试获取锁资源。
Java中提供的synchronized,ReentrantLock,ReentrantReadWriteLock都是悲观锁。
Java中提供的CAS操作,就是乐观锁的一种实现。Atomic原子性类中,也是基于CAS乐观锁实现的。
公平锁、非公平锁
公平锁是指多个线程按照请求的顺序依次获取锁,而非公平锁则允许线程插队。
Java中提供的synchronized
是非公平锁。
Java中提供的ReentrantLock
,ReentrantReadWriteLock
可以实现公平锁和非公平锁
互斥锁、共享锁、读写锁
互斥锁:同一时间点,只会有一个线程持有者当前互斥锁。
共享锁:同一时间点,当前共享锁可以被多个线程同时持有。
读写锁:读锁-读锁共享、写锁和其他锁互斥
Java中提供的synchronized、ReentrantLock是互斥锁。
Java中提供的ReentrantReadWriteLock,有互斥锁也有共享锁。
5.2 synchronized详解
同步代码块和同步方法
同步代码块:锁住{}
之间的代码。锁可以使用某个对象或类
1 | synchronized(xxx) { |
同步方法:声明某个方法是同步方法,同一时间最多被一个线程访问。
- 静态方法:此时使用的是当前类.class作为锁(类锁)
- 非静态方法:此时使用的是当前对象做为锁(对象锁)
类锁和其某个类实例的锁是相互独立的。下面代码中的两个线程可以同时执行。java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44public class syncDemo {
public static void main(String[] args) {
LockDemo lockDemo = new LockDemo();
Thread thread1 = new Thread(() -> {
lockDemo.staticMethod();
});
Thread thread2 = new Thread(() -> {
lockDemo.instanceMethod();
});
thread1.start();
thread2.start();
}
}
class LockDemo{
// 静态方法上的类锁
public static synchronized void staticMethod() {
// synchronized code
String formattedTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println("staticMethod() start Time: " + formattedTime);
try {
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("staticMethod() end Time: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
// 实例方法上的对象锁
public synchronized void instanceMethod() {
// synchronized code
String formattedTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println("instanceMethod() start Time: " + formattedTime);
try {
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println("instanceMethod() end Time: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}
JIT优化方式
锁消除:在synchronized修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,此时synchronized无效。比如只有一些局部变量。
锁粗化:如果在一个循环中,频繁的获取和释放做资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗。(不一定会触发)
1 | public void method(){ |
锁升级:锁的状态总共有四种,无锁状态、偏向锁、轻量级锁、重量级锁。
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。但是锁的升级是单向的,只能升级不能降级。
- 无锁、匿名偏向:当前对象没有作为锁存在。
- 偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断,当前指向的线程是否是当前线程 。
- 如果是,直接拿着锁资源走。
- 如果当前线程不是我,基于CAS的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁。(偏向锁状态出现了锁竞争的情况)
- 轻量级锁:会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁)
- 如果成功获取到,拿着锁资源走
- 如果自旋了一定次数,没拿到锁资源,锁升级。
- 重量级锁:就是最传统的synchronized方式,拿不到锁资源,就挂起当前线程。(用户态&内核态)
synchronized是基于对象实现的,那这些锁的信息存储在哪?
实例数据:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
内存填充:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
Java头对象是是实现synchronized锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字节来存储对象头(数组是三个字节),其主要结构是由Mark Word和Class Metadata Address组成,其结构说明如下表:
头对象结构 | 说明 |
---|---|
Mark Word | 存储对象的hashCode、锁信息或分代年龄或GC标志等信息 |
Class Metadata Address | 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。 |
Mark Word结构 | |
![]() |
|
锁状态转变流程 | |
![]() |
5.3 ReentrantLock详解
ReentrantLock和synchronized区别
- ReentrantLock是类,而synchronized是关键词。
- 底层都是基于JVM层面实现互斥锁
- 实现原理:ReentrantLock是基于AQS实现,而synchronized是关基于ObjectMonitor
- ReentrantLock功能更全面,支持公平锁和非公平锁,支持限制等待时间。
- 效率区别:ReentranLock不存在锁升级概念;synchronized存在锁升级概念;
AQS
AbstractQueuedSynchronizer,即抽象的队列同步器,是一种用来构建锁和同步器的框架。JUC下的很多内容都是基于AQS实现了部分功能,比如ReentrantLock,ThreadPoolExecutor,BlockingQueue,CountDownLatch,Semaphore,CyclicBarrier等等都是基于AQS实现。
首先AQS中提供了一个由volatile修饰,并且采用CAS方式修改的int类型的state变量。
其次AQS中维护了一个双向链表,有head,有tail,并且每个节点都是Node对象。
5.4 ReentrantReadWriteLock详解
读写锁,多个线程可以同时进行读操作,提高了并发效率。读-读不互斥,读-写互斥,写-写互斥。
实现原理
ReentrantReadWriteLock还是基于AQS实现的,还是对state进行操作,拿到锁资源就去干活,如果没有拿到,依然去AQS队列中排队。
读锁操作:基于state的高16位进行操作。
写锁操作:基于state的低16位进行操作。
ReentrantReadWriteLock依然是可重入锁。
六、阻塞队列
阻塞队列基于生产者消费者模式。
生产者存储方法:
add(E)
: 添加数据到队列,如果队列满了,无法存储,抛出异常offer(E)
: 添加数据到队列,如果队列满了,返回falseoffer(E,timeout,unit)
: 添加数据到队列,如果队列满了,阻塞timeout时间,如果阻塞一段时间,依然没添加进入,返回falseput(E)
: 添加数据到队列,如果队列满了,挂起线程,等到队列中有位置,再扔数据进去,死等!
消费者读取方法:
remove()
: 从队列中移除数据,如果队列为空,抛出异常poll()
: 从队列中移除数据,如果队列为空,返回nullpoll(timeout,unit)
: 从队列中移除数据,如果队列为空,挂起线程timeout时间,等生产者存入数据,再获取take()
:从队列中移除数据,如果队列为空,线程挂起,一直等到生产者扔数据,再获取
6.1 ArrayBlockingQueue
ArrayBlockingQueue在初始化的时候,必须指定当前队列的长度。它是循环使用固定长度的数组实现,通过两个属性,putIndex
和takeIndex
来控制放入和取出元素的下标。
6.2 LinkedBlockingQueue
LinkedBlockingQueue是基于链表的,通过头节点和尾节点控制放入和取出。可以指定最大容量,也可以不指定。
6.3 PriorityBlockingQueue
首先PriorityBlockingQueue是一个优先级队列,他不满足先进先出的概念。
会将查询的数据进行排序,排序的方式就是基于插入数据值的本身。
实现原理是基于数组实现的二叉堆。
6.4 DelayQueue
DelayQueue就是一个延迟队列,生产者写入一个消息,这个消息还有直接被消费的延迟时间。
DelayQueue会更具延迟时间进行排序,其本身也是一个PriorityBlockingQueue
其放入的元素需要实现Delayed
接口。
1 | // 外卖订单超时自动取消场景 |
6.5 SynchronousQueue
这个队列比较特殊,它不存储元素。因为SynchronousQueue没有容量。与其他BlockingQueue(阻塞队列)不同,SynchronousQueue是一个不存储元素的BlockingQueue。只是它维护一组线程,这些线程在等待着把元素加入或移出队列。
可以理解为给线程“配对”。入和出必须是配对的,否则阻塞。
注意:有两种模式,分别为公平模式和非公平模式。公平模式按照先来先配对原则(TransferQueue),非公平按照后来先配对(TransferStack)。
1 | public static void syncBlockQ() throws InterruptedException{ |
参考:
Java多线程基础知识_waitset entrylist-CSDN博客
java并发编程(荣耀典藏版)-CSDN博客