Java进阶知识

Java进阶知识,包括并发编程,NIO等

Java核心技术

Exception和Error有什么区别?

Exception和Error都是继承了Throwable类,在Java中只有Throwable类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。

Exception和Error体现了Java平台设计者对不同异常情况的分类。Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。

Error是指在正常情况下,不大可能出现的情况,绝大部分的Error都会导致程序(比如JVM自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如OutOfMemoryError之类,都是Error的子类。

Exception又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。不可查的Error,是Throwable不是Exception。

不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。

image-20251228135428400

异常处理代码比较繁琐,比如我们需要写很多千篇一律的捕获代码,或者在finally里面做一些资源回收工作。随着Java语言的发展,引入了一些更加便利的特性,比如try-with-resources和multiple catch,具体可以参考下面的代码段。在编译时期,会自动生成相应的处理逻辑,比如,自动按照约定俗成close那些扩展了AutoCloseable或者Closeable的对象。

1
2
3
4
5
6
7
8
9
// 新写法:简洁、安全
try (Scanner scanner = new Scanner(new File("test.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// 资源在此处已自动关闭,无需 finally

只需要在 try 后面的圆括号 () 中声明并初始化资源。一旦 try 代码块结束,资源会自动关闭。

谈谈final、finally、 finalize有什么不同?

final可以用来修饰类、方法、变量,分别有不同的意义,final修饰的class代表不可以继承扩展final的变量是不可以修改的而final的方法也是不可以重写的(override)。

finally则是Java保证重点代码一定要被执行的一种机制。我们可以使用try-finally或者try-catch-finally来进行类似关闭JDBC连接、保证unlock锁等动作。

finalize是基础类java.lang.Object的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize机制现在已经不推荐使用,并且在JDK 9开始被标记为deprecated。

推荐使用final关键字来明确表示我们代码的语义、逻辑意图,这已经被证明在很多场景下是非常好的实践,比如:

  • 我们可以将方法或者类声明为final,这样就可以明确告知别人,这些行为是不许修改的。

如果你关注过Java核心类库的定义或源码, 有没有发现java.lang包下面的很多类,相当一部分都被声明成为final class?在第三方类库的一些基础类中同样如此,这可以有效避免API使用者更改基础功能,某种程度上,这是保证平台安全的必要手段

  • 使用final修饰参数或者变量,也可以清楚地避免意外赋值导致的编程错误,甚至,有人明确推荐将所有方法参数、本地变量、成员变量声明成final。
  • final变量产生了某种程度的不可变(immutable)的效果,所以,可以用于保护只读数据,尤其是在并发编程中,因为明确地不能再赋值final变量,有利于减少额外的同步开销,也可以省去一些防御性拷贝的必要。

final也许会有性能的好处,很多文章或者书籍中都介绍了可在特定场景提高性能,比如,利用final可能有助于JVM将方法进行内联,可以改善编译器进行条件编译的能力等等。坦白说,很多类似的结论都是基于假设得出的,比如现代高性能JVM(如HotSpot)判断内联未必依赖final的提示,要相信JVM还是非常智能的。类似的,final字段对性能的影响,大部分情况下,并没有考虑的必要。

对于finally,明确知道怎么使用就足够了。需要关闭的连接等资源,更推荐使用Java 7中添加的try-with-resources语句,因为通常Java平台能够更好地处理异常情况,编码量也要少很多,何乐而不为呢。

另外,我注意到有一些常被考到的finally问题(也比较偏门),至少需要了解一下。比如,下面代码会输出什么?

1
2
3
4
5
6
try {
// do something
System.exit(1);
} finally{
System.out.println(“Print from finally”);
}

上面finally里面的代码可不会被执行的哦,这是一个特例。

Java平台目前在逐步使用java.lang.ref.Cleaner来替换掉原有的finalize实现。Cleaner的实现利用了幻象引用(PhantomReference),这是一种常见的所谓post-mortem清理机制。利用幻象引用和引用队列,我们可以保证对象被彻底销毁前做一些类似资源回收的工作,比如关闭文件描述符(操作系统有限的资源),它比finalize更加轻量、更加可靠。

吸取了finalize里的教训,每个Cleaner的操作都是独立的,它有自己的运行线程,所以可以避免意外死锁等问题。

注意,从可预测性的角度来判断,Cleaner或者幻象引用改善的程度仍然是有限的,如果由于种种原因导致幻象引用堆积,同样会出现问题。所以,Cleaner适合作为一种最后的保证手段,而不是完全依赖Cleaner进行资源回收。

强引用、软引用、弱引用、幻象引用有什么区别?

不同的引用类型,主要体现的是对象不同的可达性(reachable)状态和对垃圾收集的影响

所谓强引用(“Strong” Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。

软引用(SoftReference),是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当JVM认为内存不足时,才会去试图回收软引用指向的对象。JVM会确保在抛出OutOfMemoryError之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

弱引用(WeakReference)并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就可以用来构建一种没有特定约束的关系,比如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重现实例化。它同样是很多缓存实现的选择。

对于幻象引用,有时候也翻译成虚引用,你不能通过它访问对象。幻象引用仅仅是提供了一种确保对象被finalize以后,做某些事情的机制,比如,通常用来做所谓的Post-Mortem清理机制,我在专栏上一讲中介绍的Java平台自身Cleaner机制等,也有人利用幻象引用监控对象的创建和销毁。

String、StringBuffer、StringBuilder有什么区别

String是Java语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的Immutable类,被声明成为final class,所有属性也都是final的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的String对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。

StringBuffer是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用append或者add方法,把字符串添加到已有序列的末尾或者指定位置StringBuffer本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是StringBuilder。

1.字符串设计和实现考量

String是Immutable类的典型实现,原生的保证了基础线程安全,因为你无法对它内部数据进行任何修改,这种便利甚至体现在拷贝构造函数中,由于不可变,Immutable对象在拷贝时不需要额外复制数据。

再来看看StringBuffer实现的一些细节,它的线程安全是通过把各种修改数据的方法都加上synchronized关键字实现的,非常直白。其实,这种简单粗暴的实现方式,非常适合我们常见的线程安全类实现,不必纠结于synchronized性能之类的,有人说“过早优化是万恶之源”,考虑可靠性、正确性和代码可读性才是大多数应用开发最重要的因素。

为了实现修改字符序列的目的,StringBuffer和StringBuilder底层都是利用可修改的(char,JDK 9以后是byte)数组二者都继承了AbstractStringBuilder,里面包含了基本操作,区别仅在于最终的方法是否加了synchronized

另外,这个内部数组应该创建成多大的呢?如果太小,拼接的时候可能要重新创建足够大的数组;如果太大,又会浪费空间。目前的实现是,构建时初始字符串长度加16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是16)。我们如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容的开销。扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行arraycopy。

2.字符串缓存

我们粗略统计过,把常见应用进行堆转储(Dump Heap),然后分析对象组成,会发现平均25%的对象是字符串,并且其中约半数是重复的。如果能避免创建重复字符串,可以有效降低内存消耗和对象创建开销。

String在Java 6以后提供了intern()方法,目的是提示JVM把相应字符串缓存起来,以备重复使用。在我们创建字符串对象并调用intern()方法的时候,如果已经有缓存的字符串,就会返回缓存里的实例,否则将其缓存起来。一般来说,JVM会将所有的类似“abc”这样的文本字符串,或者字符串常量之类缓存起来。

看起来很不错是吧?但实际情况估计会让你大跌眼镜。一般使用Java 6这种历史版本,并不推荐大量使用intern,为什么呢?魔鬼存在于细节中,被缓存的字符串是存在所谓PermGen里的,也就是臭名昭著的“永久代”,这个空间是很有限的,也基本不会被FullGC之外的垃圾收集照顾到。所以,如果使用不当,OOM就会光顾。

在后续版本中,这个缓存被放置在堆中,这样就极大避免了永久代占满的问题,甚至永久代在JDK 8中被MetaSpace(元数据区)替代了。而且,默认缓存大小也在不断地扩大中,从最初的1009,到7u40以后被修改为60013。你可以使用下面的参数直接打印具体数字,可以拿自己的JDK立刻试验一下。

1
-XX:+PrintStringTableStatistics

你也可以使用下面的JVM参数手动调整大小,但是绝大部分情况下并不需要调整,除非你确定它的大小已经影响了操作效率。

1
-XX:StringTableSize=N

Intern是一种显式地排重机制,但是它也有一定的副作用,因为需要开发者写代码时明确调用,一是不方便,每一个都显式调用是非常麻烦的;另外就是我们很难保证效率,应用开发阶段很难清楚地预计字符串的重复情况,有人认为这是一种污染代码的实践。

幸好在Oracle JDK 8u20之后,推出了一个新的特性,也就是G1 GC下的字符串排重。它是通过将相同数据的字符串指向同一份数据来做到的,是JVM底层的改变,并不需要Java类库做什么修改。

注意这个功能目前是默认关闭的,你需要使用下面参数开启,并且记得指定使用G1 GC:

1
-XX:+UseStringDeduplication

前面说到的几个方面,只是Java底层对字符串各种优化的一角,在运行时,字符串的一些基础操作会直接利用JVM内部的Intrinsic机制,往往运行的就是特殊优化的本地代码,而根本就不是Java代码生成的字节码。Intrinsic可以简单理解为,是一种利用native方式hard-coded的逻辑,算是一种特别的内联,很多优化还是需要直接使用特定的CPU指令,具体可以看相关源码,搜索“string”以查找相关Intrinsic定义。当然,你也可以在启动实验应用时,使用下面参数,了解intrinsic发生的状态。

1
2
3
4
5
6
-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
//样例输出片段
180 3 3 java.lang.String::charAt (25 bytes)
@ 1 java.lang.String::isLatin1 (19 bytes)
...
@ 7 java.lang.StringUTF16::getChar (60 bytes) intrinsic

可以看出,仅仅是字符串一个实现,就需要Java平台工程师和科学家付出如此大且默默无闻的努力,我们得到的很多便利都是来源于此。

如果你仔细观察过Java的字符串,在历史版本中,它是使用char数组来存数据的,这样非常直接。但是Java中的char是两个bytes大小,拉丁语系语言的字符,根本就不需要太宽的char,这样无区别的实现就造成了一定的浪费。密度是编程语言平台永恒的话题,因为归根结底绝大部分任务是要来操作数据的。

其实在Java 6的时候,Oracle JDK就提供了压缩字符串的特性,但是这个特性的实现并不是开源的,而且在实践中也暴露出了一些问题,所以在最新的JDK版本中已经将它移除了。

在Java 9中,引入了Compact Strings的设计,对字符串进行了大刀阔斧的改进。将数据存储方式从char数组,改变为一个byte数组加上一个标识编码的所谓coder,并且将相关字符串操作类都进行了修改。另外,所有相关的Intrinsic之类也都进行了重写,以保证没有任何性能损失。

虽然底层实现发生了这么大的改变,但是Java字符串的行为并没有任何大的变化,所以这个特性对于绝大部分应用来说是透明的,绝大部分情况不需要修改已有代码。

动态代理是基于什么原理?

反射机制是Java语言提供的一种基础功能,赋予程序在运行时自省(introspect,官方用语)的能力。通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。

动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装RPC调用、面向切面的编程(AOP)。

实现动态代理的方式很多,比如JDK自身提供的动态代理,就是主要利用了上面提到的反射机制。还有其他的实现方式,比如利用传说中更高性能的字节码操作机制,类似ASM、cglib(基于ASM)、Javassist等。

它是一个代理机制。如果熟悉设计模式中的代理模式,代理可以看作是对调用目标的一个包装,这样我们对目标代码的调用不是直接发生的,而是通过代理完成

通过代理可以让调用者与实现者之间解耦。比如进行RPC调用,框架内部的寻址、序列化、反序列化等,对于调用者往往是没有太大意义的,通过代理,可以提供更加友善的界面。

JDK Proxy的优势:

  • 最小化依赖关系,减少依赖意味着简化开发和维护,JDK本身的支持,可能比cglib更加可靠。
  • 平滑进行JDK版本升级,而字节码类库通常需要进行更新以保证在新版Java上能够使用。
  • 代码实现简单。

基于类似cglib框架的优势:

  • 有的时候调用目标可能不便实现额外接口,从某种角度看,限定调用者实现接口是有些侵入性的实践,类似cglib动态代理就没有这种限制。
  • 只操作我们关心的类,而不必为其他相关类增加工作量。
  • 高性能。

AOP通过(动态)代理机制可以让开发者从这些繁琐事项中抽身出来,大幅度提高了代码的抽象程度和复用度。从逻辑上来说,我们在软件设计和实现中的类似代理,如Facade、Observer等很多设计目的,都可以通过动态代理优雅地实现。

int和Integer差别

int是我们常说的整形数字,是Java的8个原始数据类型(Primitive Types,boolean、byte 、short、char、int、float、double、long)之一。Java语言虽然号称一切都是对象,但原始数据类型是例外。

Integer是int对应的包装类,它有一个int类型的字段存储数据,并且提供了基本操作,比如数学运算、int和字符串之间转换等。在Java 5中,引入了自动装箱和自动拆箱功能(boxing/unboxing),Java可以根据上下文,自动进行转换,极大地简化了相关编程。

关于Integer的值缓存,这涉及Java 5中另一个改进。构建Integer对象的传统方式是直接调用构造器,直接new一个对象。但是根据实践,我们发现大部分数据操作都是集中在有限的、较小的数值范围,因而,在Java 5中新增了静态工厂方法valueOf,在调用它的时候会利用一个缓存机制,带来了明显的性能改进。按照Javadoc,这个值默认缓存是-128到127之间。

对比Vector、ArrayList、LinkedList有何区别?

这三者都是实现集合框架中的List,也就是所谓的有序集合,因此具体功能也比较近似,比如都提供按照位置进行定位、添加或者删除的操作,都提供迭代器以遍历其内容等。但因为具体的设计区别,在行为、性能、线程安全等方面,表现又有很大不同。

Vector是Java早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。

ArrayList是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。与Vector近似,ArrayList也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector在扩容时会提高1倍,而ArrayList则是增加50%。

LinkedList顾名思义是Java提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的

image-20251229101713096

Collection接口是所有集合的根,然后扩展开提供了三大类集合,分别是:

  • List,也就是我们前面介绍最多的有序集合,它提供了方便的访问、插入、删除等操作。
  • Set,Set是不允许重复元素的,这是和List最明显的区别,也就是不存在两个对象equals返回true。我们在日常开发中有很多需要保证元素唯一性的场合。
  • Queue/Deque,则是Java提供的标准队列结构的实现,除了集合的基本功能,它还支持类似先入先出(FIFO, First-in-First-Out)或者后入先出(LIFO,Last-In-First-Out)等特定行为。这里不包括BlockingQueue,因为通常是并发编程场合,所以被放置在并发包里。

Java提供的默认排序算法,具体是什么排序方式以及设计思路等。

这个问题本身就是有点陷阱的意味,因为需要区分是Arrays.sort()还是Collections.sort() (底层是调用Arrays.sort());什么数据类型;多大的数据集(太小的数据集,复杂排序是没必要的,Java会直接进行二分插入排序)等。

  • 对于原始数据类型,目前使用的是所谓双轴快速排序(Dual-Pivot QuickSort),是一种改进的快速排序算法,早期版本是相对传统的快速排序
  • 而对于对象数据类型,目前则是使用TimSort,思想上也是一种归并和二分插入排序(binarySort)结合的优化排序算法。TimSort并不是Java的独创,简单说它的思路是查找数据集中已经排好序的分区(这里叫run),然后合并这些分区来达到排序的目的。

另外,Java 8引入了并行排序算法(直接使用parallelSort方法),这是为了充分利用现代多核处理器的计算能力,底层实现基于fork-join框架(专栏后面会对fork-join进行相对详细的介绍),当处理的数据集比较小的时候,差距不明显,甚至还表现差一点;但是,当数据集增长到数万或百万以上时,提高就非常大了,具体还是取决于处理器和系统环境。

对比Hashtable、HashMap、TreeMap有什么不同?

Hashtable、HashMap、TreeMap都是最常见的一些Map实现,是以键值对的形式存储和操作数据的容器类型。

Hashtable是早期Java类库提供的一个哈希表实现,本身是同步的,不支持null键和值,由于同步导致的性能开销,所以已经很少被推荐使用。

HashMap是应用更加广泛的哈希表实现,行为上大致上与HashTable一致,主要区别在于HashMap不是同步的,支持null键和值等。通常情况下,HashMap进行put或者get操作,可以达到常数时间的性能,所以它是绝大部分利用键值对存取场景的首选,比如,实现一个用户ID和用户信息对应的运行时存储结构。

TreeMap则是基于红黑树的一种提供顺序访问的Map和HashMap不同,它的get、put、remove之类操作都是O(log(n))的时间复杂度,具体顺序可以由指定的Comparator来决定,或者根据键的自然顺序来判断。

image-20251229110255099

Hashtable比较特别,作为类似Vector、Stack的早期集合相关类型,它是扩展了Dictionary类的,类结构上与HashMap之类明显不同。

HashMap等其他Map实现则是都扩展了AbstractMap,里面包含了通用方法抽象。不同Map的用途,从类图结构就能体现出来,设计目的已经体现在不同接口上。

大部分使用Map的场景,通常就是放入、访问或者删除,而对顺序没有特别要求,HashMap在这种情况下基本是最好的选择。HashMap的性能表现非常依赖于哈希码的有效性,请务必掌握hashCode和equals的一些基本约定:

  • equals相等,hashCode一定要相等。
  • 重写了hashCode也要重写equals。
  • hashCode需要保持一致性,状态改变返回的哈希值仍然要一致。
  • equals的对称、反射、传递等特性

HashMap设计与实现是个非常高频的面试题,所以我会在这进行相对详细的源码解读,主要围绕:

  • HashMap内部实现基本点分析。
  • 容量(capacity)和负载系数(load factor)。
  • 树化 。

ashMap内部的结构,它可以看作是数组(Node[] table)和链表结合组成的复合结构,数组被分为一个个桶(bucket),通过哈希值决定了键值对在这个数组的寻址哈希值相同的键值对,则以链表形式存储。这里需要注意的是,如果链表大小超过阈值(TREEIFY_THRESHOLD, 8),图中的链表就会被改造为树形结构。

image-20251229113359998

如何保证集合是线程安全的? ConcurrentHashMap如何实现高效地线程安全?

Java提供了不同层面的线程安全支持。在传统集合框架内部,除了Hashtable等同步容器,还提供了所谓的同步包装器(Synchronized Wrapper),我们可以调用Collections工具类提供的包装方法,来获取一个同步的包装容器(如Collections.synchronizedMap),但是它们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较低下。

另外,更加普遍的选择是利用并发包提供的线程安全容器类,它提供了:

  • 各种并发容器,比如ConcurrentHashMap、CopyOnWriteArrayList。
  • 各种线程安全队列(Queue/Deque),如ArrayBlockingQueue、SynchronousQueue。
  • 各种有序容器的线程安全版本等。

具体保证线程安全的方式,包括有从简单的synchronize方式,到基于更加精细化的,比如基于分离锁实现的ConcurrentHashMap等并发实现等。具体选择要看开发的场景需求,总体来说,并发包内提供的容器通用场景,远优于早期的简单同步实现。

如果要深入思考并回答这个问题及其扩展方面,至少需要:

  • 理解基本的线程安全工具。
  • 理解传统集合框架并发编程中Map存在的问题,清楚简单同步方式的不足。
  • 梳理并发包内,尤其是ConcurrentHashMap采取了哪些方法来提高并发表现。
  • 最好能够掌握ConcurrentHashMap自身的演进,目前的很多分析资料还是基于其早期版本。

Hashtable本身比较低效,因为它的实现基本就是将put、get、size等各种方法加上“synchronized”。简单来说,这就导致了所有并发操作都要竞争同一把锁,一个线程在进行同步操作时,其他线程只能等待,大大降低了并发操作的效率。

前面已经提过HashMap不是线程安全的,并发情况会导致类似CPU占用100%等一些问题,那么能不能利用Collections提供的同步包装器来解决问题呢?

看看下面的代码片段,我们发现同步包装器只是利用输入Map构造了另一个同步版本,所有操作虽然不再声明成为synchronized方法,但是还是利用了“this”作为互斥的mutex,没有真正意义上的改进!

1
2
3
4
5
6
7
8
9
10
private static class SynchronizedMap<K,V>
implements Map<K,V>, Serializable {
private final Map<K,V> m; // Backing Map
final Object mutex; // Object on which to synchronize
// …
public int size() {
synchronized (mutex) {return m.size();}
}
// …
}

所以,Hashtable或者同步包装版本,都只是适合在非高度并发的场景下

早期ConcurrentHashMap,其实现是基于:

  • 分离锁,也就是将内部进行分段(Segment),里面则是HashEntry的数组,和HashMap类似,哈希相同的条目也是以链表形式存放。
  • HashEntry内部使用volatile的value字段来保证可见性,也利用了不可变对象的机制以改进利用Unsafe提供的底层能力,比如volatile access,去直接完成部分操作,以最优化性能,毕竟Unsafe中的很多操作都是JVM intrinsic优化过的。

BlockingQueue

BlockingQueue 是 Java 中的一个接口,它代表了一个线程安全的队列,不仅可以由多个线程并发访问,还添加了等待/通知机制,以便在队列为空时阻塞获取元素的线程,直到队列变得可用,或者在队列满时阻塞插入元素的线程,直到队列变得可用。

阻塞队列(BlockingQueue)被广泛用于“生产者-消费者”问题中,其原因是 BlockingQueue 提供了可阻塞的插入和移除方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止

BlockingQueue 继承了 Queue 接口,因此,BlockingQueue 也具有 Queue 接口的基本操作:

Queue接口提供方法,

插入元素

  1. add 将元素添加到队列尾部,如果队列满了,则抛出异常 IllegalStateException。
  2. offer 将元素添加到队列尾部,如果队列满了,则返回 false

删除元素

  1. boolean remove(Object o):从队列中删除元素,成功返回true,失败返回false
  2. E poll():检索并删除此队列的头部,如果此队列为空,则返回null

查找元素

  1. E element():检索但不删除此队列的头部,如果队列为空时则抛出 NoSuchElementException 异常;
  2. peek():检索但不删除此队列的头部,如果此队列为空,则返回 null.

除了从 Queue 接口 继承到一些方法,BlockingQueue 自身还定义了一些其他的方法,比如说插入操作:

  1. void put(E e):将元素添加到队列尾部,如果队列满了,则线程将阻塞直到有空间。
  2. offer(E e, long timeout, TimeUnit unit):将指定的元素插入此队列中,如果队列满了,则等待指定的时间,直到队列可用。

比如说删除操作:

  1. take()检索并删除此队列的头部,如有必要,则等待直到队列可用
  2. poll(long timeout, TimeUnit unit)检索并删除此队列的头部,如果需要元素变得可用,则等待指定的等待时间。

ArrayBlockingQueue

BlockingQueue 接口的实现类有 ArrayBlockingQueue、DelayQueue、LinkedBlockingDeque、LinkedBlockingQueue、LinkedTransferQueue、PriorityBlockingQueue、SynchronousQueue 等

ArrayBlockingQueue 它是一个基于数组的有界阻塞队列

  • 有界:ArrayBlockingQueue 的大小是在构造时就确定了,并且在之后不能更改。这个界限提供了流量控制,有助于资源的合理使用。
  • FIFO:队列操作符合先进先出的原则。
  • 当队列容量满时,尝试将元素放入队列将导致阻塞;尝试从一个空的队列取出元素也会阻塞。

进阶后端技术

程序如何运行起来的

-------------本文结束感谢您的阅读-------------
感谢阅读.

欢迎关注我的其它发布渠道