2021准备面试-基础篇

一.Java基础 23

1.1 重载和重写的区别(了解) 23

1.2 String和StringBuffer,StringBuilder 的区别是什么?String 为什么是不可变的?(必会) 23

1.3 自动装箱与拆箱(了解) 24

1.4 == 与 equals (必会) 24

1.5 关于final关键字的一些总结(必会) 25

1.6 Java 中的异常处理(了解) 26

1.7 error 和 exception 的区别?(了解) 26

1.8 接口和抽象类的区别是什么(必会) 27

1.9 什么是单例模式?有几种?(必会) 27

1.10 手写冒泡排序?(必会) 28

1.11 BIO、NIO、AIO 有什么区别?(了解) 29

1.12常见的数据结构有哪些?(了解) 29

1.13 Java集合体系有什么?(必会) 30

1.14 List 的三个子类的特点(必会) 33

1.15 List 和 Map、Set 的区别(必会) 33

1.16 HashMap底层实现原理(必会) 34

1.17谈一下 hashMap 中什么时候需要进行扩容,扩容 resize()又是如何实现的?(高薪常问) 37

1.18 ConcurrentHashMap特点(高薪常问) 38

1.19 HashTable(了解) 40

1.20 HashMap 和 HashTable 有什么区别?(必会) 40

1.21 HashMap,HashTable,ConcurrentHashMap之间的区别,及性能对比(必会) 40

1.22什么是线程?线程和进程的区别?(必会) 41

1.23 创建线程有几种方式(必会) 41

1.24 线程的基本方法有什么?(必会) 44

1.25 在 java 中 wait 和 sleep 方法的不同?(必会) 45

1.26 线程池原理(高薪常问) 45

1.27 线程执行的顺序(高薪常问) 47

1.28 线程池的核心参数有哪些?(高薪常问) 47

1.29死锁产生的条件以及如何避免?(高薪常问) 48

1.30 JVM是什么?JVM的基本结构 (高薪常问) 49

1.31 JVM内存结构(高薪常问) 50

1.32 类的加载, 类加载器的种类, 类加载机制(高薪常问) 51

1.33什么是GC(高薪常问) 53

1.34 JVM调优的工具有哪些?(高薪常问) 54

1.35常用的 JVM 调优的参数都有哪些?(高薪常问) 54

一.Java基础

1.1 重载和重写的区别(了解)

重载: 发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。

重写: 发生在父子类中,方法名,参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。

1.2 String和StringBuffer,StringBuilder 的区别是什么?String 为什么是不可变的?(必会)

可变性

简单的来说:String 类中使用 final 关键字字符数组保存字符串, private final char value[] ,所以 String对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在AbstractStringBuilder中也是使用字符数组保存字符串 char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。

StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。

AbstractStringBuilder.java

abstract class AbstractStringBuilder implements Appendable, CharSequence {

        char[] value;

        int count;

        AbstractStringBuilder() {

        }

        AbstractStringBuilder(int capacity) {

        value = new char[capacity];
        }

}

线程安全性

String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity.append.insert.indexOf 等公共 方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。

StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

1. 操作少量的数据 => 使用String

2. 单线程操作字符串缓冲区下操作大量数据 => 使用StringBuilder

3. 多线程操作字符串缓冲区下操作大量数据 => 使用StringBuffer

1.3 自动装箱与拆箱(了解)

**装箱:**将基本类型用它们对应的引用类型包装起来;

**拆箱:**将包装类型转换为基本数据类型;

1.4 == 与 equals (必会)

== : 它的作用是判断两个对象的地址是不是相等。即: 判断两个对象是不是同一个对象。(基本数据类型比较的是值,引用数据类型比较的是内存地址)。

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况,如下:

情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过"=="比较这两个对象。

情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

public class test1 {

public static void main(String[] args) {

    String a = new String("ab"); // a 为一个引用

    String b = new String("ab"); // b为另一个引用,对象的内容一样

    String aa = "ab"; // 放在常量池中

    String bb = "ab"; // 从常量池中查找

    if (aa == bb) // true

    System.out.println("aa==bb");

    if (a == b) // false,非同一对象

    System.out.println("a==b");

    if (a.equals(b)) // true

    System.out.println("aEQb");

    if (42 == 42.0) // true

    System.out.println("true");

    }

}

说明:

String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的equals 方法比较的是对象的值。

当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

1.5 关于final关键字的一些总结(必会)

final关键字主要用在三个地方:变量、方法、类。

1. 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

2. 当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。

3. 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。

在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。

1.6 Java 中的异常处理(了解)

在 Java 中,所有的异常都有一个共同的祖先java.lang包中的Throwable类

Throwable: 有两个重要的子类:
在这里插入图片描述

Exception(异常)Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。

1.7 error 和 exception 的区别?(了解)

Error 类和 Exception 类的父类都是 Throwable 类,他们的区别如下。

Error 类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。对于这类 错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。

Exception 类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。

Exception 类又分为运行时异常(Runtime Exception)和受检查的异常(CheckedException ),运行时异常。

1.8 接口和抽象类的区别是什么(必会)

1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),抽象类可以有非抽象的方法。

2. 接口中的实例变量默认是 final 类型的,而抽象类中则不一定。

3. 一个类可以实现多个接口,但最多只能实现一个抽象类。

4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定。

5. 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。

备注: 在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,必须重写,不然会报错。

1.9 什么是单例模式?有几种?(必会)

单例模式:某个类的实例在 多线程环境下只会被创建一次出来。

单例模式有饿汉式单例模式、懒汉式单例模式和双检锁单例模式三种。

饿汉式:线程安全,一开始就初始化。

class Singleton {
    private  static Singleton instance=new Singleton() ;
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}

懒汉式:非线程安全,延迟初始化。

class Singleton {
    private  static Singleton instance ;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance==null){
          instance = new Singleton();
        }
        return instance;
    }
}

双检锁:线程安全,延迟初始化。

class Singleton {
    private volatile static Singleton instance ;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance==null){
           synchronized (Singleton.class){
               if(instance==null){
                   instance = new Singleton();
               }
           }
        }
        return instance;
    }
}

1.10 手写冒泡排序?(必会)

class Sourt {
    public static  void sort(){
        Scanner input = new Scanner(System.in);
        int sort[] = new int[10];
        int temp;
        System.out.println("请输入10个排序的数据");
        for (int i = 0; i < sort.length; i++) {
            sort[i] = input.nextInt();
        }
        for (int i = 0; i < sort.length - 1; i++) {
            for (int j = 0; j < sort.length - i - 1; j++)    {
                if (sort[j] < sort[j + 1]) {
                     temp = sort[j];
                     sort[j] = sort[j + 1];
                     sort[j + 1] = temp;
                }
            }
        }
        System.out.println("排列后的顺序为:");
        for(int i=0;i<sort.length;i++){
            System.out.print(sort[i]+"======");
        }
    }
    public static void main(String[] args) {
        sort();

    }
}

1.11 BIO、NIO、AIO 有什么区别?(了解)

BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。

NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。

AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。

1.12常见的数据结构有哪些?(了解)

  • 数组:

数组是最常用的数据结构,数组的特点是长度固定可以用下标索引并且所有的元素的类型都是一致的。数组常用的场景有:从数据库里读取雇员的信息存储为EmployeeDetail[ ];把一个字符串转换并存储到一个字节数组中便于操作和处理等等。尽量把数组封装在一个类里,防止数据被错误的操作弄乱。另外,这一点也适合其他的数据结构。

  • 列表:

列表和数组很相似,只不过它的大小可以改变列表一般都是通过一个固定大小的数组实现的,并且在需要的时候自动调整大小。列表里可以包含重复的元素。常用的场景有,添加一行新的项到订单列表里,把所有过期的商品移出商品列表等等。一般会把列表初始化成一个合适的大小,以减少调整大小的次数。

  • 集合:

集合和列表很相似,不过它不能放重复的元素。

  • 堆栈:

堆栈只允许对最后插入元素进行操作(也就是后进先出,Last In First Out – LIFO)。如果你移除了栈顶的元素,那么你可以操作倒数第二个元素,依次类推。这种后进先出的方式是通过仅有的peek(),push()和pop()这几个方法的强制性限制达到的。

  • 队列:

队列和堆栈有些相似,不同之处在于在队列里第一个插入的元素也是第一个被删除的元素(即是先进先出)。这种先进先出的结构是通过只提供peek(),offer()和poll()这几个方法来访问数据进行限制来达到的。例如,排队等待公交车,银行或者超市里的等待列队等等,都是可以用队列来表示。

  • 链表:

链表是一种由多个节点组成的数据结构,并且每个节点包含有数据以及指向下一个节点的引用,在双向链表里,还会有一个指向前一个节点的引用。例如,可以用单向链表和双向链表来实现堆栈和队列,因为链表的两端都是可以进行插入和删除的动作的。当然,也会有在链表的中间频繁插入和删除节点的场景。Apache的类库里提供了一个TreeList的实现,它是链表的一个很好的替代,因为它只多占用了一点内存,但是性能比链表好很多。也就是说,从这点来看链表其实不是一个很好的选择。

1.13 Java集合体系有什么?(必会)

集合类存放于 Java.util 包中,主要有 3 种:set(集)、list(列表包含 Queue)和 map(映射)。

1. Collection:Collection 是集合 List、Set、Queue 的最基本的接口。

2. Iterator:迭代器,可以通过迭代器遍历集合中的数据。

3. Map:是映射表的基础接口。

在这里插入图片描述

在这里插入图片描述

1. Iterator接口

Iterator接口是一个用于遍历集合中元素的接口,主要包含hashNext(),next(),remove()三种方法。如果实现Iterator接口,那么在遍历集合中元素的时候,只能往后遍历,被遍历后的元素不会在遍历到,通常无序集合实现的都是这个接口,比如HashSet,HashMap

2. LinkedIterator接口:

LinkedIterator在Iterator的基础上又添加了add(),previous(),hasPrevious()三种方法,那些元素有序的集合,实现的一般都是LinkedIterator接口,实现这个接口的集合可以双向遍历,既可以通过next()访问下一个元素,又可以通过previous()访问前一个元素,比如ArrayList。

3.Collection (集合的最大接口)继承关系

  • List 可以存放重复的内容

  • Set 不能存放重复的内容,所以的重复内容靠hashCode()和equals()两个方法区分

  • Queue 队列接口

  • SortedSet可以对集合中的数据进行排序

Collection定义了集合框架的共性功能:

在这里插入图片描述

add方法的参数类型是Object。以便于接收任意类型对象。

集合中存储的都是对象的引用(地址)。

4.List

凡是可以操作角标的方法都是该体系特有的方法:

在这里插入图片描述

  • ArrayList 线程不安全,查询速度快

  • Vector 线程安全,但速度慢,已被ArrayList替代

  • LinkedList 链表结果,增删速度快

  • TreeList 树型结构,保证增删复杂度都是O(log n),增删性能远高于ArrayList和 LinkedList,但是稍微占用内存

5.Set

  • Set:元素是无序(存入和取出的顺序不一定一致),元素不可以重复

  • HashSet:底层数据结构是哈希表,线程不安全的, 数据不同步。

  • HashSet是如何保证元素唯一性的呢?

是通过元素的两个方法,hashCode和equals来完成。

如果元素的HashCode值相同,才会判断equals是否为true。

如果元素的hashcode值不同,不会调用equals。

注意,对于判断元素是否存在,以及删除等操作,依赖的方法是元素的 hashcode和equals方法。

  • TreeSet:底层数据结构是二叉树,存放有序:TreeSet线程不安全可以对Set集合中的元素进行排序。通过compareTo或者compare方法来保证元素的唯一性。

6.Map

Correction、Set、List接口都属于单值的操作,而Map中的每个元素都使用key------>value的形式存储在集合中。

Map集合:该集合存储键值对, 是key:value一对一对往里存, 而且要保证键的唯一性。

在这里插入图片描述

Map接口的常用子类

  • HashMap:底层数据结构是哈希表,允许使用 null 值和 null 键,该集合是数据不同步的,将hashtable替代,jdk1.2.效率高。

  • TreeMap:底层数据结构是**二叉树,**线程不同步,可以用于给map集合中的键进行排序。

1.14 List 的三个子类的特点(必会)

ArrayList 底层结构是数组,底层查询快,增删慢

LinkedList 底层结构是链表型的,增删快,查询慢

Voctor 底层结构是数组 线程安全的,增删慢,查询慢

1.15 List 和 Map、Set 的区别(必会)

结构特点:

List 和 Set 是存储单列数据的集合,Map 是存储键和值这样的双列数据的集合;

List 中存储的数据是有顺序,并且允许重复;

Map 中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的,Set中存储的数据是无序的,且不允许有重复,但元素在集合中的位置由元素的 hashcode 决定,位置是固定的(Set 集合根据 hashcode 来进行数据的存储,所以位置是固定的,但是位置不是用户可以控制的,所以对于用户来说 set 中的元素还是无序的);

实现类

List 接口有三个实现类(LinkedList:基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还存储下一个元素的地址。链表增删快,查找慢;ArrayList:基于数组实现,非线程安全的,效率高,便于索引,但不便于插入删除;Vector:基于数组实现,线程安全的,效率低)。

Map 接口有三个实现类(HashMap:基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 值和 null键;HashTable:线程安全,低效,不支持 null 值和 null 键;LinkedHashMap:是 HashMap 的一个子类,保存了记录的插入顺序;SortMap 接口:TreeMap,能够把它保存的记录根据键排序,默认是键值的升序排序)。

Set 接口有两个实现类(HashSet:底层是由 HashMap 实现,不允许集合中有重复的值,使用该方式时需要重写 equals()和 hashCode()方法;LinkedHashSet:继承与 HashSet,同时又基于 LinkedHashMap 来进行实现,底层使用的是 LinkedHashMp)。

1.16 HashMap底层实现原理(必会)

HashMap根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值.因而具有很快的访问速度,但是遍历顺序却不确定的.HashMap最多只允许一条记录的键为null,允许多条记录的值为null。

HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。

如果需要满足线程安全,可以用Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap,HashTable.如下详细介绍。

  • JDK1.8之前 (数组+链表)

  • 最开始存入数据的时候

  1. JDK1.8之前HashMap底层是数组和链表结合在一起使用,也就是链表散列.数组的长度规定是2的幂.数组中存放的对象是Entry<key,value>对象 ,不允许有重复的key存在,为什么呢?

  2. 首先,先判断key存放的位置, HashMap通过key的hashCode经过扰动函数处理过后得到hash值,然后通过(n-1)&hash判断当前元素存放的位置(这里的n指的是数组的长度). 也可以理解:key%数组长度=对应数组的索引下标.然后将value存入到entry对象中。

所谓扰动函数知道就是HashMap的hash方法.使用hash方法也就是扰动函数是为了防止一些实现比较差的hashCode()方法,换句话说使用扰动函数之后可以减少碰撞。

  1. 为什么使用(n-1)&length长度呢?

(1)保证数组不会越界:

首先我们要知道,在HashMap和ConcurrentHashMap中,数组的长度按规定一定是2的幂(2的n次方)因此,数组的长度的二进制形式是:10000…000,1后面有一堆0。那么tab.length - 1 的二进制形式就是01111…111,0后面有一堆1。最高位是0, 和hash值相"与",结果值一定不会比数组的长度值大,因此也就不会发生数组越界.

(2)保证元素尽可能的均匀分布

在操作的时候,链表中的元素越多,效率越低,因为要不停的对链表循环比较.所以,一定要使哈希均匀分布,尽量减少哈希冲突,提高效率。

  • 继续存入数据

继续存入数据,还是要通过第1步计算key在数组中的索引位置. 如果当前位置存在元素的话,再通过key的equal()方法判断key是否相同,如果相同value值就会覆盖; 如果key的equals()方法不同,则在数组对应索引位置变为链表存储新的Entry<key,value>。

  • 拉链法

上一步说到的链表是拉链法: 将链表和数组相结合.也就是说创建一个链表数组,数组中每一格就是一个链表.若约到哈希冲突,则将冲突的值加到链表中即可.

在这里插入图片描述

  • JDK1.8之后(数组+链表+红黑树)

如果链表的长度超过8则转为红黑树, 当红黑树中的元素小于6时又变为链表(有这些变化的原因就是综合时间复杂度以及空间复杂度的考虑)获取时,直接找到key的hash值对应的下标,在进一步用equels方法判断key是否相同,从而找到对应值则返回找不到则返回null。

相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阀值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

在这里插入图片描述
在这里插入图片描述

1.17谈一下 hashMap 中什么时候需要进行扩容,扩容 resize()又是如何实现的?(高薪常问)

调用场景:

  1. 初始化数组 table

  2. 当数组 table 的 size 达到阙值时进行扩容

实现过程:

通过判断旧数组的容量是否大于0来判断数组是否初始化过。

  • 如果小于0:进行初始化,判断是否调用无参构造器。

如果调用了无参构造器: 使用默认的大小和阙值<阈值 12. 阀值=默认 大小为16乘以负载因子0.75。

如果没有调用无参构造器: 使用构造函数中初始化的容量, 当然这个容 量是经过tableSizefor 计算后的 2 的次幂数) 。

  • 如果大于0: 进行扩容,扩容成两倍(小于最大值的情况下),之后在进行将元素重新进行与运算复制到新的散列表中。

概括的讲:

扩容需要重新分配一个新数组,新数组是老数组的2倍长,然后遍历整个老结**构,**把所有的元素挨个重新hash分配到新结构中去。PS:可见底层数据结构用到了数组,到最后会因为容量问题都需要进行扩容操作。

1.18 ConcurrentHashMap特点(高薪常问)

Segment 段

ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表"部分"或"一段"的意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了"槽"来代表一个segment。

线程安全(Segment 继承 ReentrantLock 加锁)

简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个Segment 是线程安全的,也就实现了全局的线程安全。

在这里插入图片描述

并行度(默认 16)

concurrencyLevel:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。

Java8 实现 (引入了红黑树)

Java8 对 ConcurrentHashMap 进行了比较大的改动,Java8 也引入了红黑树

1.19 HashTable(了解)

Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。

1.20 HashMap 和 HashTable 有什么区别?(必会)

HashMap 是线程不安全的,HashMap 是一个接口,是 Map 的一个子接口,是将键映射到值得对象,不允许键值重复,允许空键和空值;由于非线程安全,HashMap 的效率要较 HashTable 的效率高一些.

HashTable 是线程安全的一个集合,不允许 null 值作为一个 key 值或者 Value 值;

HashTable 是 sychronize,多个线程访问时不需要自己为它的方法实现同步,而 HashMap 在被多个线程访问的时候需要自己为它的方法实现同步;

1.21 HashMap,HashTable,ConcurrentHashMap之间的区别,及性能对比(必会)

**性能:**ConcurrentHashMap(线程安全) > HashMap > HashTable(线程安全)

区别对比一(HashMap和HashTable区别):

1、HashMap是非线程安全的,HashTable是线程安全的。

2、HashMap的键和值都允许有null值存在,而HashTable则不行。

3、因为线程安全的问题,HashMap效率比HashTable的要高。

4、Hashtable是同步的,而HashMap不是。因此,HashMap更适合于单线程环境,而Hashtable适合于多线程环境。一般现在不建议用HashTable, ①是HashTable是遗留类,内部实现很多没优化和冗余。②即使在多线程环境下,现在也有同步的ConcurrentHashMap替代,没有必要因为是多线程而用HashTable。

区别对比二(HashTable和ConcurrentHashMap区别):

HashTable使用的是Synchronized关键字修饰,ConcurrentHashMap是使用了锁分段技术来保证线程安全的。

Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。

ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。

1.22什么是线程?线程和进程的区别?(必会)

**线程:**是进程的一个实体,是 cpu 调度和分派的基本单位,是比进程更小的可以独立运行的基本单位。

**进程:**具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位。

**特点:**线程的划分尺度小于进程,这使多线程程序拥有高并发性,进程在运行时各自内存单元相互独立,线程之间 内存共享,这使多线程编程可以拥有更好的性能和用户体验。

注意:多线程编程对于其它程序是不友好的,占据大量 cpu 资源。

1.23 创建线程有几种方式(必会)

1.继承 Thread 类

Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。

public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("MyThread.run()");
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }

}

2.实现Runnable接口

如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现

一个 Runnable 接口。

public class MyThread implements Runnable{
    @Override
    public void run(){
        System.out.println("MyThread.run()");
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread);
        thread.run();
    }

}

3.实现Callable接口

Callabled接口有点儿像是Runnable接口的增强版,它以call()方法作为线程执行体, call()方法比run()方法功能更强大。

call()方法可以有返回值,可以声明抛出异常类

获取call()方法里的返回值: 通过FutureTask类(实现Future接口)的实例对象的get()方法得到,得到结果类型与创建TutureTask类给的泛型一致。

具体代码实现

1.定义实现Callable接口的实现类,并实现call()方法。注意:Callable有泛型限制,与返回值类型一致。这里是Integer

public class ThirdThread implements Callable<Integer>{//重写call()方法}

2.再创建Callable实现类的实例tt。

ThirdThread tt = new ThirdThread();

3.使用FutureTask类包装Callable的实例tt。

FutureTask<Integer> task = new FutureTask<Integer>(tt);//注意:泛型限制与返回结果一致。

4.以FutureTask对象(task)作为Thread的target来创建线程,并启动。

new Thread(task, “线程”).start();

5.调用FutureTask对象(task)的get()方法获得返回值

Integer result = task.get();//会有异常

  1. 通过线程池方法
  • 什么是线程池,如何使用?

线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率。

在 JDK 的 java.util.concurrent.Executors 中提供了生成多种线程池的静态方法.

在这里插入图片描述

然后调用他们的 execute 方法即可。

合理利用线程池能够带来三个好处。

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

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

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

  • 常用的线程池有哪些?

**newSingleThreadExecutor:**创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

**newFixedThreadPool:**创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。

**newCachedThreadPool:**创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。

**newScheduledThreadPool:**创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。

1.24 线程的基本方法有什么?(必会)

线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等。

1.线程等待(wait)

调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。

2.线程睡眠(sleep)

sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态.

3.线程让步(yield)

yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下, 优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对 线程优先级并不敏感。

4.线程中断(interrupt)

中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)

5.Join 等待其他线程终止

join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸.

6.线程唤醒(notify)

Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。

1.25 在 java 中 wait 和 sleep 方法的不同?(必会)

最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。wait 通常被用于线程间交互,sleep 通常被用于暂停执行。

1.26 线程池原理(高薪常问)

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。

线程复用:

每一个 Thread 的类都有一个 start 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run 方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写 Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。

线程池的组成:

一般的线程池主要分为以下 4 个组成部分:

1. 线程池管理器:用于创建并管理线程池

2. 工作线程:线程池中的线程

3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行

4. 任务队列:用于存放待处理的任务,提供一种缓冲机制

拒绝策略:

线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。

JDK 内置的拒绝策略如下:

1. AbortPolicy : 直接抛出异常,阻止系统正常运行。

2. CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。

3. DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。

4. DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案

Java 线程池工作过程:

1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:

a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;

b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;

c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。

3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。

4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

1.27 线程执行的顺序(高薪常问)

  1. 当线程数小于核心线程数时,会一直创建线程直到线程数等于核心线程数;

  2. 当线程数等于核心线程数时,新加入的任务会被放到任务队列等待执行;

  3. 当任务队列已满,又有新的任务时,会创建线程直到线程数量等于最大线程数;

  4. 当线程数等于最大线程数,且任务队列已满时,新加入任务会被拒绝。

1.28 线程池的核心参数有哪些?(高薪常问)

默认参数:

corePoolSize = 1

queueCapacity = Integer.MAX_VALUE

maxPoolSize = Integer.MAX_VALUE

keepAliveTime = 60秒

allowCoreThreadTimeout = false

rejectedExecutionHandler = AbortPolicy()

具体讲解:

1.corePoolSize(核心线程数)

(1)核心线程会一直存在,即使没有任务执行;

(2)当线程数小于核心线程数的时候,即使有空闲线程,也会一直创建线程直到达到核心线程数;

(3)设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭。

2.queueCapacity(任务队列容量)

也叫阻塞队列,当核心线程都在运行,此时再有任务进来,会进入任务队列,排队等待线程执行。

3.maxPoolSize(最大线程数)

(1)线程池里允许存在的最大线程数量;

(2)当任务队列已满,且线程数量大于等于核心线程数时,会创建新的线程执行任务;

(3)线程池里允许存在的最大线程数量。当任务队列已满,且线程数量大于等于核心线程数时,会创建新的线程执行任务。

4.keepAliveTime(线程空闲时间)

(1)当线程空闲时间达到keepAliveTime时,线程会退出(关闭),直到线程数等于核心线程数;

(2)如果设置了allowCoreThreadTimeout=true,则线程会退出直到线程数等于零。<allowCoreThreadTimeout(允许核心线程超时)>

当线程数量达到最大线程数,且任务队列已满时,会拒绝任务;

调用线程池shutdown()方法后,会等待执行完线程池的任务之后,再shutdown()。如果在调用了shutdown()方法和线程池真正shutdown()之间提交任务,会拒绝新任务。

1.29死锁产生的条件以及如何避免?(高薪常问)

死锁产生的四个必要条件:

互斥:一个资源每次只能被一个进程使用(资源独立)。

请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放(不释放锁)。

不剥夺:进程已获得的资源,在未使用之前,不能强行剥夺(抢夺资源)。

循环等待:若干进程之间形成一种头尾相接的循环等待的资源关闭(死循环)。

避免死锁:

1. 破坏"互斥"条件:系统里取消互斥、若资源一般不被一个进程独占使用,那么死锁是肯定不会发生的,但一般"互斥"条件是无法破坏的,因此,在死锁预防里主要是破坏其他三个必要条件,而不去涉及破坏"互斥"条件。

2. 破坏"请求和保持"条件:

方法1:所有的进程在开始运行之前,必须一次性的申请其在整个运行过程各种所需要的全部资源。

优点:简单易实施且安全。​

缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。

方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到,已经使用完毕的资源,然后再去请求新的资源。这样的话资源的利用率会得到提高,也会减少进程的饥饿问题。

3. 破坏"不剥夺"条件:当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂的释放或者说被抢占了。

4. 破坏"循环等待"条件:可以通过定义资源类型的线性顺序来预防,可以将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于i的资源。

1.30 JVM是什么?JVM的基本结构 (高薪常问)

虚拟机,一种能够运行java字节码的虚拟机。

  • 类加载子系统

  • 加载 .class 文件到内存。

  • 内存结构

  • 运行时的数据区。

  • 执行引擎

  • 执行内存中的.class,输出执行结果(包含GC:垃圾收集器)。

  • 本地方法的接口。

  • 本地方法库。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-41LLRUpq-1609916912495)(media/image21.png)]{width=“5.088888888888889in” height=“5.213888888888889in”}

1.31 JVM内存结构(高薪常问)

JDK1.7

程序计数器

就是一个指针,指向方法区中的方法字节码(用来存储指向下一个指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。

Java虚拟机栈

Java线程执行方法的内存模型,一个线程对应一个栈,每个方法在执行的同时都会创建一个栈帧(用于存储局部变量表,操作数栈,动态链接,方法出口等信息)不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。

本地方法栈

和栈作用很相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行native方法服务。登记native方法,在Execution Engine执行时加载本地方法库。

Java虚拟机管理的最大的一块内存区域,Java堆是线程共享的,用于存放对象实例。也就是说对象的出生和回收都是在这个区域进行的。

方法区

线程共享,用于存储已经被虚拟机加载的类信息、常量、静态变量等数据。

JDK1.8

在这里插入图片描述

JDK1.8与1.7最大的区别是在1.8中方法区是由元空间(元数据区)来实现。常量池移到堆中。

1.32 类的加载, 类加载器的种类, 类加载机制(高薪常问)

  • 类加载

1.加载

将.class文件从磁盘读到内存。

2.连接

2.1 验证: 验证字节码文件的正确性。

2.2 准备: 给类的静态变量分配内存,并赋予默认值。

2.3 解析: 类装载器装入类所引用的其它所有类。

3.初始化

为类的静态变量赋予正确的初始值,上述的准备阶段为静态变量赋予的是虚拟机默认的初始值,此处赋予的才是程序编写者为变量分配的真正的初始值,执行静态代码块。

4.使用

5.卸载

  • 类加载器的种类

1. 启动类加载器(Bootstrap ClassLoader)

负责加载JRE的核心类库,如JRE目标下的rt.jar,charsets.jar等。

2. 扩展类加载器(Extension ClassLoader)

负责加载JRE扩展目录ext中jar类包。

3. 系统类加载器(Application ClassLoader)

负责加载ClassPath路径下的类包。

4. 用户自定义加载器(User ClassLoader)

负责加载用户自定义路径下的类包。

  • 类加载机制

全盘负责委托机制

当A类中引用B类,那么除非特别指定B类的类加载器,否则就直接使用加载A类的类加载器加载B类。

双亲委派机制

指先委托父类加载器寻找目标类,在找不到的情况下再在自己的路径中查找并载入目标类。

1.33什么是GC(高薪常问)

内存空间是有限的,那么在程序运行时如何及时的把不再使用的对象清除将内存释放出来,这就是GC要做的事。

  • GC的区域在哪里?

JVM中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理。因此,我们的内存垃圾回收主要集中于 Java 堆和方法区中,在程序运行期间,这部分内存的分配和使用都是动态的。

  • GC的操作对象是什么?

需要进行回收的对象就是已经没有存活的对象,判断一个对象是否存活常用的有两种办法:引用计数和可达分析。

  • 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

  • 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

在Java语言中,GC Roots包括:

  • 虚拟机栈中引用的对象。

  • 方法区中类静态属性实体引用的对象。

  • 方法区中常量引用的对象。

  • 本地方法栈中JNI引用的对象。

  • GC的时机是什么?

(1)程序调用System.gc时可以触发。

(2)系统自身来决定GC触发的时机(根据Eden区和From Space区的内存大小来决定。当内存大小不足时,则会启动GC线程并停止应用线程)。

  • GC做了哪些事?

主要做了清理对象,整理内存的工作。

  • GC常用算法

GC常用算法有:标记-清除算法,标记-压缩算法,复制算法,分代收集算法。目前主流的JVM(HotSpot)采用的是分代收集算法。

标记-清除算法

为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。

标记-压缩算法(标记-整理)

标记-压缩法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。

复制算法

该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。

分代收集算法

现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。

1.34 JVM调优的工具有哪些?(高薪常问)

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。

1. jconsole:用于对 JVM 中的内存、线程和类等进行监控;

2. jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

1.35常用的 JVM 调优的参数都有哪些?(高薪常问)

XX比X的稳定性更差,并且版本更新不会进行通知和说明。

-Xms:s为strating,表示堆内存起始大小。

-Xmx:x为max,表示最大的堆内存(一般来说-Xms和-Xmx的设置为相同大小,因为当heap自动扩容时,会发生内存抖动,影响程序的稳定性)。

-Xmn:n为new,表示新生代大小(-Xss:规定了每个线程虚拟机栈(堆栈)的大小)。

-XX:SurvivorRator=8 表示堆内存中新生代、老年代和永久代的比为8:1:1。

-XX:PretenureSizeThreshold=3145728表示当创建(new)的对象大于3M的时候直接进入。

-XX:MaxTenuringThreshold=15表示当对象的存活的年龄(minor gc一次加1)大于多少时,进入老年代。

-XX:-DisableExplicirGC表示是否(+表示是,-表示否)打开GC日志。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值