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

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

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

目 录CONTENT

文章目录

Java相关面试题

乌拉队长
2022-04-26 / 0 评论 / 0 点赞 / 433 阅读 / 23,868 字

Java相关面试题

关键字部分

final关键字的用法

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

abstract class和interface的区别

相同点

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

不同点

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

final, finally, finalize的区别

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

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

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

transient关键字

  • 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。

  • transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。

  • 被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。

具体细节参见:Java transient关键字使用小记

extends 和super 泛型限定符

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


数据类型部分

int和Integer的区别

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

  • 原始类型:boolean、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.在多线程且操作大量字符串用StringBuffer,线程安全,可修改。

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

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

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

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

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

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

方法部分

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

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

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

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

hashcode和equals有什么联系

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

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

类和接口部分

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

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

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

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

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

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

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

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

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

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时,输出的记录是排过序的。

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为每个节点存储两个引用,一个指向前一个元素,另一个指向后一个元素

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

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

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

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

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

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 相当于“外部排序器”,前者一次定义即可,后者可以在不修改源码的情况下进行排序,各有所长。

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

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

什么是迭代器

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

Iterator和ListIterator的区别

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

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左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

HashMap和Hashtable的区别

  • 相同点:

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

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

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

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

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

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

ConcurrentHashMap的原理

ConcurrentHashMap的实现原理与使用

TreeMap的底层实现

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

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

特性部分

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

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

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应用

运算符部分

&和&&的区别

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

== 和 equals 的区别

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

值传递和引用传递是什么

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

其他部分

Java和JavaScript的区别

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

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

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

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

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

JVM虚拟机部分

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堆一样,是各个线程共享的内存区域。它主要存放已被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、常量、静态变量、即时编译器编译后的代码等数据。
虽然Java虚拟机规范把方法区描述为堆的一个非逻辑区域,但是它有一个别名非堆(Non-Heap),目的应该是为了和Java堆分开。
一般的,方法区上 执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一(HotSpot),但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。

运行时常量池

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

直接内存

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

直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是 JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区 (Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。 由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。

JVM内存模型说明

直接说内存模型太抽象了,下面通过举个栗子来说明一下,在类创建和程序执行过程中数据具体存在哪里。

1)JVM内存分配策略

Java程序运行时的内存分配策略有三种:静态分配栈式分配堆分配,三种分配策略使用的内存空间分别是静态存储区(即方法区)、栈区(即虚拟机栈和本地方法栈)以及堆区(Java堆)

  • 静态存储区(方法区):主要存放静态数据、全局static数据常量。这块内存在程序编译时就已经分配好了,并且在程序整个运行期间都存在。
  • 栈区(虚拟机栈和本地方法栈):当方法被执行时,方法体内的局部变量(其中包括基本数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存都将被自动释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。此外,虚拟机栈和本地方法栈为线程私有,他们在线程启动时创建,随着线程结束而死亡。
  • 堆区(Java堆):又称动态内存分配,通常就是指在程序运行时直接new处理的内存,也就是对象的实例。这部分内存在不使用时将会由Java垃圾回收器来负责回收。

2)栈与堆的区别

在方法体内定义的(局部变量)一些基本数据类型的变量和对象的引用都是在方法的栈内存中创建和分配的。当在一段方法块中定义了一个变量时,Java就会在栈内存中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给他的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存用了存放所有由new创建的对象(包括该对象中的所有成员变量)和数组。在堆中分配的内存,将由Java垃圾回收器来自动管理。在堆中创建了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是堆内存中对象的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

栗1

public class Demo{
	int s1 = 10;
    Demo demo1 = new Demo();
    
    public void method(){
        int s2 = 20;
        Demo demo2 = new Demo();
    }
    
    public static void main(String[] args){
        Demo demo3 = new Demo();
    }
}

栗1中:

Demo类的局部变量s2和引用变量demo2都存在栈中,因为他们在方法体中定义和创建,因此属于局部变量,并且是线程私有的。但demo2所指向的对象是存在于堆上的,因为所有的对象实体和数组实体都在堆中创建和分配内存。

demo3指向的对象实体存在堆中,包括这个对象的所有成员变量s1和demo1,而demo3本身在main方法中定义,属于main方法的局部变量,因此demo3的引用变量存在于栈中。

其中特别需要注意的是:

1) 局部变量基本数据类型和引用变量存储在中,引用变量的对象实体存储于中。——因为他们属于方法中的变量,生命周期随方法执行结束而结束。

2) 成员变量全部存储于堆中(包括基本数据类型、引用和引用的对象实体)。——因为他们属于类,类对象终究是要被new出来使用的。

如何解决OOM报错

  1. 增加JVM分配内存

  2. 使用内存分析工具

    1. eclipse插件:MAT

    2. IDEA插件:JProfile

      1. 这两个插件的功能类似

      2. 使用JProfile时,需要通过修改VM初始化参数生成Java的dump文件,具体参数如下:

        1. /**
           * -Xms   设置初始化分配的内存大小,默认为本机物理内存的1/64
           * -Xmx   设置最大分配的内存大小,默认为本机物理内存的1/4
           * -XX:+HeapDumpOnOutOfMemoryError   当出现OOM错误时将运行的Java文件dump到本地
           */
          -Xms10m -Xmx100m -XX:+HeapDumpOnOutOfMemoryErr
          
        2. dump后的文件通常位于当前项目的src目录下

        3. 使用JProfile工具打开dump文件,查看其中最大的对象以及报错的行数

轻GC(Minor GC或Young GC)和重GC(Full GC)分别在什么时候发生

从新生代(包括EdenSurvivor区域)回收内存被称为轻GC。执行轻GC操作时,不会影响到永久代。

轻GC的触发时机:当JVM无法为一个新的对象分配空间时会出发轻GC,比如当Eden区满了。

重GC的触发时机

  1. 发生轻GC之前进行检查,如果“老年代可用的连续内存空间”<“新生代所有轻GC后升入老年代的对象总和的平均大小”,则说明本次轻GC后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间的大小,此时必须先触发一次重GC给老年代腾出更多空间,然后再执行轻GC。
  2. 执行轻GC之后有一批对象需要放入老年代,此时老年代已经没有足够的空间存放这些对象了,此时必须立即触发一个重GC。
  3. 老年代内存使用率超过了92%,则直接触发重GC(这个比例是可以通过参数调整的)。

Major GC是清理老年代,Full GC是清理整个堆空间——包括新生代和永久代

Java垃圾收集器(或者叫垃圾回收器)

  1. Serial收集器:新生代收集器,使用单线程进行GC(即垃圾回收),采用(停止)复制算法,停止的意思是指当回收内存时,需要暂停其他所有线程的执行。
  2. ParNew收集器:新生代收集器,Serial收集器的多线程版,采用(停止)复制算法。
  3. Parallel Scavenge收集器:新生代收集器,采用(停止)复制算法,关注CPU的吞吐量,即运行用户代码的时间/总时间。
  4. Serial Old收集器:老年代收集器,使用单线程进行GC,采用标记整理算法。
  5. Parallel Old收集器:老年代收集器,使用多线程进行GC,采用标记整理算法。
  6. CMS收集器:老年代收集器,多线程GC,采用标记清除算法,优点是并发收集,停顿小。

Java垃圾回收算法

  1. 引用计数(Reference Counting)

    假设有一个对象A,任何对象对A进行引用后,A对象的引用计数器就 +1,如果引用失败则计数器 -1,如果A的引用计数器的值为0,则说明A没有被引用,可以被回收。

  2. 标记清除算法(Mark-Sweep)

    如图所示:

image.png

标记整除算法主要分为两个阶段:

​ 1)标记阶段:标记出所有需要被回收的对象(如果当前对象已经被其他对象所引用,说明不需要回收,则不被标记)

​ 2)清除阶段:回收被标记的对象所占的空间

优点:实现容易 缺点:容易产生内存碎片

  1. 标记复制算法(Mark-Copying)【也叫复制算法(Copying)】

    image.png

    1)为了解决标记清除算法的缺点,标记复制算法被提出来。他将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。

    2)很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

    优点:实现简单,运行高效且不容易产生内存碎片 缺点:对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半

  2. 标记整理算法(Mark-Compact)

    image.png

    为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

  3. 分代收集算法(Generational Collection)

    1)分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

    2)目前大部分垃圾收集器对于新生代都采取标记复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

    3)而由于老年代的特点是每次回收都只回收少量对象,一般使用的是标记整理算法。

    注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

Java对象的创建过程

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

内存分配

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

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

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

对象的访问定位

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

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

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

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。


多线程部分

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

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

并发与并行的区别

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

产生死锁的四个必要条件

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

如何预防和避免线程死锁

如何预防死锁?

破坏死锁的产生的必要条件即可:

  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修饰普通成员方法时,锁住的是this对象;

synchronized和lock

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

构造方法可以使用 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;
}

细节小问题汇总

1. 为什么会出现4.0-3.6=0.40000001这种现象?

答:原因简单来说是这样:2进制的小数无法精确的表达10进制小数,计算机在计算10进制小数的过程中要先转换为2进制进行计算,这个过程中出现了误差。

2. 一个十进制的数在内存中是怎么存的?

答:补码的形式。

3. 静态变量存在什么位置?

答:方法区

4. 请判断当一个对象被当作参数传递给一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?

答:值传递。Java 编程语言只有值传递参数。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的内容可以在被调用的方法中改变,但对象的引用是永远不会改变的。【详解见 深入理解Java中方法的参数传递机制

5. ArrayList是否会越界?

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

6. ConcurrentHashMap锁加在了哪些地方?

答:加在每个Segment 上面


Reference

0

评论区