侧边栏壁纸
博主头像
乌拉队长博主等级

你只管努力,其余的交给命运

  • 累计撰写 122 篇文章
  • 累计创建 34 个标签
  • 累计收到 31 条评论

目 录CONTENT

文章目录

面试知识总结(三)

乌拉队长
2021-10-26 / 0 评论 / 0 点赞 / 711 阅读 / 22,130 字

Java面经总结

Java SE部分

Java中是如何支持正则表达式的?

Java中的String类提供了正则表达式操作的方法,包括:matches(),replace(),replaceFirst(),split(). 此外,Java中可以用Pattern类表示正则表达式对象,它提供了丰富的API进行各种正则表达式操作。

Java和JavaScript的区别

  • 基于对象和面向对象:JavaScript是一种基于对象和事件驱动的语言,Java是一种面向对象的语言
  • 解释型和编译型:JavaScript是一种解释型语言,其源码执行前不需要编译,由浏览器负责解释;Java是编译型语言,其源码在执行之前,必须经过编译才能被机器执行。
  • 强类型和弱类型:Java是强类型语言,所有变量在编译之前都必须作变量类型的声明;JavaScript是弱类型语言,甚至在使用变量之前可以不作声明,JavaScript的解释器在运行时推断其变量类型。
  • 格式不一样

&和&&的区别

&运算符有两种用法:按位与;逻辑与。&&是短路与。短路与的意思是:当两个判断条件结果均为true时,结果才为true;并且,只有第一个条件为true时,才会去判断第二个条件,如果第一个条件结果为false,则不会去判断第二个条件。

int和Integer的区别

从Java 5 开始,引入了自动装箱/拆箱机制,Java为每个基础数据类型提供了包装类:

  • 原始类型:blooean、char、byte、short、int、float、double、long
  • 包装类型:Boolean、Character、Byte、Short、Integer、Float、Double、Long

String和StringBuffer的区别

String类提供了数值不可变的字符串,StringBuffer类提供的字符串可以修改;当你知道字符数据需要改变时,应该使用StringBuffer类来存储数据。两个字符串相加,原理是调用StringBuilder的append()方法进行处理的。

1.操作数量较少的字符串用String,不可修改的字符串;
2.在单线程且操作大量字符串用StringBuilder,速度快,但线程不安全,可修改;
3.在多线程且操作大量字符串用StringButter,线程安全,可修改。

String是基本数据类型吗?可以被继承吗?

java.lang.String类是被final修饰的类,不能被继承,存储的数值不可以被改变,不是基本数据类型。

数组(Array)和列表(ArrayList)的区别

  • Array可以存储基本数据类型和对象类型数据,ArrayList只能存储对象类型。
  • Array大小是固定的,ArrayList大小是动态变化的。
  • ArrayList提供了更多的方法和特性,如:addAll(),removeAll(),iterator()等。
  • 对于基本数据类型来说,ArrayList使用自动装箱来减少编码工作量,但是当处理固定大小的基本数据类型的时候,这种方式相对较慢

值传递和引用传递是什么

  • 值传递是针对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响改变量的值。
  • 引用传递一般是针对对象变量而言的,传递的是该对象地址的一个副本,并不是原对象本身,所以对引用变量操作会同时改变原对象。
  • 一般认为,Java中的传递都是值传递。

Lambda表达式的优缺点

优点:

  • 简洁
  • 非常容易并行计算
  • 可能代表未来的编程趋势

缺点:

  • 若不用并行计算,很多时候计算速度没有比传统的 for 循环快。(并行计算有时需要预热才显示出效率优势)
  • 不容易调试
  • 若其他程序员没有学过 lambda 表达式,代码不容易让其他语言的程序员看懂

Java8新特性

  • Lambda表达式 -- Lambda允许把函数作为一个方法的参数(函数作为参数传递到方法中)
  • 方法引用 -- 方法引用将提供了非常有用的引用,可以直接引用已有的类或对象(实例)的方法或者构造器,与Lambda表达式一起使用,方法引用可以是代码的构造更简洁,降低代码的冗余度
  • 默认方法 -- 默认方法就是在一个接口中有了一个实现的方法
  • 新工具 -- 新的编译工具,如:Nashorn引擎jjs、类依赖分析器jdeps
  • Stream API -- 新添加的Stream API(java.util.stream)把真正的函数式编程风格引入到java中
  • Date Time API -- 加强对日期和时间的处理
  • Optional -- Optional类已成为Java 8类库的一部分,用来解决空指针异常
  • Nashorn,JavaScript引擎 -- Java 8 提供了一个新的Nashorn JavaScript引擎,它允许我们可以在JVM上运行特定的JavaScript应用

==和equals的区别

  • 当比较的两个变量是基本数据类型时,==比较的是两个变量的值;当比较的两个变量是对象类型时,比较的是两个变量在内存中的地址,即比较两个对象指向的是否是同一个地址
  • java中所有的类均继承于java.lang.Object类,而在Object类中,equals方法同样是使用==来实现的。所以:
    当比较的两个变量是基本数据类型时,equals方法比较的仍然是两个变量的值;当比较的两个变量是对象类型时,需要看比较的两个对象的类的实现中是否重写了equals方法,如果未重写equals方法,则比较的还是两个对象在内存中的地址,否则,就需要按照类中的equals方法定义来进行比较。
  • 比如,Java中String、Integer、Date类的equals方法有其自身的实现,而不再是比较类在堆内存中的存放地址

Object类中的hashcode方法是如何实现的

Object类的hashcode方法是一个本地方法,也就是用C语言或者C++实现的,该方法返回与对象在内存中的逻辑地址相关的值

为什么重写equals方法还要重写hashcode方法

因为HashMap类在判断相等时,先判断key的hashcode是否相同再判断其equals是否相同,如果只重写其中任意一个,则会导致其判断发生错误。

hashcode和equals有什么联系

Java对象的equals方法和hashcode方法是这样规定的:

  • 相同(相等)的对象必须有相同(相等)的哈希码
  • 两个对象拥有相同的hashcode,它们并不一定相同

map的分类和常见情况(用法)

  • Java为数据结构中的映射定义了一个接口java.util.map,其主要有四个实现类:HashMap、LinkedHashMap、HashTable、TreeMap。
  • Map主要用于存储键值对,它不允许键重复,但允许值重复。
  • HashMap是最常用的一个Map,它使用键的hashcode值存储数据,它直接根据键值来查找和获取记录数据,具有非常快的访问速度,遍历时,获取的数据顺序是随机的;HashMap最多只允许一条记录的键值为null,但允许多条记录的值为null;HashMap是非线程同步的,它允许多个线程同时操作HashMap,因此可能会导致数据不一致,如果需要同步,可以使用Collections包的synchronizedMap方法是HashMap具有同步能力,或者直接使用ConcurrentHashMap类代替HashMap。
  • HashTable与HashMap一样,它继承于Dictionary类,不同的是:HashTable不允许键和值为null,但它支持线程同步,即任一时刻只有一个线程可以写HashTable,这也导致写入HashTable比较慢。
  • LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,当调用Iterator时,先得到的记录肯定是先插入的。一般情况下,在遍历时LInkedHashMap比HashMap慢,但有一种情况例外,即:当HashMap的容量很大而实际存储的数据很少时,LinkedHashMap比HashMap的遍历速度要快,因为HashMap的遍历速度和总容量有关,而LinkedHashMap的遍历速度只和实际存储的数据容量有关。
  • TreeMap实现SortMap接口,能够把它保存的数据根据键值排序,默认按照键值升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,输出的记录是排过序的。

final关键字的用法

  • 当一个类被final关键字修饰时,表明该类不可以被继承;final类中的变量可以根据需要设置为final类型,但需要注意的是final类中的所有成员方法都会被隐式的指定为final方法;
  • 对于一个被final修饰的变量,如果变量是基本数据类型,一旦该变量被初始化,那么这个变量的值就不可以被修改;
  • 如果是引用型变量,一旦该变量被初始化之后,该变量所指向的对象便不可以重新指向另一个对象;

synchronized和lock

  • synchronized是Java的关键字,当用它来修饰一个方法或者代码块时,能够保证同一时刻最多只有一个线程可以执行该段代码。在JDK1.5之后引入了自旋锁、锁粗化、轻量级锁、偏向锁来优化关键字性能
  • Lock是一个接口,synchronized在发生异常时,会自动释放线程占用的锁,因此不会导致死锁的发生;而Lock在发生异常时,如果没有使用unlock主动的释放锁,则很可能发生死锁,因此在使用Lock时,应在finally代码块中手动释放锁;
  • Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待锁的线程会一直等待下去,不能响应中断;
  • 通过Lock可以有没有成功获取锁,synchronized却无法办到。

synchronized修饰代码块时,锁住的对象由程序员自己指定;synchronized修饰类的静态方法时,锁住的是类;synchronized修饰普通成员方法时,锁住的是this对象;

Java概念中,什么是构造函数、构造函数重载、复制构造函数

  • 当新对象被创建时,构造函数会被调用;如果一个类没有实现构造函数,Java编译器会给这个类创建默认的构造函数;
  • Java中的构造函数重载和方法重载很类似,可以为一个类创建多个构造函数,但他们必须有不同的参数列表;
  • Java不支持像C++那样的复制构造函数,这个不同的点是因为如果你不创建默认的构造函数的情况下,Java不会为你创建默认的复制构造函数;

Query接口的list方法和iterate方法有什么区别

  • list()方法无法利用一级缓存和二级缓存(对缓存只写不读),它只能在开启查询缓存的前提下使用查询缓存;iterate()方法可以充分利用缓存,如果目标数据只读或者读取频繁,使用iterate()方法可以减少性能开销。
  • list()方法不会引起N+1查询问题,而iterate()方法可能引起N+1查询问题

内部类可以引用他包含类的成员吗,如果可以,有没有什么限制吗?

Java支持四种内部类:静态内部类、成员内部类、局部内部类、匿名内部类;

  • 静态内部类(static nested class)即内部类被static关键字修饰,此时内部类只能引用外部类中的static成员变量和方法,因为静态方法总是与类(Class)相关联,而动态方法总是与实例对象(instance object)相关联;

  • 成员内部类即与外部类的成员函数处于同一层级的内部类,成员内部类也是最常见的内部类,此时内部类可以访问外部类的所有成员变量和方法(包括private成员和静态成员);

    • 【需要注意的是:】当成员内部类拥有和外部类相同的成员变量或方法时,会发生隐藏现象,即默认访问的是成员内部类的成员变量和方法,如果想要访问外部类的同名成员变量或者方法时,需要按照下面的方式访问:
      • 外部类.this.成员变量
      • 外部类.this.成员方法
  • 局部内部类即该内部类位于外部类的成员函数的内部,此时内部类可以访问外部类的所有成员变量和方法,但是不能随便访问局部变量,除非该局部变量被final修饰;

  • 匿名内部类即局部内部类的简写格式,此时内部类的访问权限和局部内部类相同;

    • 【定义匿名内部类的几个条件:】
      • 前提:必须继承一个父类或者实现一个接口
      • 格式:new 父类或者接口名{定义子类的内容}
      • 限制:匿名内部类中定义的成员方法最好不要超过3个

abstract class和interface的区别

相同点:

  • 都不能被实例化
  • 接口的实现类和抽象类的子类,只有实现全部的抽象方法才能被实例化

不同点:

  • 接口只能定义抽象方法不能实现方法,抽象类既可以定义抽象方法也可以实现方法
  • 接口强调的是功能,抽象类强调的是所属关系
  • 单继承,多实现。一个类只能继承一个抽象类,但是可以实现多个接口
  • 接口中定义的变量必须是public static final类型,且必须初始化;抽象类中可以定义任意类型的成员变量
  • 接口中定义的方法必须是public abstract类型的,即公共抽象方法;抽象类则可以定义抽象方法和普通方法
  • 接口中不能定义构造方法;,而抽象类中可以定义构造方法

final, finally, finalize的区别

  • final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。
  • finally是异常处理语句结构的一部分,表示总是执行。
  • finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。

Comparable和Comparator接口的作用以及它们的区别

Comparable 是一个排序接口,如果一个类实现了该接口,说明该类本身是可以进行排序的。注意,除了基本数据类型(八大基本数据类型) 的数组或是List,其余类型的对象,Collections.sort或Arrays.sort 是不支持直接进行排序的,因为对象本身是没有“顺序”的,除非你实现了Comparable 接口或是自定义了Comparator 比较器,指定了排序规则,才可以进行排序。
Comparable 接口仅包含一个方法compareTo:

// 泛型T表示要进行比较的对象所属的类型,compareTo 比较对象之间的值的大小关系,如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数
public interface Comparable<T> {
    public int compareTo(T o);
}

Comparator 源码中主要的两个接口方法:

// compare  中返回比较结果,如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
public interface Comparator<T>{
    int compare(T o1, T o2);
    boolean equals(Object obj);
}

compare 是主要方法,必须要实现,equals 方法可以不实现。
Comparable 在类的内部定义排序规则,Comparator 在外部定义排序规则,Comparable 相当于“内部排序器”,Comparator 相当于“外部排序器”,前者一次定义即可,后者可以在不修改源码的情况下进行排序,各有所长。

是否可以在static环境中访问非static变量

static变量在Java中是属于类的,它在所有的实例中的值是一样的。当类被Java虚拟机载入的时候,会对static变量进行初始化。如果你的代码尝试不用实例来访问非static的变量,编译器会报错,因为这些变量还没有被创建出来,还没有跟任何实例关联上。

extends 和super 泛型限定符

Java 泛型 <? super T> 中 super 怎么 理解?与 extends 有何不同? - 胖君的回答 - 知乎

类加载机制,双亲委派模型及其优点

类加载机制,双亲委派模型及其优点

StringBuffer和StringBuilder有什么区别,底层实现上呢

StringBuffer是线程安全的,StringBuilder是非线程安全的,底层实现上,StringBuffer比StringBuilder多加了一个synchronized修饰符

Object类包含的方法并简单说明

  • Object(): 默认构造方法
  • clone(): 创建并返回该对象的一个副本
  • equals(): 判断某个对象与此对象是否相等
  • finalize(): 当垃圾回收器确定不存在该对象的更多引用时,由该对象的垃圾回收器调用该方法
  • getClass(): 返回该对象的运行时类
  • hashcode(): 返回该对象的哈希值
  • toString(): 返回该对象的字符串表示
  • notify(): 唤醒在该对象监视器上等待的单个线程
  • notifyAll(): 唤醒在该对象监视器上等待的所有线程
  • wait(): 导致当前的线程等待,直到其他线程调用该对象的notify()或者notifyAll()方法
  • wait(long timeout): 导致当前的线程等待,直到其他线程调用该对象的notify()或者notifyAll()方法,或者超过指定的时间量
  • wait(long timeout,int nanos): 导致当前线程等待,直到其他线程调用该对象的notify()或者notifyAll()方法,或者超过指定的时间量,或者其他某个线程中断当前线程

Collection 和 Collections的区别

  • Collection是集合类的上级接口,继承于他的接口主要有Set 和List
  • Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作

List、Set、Map在存取元素时,有什么特点

  • List以特定的索引来存取元素,且允许重复元素
  • Set不允许有重复的元素,且去重是通过对象的equals方法来实现的
  • Map中保存的是键值对映射,支持一对一和一对多映射关系
  • Set和Map都有基于哈希存储和排序树的两种版本:
    • 基于哈希存储的版本理论上在存取时的时间复杂度为O(1)
    • 基于排序树版本的实现在插入和删除元素时,根据元素或者元素的键值来构成排序树以达到排序和去重的效果

ArrayList,Vector, LinkedList 的存储性能和特性

  • ArrayList和Vector的底层实现都是使用数组的方式存储数据,此数组的容量大于实际存储的数据容量以便增加和插入元素,他们都允许直接按序号索引元素,但是插入元素要涉及到数组元素移动等内存操作,因此索引元素快而插入元素慢
  • Vector中的方法由于添加了synchronized修饰,因此Vector是线程安全的容器,但效率要比ArrayList慢,因此已经是Java中的遗留容器
  • LinkedList使用双向链表实现存储(将内存中的零散内存单元通过附加的引用关联起来,形成一个可以按序号索引的线性结构,这种连式存储结构与数组的连续存储结构比起来,内存的利用率更高),按序号索引数据需要进行前向或后向遍历,但插入数据只需要记录本项的前后项即可,所以插入速度较快
  • Vector属于遗留容器(Java早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties都是遗留容器),已经不推荐使用,但是由于ArrayList和LinkedListed都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类Collections中的synchronizedList方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强实现)

ArrayList和LinkedList的区别

  • 相同点:

    • ArrayList和LinkedList都实现了List接口
  • 不同点:

    • ArrayList是基于索引的数据接口,底层实现是数组,而LinkedList是基于链表的数据接口,底层实现是链表
    • ArrayList进行随机访问(查找)的时间复杂度是O(1),而LinkedList是O(n)
    • 相较于ArrayList,LinkedList的插入、删除速度更快,因为它不需要像ArrayList那样进行元素的移动和重新计算大小
    • 相较于ArrayList,LinkedList更占内存,因为LinkedList为每个节点存储两个引用,一个指向前一个元素,另一个指向后一个元素

HashMap和Hashtable的区别

  • 相同点:

    • HashMap和HashTable都实现了Map接口
  • 不同点:

    • HashMap允许键和值为null,而HashTable不允许
    • HashTable是同步的(线程安全),而HashMap不是,因此HashMap适用于单线程环境,而HashTable适用于多线程环境
    • HashMap提供了可供迭代的键的集合,因此HashMap是快速失败的
    • HashTable提供了对键的枚举
    • 一般认为HashTable是遗留的类,所以不建议使用,在多线程环境下可以使用ConcurrentHashMap

快速失败(fail-fast)和安全失败(fail-safe)的区别

  • Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响
  • java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的
  • 快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常

什么是迭代器

  • Iterator提供了统一遍历操作集合元素的统一接口, Collection接口实现Iterable接口
  • 每个集合都通过实现Iterable接口中iterator()方法返回Iterator接口的实例, 然后对集合的元素进行迭代操作
  • 有一点需要注意的是:在迭代元素的时候不能通过集合的方法删除元素, 否则会抛出ConcurrentModificationException 异常. 但是可以通过Iterator接口中的remove()方法进行删除.

Iterator和ListIterator的区别

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

Java集合类框架的基本接口有哪些

  • Collection:代表一组对象,每一个对象都是它的子元素
  • Set:不包含重复元素的Collection
  • List:有顺序的collection,并且可以包含重复元素
  • Map:可以把键(key)映射到值(value)的对象,键不能重复

ConcurrentHashMap的原理

ConcurrentHashMap的实现原理与使用

TreeMap的底层实现

TreeMap实现了SotredMap接口,它是有序的集合。而且是一个红黑树结构,每个key-value都作为一个红黑树的节点。如果在调用TreeMap的构造函数时没有指定比较器,则根据key执行自然排序,如果指定了比较器则按照比较器来进行排序。
红黑树是一个更高效的检索二叉树,有如下特点:

  1. 每个节点只能是红色或者黑色
  2. 根节点永远是黑色的
  3. 所有的叶子的子节点都是空节点,并且都是黑色的
  4. 每个红色节点的两个子节点都是黑色的(不会有两个连续的红色节点)
  5. 从任一个节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点(叶子节点到根节点的黑色节点数量每条路径都相同)
  • 关于红黑树的节点插入操作,首先是改变新节点,新节点的父节点,祖父节点,和新节点的颜色,能在当前分支通过节点的旋转改变的,则通过此种操作,来满足红黑书的特点。
  • 如果当前相关节点的旋转解决不了红黑树的冲突,则通过将红色的节点移动到根节点解决,最后在将根节点设置为黑色

HashMap的容量为什么是2的n次幂

hashmap为什么初始容量是2的指数幂

ArrayList和LinkedList,如果一直在list的尾部添加元素,用哪种方式的效率高

当输入的数据一直是小于千万级别的时候,大部分是LinkedList效率高,而当数据量大于千万级别的时候,就会出现ArrayList的效率比较高了。
原来 LinkedList每次增加的时候,会new 一个Node对象来存新增加的元素,所以当数据量小的时候,这个时间并不明显,而ArrayList需要扩容,所以LinkedList的效率就会比较高,其中如果ArrayList出现不需要扩容的时候,那么ArrayList的效率应该是比LinkedList高的,当数据量很大的时候,new对象的时间大于扩容的时间,那么就会出现ArrayList的效率比LinkedList高了

在ArrayLIst和LinkedList尾部加元素,谁的效率高

如果hashMap的key是一个自定义的类,怎么办

使用HashMap,如果key是自定义的类,就必须重写hashcode()和equals()

HashMap具体如何实现的

Hashmap基于数组实现的,通过对key的hashcode & 数组的长度得到在数组中位置,如当前数组有元素,则数组当前元素next指向要插入的元素,这样来解决hash冲突的,形成了拉链式的结构。put时在多线程情况下,会形成环从而导致死循环。数组长度一般是2n,从0开始编号,所以hashcode & (2n-1),(2n-1)每一位都是1,这样会让散列均匀。需要注意的是,HashMap在JDK1.8的版本中引入了红黑树结构做优化,当链表元素个数大于等于8时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。


为什么程序计数器、虚拟机栈和本地方法栈是线程私有的,而堆和方法区是线程共享的

  • 程序计数器私有主要是为了线程切换后能恢复到正确的执行位置
  • 为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的
  • 堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

并发与并行的区别

  • 并发: 同一时间段,多个任务都在执行 (单位时间内不一定同时执行)
  • 并行: 单位时间内,多个任务同时执行

产生死锁的四个必要条件

  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何预防和避免线程死锁

如何预防死锁? 破坏死锁的产生的必要条件即可:

  1. 破坏请求与保持条件 :一次性申请所有的资源。
  2. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  3. 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态 指的是系统能够按照某种进程推进顺序(P1、P2、P3.....Pn)来为每个进程分配所需资源,直到满足每个进程对资源的最大需求,使每个进程都可顺利完成。称<P1、P2、P3.....Pn>序列为安全序列。

synchronized 关键字

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

另外,在 Java 早期版本中,synchronized 属于 重量级锁,效率低下。

  • synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁。
  • synchronized 关键字加到实例方法上是给对象实例上锁。
  • 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!

构造方法可以使用 synchronized 关键字修饰么

构造方法不能使用 synchronized 关键字修饰。

构造方法本身就属于线程安全的,不存在同步的构造方法一说。

synchronized 关键字的底层原理

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

不过两者的本质都是对对象监视器 monitor 的获取。

synchronized 关键字和 volatile 关键字的区别

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

  • volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

sleep() 方法和 wait() 方法区别和共同点

  • 两者最主要的区别在于:sleep() 方法没有释放锁,而 wait() 方法释放了锁
  • 两者都可以暂停线程的执行。
  • wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法

这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

实现 Runnable 接口和 Callable 接口的区别

Runnable 接口 不会返回结果或抛出检查异常,但是 Callable 接口 可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口 ,这样代码看起来会更加简洁。

工具类 Executors 可以实现将 Runnable 对象转换成 Callable 对象。(Executors.callable(Runnable task) 或 Executors.callable(Runnable task, Object result))

execute()方法和 submit()方法的区别

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  • submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

ThreadPoolExecutor构造函数重要参数分析

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 核心线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

ThreadPoolExecutor其他常见参数:

  • keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
  • unit : keepAliveTime 参数的时间单位。
  • threadFactory :executor 创建新线程的时候会用到。
  • handler :饱和策略。关于饱和策略下面单独介绍一下。
/**
 * 用给定的初始参数创建一个新的ThreadPoolExecutor。
 */
public ThreadPoolExecutor(int corePoolSize,
                      int maximumPoolSize,
                      long keepAliveTime,
                      TimeUnit unit,
                      BlockingQueue<Runnable> workQueue,
                      ThreadFactory threadFactory,
                      RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
            throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

Java内存区域

JDK1.8之前:
image.png
图片出自Java 内存区域详解

JDK1.8之后:
image.png

图片出自Java 内存区域详解

线程私有的

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的

  • 方法区
  • 直接内存 (非运行时数据区的一部分)

程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

程序计数器主要有两个作用:

  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

Java虚拟机栈

与程序计数器一样,Java虚拟机栈也是线程私有的,他的生命周期和线程相同,描述的是Java方法执行的内存模型,每次方法调用的数据都是通过栈传递的。

Java内存可以粗糙的分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中的局部变量表部分。(实际上,Java虚拟机栈是由一个个的栈帧组成的,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息)

局部变量表主要存放了在编译时期可知的各种数据类型(boolean、byte、short、int、long、double、float、char)、对象引用(reference类型,他与对象本身不同,可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或其他与此对象相关的位置)。

Java虚拟机栈会出现两种错误:StackOverFlowError和OutOfMemoryError:

  • StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求的深度超过Java虚拟机栈的最大深度时,就会抛出StackOverFlowError错误
  • OutOfMemoryError:Java虚拟机栈的内存大小允许动态扩展,如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

Java虚拟机栈也是线程私有的,每个线程都有自己的Java虚拟机栈,且随着线程的创建而创建,随着线程的死亡而死亡。

方法/函数如何调用?

Java栈可以类比数据结构中的栈,Java栈中保存的主要内容是栈帧,每一次方法调用都会有一个对应的栈帧被压入Java栈,每一次方法调用结束,都会有一个栈帧被弹出。

Java方法有两种返回方式:return语句和抛出异常; 不管哪种方式都会导致栈帧被弹出。

本地方法栈

本地方法栈和虚拟机栈的作用非常相似。区别是:虚拟机栈为Java方法(字节码)服务,而本地方法栈为本地方法(Native method)服务。在HotSpot虚拟机中,本地方法栈和Java虚拟机栈合二为一。(HotSpot为当前Java中默认使用的虚拟机)

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态连接、方法出口信息。

本地方法栈也会出现StackOverFlowError和OutOfMemoryError异常。

Java堆是Java虚拟机所管理的内存中最大的一块,Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的是存放对象实例,几乎所有的对象实例和数组都在这里分配内存。

但是随着JIT(即时编译)编译器和逃逸分析技术的逐渐成熟,这种“几乎”开始变得不那么绝对。从JDK1.7开始,已默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被方法外部所使用(即未逃逸出去),那么对象可以直接在栈上分配。

Java堆是垃圾收集器管理的主要区域,因此也被成为GC堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在的垃圾收集器都采用分代垃圾收集算法,因此Java堆还可以细分为:新生代和老生代,再进一步可以分为:Eden空间、From Survivor、To Survivor,进一步细分的目的是为了更好地回收内存,或者更快地分配内存。

在JDK1.7和JDK1.7之前,堆内存通常被分为:

  • 新生代(Young Generation)
  • 老生代(Old Generation)
  • 永生代(Permanent Generation)

image.png
图片出自Java 内存区域详解

JDK1.8版本之后方法区(HotSpot的永久代)被彻底移除了(其实从JDK1.7就开始了),取而代之的是元空间,元空间使用的是直接内存。

image.png
图片出自Java 内存区域详解

方法区

方法区与Java堆一样,是各个线程共享的内存区域。它主要存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
虽然Java虚拟机规范把方法区描述为堆的一个非逻辑区域,但是它有一个别名非堆(Non-Heap),目的应该是为了和Java堆分开。

运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译时期生成的各种字面量和符号引用)。

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁使用,也会导致OutOfMemoryError异常。

Java对象的创建过程

类加载检查-->分配内存-->初始化零值-->设置对象头-->执行init方法

内存分配

分配内存的方式有两种:指针碰撞和空闲列表,选择哪种方式取决于Java堆是否规整,而Java堆是否规整取决于GC收集器的算法是“标记-清除”,还是“标记-整理”(也称作“标记-压缩”),值得注意的是复制算法内存也是规整的。

image.png
图片出自Java 内存区域详解

Java虚拟机采用两种方式来保证线程安全:

  • CAS+失败重试:CAS是乐观锁的一种实现方式。所谓乐观锁就是:每次不加锁,而是假设没有冲突,然后去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用CAS配合失败重试的方式来保证更新操作的原子性。
  • TLAB:为每个线程预先在Eden区分配一块内存,JVM在给线程中的对象分配内存时,先在TLAB中分配,当对象大于TLAB中的剩余内存或TLAB内存已用尽时,再采用上述的CAS+失败重试机制进行内存分配。

对象的访问定位

对象的访问方式由虚拟机的实现而定,目前主流的访问方式为:句柄直接指针 两种方式。

8 种基本类型的包装类和常量池

ava 基本类型的包装类的大部分都实现了常量池技术。

Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True Or False。

两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。

Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true
Integer i11 = 333;
Integer i22 = 333;
System.out.println(i11 == i22);// 输出 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false

所有整型包装类对象之间值的比较,全部使用 equals 方法比较。

类的生命周期

image.png
图片出自Java 内存区域详解

类加载过程

image.png
图片出自Java 内存区域详解

加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了。

类加载器

JVM中内置了三个重要的ClassLoader,除了BootstrapClassLoader以外,其他类加载器均由Java实现并继承于java.lang.ClassLoader:

  • BootstrapClassLoader(启动类加载器):最顶层的加载器,由C++实现,负责加载%JAVA_HOME%/lib目录下的jar包和类或被-Xbootclasspath参数所指定的路径中的所有类
  • ExtensionClassLoader(扩展类加载器):主要负责加载%JRE_HOME%/lib/ext目录下所有的jar包和类,或被java.ext.dirs系统变量所指定的路径下的jar包
  • AppClassLoader(应用程序类加载器):面向用户的加载器,负责加载当前应用classpath下所有的jar包和类

双亲委派模型

介绍:每一个类都有一个对应它的类加载器,系统中的ClassLoader在协同工作时会默认使用双亲委派模型。双亲委派模型即:在类加载的时候,系统会首先判断当前类是否已被加载过,已经加载过则直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派为父类加载器的loadClass方法来处理,因此所有的请求最终都应该传送到最顶层的启动类加载器BootstrapClassLoader中。当父类加载器无法处理时,才会自己处理。当父类加载器为null时,会使用启动类加载器BootstrapClassLoader作为父类加载器。

好处:双亲委派模型保证了Java程序的稳定运行,可以避免类被重复加载(JVM区分不同类的方式不仅仅根据类名,相同文件的类被不同类加载器加载产生的是两个不同的类),也保证了Java的核心API不被篡改。如果没有双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题。比如:我们编写一个java.lang.Object类的话,那么运行的时候,系统就会出现多个不同的Object类。

如果我们不想用双亲委派模型怎么办

自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法

自定义类加载器

除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader。


细节小问题汇总

  • 为什么会出现4.0-3.6=0.40000001这种现象?
    答:原因简单来说是这样:2进制的小数无法精确的表达10进制小数,计算机在计算10进制小数的过程中要先转换为2进制进行计算,这个过程中出现了误差。

  • 一个十进制的数在内存中是怎么存的?
    答:补码的形式。

  • 静态变量存在什么位置?
    答:方法区

  • 请判断当一个对象被当作参数传递给一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
    答:值传递。Java 编程语言只有值传递参数。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的内容可以在被调用的方法中改变,但对象的引用是永远不会改变的。【详解见 深入理解Java中方法的参数传递机制

  • ArrayList是否会越界?
    答:ArrayList并发add()可能出现数组下标越界异常。

  • ConcurrentHashMap锁加在了哪些地方?
    答:加在每个Segment 上面

操作系统

系统调用的分类

按功能:

  • 设备管理。完成设备的请求或释放,以及设备启动等功能。
  • 文件管理。完成文件的读、写、创建及删除等功能。
  • 进程控制。完成进程的创建、撤销、阻塞及唤醒等功能。
  • 进程通信。完成进程之间的消息传递或信号传递等功能。
  • 内存管理。完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。

进程和线程的区别

线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

进程有哪几种状态

  • 创建状态(new) :进程正在被创建,尚未到就绪状态。
  • 就绪状态(ready) :进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
  • 运行状态(running) :进程正在处理器上上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。
  • 阻塞状态(waiting) :又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
  • 结束状态(terminated) :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。

进程间的通信方式

  1. 管道/匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
  2. 有名管道(Names Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
  3. 信号(Signal) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
  4. 消息队列(Message Queuing) :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
  5. 信号量(Semaphores) :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
  6. 共享内存(Shared memory) :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
  7. 套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。

线程间的同步的方式

  1. 互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
  2. 信号量(Semphares) :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量
  3. 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操

进程的调度算法

  • 先到先服务(FCFS)调度算法 : 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
  • 短作业优先(SJF)调度算法 : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
  • 时间片轮转调度算法 : 时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称 RR(Round robin)调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
  • 多级反馈队列调度算法 :前面介绍的几种进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前被公认的一种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。
  • 优先级调度 : 为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。

常见的页面置换算法有哪些

  • OPT 页面置换算法(最佳页面置换算法) :最佳(Optimal, OPT)置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。一般作为衡量其他置换算法的方法。
  • FIFO(First In First Out) 页面置换算法(先进先出页面置换算法) : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。
  • LRU (Least Recently Used)页面置换算法(最近最久未使用页面置换算法) :LRU算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。
  • LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法) : 该置换算法选择在之前时期使用最少的页面作为淘汰页。

Reference

Java并发编程:Synchronized及其实现原理
学习java应该如何理解反射?
内部类可以引用它的包含类的成员吗?有没有什么限制?
Java内部类详解
深入理解Java中方法的参数传递机制
Java中Comparable和Comparator比较
Java 泛型 <? super T> 中 super 怎么 理解?与 extends 有何不同? - 胖君的回答 - 知乎
类加载机制,双亲委派模型及其优点
ConcurrentHashMap的实现原理与使用
hashmap为什么初始容量是2的指数幂
在ArrayLIst和LinkedList尾部加元素,谁的效率高
JavaGuide

0

评论区