Java 面试题整理(语法和集合部分)

文章目录

  • 1.Java概述
    • 1.1 JVM、JRE和JDK的关系
    • 1.2 Java 语言的特点有哪些
    • 1.3 Java和C++的区别
  • 2.Java基础语法
    • 2.1 九种基本数据类型
    • 2.2 Object 类的公用方法
    • 2.3 final 关键字的用法
    • 2.4 String,StringBuffer和StringBuilder区别
    • 2.5 String str = "i" 与String str = new String("i")一样吗?
    • 2.6 接口和抽象类的区别是什么
    • 2.7 反射机制:框架设计的灵魂
    • 2.8 BIO与NIO、AIO的区别
    • 2.9 File的常用方法
    • 2.10 创建对象的几种方式
  • 3.集合知识点总结:
    • 3.1 体系框架
    • 3.2 ArrayList、LinkedList、Vector的区别和实现原理
    • 3.3 迭代器 Iterator 是什么?
    • 3.4 ArrayList集合一次加入一万条数据,应该如何提高效率
    • 3.5 ArrayList 和LinkedList的区别是什么?
    • 3.6 HashMap的底层实现原理
    • 3.7 HashMap与Hashtable的区别
    • 3.8 HashMap的源码解析
    • 3.9 HashMap 为什么线程不安全
    • 3.10 ConcurrentHashMap基本原理以及线程安全问题
      • 3.10.1ConcurrentHashMap和HashTable的区别
      • 3.10.2 CuncurrentHashMap 基本原理

通过参考网上诸多大佬的博客,归纳整理的一部分Java 面试资料,仅供大家参考

1.Java概述

1.1 JVM、JRE和JDK的关系

  • JVM
    Java Virtual Machine是Java虚拟机,Java程序需要运行在Java虚拟机上,不同开发平台由不同的指令集构成,在各个不同的开发平台上安装有不同的JVM,来达到在不同的平台上运行相同代码的原理。因此Java语言可以实现跨平台特性。
  • JRE
    Java Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。核心类库主要包括java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等。
  • JDK
    Java Development Kit 是用于支持Java程序开发的最小环境,其中包括JRE。JRE是支持Java程序运行的最小环境。

1.2 Java 语言的特点有哪些

  1. 简单性:语法简单,无需深奥训练即可掌握
  2. 面向对象:封装、继承、多态
  3. 跨平台:由于Java虚拟机的存在,可实现平台无关性
  4. 健壮性:Java语言的强类型机制,异常处理等机制,使得开发人员能够快速定位bug
  5. 支持网络编程
  6. 支持多线程
  7. 安全性

1.3 Java和C++的区别

  1. Java是解释型语言,所谓的解释型语言,就是源码会先经过一次编译,成为中间码,中间码再被解释器解释成机器码。对于Java而言,中间码就是字节码(.class),而解释器在JVM中内置了。
  2. C++是编译型语言,所谓编译型语言,就是源码一次编译,直接在编译的过程中链接了,形成了机器码。
  3. C++比Java执行速度快,但是Java可以利用JVM跨平台。
  4. Java是纯面向对象的语言,所有代码(包括函数、变量)都必须在类中定义。而C++中还有面向过程的东西,比如是全局变量和全局函数。
  5. C++中有指针,Java中没有,但是有引用。
  6. C++支持多继承,Java中类都是单继承的。但是继承都有传递性,同时Java中的接口是多继承,类对接口的实现也是多实现。
  7. C++中,开发需要自己去管理内存,但是Java中JVM有自己的GC机制,虽然有自己的GC机制,但是也会出现OOM和内存泄漏的问题。C++中有析构函数,Java中Object的finalize方法。
  8. C++运算符可以重载,但是Java中不可以。同时C++中支持强制自动转型,Java中不行,会出现ClassCastException(类型不匹配)。

2.Java基础语法

2.1 九种基本数据类型

Java 面试题整理(语法和集合部分)_第1张图片

自动装箱拆箱:
一般我们要创建一个类的对象实例的时候,我们会这样:
Class a = new Class(parameter); 而当我们创建一个Integer对象时,却可以这样:int i = 100; (注意:不是 Integer i = new Integer(100); )
实际上,执行上面那句代码的时候,系统为我们执行了:Integer i = Integer.valueOf(100);
此即基本数据类型的自动装箱功能。
而当我们执行 Integer i = 1; int t = i; 时,便把对象中的数据从对象中取出,即实现了自动拆箱。

Integer 的自动拆箱

//在-128~127 之外的数
 Integer i1 =200;  
 Integer i2 =200;          
 System.out.println("i1==i2: "+(i1==i2));                   
 // 在-128~127 之内的数
 Integer i3 =100;  
 Integer i4 =100;  
 System.out.println("i3==i4: "+(i3==i4));
 输出的结果是:
    i1==i2: false
    i3==i4: true

在Java 5后,在Integer的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。当Integer需要自动装箱时,如果在整数区间-128 ~ 127 之中,会直接引用缓存中的对象,避免了新建对象。如此以来,即可避免装箱或拆箱操作频繁的创建对象。
具体实现源码如下:

    @HotSpotIntrinsicCandidate
    public static Integer valueOf(int i) {
        return i >= -128 && i <= Integer.IntegerCache.high ? Integer.IntegerCache.cache[i + 128] : new Integer(i);
    }

2.2 Object 类的公用方法

    @HotSpotIntrinsicCandidate
    public Object() {
    }

    @HotSpotIntrinsicCandidate
    public final native Class<?> getClass();

    @HotSpotIntrinsicCandidate
    public native int hashCode();

    public boolean equals(Object obj) {
        return this == obj;
    }

    @HotSpotIntrinsicCandidate
    protected native Object clone() throws CloneNotSupportedException;

    public String toString() {
        return this.getClass().getName() + "@" + Integer.toHexString(this.hashCode());
    }

    @HotSpotIntrinsicCandidate
    public final native void notify();

    @HotSpotIntrinsicCandidate
    public final native void notifyAll();

    public final void wait() throws InterruptedException {
        this.wait(0L);
    }

    public final native void wait(long var1) throws InterruptedException;

    public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
        if (timeoutMillis < 0L) {
            throw new IllegalArgumentException("timeoutMillis value is negative");
        } else if (nanos >= 0 && nanos <= 999999) {
            if (nanos > 0) {
                ++timeoutMillis;
            }

            this.wait(timeoutMillis);
        } else {
            throw new IllegalArgumentException("nanosecond timeout value out of range");
        }
    }
  1. clone()方法 :返回一个独立的对象,和原对象地址不同,属性相同。并且只有实现Cloneable接口才能调用clone方法。返回的对象为Object类型,可通过强转转回原对象类型。
  2. equals()方法:判断两个对象是否相等,和“==”作用相同,一般在子类中被重写。
  3. hashCode()方法:该方法时为了配合基于散列的集合可以正常运行(例如,当向集合中插入对象时,如何判别在集合中是否已经存在该对象?(集合中不允许重复的元素存在)),重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。
  4. getClass()方法:final方法,返回此Object的运行时类。
  5. wait() 方法
    使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。
    wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。
    调用该方法后当前线程进入睡眠状态,直到以下事件发生:
    • 其他线程调用了该对象的notify方法
    • 其他线程调用了该对象的notifyAll方法
    • 其他线程调用了interrupt中断该线程
    • 时间间隔到了
      此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常
  6. notify() 方法:唤醒该对象某个处于等待状态的线程。
  7. notifyAll()方法:唤醒在该对象上等待的所有线程
  8. toString()方法:把类转换成字符串,一般子类都会进行重写。

2.3 final 关键字的用法

  • 项目被final修饰的类不可以被继承
  • 被final修饰的方法不可以被重写
  • 被final修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变.
  • 被final修饰的方法,JVM会尝试将其内联,以提高运行效率
  • 被final修饰的常量,在编译阶段会存入常量池中.

2.4 String,StringBuffer和StringBuilder区别

String StringBuffer StringBuilder
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间 StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。 可变类,速度更快
不可变 可变类 可变类
线程安全 线程不安全
多线程操作字符串 单线程操作字符串

2.5 String str = “i” 与String str = new String(“i”)一样吗?

当然不一样。原因很简单,因为他们不是同一个对象。
首先来看String str = “i”,这句话的意思是把“i”这个值在内存中的地址赋给str,如果再有String str3 = “i”,那么这句话也是把"i"这个值在内存中的地址赋给str3,这两个引用的是同一个地址值,他们共享同一个内存。
而String str2 = new String(”i“);则是把new String("i”)的对象地址赋给str2,需要注意的是这句话是新创建一个对象。如果再有String str4 = new String(“i”); 那么又相当于新创建了一个对象,然后把对象的地址赋给str4,虽然str2和str4 所指的对象的值是相同的,但他们仍然不是同一个对象。
需要注意的是:String str = "i”; 因为String 是final类型的,所以"i"应该是在常量池。而new String(“i”);则是把新建对象到堆内存。

2.6 接口和抽象类的区别是什么

  1. 抽象类可以提供成员方法的实现体并且成员方法可以是各种类型的,而接口中只能存在public abstract static方法;
  2. 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
  3. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
  4. 设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。
  5. 抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。

2.7 反射机制:框架设计的灵魂

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

反射的应用
在日常业务开发中很少会用到反射机制。但实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/mybatis 等框架也大量使用到了反射机制。

  1. 我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;
  2. Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:
1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 
2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 
3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性

2.8 BIO与NIO、AIO的区别

先理解一下几个概念,同步与异步,阻塞与非阻塞。

同步和异步
同步就是一个任务的完成需要另外一个任务时,只有等被依赖的任务完成后,这个任务才算完成。要么两个任务都成功,要么都失败,两个任务的状态保持一致。
而异步I/O则不同,异步I/O不需要等被以来的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。
具体而言,就是当Java程序执行同步I/O时,Java自己处理I/O读写;而当Java程序处理异步IO时,Java程序把I/O读写委托给操作系统执行。

阻塞与非阻塞
首先来理解一下什么叫阻塞,一个I/O请求,在线程中进行,当这个I/O请求没有数据或没有有效数据来完成时,这个请求会进行等待,这个等待就是阻塞。但由于这个进程在等待数据,就会导致其它I/O操作无法完成。而非阻塞就是在当前进程阻塞的这个时候,CPU可以继续完成其它操作。虽然非阻塞表面上会提高CPU利用率,但是会增加系统的线程切换成本。所以,二者各有利弊。

接下来来整理几种IO操作的区别:

BIO:同步阻塞I/O模式:即Java运行一个I/O操作,然后等待IO操作执行完成,CPU一直等待IO操作执行,等这个操作执行后再执行其它操作。
NIO:同步非阻塞模式:由Java程序执行一个I/O操作,但执行I/O操作期间CPU没有一直在等待该操作执行,而去执行其它操作。期间CPU不断检查一下I/O进程执行情况,看是否执行完毕,好执行下一步操作。
AIO:异步非阻塞I/O模式:这种I/O模式CPU不进行等待,I/O操作执行完后通知CPU,然后CPU再执行后续代码。

三种模式各有优缺点,
BIO是最简单的一种用法,也是成本最低的一种,但CPU大部分时间处于空闲状态。主要应用于Apache,Tomcat等并发量不高的场景。
NIO是提升性能的常用手段,常用于网络连接,和 BIO比,虽然能提升性能,但是会增加CPU功耗。主要应用于Nginx,Netty等高并发量场景。
AIO:这种组合方式比较复杂,只有非常复杂的分布式情况下会使用适用于连接数目多且连接长的架构,充分调用OS参与并发工作。JDK7开始支持。

2.9 File的常用方法

创建

  • createNetFile() 在指定位置创建一个空文件,成功就返回true,如果已存在就不创建,返回false
  • mkdir() 在指定位置创建一个单极文件夹。
  • mkdirs() 在指定位置创建一个多级文件夹。

删除

  • delete() 删除文件或者一个文件夹,不能删除非空文件夹,马上删除文件,返回一个布尔值。
  • deleteOnExit() jvm退出时删除文件或者文件夹,用于删除临时文件,无返回值。

判断

  • exists() 文件或文件夹是否存在
  • isFile() 是否是一个文件,如果不存在,则始终为false
  • isAbsolute 测试此抽象路径名是否为绝对路径名

获取

  • getName() 获取文件或文件夹的名称,不包含上级路径。
  • getAbsolutePath() 获取文件的绝对路径,与文件是否存在没有关系。

文件夹相关

  • list() 返回目录下的文件或者目录名,包含隐藏文件。对于文件这样操作会返回null.
  • listFiles() 返回目录下的文件或者目录对象(File类实例),包含隐藏文件。对于文件这样操作会返回null。

2.10 创建对象的几种方式

  1. 使用new关键字创建对象最常见的一种方式,但是会增加耦合度。无论使用什么框架,都要减少new的使用来减少耦合度。
  2. 使用反射机制创建对象:Class类的newInstance方法
  3. 采用clone:需要有一个已经分配了内存的源对象,创建新对象时,首先应该分配一个和源对象一样大的内存空间。
  4. 采用序列化机制;使用序列化时,要实现serializable接口,将一个对象序列化到磁盘上,而采用反序列化可以将磁盘上的对象信息转化到内存中。

3.集合知识点总结:

3.1 体系框架

Java 面试题整理(语法和集合部分)_第2张图片

Collection

  1. List
    • Arraylist:Object数组
    • Vector:Object数组
    • LinkedList:双向链表
  2. Set
    • HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素
    • LinkedHashSet: LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的
    • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)

Map

  1. HashMap:JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
  2. LinkedHashMap :继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
  3. Hashtable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
  4. TreeMap:红黑树(自平衡的排序二叉树)

3.2 ArrayList、LinkedList、Vector的区别和实现原理

ArrayList LinkedList Vector
存储结构 基于数组实现的 基于双向链表实现 基于数组实现的
线程安全性 不具有有线程安全性,多线程环境下需使用synchronizedList 不具有有线程安全性,多线程环境下需使用synchronizedList Vector实现线程安全的,即它大部分的方法都包含关键字synchronized,但是Vector的效率没2有ArraykList和LinkedList高。:
扩容机制 Object的数组形式来存储的,元素不够时,扩容至原来的1.5倍 Object的数组形式来存储的,元素不够时,扩容至原来的2倍

3.3 迭代器 Iterator 是什么?

迭代器是一种设计模式,也是一种轻量级对象,创建它的代价非常小。它提供了一些专用的方法来统一处理集合中的元素,为各种容器都提供了公共的操作接口,隔离对容器的遍历和底层实现,从而进一步降低代码耦合度。
具体作用过程:

首先,创建了一个List的集合对象,并放入了俩个字符串对象,然后通过iterator()方法得到迭代器。iterator()方法是由Iterable接口规定的,ArrayList对该方法提供了具体的实现,在迭代器Iteartor接口中,有以下3个方法:
1、hasNext() 该方法英语判断集合对象是否还有下一个元素,如果已经是最后一个元素则返回false
2、next() 把迭代器的指向移到下一个位置,同时,该方法返回下一个元素的引用
3、remove() 从迭代器指向的Collection中移除迭代器返回的最后一个元素,该操作使用的比较少。

        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        ListIterator<Integer> it=list.listIterator();//返回一个list接口中的特有迭代器
        while (it.hasNext()) {
            System.out.println(it.next());
        }

3.4 ArrayList集合一次加入一万条数据,应该如何提高效率

因为ArrayList的底层是由数组实现的,并且数组的默认值为10,如果插入10000条数据的话需要不断对数组进行扩容,会浪费大量的时间。所以当我们已经加入数据量较大时,可以调用ArrayList的指定容器的构造方法

    public static void main(String[] args) {
        int n = 10000000;
        Object o = new Object();
        List<Object> list1 = new ArrayList<>();
        long start = System.currentTimeMillis();
        for (int i = 0;i < n; i++) {
            list1.add(o);
        }
        System.out.println(System.currentTimeMillis() - start);

        List<Object> list2 = new ArrayList<>(n);
        long start2 = System.currentTimeMillis();
        for (int i = 0;i < n; i++) {
            list2.add(o);
        }
        System.out.println(System.currentTimeMillis() - start2);
    }

3.5 ArrayList 和LinkedList的区别是什么?

  1. ArrayLIst 是实现了基于动态数组的数据结构,LinkedList是基于链表的数据结构。
  2. 对于随机访问的get和set,ArrayList 绝对优于LinkedList,因为LinkedList要移动指针。
  3. 对于新增加的addremoveLinkedList比较占优势,因为ArrayList要移动数据。

总的来说,当操作是一列数据的后面加数据而不是在前面或中间,并且需要随机访问其中元素时,使用ArrayList会提供比较好的性能,当你的操作是一系列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList。

3.6 HashMap的底层实现原理

HashMap采用数组+链表实现,即通过链表处理冲突,同一Hash的值的元素都存储在同一个链表中。数据元素使用Entry节点存储,每个Entry就是一个key-value的键值对。HashMap底层用一个Entye数组来保存所有的key-value键值对。在JDK1.6,JDK1.7中,HashMap使用的是头插法,但头插法容易出现逆序或出现环形链表死循环问题(见3.9)。但在JDK1.8之后变成了数组+链表+红黑树使用了尾插法,从而能够避免出现逆序且链表死循环问题。当需要存储一个Entry对象时,会根据hash算法来决定在其数组中的位置,在根据hash算法找到其在数组中的存储位置;当需要取出一个Entry对象时,也会根据hash算法找到其在数组中的存储位置,根据equals方法从该位置上的链表取出Entry。
扩容机制
首先,默认初始化capacity(容量)为16,负载因子,计算出来一个threshold(阈值)作为一个扩容的阈值。在put时先判断,size是否大于阈值,如果大于阈值,就要进resize()操作,会扩容为原来的2倍,把原来的数组进行resize()操作。

resize函数源码解读

3.7 HashMap与Hashtable的区别

  1. HaspMap是非线程安全的,HashTable是线程安全的;HashTable内部的方法基本都经过synchronized修饰。
  2. 因为线程安全问题,HashMap要比HashTable效率高一点,HashTable基本被淘汰。
  3. HashMap允许有空值存在,而在HashTable中put进的健值只要有一个为空,直接抛出NullPointerException。
    所以在单线程开发中,因为速度问题,一般推荐用HashMap,而需要完全线程安全时,使用HashTable。不过Java5以上的话,推荐用ConcurrentHashMap,HashTable已经是马上要淘汰的遗留类。

3.8 HashMap的源码解析

Hash函数源码解析:源码解析
HashMap源码解读:源码解析
LinkedHashMap 源码解读:源码解析

3.9 HashMap 为什么线程不安全

1.put的时候导致多线程数据不一致

比如有两个线程A和B,首先A希望插入一个记录(键值对)到HashMap中,首先要计算落到的桶的坐标,然后获取该桶里面的链表头节点,此时线程A的时间片用完了,而此时线程B被调度执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面。假设线程A插入的记录计算出来的索引值和线程B计算出来的是一样的,那么当线程B成功插入后,线程A再次被调度执行,它仍然持有过期的链表头但它对此一无所知,以至于它覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据的不一致。

2.扩容的时候造成链表死循环 jdk1.7中
Java 面试题整理(语法和集合部分)_第3张图片

我们假设有两个线程同时需要执行resize操作,我们原来的桶数量为2,记录数为3,需要把桶扩容到4,原来的记录分别是:[3,a],[7,b],[5,c]。然后看扩容部分的源代码:

do {
    Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
} while (e != null);

假设线程A执行到了Entry next = e.next这一句,时间片用完了就被挂起,此时的e = [3,a] , next = [7 , b]。线程B被调度执行并且顺利完成了resize操作,需要注意的是,此时的[7,b]的next为[3,a]。此时线程A重新被调度运行,此时的A持有的引用是已经被线程B resize之后的结果,那么这时候e = [7,b],next = [3,a]。线程A 首先将[7,B]迁移到新的数组上,然后再处理[3,A],这时候[7,b]被连接到了[3,a]的后面。注意此时因为线程B的resize导致了[7,B]的next已经指向了[3,A],环形链表出现了,造成了线程安全问题。

3.10 ConcurrentHashMap基本原理以及线程安全问题

3.10.1ConcurrentHashMap和HashTable的区别

我们都知道CurrentHashMapHashTable都可以用于多线程环境,但当HashTable的大小增加到一定的时候,性能会急剧下降。因为HashTable实现线程安全的原理是将整个Hash表锁住,数据量增大的时候迭代需要被锁定很长时间。而ConcurrentHashMap引入了分割,不论它变的多么大,仅仅锁住map的某个部位,其它的线程不需要等到迭代完才能访问map。总而言之,在迭代的过程中,CurrentHashMap仅仅锁住map的某个部分,而HashTable则会锁住整个map。

3.10.2 CuncurrentHashMap 基本原理

ConcurrentHashMao 采用了非常精妙的“分段锁”,主干由若干个Segment数组实现。

//继承ReentrantLock ,实现一把分段锁
static class Segment<K, V> extends ReentrantLock implements Serializable {
    private static final long serialVersionUID = 2249069246763182397L;
    final float loadFactor;

    Segment(float lf) {
        this.loadFactor = lf;
    }
}

Segment继承了ReentrantLock,所以它是一种可重入锁。在ConcurrentHashMap,一个Segment就是一个子Hash表,Segment里面维护了一个HashEntry数组,并发环境下,对于不同的Segment的数据进行操作是不用考虑锁竞争的。对于同一个Segment的操作才考虑线程同步。
换句话说,ConcurrentHashMap相对于很多个HashMap,Segment类似于一个HashMap,一个Segment维护了一个HashEntry数组。

你可能感兴趣的:(Java进阶)