参考
《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》 《Java并发编程的艺术》 《Java高级程序员面试笔试宝典》 CyC2018/CS-Notes Snailclimb/JavaGuide
基础
1. 基本类型、包装类型、缓存池
基本类型 大小(位) 包装类型 默认缓存池对象范围 byte 8 Byte -128~127 char 16 Character 0~127 short 16 Short -128~127 int 32 Integer -128~127 float 32 Float 无 long 64 Long -128~127 double 64 Double 无 boolean 1或32(int) Boolean true/false
以Integer为例,当Integer的内部类IntegerCache被加载时,它会创建值为-128~127的256个Integer对象,放入缓存中,当调用Integer.valueOf(...)
方法时,就直接从缓存池中拿对象。 IntegerCache的上界可以调整,通过VM参数-XX:AutoBoxCacheMax=<size>
调用==比较 ==
:基本类型比较值,引用类型比较地址
情况 解释 int a;int b; 都是基本类型,返回值比较的结果 int a;Integer b; 将b拆箱为基本类型,返回值比较的结果 Integer a = 100;Integer b = 100 a和b都是引用类型,比较地址。100在缓存区内,因此a和b都指向Integer(100)这个对象,返回True。(备注:Integer x = 100与Integer x = Integer.valueOf(100)是等价的) Integer a = 1000;Integer b = 1000 引用类型比较地址,1000没在缓存区,都返回新对象,返回False Integer a = 100;Integer b = new Integer(100) new Integer(100)创建了新对象,与缓存区对象是不同的,返回False(备注:new Integer()在Java9中过时)
2. String、StringBuilder、StringBuffer,String Pool
类 字符串内容是否可变 是否线程安全 如何实现线程安全 String 不可变 是 只读,char数组使用final修饰,并且不公开修改char数组的方法
StringBuilder 可变 否 / StringBuffer 可变 是 内部读写方法使用synchronized同步
String不可变的主要好处:线程安全
、实现字符串常量池的需要
、String作为哈希表的key,hashcode不变
、更安全:作为网络连接/IO操作的参数
语句执行时的内存情况
可以在idea的Debug中,观察语句执行时的内存情况
语句 解释 String s = “abc” 在Pool中寻找"abc"对象,存在就把s指向该对象,不存在就新建对象,让s指向它。(即新创建0个或1个String对象) String s = new String(“abc”) 在Pool中寻找"abc"对象,不存在就先在Pool中创建"abc"对象。然后新建1个对象,将char数组指向Pool中的对象的char数组。(即新建(1个String对象,0个char数组)或(2个String对象,1个char数组)) String s = “abc”.intern() 等价于String s = "abc"的执行情况 String s = t.intern() 如果t本来就指向常量池对象,那么将s指向那个常量池对象即(s==t),如果t指向的是堆中的字符串对象,那么在常量池中寻找t的内容的对象,将s指向它
明确执行时的内存情况后,判定就很简单了
情况 解释 String s = “a”;String t = “a” True,s和t都指向常量池的"a"对象 String s = “a”,String t = new String(“a”) False,s指向常量池的"a",t指向堆中的"a"(备注:此时t的char[]指向s的char[],t并没有复制一份字符数据) String x = new String(“a”),String s = x.intern(),String t = “a” True,x指向堆中的"a",x.intern()返回Pool中的"a",t也指向Pool中的"a"
3. 引用传递还是值传递?
对于基本类型,参数传递的是值的拷贝
,修改参数的值,不会引起原值的更变 对于引用类型,参数传递的是地址的拷贝
(地址也是值),修改参数的指向,不会引起原引用的指向的改变,但可以通过该地址访问到堆中的内容,更改堆中对象的数据。
main ( ) {
int i = 1 ;
int [ ] nums = new int [ 1 ] ;
nums[ 0 ] = 1 ;
func ( i, nums) ;
}
void func ( int i , int [ ] nums) {
i = 2 ;
nums[ 0 ] = 2 ;
nums = new int [ 10 ] ;
}
4. ==、equals()、hashCode()
作用 – ==
: 对于基本类型判断值
是否相等,对于引用类型判断地址
是否相等,即是否指向同一个对象。 – equals()
: 如果重写了equals()方法就按照重写的逻辑判断,否则相当于==
,equals表示两个对象内容上是相同的。 – hashCode()
: 返回对象的hash值,Hash表会使用这个方法。 equals()
和hashCode()
一定要同时被重写,因为在使用hash表时,判断两个对象是否相同,需要这两个方法都返回TruehashCode相同的对象,equals()不一定相同
,因为同一个hashCode决定的索引位置可能会放置多个不同的对象,见下文HashMap的笔记。
5. 重写、重载、访问权限
重写与重载 – 重写override父类方法 – 方法重载overwrite,方法名相同,参数等可以不同。 访问权限
修饰符 解释 public 在任何位置都可以访问 protected 在当前包任何位置和子类调用super访问 默认 当前包内访问 private 本类中访问
6. static关键字
修饰内容 解释 静态变量 属于类,通过类访问,内存中只存在1份 静态方法 属于类,直接通过类访问,不依赖于实例 静态语句块 类加载时执行一次 静态内部类 创建时,不依赖于外部类的实例。非静态内部类需要外部类的实例才能创建 静态导包 导包后可以直接在当前文件中使用静态变量和静态方法,不需要书写类名
8. final关键字
修饰内容 解释 final变量 变量值不能修改(基本类型的值不能修改;引用类型的地址不能修改,即不能指向其他对象) final方法 不能被子类重写 final类 不能被继承
9. 反射
每个类都有1个Class对象,可以通过这个对象获取类的字段、方法、构造器
等信息。 通过反射可以访问没有权限的字段、以及调用没有权限的方法。 缺点:性能开销、安全限制、内部暴露。
10. 异常
分为两类:Error和Exception Error
为无法处理的错误,如OutOfMemoryError,StackOverFlowError。Exception
为可以被程序处理的意料中的异常,可以通过try catch处理
11.泛型
参考Java | 关于泛型能问的都在这里了 10 道 Java 泛型面试题
什么是泛型?
在定义类、接口和方法时,可以附带类型参数,使其变成泛型类
、泛型接口
和泛型方法
。
< T> T func ( T t) { }
static < T> T func ( T t) { }
class A < T> {
T t;
T func ( T t) { . . . }
static < T> T func ( T t) { . . . }
}
interface B < T> { . . . }
泛型的好处?
健壮
(在编译时进行更强的类型检查)、简洁
(消除强转,编译后自动会增加强转)、通用
(代码可适用于多种类型).
什么是类型擦除机制?
为了向下兼容,编译器会把泛型信息擦除,在运行时保留类型参数的上界。 类型擦除只是擦除Code 属性中的泛型信息,在类常量池属性中其实还保留着泛型信息,因此可以通过反射获取泛型信息。
类型擦除的步骤?
擦除所有类型参数信息,如果类型参数是有界的,则将每个参数替换为其第一个边界;如果类型参数是无界的,则将其替换为 Object (必要时)插入类型转换,以保持类型安全 (必要时)生成桥接方法以在子类中保留多态性
泛型的限制,类型擦除的影响?
无法对参数为泛型类型的方法进行重载 不能在泛型类中的静态成员类型和静态方法中使用泛型参数类型。 不能创建泛型数组
void func ( List< Integer> list) { . . . }
void func ( List< String> list) { . . . }
class A < T> {
}
List< String> [ ] arr = new List < String> [ ] ;
有哪些类型限定符?
<? extends T>
类型参数接受T的子类,<T extends >
<? super T>
类型参数接受T的父类另外<?>等价于<? extends Object>类型参数接受任意类
< T extends Number > T func ( T t) { . . . }
class A < T extends Number > { . . . }
interface B < T extends Number >
List< ? extends Number > list = new ArrayList < > ( ) ;
void printNumber ( List< ? extends Number > list) { . . . } ;
printNumber ( Arrays. asList ( 1 , 2 ) ) ;
printNumber ( Arrays. asList ( 1.0 , 2.0 ) ) ;
12.IO
IO类型 解释 应用场景 BIO 阻塞IO,处理流数据 低负载、低并发 NIO 非阻塞IO,处理块数据 高负载、高并发 AIO 异步非阻塞IO 不阻塞线程,异步回调
容器
1. ArrayList
https://blog.csdn.net/weixin_42818402/article/details/106235128
默认容量为10
,添加数据时,如果容量不够,会触发扩容方法grow(minCapacity)
。 扩容时,默认扩容为之前的1.5倍
,如果这个1.5倍还小于minCapacity,那么就直接扩容到minCapacity
。扩容时,新建数组,然后把旧数据拷贝进去。
elementData是用transient
修饰的,表明这个对象不会被自动序列化,因为elementData是一个缓冲区,真正的数据部分只占其中的一部分,不需要对其全部进行序列化。 java在writeObject方法中,手动控制了只序列化elementData中有数据的部分。
如果已知ArrayList需要的大致长度,可以在创建对象时指定容量。 如果需要大量add/addAll操作,那么可以先使用ensureCapacity方法进行手动扩容,避免多次扩容带来计算开销。
2. HashMap
https://blog.csdn.net/weixin_42818402/article/details/106628340
Java8之前HashMap的底层实现是数组+链表
,Java8之后是数组+链表+红黑树
查找 计算key的hash值,对长度取模,得到数组索引,然后沿着链表查找。 扩容
默认数组长度capacity为16,当键值对数量size>threshold(threshold=loadFactor*capacity)时,需要进行扩容操作,扩容为之前的2倍。 loadFactor为什么是0.75?loadFactor小,扩容越频繁,空间消耗大,hash碰撞几率小,查找速度快;loadFactor大,扩容不频繁,空间消耗小,hash碰撞几率大,查找速度慢。因此loadFactor允许使用者在空间和时间
上做权衡. Java8中,当链表长度大于建树阈值(默认为8后就转换为红黑树)
3. ConcurrentHashMap
底层数据结构与HashMap相同,采用分段锁
(Segment),每个分段锁维护着几个桶,多个线程可以同时访问不同分段锁上的桶,并发度为锁的个数,默认为16 size操作:每个segment维护一个count变量记录个数,size操作时,首先不加锁,遍历所有segment把count加起来,两次这个操作,如果结果一致,就返回,不一致就对每个segment加锁获取count Java7中的Segment继承自ReentrantLock
实现锁机制,Java8中使用CAS
实现锁机制,CAS
失败时使用synchronized
。
4. 其他
容器 数据结构 是否线程安全 重点 备注 Vector 数组 是 使用synchronized实现线程安全,每次扩容为之前的2倍 已弃用,list实现线程安全可以使用Collections.synchronizedList(list)
或使用CopyOnWriteArrayList
CopyOnWriteArrayList 数组 是 读写分离
,写操作在一个复制的数组上加锁写入,写完后复制到原数组,读操作不用加锁,但会引发读不到最新数据问题使用synchronized实现锁 Stack 数组 是 继承自Vector,提供栈相关方法 已弃用,可以使用Deque实现Stack,Deque<Integer> stack = new LinkedList<>()
PriorityQueue 数组 否 实现队列接口Queue,是最大堆和最小堆的实现 LinkedList 双向链表(双向队列) 否 实现了双向队列Deque接口 HashTable 数组+链表 是 synchronized实现全局锁,开销较大 已弃用,被ConcurrentHashMap替代 LinkedHashMap HashMap+双向链表 否 维护了插入顺序,遍历时按照插入顺序遍历 可以用来实现LRU缓存,见算法总结 TreeMap 红黑树 否 可以实现自定义排序,决定插入位置 注意查找时间为O(logn)比HashMap慢 HashSet HashMap 否 HashSet中的每个元素被包裹成Entry放入HashMap中,全部元素共用一个值
JVM
1. Java内存模型
【JVM】(1)Java内存区域
介绍一下Java内存区域以及各块区域存储的内容。 Java内存分为运行时内存和直接内存,运行时内存分为线程共享内存和线程私有内存。线程共享内存是堆内存,主要存放对象的实例,是垃圾回收器工作的区域,在Java8之前方法区位于堆中,方法区存放常量和静态变量等信息,Java8之后,方法区称为元空间放入直接内存。线程私有内存包括虚拟机栈、本地方法栈和程序计数器,虚拟机栈存放栈帧,栈帧中存放局部变量表,本地方法栈执行Native方法,程序计数器存放下一条字节码指令。
2. 垃圾收集
【JVM】(2)垃圾收集
垃圾回收器如何判断对象没有引用? 两种方法:引用计数法和可达性分析算法。引用分析法给对象添加一个引用计数,当存在一个引用时,计数加1,当计数为0时,就需要被回收,这种方法不能解决循环引用问题。可达性分析算法以CG Root为起点进行遍历,没有遍历到的对象就是需要被回收的,JVM采用这种方法。 有哪些引用类型? 强引用,指针为空时,被回收;软引用,发生OOM时,被回收;弱引用,GC时被回收;虚引用,无法得到对象实例。 堆为什么分为新生代和老年代? 对象首先在新生代分配,然后经过多次GC,没有被回收的对象逐渐进入老年代。划分区域后,可以指定回收的区域,比如MinorGC回收新生代,速度较快,性价比更高,FullGC回收整堆,速度较慢,性价比较低。 有哪些垃圾收集算法? 标记-清除算法:先标记需要回收的对象,再清除,会造成碎片 标记-复制算法:将内存分为两块,只使用其中一块,先标记需要回收的对象,然后复制到空白区域,再将另一块全部清空,浪费一半的内存 标记-整理算法:将存活的对象移动到内存的一端,然后清除掉边界以外的内存。耗时。 介绍一下G1收集器 G1把Java堆划分为多个大小相等的独立区域(Region),每个Region都可以扮演新生代的Eden空间、Survivor空间、老年代空间。经过初始标记,并发标记,最终标记,筛选回收四个阶段。优点是:不会产生碎片,以及可以指定最大停顿时间,在这个停顿时间内获取最大的收益。 什么时候出现Full GC? 调用System.gc()(不一定出现),老年代空间不足。
3. 类文件结构与加载机制
类加载的过程是怎样的? 首先读取class文件,然后验证文件正确性,然后将static变量赋0值,准备常量值,最后初始化static变量和static代码块。 类什么时候开始初始化? 只有使用到这个类的时候才开始初始化,比如new 对象,访问static变量和方法,反射调用等 有哪些类加载器? 启动类加载器、扩展类加载器、应用程序类加载器。 介绍一下双亲委派模型? 类加载的时候,首先会把该请求委派该父类加载器的 LoadCLass()处理,因此所有的请求最终都应该传送到顶层的启动类加载器Bootstrap CLassLoader中当父类加载器无处理时,才由自己来处理。这反映了一种优先层级关系。
4. JVM性能监控、调优、实践
参数 功能 -Xms2G,-Xmx5G 设置堆内存最小2G,最大5G -XX:NewSize=256M,-XX:MaxNewSize=1024M 设置新生代最小256M,最大1024M -Xmn256M 设置新生代大小固定为256M -XX:NewRatio=1 新生代与老年代内存大小比值为1 -XX:MetaspaceSize=1G,-XX:MaxMetaspace=2G 元空间最小1G,最大2G -XX:SurvivorRatio=2 设置两个survivor区和eden区的比例为1:1:8 -Xss100K 设置栈的大小 -XX:+UseG1GC 使用G1垃圾回收器 -Xloggc:/path/to/gc.log 记录gc到log文件 -XX:+printGC 打印GC情况
名称 功能 jps 查看本地java进程 jstat 运行数据 jinfo 配置信息 jstack 线程信息
并发
1. 多线程基础
【Java并发】(1)多线程基础
线程与进程的区别? 进程是系统运行程序的基本单位,线程是进程创建的更小的执行单位。一个JVM进程可以创建多个线程,共享进程中的堆和直接内存的资源,每个线程也有自己的线程私有内存区域。线程也是轻量级进程,线程切换工作时负担更小一些。 使用多线程有哪些好处与问题? 提高并发量,提高IO效率,提升多核CPU的利用率。 线程安全问题,上下文切换问题,死锁问题。 并发与并行的区别? 并发:多个任务的指令交替执行,但在一个时刻只有1条指令在执行 并行:多条指令同时执行,如多核CPU、超线程技术同时执行多条指令 线程有哪些状态? – 线程被构建后进入初始
状态 – 调用start()后进入运行
状态 – 如果需要进入同步代码块,但锁没有释放,就进入阻塞
状态,获取到锁后进入运行状态 – 调用wait(),进入等待
状态,其他线程notify()通知后进入运行状态 – 调用sleep(millis),wait(millis)进入超时等待
状态,时间到后返回运行状态,未释放锁。 – 执行完毕或异常,进入终止
状态。
2. 线程与线程池
【Java并发】(2)使用线程和线程池
创建线程有哪些方法? 实现Runnable接口;实现Callable接口,使用get()获取返回值;继承Thread类。 Thread类的方法
方法 解释 sleep(millis) 休眠,让出CPU占用,不释放锁 yield() 通知调度器当前线程需要让出cpu占用,但调度器也有可能忽略这个通知 start() 调用Native方法start0(),启动新线程 run() 直接对run()的调用不会产生新线程 interrupt() 中断该线程,如果线程处于阻塞、等待、超时等待状态,会抛出InterruptedException join() 让其他线程先执行,执行完毕后再执行本线程
Object类的方法
方法 解释 wait() 进入等待状态释放锁 notify() 通知随机一个等待的线程进入运行状态 notifyAll() 通知所有等待的线程进入运行状态
线程池有哪些好处? 重复利用已经创建的线程,降低资源消耗;可以对池中的线程进行管理分配和监控。 解释一下线程池的构造方法? ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
参数 解释 corePoolSize 核心线程数 maximumPoolSize 最大线程数 keepAliveTime 非核心线程空闲后的存活时间 workQueue 等待处理的任务队列 handler 饱和处理策略
介绍线程池的工作流程 向线程池提交任务时,首先判断当前工作线程数是否达到核心线程数,没达到就直接创建Worker线程,执行任务;否则判断等待队列是否满了,没满就加入队列,否则判断当前线程数是否已经达到最大线程数,没达到就创建Worker线程执行任务,否则执行饱和策略。 饱和策略有哪几种? 4种:抛出异常、在调用线程执行、忽略、丢弃队列头部的任务。另外还可以自定义策略。 为什么最好不使用Executors创建线程池? FixedThreadPool和SingleThreadExecutor使用无界队列,CachedThreadPool允许最大线程为Integer.MAX_VALUE,可能会造成OOM ScheduledThreadPoolExecutor和Timer的区别? 都可以定期执行任务,Timer单线程,ScheduledThreadPool可以创建多个线程。 怎样确认线程池的大小? 线程池大小与CPU核心数和任务性质有关,CPU密集型任务,线程池大小为N+1,IO密集型任务,线程池大小为2N。
3. 线程安全
什么是线程安全?线程安全的需要满足的三大特性? – 线程安全就是在多线程情况下,对共享内存的使用,不会因为不同线程的访问和修改而发生不期望的情况。 – 原子性
:一个或多个指令要么全部执行要么不执行,不会被其他线程中断。 – 可见性
:一个线程修改了共享变量,其他线程会丢弃旧数据,读取新数据。 – 有序性
:一个线程内部的指令重排列不会对另一个线程造成影响。 有哪些实现线程安全的方式? – 不可变
。不可变的对象,如final基本类型,String,不可变集合等,在所有线程的读取中都是一致的。 – 互斥同步
。synchronized和ReentrantLock加锁实现。 – 非阻塞同步
。使用CAS乐观锁。如Atomic原子类。 – 无同步方案
。栈封闭,局部变量在栈帧中,不共享,不存在线程安全问题;可重入代码,不访问堆上的共享数据;线程本地存储ThreadLocal。 谈一下对于synchronized关键字的了解。 synchronized控制多线程对于共享资源的互斥访问,保证被它修饰的方法或代码块在同一时刻只有1个线程执行,如果1个线程需要进入同步方法或同步代码块需要先获取对象的锁。 synchronized在项目中有哪些使用? 比如单例模式 synchronized有哪些使用方式? 修饰实例方法,需要获取实例对象的锁;修饰静态方法,需要获取class对象的锁;修饰代码块,需要获取指定对象的锁。 JDK1.6之后,JVM对synchronized关键字做了哪些优化?
优化 解释 自旋锁 线程没有获取到锁时,不进入阻塞状态,而是进入忙循环一段时间,直到获取锁 锁消除 对检测出来不可能存在多线程竞争的共享数据进行锁的消除 锁粗化 如果一系列操作需要反复加锁解锁,那么可以将加锁解锁的范围扩大,减少重复操作 轻量级锁 偏向锁
谈一下对于volatile关键字的了解。 volatile可以满足轻量级的线程安全,保证可见性和有序性,不保证原子性。 volatile通过禁止指令重排列实现有序性,volatile前面的指令一定先于后面的指令执行,但前后的指令内部可能会重排列。 volatile通过内存屏障实现可见性,volatile变量的修改会立即更新到主存,读取会重新从主存中读取。 谈一下对于ThreadLocal的理解。 Java ThreadLocal的演化、实现和场景(转) ThreadLocal是线程本地变量,多线程访问共享数据的时候,可以将共享数据的副本放入每个线程的私有变量中,每个线程通过ThreadLocal实例读取到的其实是自己线程内部的ThreadLocalMap成员变量中的数据,这也是线程安全的一种解决方案,没有共享变量就不需要进行线程控制,也就没有线程安全问题。 举例说明ThreadLocal类的使用场景。 比如SimpleDateFormat,这是线程不安全的。想实现线程安全,有3种方法 : 1. 加锁 ; 2. 每个线程内部创建一个SimpleDateFormat实例;3. 使用ThreadLocal。加锁需要线程控制,有时间开销;二三两种方法实际上是等价的,用空间换时间。但使用ThreadLocal可以简化对象的创建以及相关功能的封装。
public class DateUtil {
private static final ThreadLocal< SimpleDateFormat> format = ThreadLocal. withInitial ( ( ) - > new SimpleDateFormat ( "yyyy-MM-dd HH:mm:ss" ) ) ;
public static String formatDate ( Date date) {
return format. get ( ) . format ( date) ;
}
}
谈一下ThreadLocal的get和set方法的流程。 当调用get()或set()时,ThreadLocal对象会首先得到当前线程的ThreadLocalMap对象,这是一个哈希表,然后将ThreadLocal作为key,从中查找。解决Hash冲突的方法为线性探测法。 ThreadLocal会产生什么问题? 面试:为了进阿里,死磕了ThreadLocal内存泄露原因 内存泄露。 – 由于ThreadLocalMap中的key对于ThreadLocal是弱引用,如果外部没有对ThreadLocal的强引用,那么ThreadLocal会被GC掉,这样ThreadLocalMap中的key为空,value就不能被回收掉了。解决方案是将ThreadLocal用private static修饰,这样外部始终存在强引用。 – 如果key使用强引用,那么外部没有对ThreadLocal的强引用时,ThreadLocal和Entry都不会被回收掉,也会造成内存泄露。解决方案是先remove掉ThreadLocalMap中的键值对,然后断开强引用。
4. JUC框架
介绍一下Atomic原子类。 并发的核心:CAS 是什么?Java8是如何优化 CAS 的? Atomic原子类使用volatile和CAS来保证类中的每个方法都是线程安全的,比如AtomicInteger中的Integer使用volatile修饰,可以保证有序性和可见性,其中的每个方法使用CAS乐观锁保证操作的原子性。 CAS是乐观锁的一种实现,首先读取值,然后通过一条原子的CAS指令将值设定为新值,这条CAS指令首先比较目标值是否为读取的值,是的话就设置为新值,不是的话就继续读取值,重复这个过程,直到设置成功。 CAS会产生什么问题? ABA问题,如果一个线程读取值后,另一个线程将值修改后又改回了原值,这样之前那个线程的CAS操作是成功的,因为值没有变化。解决这个问题可以使用AtomicStampedReference进行版本控制。 谈一下关于AQS的理解。 我画了35张图就是为了让你深入 AQS AQS是抽象队列同步器,是一个抽象类,JUC包中的很多类都是基于AQS来实现的,AQS中维护了一个state变量和一个线程等待队列。state代表共享资源状态,state=0表示资源空闲,state不为0表示资源被占用。当线程需要获取锁时,首先通过CAS操作将state置为1,成功后就获取到锁,失败后就加入等待队列。当线程释放锁时,会通知唤醒队列中的线程,这些线程继续通过CAS操作获取锁。 JUC框架的使用 【Java并发】(4)JUC框架
类 解释 主要方法 ReentrantLock 可重入锁,控制同步代码块访问,可以实现公平锁和非公平锁 加锁lock(),解锁unlock() Condition 监视器,可以有多个监视器,可以唤醒指定的1个或多个线程,每个监视器有1条等待队列,存放等待的线程 等待await(),唤醒signal(),唤醒全部signalAll() CountDownLatch 一个或多个线程等待其他线程完成操作,类似join(),比join()控制粒度更高,join需要等待其他线程执行完毕,latch只需要监控计数器为0 await():阻塞线程,等待计数器归0;countDown()计数器减1 CyclicBarrier 一组线程到达屏障时被阻塞,直到最后一个线程到达屏障时,所有线程都被唤醒 await():表示线程到达屏障,reset():重置,后可以继续使用 Semaphore Semaphore是流量控制工具,控制某一代码块同时可以被多少线程同时访问,比线程池的控制更精细化 acquire()获取访问权,release()释放访问权 Exchanger 线程数据交换 exchange():第一个线程阻塞,第二个线程调用时交换
CountDownLatch比join好在哪? CountDownLatch与join方法功能类似,但比join方法控制能力更高,join只能等待其他线程完成其全部工作后,才能返回。而CountDownLatch的计数器只监控数值是否为0,也就是说,该数值与线程数无关,一个线程可以调用countDown()方法多次。例如一个线程包含多个任务,我们只需要等待其完成一个任务即可,使用join只能等待该线程完成全部任务。 Semaphore和线程池有什么区别? 线程池控制的是线程级别的同时并发数量,而Semaphore控制的是代码块级别的同时并发数量,细粒度更高,如一个线程可能同时需要进行数据库操作和文件操作等,就需要Semaphore来精细控制。