Java面试手册——高频问题总结(二)

这里将Java集合和垃圾回收的知识总结,放到(二)中。对Java平台的理解、Java基础知识、面向对象请参考Java面试手册——高频问题总结(一)

Java高频问题面试:

序号文章
1Java面试手册——高频问题总结(一)
2Java面试手册——高频问题总结(二)
3Java基础面试突击
4Java虚拟机——JVM总结
5JVM面试突击

文章目录

四、Java集合

1. Java集合框架的基础接口有哪些?

Collection为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。Java平台不提供这个接口任何直接实现。

Set是一个不能包含重复元素的集合。

List是一个有序集合,可以包含重复元素。可以通过它的索引来访问任何元素。List更像长度动态变换的数组。

Map是一个将key映射到value的对象。一个Map不能包含重复的key:每个key最多只能映射一个value。

一些其它的接口有Queue、Dequeue、SortedSet、SortedMap 和 ListIteator。

2. Collection 和 Collections 有什么区别?

Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如List、Set 等。

Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供排序方法:Collections.sort(list)。

3. List、Set、Map是否继承自Collection接口?

List,Set是,Map不是。Map是键值对映射容器,与List和Set有明显的区别,而Set存储的零散的元素且不允许有重复的元素,List是线性结构的容器,适用于按数值索引访问元素的情况。

4. HashMap 和 HashTable 的区别?

HashMap 和 HashTable 都实现了 Map 接口,很多特性相似。不同点如下:

  1. HashMap 允许 key 和 value 为 null,而 HashTable 不允许
  2. HashTable 是同步的,而 HashMap 不是,所以HashMap适合单线程环境,HashTable适合多线程环境。
  3. 在Java1.4 中引入了LinkedHashMap,HashMap 的一个子类,想要遍历顺序,很容易从HashMap 转向 LinkedHashMap,但是HashTable的顺序是不可预知的。
  4. HashMap提供对key的Set进行遍历,因为它是fail-fast 的,但HashTable 提供对key的Enumeration 进行遍历,它不支持fail-fast。
  5. HashTable 被认为是个遗留的类,如果寻求在迭代的时候修改Map, 应该使用ConcurrentHashMap。

HashTable同步,HashMap异步:同步指多线程安全,异步指多线程不安全。在Hashtable中,所有的public方法都加上了synchronized,所以说Hashtable是同步的,而HashMap没有,所以HashMap是异步的。

详细请参考:对比HashTable、HashMap、TreeMap有什么不同?

5. ArrayList、Vector 和 LinkedList 的区别是什么?ArrayList 是否会越界?

ArrayList 和 LinkedList的区别

  1. 数据结构实现: ArrayList是动态数组的数据结构实现,而LinkedList 是双向链表的数据结构实现。
  2. 随机访问效率: ArrayList 比 LinkedList在随机访问的时候效率要高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
  3. 增加和删除效率: 在非首尾的增加和删除操作,LinkedList要比ArrayList效率要高,因为ArrayList增删操作要影响数组内的其他数据的下标。

综合来说,在需要频繁读取集合中的元素时,更推荐使用ArrayList,而在插入和删除操作较多时,更推荐使用LinkedList。

ArrayList 和 Vector 的异同

ArrayList和Vector在很多时候都很类似。

  1. 两者都是基于索引的,内部由一个数组支持。
  2. 两者维护插入的顺序,我们可以根据插入顺序来获取元素。
  3. ArrayList和Vector的迭代器实现都是fail-fast的。
  4. ArrayList和Vector两者允许null值,也可以使用索引值对元素进行随机访问。

以下是ArrayList和Vector的不同点。

  1. Vector是同步的,而ArrayList不是。然而,如果你寻求在迭代的时候对列表进行改变,你应该使用CopyOnWriteArrayList。
  2. ArrayList比Vector快,它因为有同步,不会过载。
  3. ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表。

详细请参考:对比Vector、ArrayList、LinkedList有何区别?

ArrayList 并发 add() 可能出现数组下标越界异常。

6. Array 和 ArrayList 区别?
  1. Array 可以存储基本数据类型和对象,ArrayList只能存储对象。
  2. Array 是指定固定大小,而ArrayList 大小是自动扩展的。
  3. Array 内置方法没有ArrayList 多,比如 addAll、removeAll、iteration等方法只有ArrayList有。
7. HashMap 和 ConcurrentHashMap 的区别?

HashMap是线程不安全的,put时在多线程情况下,会形成环从而导致死循环。CoucurrentHashMap 是线程安全的,采用分段锁机制,减少锁的粒度。

8. HashMap的内部具体如何实现(高频问题)?

从结构上来讲,HashMap 是数组+链表+红黑树 (JDK1.8增加了红黑树部分)实现的,如下图所示:
在这里插入图片描述两个问题:数据底层具体存储的是什么?这样的存储方式有什么优点?

(1)HashMap类中有一个非常重要的字段就是Node[] table, 即哈希桶数组,明显它是一个Node数组。看一下Node[JDK1.8]:
在这里插入图片描述Node是HashMap的一个内部类,实现了Map.Entry接口,本质上就是一个映射(键值对)。上图中每个黑色圆点就是一个Node对象。

(2)HashMap就是使用哈希表来存储的。哈希表为解决哈希冲突,Java中HashMap采用链地址法。

如果哈希桶数组很大,即使较差的Hash算法也会比较分散,如果哈希桶数组数组很小,即使好的Hash算法也会出现较多碰撞,所以就需要在空间成本和时间成本之间权衡,其实就是在根据实际情况确定哈希桶数组的大小,并在此基础上设计好的hash算法减少Hash碰撞。那么通过什么方式来控制map使得Hash碰撞的概率又小,哈希桶数组(Node] table)占用空间又少呢?答案就是好的Hash算法和扩容机制

9. HashMap 底层,负载因子,为啥是2^n?

负载因子默认是0.75,2^n 是为了让散列更加均匀,例如出现极端情况都散列在数组中的一个下标,那么HashMap会由 O(1) 复杂度退化为 O(n) 的。

10. ConcurrentHashMap的原理是什么?ConcurrentHashMap 锁加在了什么地方?

(1)ConcurrentHashMap的原理是什么?
ConcurrentHashMap类中包含两个静态内部类HashEntrySegment

HashEntry 用来封装映射表的键/值对Segment用来充当锁的角色,每个Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个HashEntry对象链接起来的链表。一个ConcurrentHashMap 实例中包含由若干个Segment对象组成的数组。HashEntry 用来封装散列映射表中的键值对。在HashEntry类中, key,hash 和 next域都被声明为final 型,value域被声明为 volatile型

在ConcurrentHashMap中,在散列时如果产生“碰撞”,将采用“分离链接法”来处理“碰撞”:把“碰撞”的 HashEntry对象链接成一个链表。由于HashEntry的next域为final 型,所以新节点只能在链表的表头处插入。下图是在一个空桶中依次插入A,B,C三个HashEntry对象后的结构图:

插入三个节点后桶的结构示意图:
在这里插入图片描述
注意:由于只能在表头插入,所以链表中节点的顺序和插入的顺序相反。

Segment类继承于ReentrantLock 类,从而使得Segment对象能充当锁的角色。每个Segment 对象用来守护其(成员对象 table 中)包含的若干个桶。

(2)ConcurrentHashMap 锁加在了什么地方?

加在每个 Segment 上面。

(3)ConcurrentHashMap有什么优势,1.7,1.8区别?

Concurrenthashmap线程安全的,1.7是在 jdk1.7中采用Segment + HashEntry的方式进行实现的,lock 加在Segment上面。1.7size计算是先采用不加锁的方式,连续计算元素的个数,最多计算3次:

  1. 如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;
  2. 如果前后两次计算结果都不同,则给每个Segment进行加锁,再计算一次元素的个数;

1.8 中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS+ Synchronized来保证并发安全进行实现,1.8中使用一个volatile类型的变量baseCount记录元素的个数,当插入新数据或则删除数据时,会通过addCount()方法更新baseCount,通过累加baseCount和CounterCell数组中的数量,即可得到元素的总个数。

11. TreeMap 底层,红黑树原理?

TreeMap 的实现就是红黑树数据结构,也就说是一棵自平衡的排序二叉树,这样就可以保证当需要快速检索指定节点。

红黑树的插入、删除、遍历时间复杂度都为0(log N),所以性能上低于哈希表。但是哈希表无法提供键值对的有序输出,红黑树因为是排序插入的,可以按照键的值的大小有序输出。

红黑树性质:

  1. 性质1:每个节点要么是红色,要么是黑色。
  2. 性质2:根节点永远是黑色的。
  3. 性质3:所有的叶节点都是空节点(即null),并且是黑色的。
  4. 性质4:每个红色节点的两个子节点都是黑色。(从每个叶子到根的路在上个会有网个连续的红色节点)。
  5. 性质5:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。
12. 什么是迭代器?Iterator 和 ListIterator 的区别是什么?

什么是迭代器?

Iterator 提供了统一遍历操作集合元素的统一接口, Collection接口实现Iterable接口。

每个集合都通过实现Iterable接口中iterator()方法返回Iterator接口的实例,然后对集合的元素进行迭代操作。

有一点需要注意的是:在迭代元素的时候不能通过集合的方法删除元素,否则会抛出ConcurrentModificationException异常.但是可以通过Iterator接口中的remove()方法进行删除。

lterator和 ListIterator的区别是:

  1. Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。
  2. lterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
  3. ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。
13. HashSet 的实现原理?

HashSet 是基于HashMap实现的,HashSet的底层使用HashMap来保存所有元素,因为HashSet的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成的,HashSet不允许有重复的值。

14. 快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?

Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。

java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的

快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。

15. HashMap操作注意事项以及优化?
  1. 扩容是一个特别耗性能的操作、所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。
  2. 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。
  3. HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap
  4. JDK1.8引入红黑树大程度优化了HashMap的性能。

五、 JVM 和 垃圾回收 GC

1. JVM、JDK和JRE的关系

Java面试手册——高频问题总结(一)中JVM、JDK和JRE的关系。

2. JVM回收算法和回收器,CMS采用哪种回收算法,怎么解决内存碎片问题?

Java虚拟机——JVM总结中垃圾回收算法。

3. 类加载过程

Java虚拟机——JVM总结中类加载过程。

4. JVM分区,JVM内存结构

Java虚拟机——JVM总结中JVM分区,JVM内存结构。

5. Java虚拟机的作用?

解释运行字节码程序,消除平台相关性。

JVM将Java字节码解释为具体平台的具体指令。一般的高级语言如要在不同的平台上运行,至少需要编译成不同的目标代码。而引入JVM后,Java语言在不同平台上运行时不需要重新编译。Java语言使用模式Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。

假设一个场景,要求stop the world时间非常短,你会怎么设计垃圾回收机制?

绝大多数新创建的对象分配在Eden 区。

在Eden区发生一次GC后,存活的对象移到其中一个Survivor 区。

在Eden区发生一次GC后,对象是存放到Survivor区,这个Survivor区已经存在其他存活的对象。

一旦一个Survivor区已满,存活的对象移动到另外一个Survivor区。然后之前那个空间已满Survivor区将置为空,没有任何数据。

经过重复多次这样的步骤后依旧存活的对象将被移到老年代。

6. GC如何判断对象需要被回收?

即使在可达性分析算法中不可达的对象,也并非是“非回收不可”的, 这时候它们暂时处于“等待”阶段,要真正宣告一个对象回收,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。(即意味着直接回收)

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue 的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue 队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。

finalize()方法是对象逃脱回收的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中跳出回收一一只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

7. JVM内存模型是什么?

Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。

本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。其关系模型图如下图所示:

在这里插入图片描述

8. JVM是如何实现线程的?

线程是比进程更轻量级的调度执行单位。线程可以把一个进程的资源分配和执行调度分开。一个进程里可以启动多条线程,各个线程可共享该进程的资源(内存地址,文件I0等),又可以独立调度。线程是CPU调度的基本单位。

主流OS都提供线程实现。Java语言提供对线程操作的同一API,每个已经执行start(),且还未结束的java.lang.Thread类的实例,代表了一个线程。

Thread类的关键方法,都声明为Native。这意味着这个方法无法或没有使用平台无关的手段来实现,也可能是为了执行效率。

实现线程的方式:

使用内核线程实现,内核线程(Kernel-Level Thread, KLT)就是直接由操作系统内核支持的线程。

9. JVM最大内存限制是多少?

(1) 堆内存分配

JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、一Xmx相等以避免在每次GC后调整堆的大小。

(2) 非堆内存分配

JVM使用-XX:PermSize 设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。

(3) VM最大内存

首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit 以上的处理器就不会有限制了。

10. 描述一下JVM加载class文件的原理机制?

JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader是一个重要的Java运行时系统组件。它负责在运行时查找和装入类文件的类。

Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。

11. 除了哪个区域外,虚拟机内存其它运行区域都会发生OutOfMemoryError? 什么情况下会出现堆内存溢出?

程序计数器。

堆内存存储对象的实例。我们只要不断的创建对象,并保证gc roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大,堆容量限制后,产生内存溢出异常。

空间什么情况下会抛出OutOfMemoryError?

如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError。

12. Java中内存泄漏是什么?什么时候会出现内存泄漏?

Java中的内存泄漏,广义并通俗的说:不再会被使用的对象的内存不能被回收,就是内存泄漏。

如果长生命周期的对象持有短生命周期的引用,就可能出现内存泄漏。

13. 垃圾回收器的原理是什么?

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当GC确定一些对象为”不可达”时,GC就有责任回收这些内存空间。可以。程序员可以手动执行System.gc(),通知GC运行,但是Java 语言规范并不保证GC一定会执行。

六、Java 多线程

1. 什么是进程?什么是线程?

进程是系统中正在运行的一个程序,程序一旦运行就是进程。

进程可以看成程序执行的一个实例。进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

2. 线程的实现方式?创建线程的方法?

创建线程有如下三种方式:

一、继承Thread类创建线程类

  • (1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
  • (2)创建Thread子类的实例,即创建了线程对象。
  • (3)调用线程对象的start()方法来启动该线程。

二、通过Runnable接口创建线程类

  • (1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  • (2)创建Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  • (3)调用线程对象的start()方法来启动该线程。

三、通过Callable和 Future创建线程

  • (1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

  • (2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

  • (3)使用FutureTask对象作为Thread对象的target创建并启动新线程。(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

3. Thread类中的 start() 和 run() 方法有什么区别?启动一个线程是用哪个方法?

start() 和 run() 方法的区别:

  1. start ()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程,这时此线程是处于就绪状态,并没有运行。然后通过此Thread类调用方法run()来完成其运行操作的,这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束,此线程终止。然后CPU再调度其它线程。
  2. run ()方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码;程序中只有主线程——这一个线程,其程序执行路径还是只有一条,这样就没有达到写线程的目的。

启动一个线程是调用 start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。run() 方法可以产生必须退出的标志来停止一个线程。

4. 解释下线程的几种可用状态?

1.新建( new ) : 新创建了一个线程对象。

2.可运行( runnable ) : 线程对象创建后,其他线程(比如 main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu的使用权。

3.运行( running ) : 可运行状态( runnable )的线程获得了cpu时间片( timeslice ) ,执行程序代码。

4.阻塞( block ) : 阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu timeslice ,暂时停止运行。直到线程进入可运行( runnable )状态,才有机会再次获得cputimeslice转到运行( running )状态。阻塞的情况分三种:

  • (一).等待阻塞:运行( running )的线程执行o . wait()方法,JVM 会把该线程放入等待队列( waitting queue )中。
  • (二).同步阻塞:运行(running )的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池( lock pool )中。
  • (三).其他阻塞:运行( running )的线程执行Thread . sleep ( long ms )或t . join()方法,或者发出了I/0请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者Ⅰ/0处理完毕时,线程重新转入可运行(runnable)状态。

5.死亡( dead ) : 线程run ()、 main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

5. 多线程同步的方法?

可以使用synchronized、lock、volatile和ThreadLocal来实现同步。

6. 如何在两个线程间共享数据?

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。实现Runnable接口或callable接口,适合多个相同或不同的程序代码的线程去共享同一个资源。

多个线程共享数据分两种情况:

  1. 如果多个线程执行同一个Runnable实现类中的代码,此时共享的数据放在Runnable实现类中;
  2. 如果多个线程执行不同的Runnable实现类中的代码,此时共享数据和操作共享数据的方法封装到一个对象中,在不同的Runnable实现类中调用操作共享数据的方法。
7. 如何线程安全的实现一个计数器?

可以使用加锁, 比如 synchronized 或者 lock。 也可以使用 Concurrent 包下的原子类。

8. 线程池的种类?运行流程,参数,策略?线程池有什么好处?

线程池的种类:

1、newFixedThreadPool 创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

2、newCachedThreadPool 创建一个可缓存的线程池。这种类型的线程池特点是:

  • 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
  • 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。

3、newSingleThreadExecutor 创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行(我觉得这点是它的特色)。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

4、newScheduleThreadPool 创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。(这种线程池原理暂还没完全了解透彻)

线程池的运行流程,参数,策略:

线程池主要就是指定线程池核心线程数大小,最大线程数,存储的队列,拒绝策略,空闲线程存活时长。当需要任务大于核心线程数时候,就开始把任务往存储任务的队列里,当存储队列满了的话,就开始增加线程池创建的线程数量,如果当线程数量也达到了最大,就开始执行拒绝策略,比如说记录日志,直接丢弃,或者丢弃最老的任务。

线程池的好处:

第一:降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能执行。

第三:提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

9. 讲一下AQS?

AQS其实就是一个可以给我们实现锁的框架

内部实现的关键是:先进先出的队列、state状态定义了内部类ConditionObject

拥有两种线程模式独占模式和共享模式。

在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建,一般我们叫 AQS为同步器。

10. 如何理解Java多线程回调的方法?

所谓回调,就是客户程序C调用服务程序S中的某个方法A,然后S又在某个时候反过来调用C中的某个方法B,对于C来说,这个B便叫做回调方法。

11. 同步方法和同步代码块的区别?

区别:

同步方法默认用 this 或者当前类 class 对象作为锁;

同步代码块可以选择以什么来加锁,比同步方法要更细粒度,我们可以选择只同步会发生同步的问题的部分代码而不是整个方法。

12. sleep() 和 wait() 有什么区别?

sleep线程类(Thread )的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁

waitObject类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出 notify 方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态

sleep用Thread调用,在非同步状态下就可以调用, wait用同步监视器调用,必须在同名代码中调用。

13. 在监视器(Monitor)内部,如何做到线程同步的?程序应该做哪种级别的同步?

监视器和锁在Java 虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。

14. 同步和异步有何异同,在什么情况下分别使用他们?举例说明。

如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。

当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。

15. 说出所知道的线程同步的方法?

wait()∶使一个线程处于等待状态,并且释放所持有的对象的lock。

sleep(): 使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。

notify(): 唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。

Allnotity(): 唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

16. 线程的sleep() 和yield() 方法有什么区别?
  1. sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
  2. 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
  3. sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
  4. sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。
17. 如何保证线程安全?

通过合理的时间调度,避开共享资源的存取冲突。另外,在并行任务设计上可以通过适当的策略,保证任务与任务之间不存在共享资源,设计一个规则来保证一个客户的计算工作和数据访问只会被一个线程或一台工作机完成,而不是把一个客户的计算工作分配给多个线程去完成。

18. CyclicBarrier 和 CountDownLatch的区别?

CountDownLatch:

计数器: 计数器只能使用一次。

等待: 一个线程或多个等待另外n个线程完成之后才能执行。

CyclicBarrier:

计数器: 计数器可以重置(通过reset()方法)。

等待: n个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

19. 讲一下公平锁和非公平锁在reetranklock里的实现。

如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,FIFO。

对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁还需要判断当前节点是否有前驱节点,如果有,则表示有线程比当前线程更早请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

20. 讲一下Synchronized,可重入是怎么实现的?

每个锁关联一个线程持有者和一个计数器。

  1. 当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。

  2. 当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。

  3. 当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0, 则释放该锁。

21. 锁和同步的区别。

(1)用法上的不同:

synchronized 既可以加在方法上,也可以加载特定代码块上,而lock需要显示地指定起始位置和终止位置。

synchronized是托管给JVM执行的,lock的锁定是通过代码实现的,它有比synchronized更精确的线程语义。

(2)性能上的不同:
lock接口的实现类ReentrantLock,不仅具有和synchronized相同的并发性和内存语义,还多了超时的获取锁、定时锁、等候和中断锁等。

在竞争不是很激烈的情况下,synchronized的性能优于ReentrantLock,竞争激烈的情况下synchronized 的性能会下降的非常快,而ReentrantLock 则基本不变。

(3)锁机制不同:

synchronized获取锁和释放锁的方式都是在块结构中,当获取多个锁时,必须以相反的顺序释放,并且是自动解锁。而Lock则需要开发人员手动释放,并且必须在finally中释放,否则会引起死锁。

22. 什么是死锁?如何确保N个线程可以访问N个资源同时又不导致死锁?

什么是死锁?

两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是这些线程都陷入了无限的等待中。

例如,如果线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了。线程1永远得不到B,线程2也永远得不到A,并且它们永远也不会知道发生了这样的事情。为了得到彼此的对象(A和B),它们将永远阻塞下去。这种情况就是一个死锁。

如何确保N个线程可以访问N个资源同时又不导致死锁?

使用多线程的时候,一种非常简单的避免死锁的方式就是: 指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。

预防死锁,预先破坏产生死锁的四个条件。互斥不可能破坏,所以有如下三种方法:

  1. 破坏请求和保持条件,进程必须等所有要请求的资源都空闲时才能申请资源,这种方法会使资源浪费严重(有些资源可能仅在运行初期或结束时才使用,甚至根本不使用).允许进程获取初期所需资源后,便开始运行,运行过程中再逐步释放自己占有的资源,比如有一个进程的任务是把数据复制到磁盘中再打印,前期只需获得磁盘资源而不需要获得打印机资源,待复制完毕后再释放掉磁盘资源。这种方法比第一种方法好,会使资源利用率上升。
  2. 破坏不可抢占条件,这种方法代价大,实现复杂。
  3. 破坏循坏等待条件,对各进程请求资源的顺序做一个规定,避免相互等待。这种方法对资源的利用率比前两种都高,但是前期要为设备指定序号,新设备加入会有一个问题,其次对用户编程也有限制。
23. 简述Synchronized 和 java.util.cncurrent.locks.Lock的异同?

主要相同点: Lock 能完成synchronized 所实现的所有功能。

主要不同点: Lock有比 synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally 从句中释放。

七、IO 和 NIO

1. 什么是IO流?

它是一种数据的流从源头流到目的地。比如文件拷贝,输入流和输出流都包括了。输入流从文件中读取数据存储到进程(process)中,输出流从进程中读取数据然后写入到目标文件。

2. Java中有几种类型的流?

按照单位大小:字符流、字节流。
按照流的方向:输入流、输出流。

3. 字节流和字符流哪个好?怎么选择?
  1. 绝大多数情况下使用字节流会更好,因为字节流是字符流的包装,而大多数时候IO操作都是直接操作磁盘文件,所以这些流在传输时都是以字节的方式进行的(图片都是按字节存储的)。
  2. 如果对于操作需要通过IO在内存中频繁处理字符串的情况使用字符流会好些,因为字符流具备缓冲区,提高了性能。

字节流是, 选择BufferedInputStream 和 BufferedOutputStream
字符流是,选择BufferedReader 和 BufferedWriter

4. IO模型有几种?分别介绍一下。

阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。

阻塞IO(blocking IO)

应用程序调用一个IO函数,导致应用程序阻塞,如果数据已经准备好,从内核拷贝到用户空间,否则一直等待下去

一个典型的读操作流程大致如下图,当用户进程调用recvfrom这个系统调用时,kernel就开始了IO的第一个阶段:准备数据,就是数据被拷贝到内核缓冲区中的一个过程〈很多网络IO数据不会那么快到达,如没收一个完整的UDP包),等数据到操作系统内核缓冲区了,就到了第二阶段:将数据从内核缓冲区拷贝到用户内存,然后kernel返回结果,用户进程才会解除block状态,重新运行起来。

blocking IO的特点就是在IO执行的两个阶段用户进程都会block住
在这里插入图片描述非阻塞IO(nonblocking IO)

非阻塞I/О模型,我们把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间

当用户进程发出read操作时,如果kernel中数据还没准备好,那么并不会block用户进程,而是立即返回error,用户进程判断结果是error,就知道数据还没准备好,用户可以再次发read,直到kernel中数据准备好,并且用户再一次发read操作,产生system call,那么kernel 马上将数据拷贝到用户内存,然后返回;

所以nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。

阻塞IO一个线程只能处理一个IO流事件,要想同时处理多个IO流事件要么多线程要么多进程,这样做效率显然不会高,而非阻塞IO可以一个线程处理多个流事件,只要不停地询所有流事件即可,当然这个方式也不好,当大多数流没数据时,也是会大量浪费CPU资源;为了避免CPU空转,引进代理(select和poll,两种方式相差不大),代理可以观察多个流I/O事件,空闲时会把当前线程阻塞掉,当有一个或多个I/O事件时,就从阻塞态醒过来,把所有IO流都轮询一遍,于是没有IO事件我们的程序就阻塞在select方法处,即便这样依然存在问题,我们从select出只是知道有IO事件发生,却不知道是哪几个流,还是只能轮询所有流,epoll这样的代理就可以把哪个流发生怎样的IO事件通知我们。

在这里插入图片描述

多路复用IO模型(IO multiplexing)

I/O多路复用就在于单个进程可以同时处理多个网络连接IO,基本原理就是select,poll,epoll这些个函数会不断轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程,这三个functon会阻塞进程,但和IO阻塞不同,这些函数可以同时阻塞多个IO操作,而且可以同时对多个读操作,写操作IO进行检验,直到有数据到达,才真正调用IO操作函数,调用过程如下图;

所以IO多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中任意一个进入就绪状态,select函数就可以返回。

IO多路复用的优势在于并发数比较高的IO操作情况,可以同时处理多个连接,和bloking IO一样socket是被阻塞的,只不过在多路复用中socket是被select阻塞,而在阻塞IO中是被socket IO给阻塞。

在这里插入图片描述

信号驱动IO模型

可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们,通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已经准备好待处理。

特点:等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。

在这里插入图片描述

异步IO(asynchronous IO)

异步IO告知内核启动某个操作,并让内核在整个操作(包括将内核数据复制到我们自己的缓冲区)完成后通知我们,调用aio_read (Posix异步I/O函数以aio或lio开头)函数,给内核传递描述字、缓冲区指针、缓冲区大小(与read相同的3个参数)、文件偏移以及通知的方式,然后系统立即返回。我们的进程不阻塞于等待I/O操作的完成。当内核将数据拷贝到缓冲区后,再通知应用程序。

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

在这里插入图片描述

5. NIO和IO的区别?以及使用场景?

NIO即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。

在这里插入图片描述

NIO是为弥补传统IO的不足而诞生的,但是尺有所短寸有所长,**NIO也有缺点,因为NIO是面向缓冲区的操作,每一次的数据处理都是对缓冲区进行的,那么就会有一个问题,在数据处理之前必须要判断缓冲区的数据是否完整或者已经读取完毕,如果没有,假设数据只读取了一部分,那么对不完整的数据处理没有任何意义。**所以每次数据处理之前都要检测缓冲区数据。

那么NIO和IO各适用的场景是什么呢?

如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,这时候用NIO处理数据可能是个很好的选择。

而如果只有少量的连接,而这些连接每次要发送大量的数据,这时候传统的IO更合适。使用哪种处理数据,需要在数据的响应等待时间和检查缓冲区数据的时间上作比较来权衡选择。

6. NIO的核心组件是什么,分别介绍一下?

channel、buffer、selector

什么是Channel?

一个Channel(通道)代表和某一实体的连接,这个实体可以是文件、网络套接字等。也就是说,通道是Java NIO提供的一座桥梁,用于我们的程序和操作系统底层I/O服务进行交互。

通道是一种很基本很抽象的描述,和不同的I/O服务交互,执行不同的I/O操作,实现不一样,因此具体的有FileChannel、SocketChannel等。

通道使用起来跟Stream比较像,可以读取数据到Buffer中,也可以把Buffer中的数据写入通道。

在这里插入图片描述
当然,也有区别,主要体现在如下两点:

  1. 一个通道,既可以读又可以写,而一个Stream是单向的(所以分InputStream 和 OutputStream)
  2. 通道有非阻塞 I/O 模式

Java NIO 中最常用的通道实现?

  1. FileChannel: 读写文件
  2. DatagramChannel:UDP协议网络通信
  3. SocketChannel:TCP协议网络通信
  4. ServerSocketChannel:监听TCP连接

Buffer是什么?

NIO中所使用的缓冲区不是一个简单的byte数组,而是封装过的Buffer类,通过它提供的API,我们可以灵活的操纵数据。

与Java基本类型相对应,NIO提供了多种Buffer类型,如ByteBuffer、CharBuffer、IntBuffer等,区别就是读写缓冲区时的单位长度不一样(以对应类型的变量为单位进行读写)。

核心Buffer的实现有哪些?

核心的buffer实现有哪些:ByteBuffer、CharBuffer、DoubleBuffer、FloatBufferr、IntBuffer、LongBuffer、ShortBuffer,涵盖了所有的基本数据类型(4类8种,除了Boolean)。也有其他的buffer如MappedByteBuffer。

Buffer读写数据基本操作:

  1. 将数据写入buffer
  2. 调用buffer.flip()
  3. 将数据从buffer中读取出来
  4. 调用buffer.clear()或者buffer.compact()

在写buffer的时候,buffer会跟踪写入了多少数据,需要读buffer的时候,需要调用flip()来将buffer从写模式切换成读模式,读模式中只能读取写入的数据,而非整个buffer。

当数据都读完了,你需要清空buffer以供下次使用,可以有2种方法来操作:调用clear()或者调用compact()

区别: clear方法清空整个buffer,compact方法只清除你已经读取的数据,未读取的数据会被移到buffer的开头,此时写入数据会从当前数据的末尾开始。

Selector是什么?

Selector(选择器)是一个特殊的组件,用于采集各个通道的状态(或者说事件)。我们先将通道注册到选择器,并设置好关心的事件,然后就可以通过调用select() 方法,静静地等待事件发生。

通道可以监听哪几个事件?

通道有4个事件可供监听:

  1. Accept:有可以接受的连接
  2. Connect:连接成功
  3. Read:有数据可读
  4. Write:可以写入数据

为什么要用Selector?

如果用阻塞I/О,需要多线程(浪费内存)﹐如果用非阻塞I/O需要不断重试(耗费CPU)。Selector的出现解决了这尴尬的问题,非阻塞模式下,通过Selector,我们的线程只为已就绪的通道工作,不用盲目的重试了。比如,当所有通道都没有数据到达时,也就没有Read事件发生,我们的线程会在select()方法处被挂起,从而让出了CPU资源。

Selector处理多Channel

要使用一个Selector,要注册这个Selector的Channels。然后调用Selector的select()方法。这个方法会阻塞,直到它注册的Channels当中有一个准备好了的事件发生。 当select() 方法返回的时候,线程可以处理这些事件,如新的连接的到来,数据收到了等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值