前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Q&A:Java

Q&A:Java

作者头像
小简
发布2023-04-06 13:21:49
6110
发布2023-04-06 13:21:49
举报
文章被收录于专栏:简言之

基础

🌟Integer 和int区别

基本类型和包装类型的区别?

区别

Integer

int

初始值

null

0

存储位置

用于泛型

可用于

可以

占用空间

较大

较小

方法

封装了方法,更灵活

为什么有包装类型?Java是面向对象的嘛,集合里面只能存储对象

🌟重载和重写的区别

  • 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关。发生在编译期
  • 重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。发生在运行期
    • 如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。
    • 如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。

🌟静态变量和实例变量的区别

  • 静态变量:静态变量可以被类的所有实例共享。无论一个类创建了多少个对象,它们都共享同一份静态变量。
  • 实例变量:属于某一实例,需要先创建对象,然后通过对象才能访问到它。

🌟静态方法和实例方法有何不同

1、调用方式

在外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名 的方式,而实例方法只有后对象.方法名 这种方式。也就是说,调用静态方法可以无需创建对象

2、访问类成员是否存在限制

静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。

🌟静态方法为什么不能调用非静态成员

  1. 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
  2. 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。

🌟成员变量与局部变量的区别?

  • 语法形式 :从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  • 存储方式 :从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  • 生存时间 :从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
  • 默认值 :从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

🌟String 为什么是不可变的?

  1. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
  2. 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。

🌟String、StringBuffer、StringBuilder区别

1.可变性

  • String不可变,StringBuilderStringBuffer是可变的

2.线程安全性

  • String由于是不可变的,所以线程安全。
  • StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
  • StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。

3.性能

  • StringBuilder > StringBuffer > String

🌟为什么String要设计成不可变的呢

  1. 可以缓存 hash 值。 例如String 用做 HashMap 的 key,不可变的特性可以使得 hash值也不可变, 因此只需要进行一次计算。
  2. 常量池优化。 String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。
  3. 线程安全。 String 不可变性天生具备线程安全,可以在多个线程中安全地使用。

🌟Object 类的常见方法有哪些?

Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:

代码语言:javascript
复制
/**
 * native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
 */
public final native Class<?> getClass()
/**
 * native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
 */
public native int hashCode()
/**
 * 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
 */
public boolean equals(Object obj)
/**
 * naitive 方法,用于创建并返回当前对象的一份拷贝。
 */
protected native Object clone() throws CloneNotSupportedException
/**
 * 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
 */
public String toString()
/**
 * native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
 */
public final native void notify()
/**
 * native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
 */
public final native void notifyAll()
/**
 * native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
 */
public final native void wait(long timeout) throws InterruptedException
/**
 * 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒。。
 */
public final void wait(long timeout, int nanos) throws InterruptedException
/**
 * 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
 */
public final void wait() throws InterruptedException
/**
 * 实例被垃圾回收器回收的时候触发的操作
 */
protected void finalize() throws Throwable { }

🌟== 和 equals() 的区别

== 对于基本数据类型,比较的是值;对于引用数据类型,比较的是内存地址。

equals 对于没有重写equals方法的类,equals方法和==作用类似;对于重写过equals方法的类,equals比较的是值。

🌟为什么重写 equals() 时必须重写 hashCode() 方法

因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

  • 如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。在HashSet中,会导致都能添加成功,那么HashSet中会出现很多重复元素,HashMap也是同理(因为HashSet的底层就是通过HashMap实现的),会出现大量相同的Key。所以重写equals方法后,hashCode方法也必须重写。
  • 同时因为两个对象的hashCode值不同,则它们一定不相等,所以先计算对象的hashCode值可以在一定程度上判断两个对象是否相等,提高了集合的效率。

总结一下,一共两点:第一,在HashSet等集合中,不重写hashCode方法会导致其功能出现问题;第二,可以提高集合效率。

🌟面向对象三大特性

  • 封装就是隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别。
  • 继承就是子类继承父类的特征和行为,使得子类对象具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
  • 多态是同一个行为具有多个不同表现形式或形态的能力。在Java语言中,多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,
  • 在Java中实现多态的三个必要条件:继承、重写、向上转型。继承和重写很好理解,向上转型是指在多态中需要将子类的引用赋给父类对象。

🌟面向对象五大基本原则

  • 单一职责原则(Single-Resposibility Principle)一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
  • 开放封闭原则(Open-Closed principle)软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。
  • 里氏替换原则 (Liskov-Substituion Principle)子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。
  • 依赖倒置原则(Dependecy-Inversion Principle)依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
  • 接口隔离原则(Interface-Segregation Principle)使用多个小的专门的接口,而不要使用一个大的总接口。

🌟抽象类和接口的对比

在Java语言中,abstract classinterface是支持抽象类定义的两种机制。

抽象类:用来捕捉子类的通用特性的,用于代码复用。接口:抽象方法的集合,用于对类的行为进行约束。

相同点:

  • 接口和抽象类都不能实例化
  • 都包含抽象方法,其子类都必须覆写这些抽象方法
  • 都可以有默认实现的方法

不同点:

类型

抽象类

接口

定义

abstract class

Interface

实现

extends

implements

继承

抽象类可以继承一个类和实现多个接口;子类只可以继承一个抽象类

接口只可以继承多个接口;子类可以实现多个接口

变量

访问修饰符默认是 default,可以是public、protected

只能是public static final

方法

public、protected和default

public abstract、default

构造器

抽象类可以有构造器

接口不能有构造器

🌟BIO,NIO,AIO 有什么区别?

https://mp.weixin.qq.com/s/IAWrXznU4DlJFa8tTcthPw

  • BIO (Blocking I/O):服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制来改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务端资源要求比较高,并发局限于应用中,在jdk1.4以前是唯一的io
  • NIO (New I/O):服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,jdk1,4开始支持
  • AIO (Asynchronous I/O):服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由操作系统先完成了再通知服务器用其启动线程进行处理。AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,jdk1.7开始支持。

这些概念看着比较枯燥,可以从这个经典的烧开水的例子去理解

BIO :来到厨房,开始烧水,并坐在水壶面前一直等着水烧开。

NIO:来到厨房,开始烧水,不一直坐在水壶前面等,而是做些其他事,然后每隔几分钟到厨房看一下水有没有烧开。

AIO:来到厨房,开始烧水,不一直坐在水壶前面等,而是在水壶上面装个开关,水烧开之后它会通知我。

泛型

泛型提供编译时类型安全检测机制,通过泛型参数可以指定传入的对象类型,编译时可以对泛型参数进行检测

泛型擦除:泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉。Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。

编译时,检查添加元素的类型,更安全,减少了类型转换次数,提高效率。比如原生的List返回类型是Object对象,需要手动转换类型才能使用,使用泛型后编译器自动转换

泛型类、泛型接口、泛型方法

支持通配符 <?> :支持任意泛型类型 <? extends A>:支持A类以及A类的子类,规定了泛型的上限 <? super A>:支持A类以及A类的父类,不限于直接父类,规定了泛型的下限

构建集合工具类,自定义接口通用返回结果、excel导出类型

反射

通过反射可以运行时获取任意一个类的所有属性和方法,还可以调用这些方法和属性。

Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。 这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。

优点:运行期类型的判断,动态加载类,提高代码灵活度。

缺点:使用反射基本是解释执行,对执行速度有影响。安全问题。比如可以无视泛型参数的安全检查

注解

主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。

注解只有被解析之后才会生效,常见的解析方法有两种:

编译期直接扫描 :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。

运行期通过反射处理 :像框架中自带的注解(比如 Spring 框架的 @Value 、@Component)都是通过反射来进行处理的。

序列化

序列化: 将数据结构或对象转换成二进制字节流的过程

反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程

序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。

序列化协议对应于 TCP/IP 4 层的应用层,对应于7 层的表示层

为什么不推荐使用 JDK 自带的序列化?

不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。

性能差 :相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。

存在安全问题 :序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。

SPI

专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。

SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。

很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。

SPI 的优缺点?

通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:

需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。

当多个 ServiceLoader 同时 load 时,会有并发问题。

语法糖

语法糖 代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法。实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。

Java 中最常用的语法糖主要有泛型、自动拆装箱、增强 for 循环、try-with-resources 语法、lambda 表达式、变长参数、枚举、内部类等。

foreach底层是怎么实现的

  • 底层原理其实就是基于普通的 for 循环和迭代器
  • 对于数组,foreach 循环实际上还是用的普通的 for 循环
  • 对于集合,foreach 循环实际上是用的iterator 迭代器迭代

foreach和for有什么区别?

  • foreach不可以删除/修改集合元素,而for可以
  • foreach适用于只是进行集合或数组遍历,for则在较复杂的循环中效率更高。

java8新特性

lambda 表达式、Stream流式编程、新时间日期 API、接口默认方法与静态方法

集合

🌟集合的好处

  • 数组
    • 1)长度开始时必须指定,而且一旦指定,不能更改
    • 2)保存的必须为同一类型的元素
    • 3)使用数组进行增加/删除元素比较麻烦
  • 集合
    • 1)可以动态保存任意多个对象,使用比较方便!
    • 2)提供了一系列方便的操作对象的方法: add、remove、set、get等
    • 3)使用集合添加,删除新元素简洁了

🌟ArrayList、LinkedList和Vector的区别

  • ArrayList底层使用 Object[]存储,线程不安全,有预留的内存空间
    • 末尾插入O(1),中间i处插入O(n-i)
  • LinkedList底层使用双向链表数据结构,线程不安全,没有预留的内存空间,不可通过序号快速获取对象,但每个节点都有两个指针占用了内存
    • 末尾插入O(1),中间i处插入O(n-i),但不需要移动元素
  • Vector底层使用 Object[]存储,线程不安全

🌟ArrayList扩容机制

  • 创建 ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 容量为 0
    • 第 1 次添加,则扩容 elementData 为 10(懒汉思想)
    • 如需再次扩容,则扩容 elementData 为 1.5 倍
  • 创建 ArrayList 对象时,如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小
    • 如果需要扩容,则直接扩容 elementData 为 1.5 倍

🌟HashMap 和 Hashtable 的区别

  • HashMap线程不安全,底层数据结构是数组+链表+红黑树可以存储null的key和value,null的key只能有一个,null的value可以有多个。默认初始容量为16每次扩充变为原来2倍。创建时如果给定了初始容量,则扩充为2的幂次方大小。插入元素后如果链表长度大于阈值(默认为8),先判断数组长度是否小于64,如果小于,则扩充数组,反之将链表转化为红黑树,以减少搜索时间。
  • HashTable线程安全,其内部方法基本都经过synchronized修饰,底层数据结构是数组+链表不可以有null的key和value,否则会抛出 NullPointerException默认初始容量为11,每次扩容变为原来的2n+1。创建时给定了初始容量,会直接用给定的大小。它基本被淘汰了,要保证线程安全可以用ConcurrentHashMap。

🌟HashMap 和 TreeMap 区别

TreeMapHashMap 都继承自AbstractMapTreeMap它还实现了NavigableMap接口和SortedMap 接口。

  • 实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。
  • 实现SortedMap接口让 TreeMap 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。

相比于HashMap来说 TreeMap 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力

🌟HashMap 和 HashSet 区别

  • HashMap实现了Map接口,用于存储键值对
  • HashSet实现了Set接口,用于存储对象,基于HashMap实现的,只是value都指向了一个虚拟对象,只用到了key

🌟HashSet、LinkedHashSet 和 TreeSet 的区别

  • HashSetLinkedHashSetTreeSet 都是 Set 接口的实现类,都能保证元素唯一,并且都线程不安全
  • HashSetLinkedHashSetTreeSet 的主要区别在于底层数据结构不同。HashSet 的底层数据结构是哈希表(基于 HashMap 实现)。LinkedHashSet 的底层数据结构是数组和双向链表+红黑树,元素的插入和取出顺序满足 FIFO。TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。
  • HashSet 用于不需要保证元素插入和取出顺序的场景,LinkedHashSet 用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet 用于支持对元素自定义排序规则的场景。

🌟ArrayDeque 与 LinkedList 的区别

ArrayDequeLinkedList 都实现了 Deque 接口,两者都具有队列的功能

  • ArrayDeque 是基于可变长的数组和双指针来实现,而 LinkedList 则通过链表来实现。
  • ArrayDeque 不支持存储 NULL 数据,但 LinkedList 支持。
  • ArrayDeque 是在 JDK1.6 才被引入的,而LinkedList 早在 JDK1.2 时就已经存在。
  • ArrayDeque 插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 LinkedList 不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。

从性能的角度上,选用 ArrayDeque 来实现队列要比 LinkedList 更好。此外,ArrayDeque 也可以用于实现栈。

🌟comparable 和 Comparator 的区别

comparable 接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序

comparator接口实际上是出自 java.util 包 它有一个compare(Object obj1, Object obj2)方法用来排序

🌟HashSet 如何检查重复?

当你把对象加入HashSet时,HashSet 会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcodeHashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。

🌟HashMap 的底层实现

JDK1.8 之前 HashMap 底层是 数组+链表 。采用的是头插法,先扩容在插入数据,扩容时需要rehash

JDK1.8 之后 HashMap 底层是 数组+链表+红黑树 。采用的是尾插法,先插入数据后扩容,不需要重新计算hash值

🌟HashMap的扩容机制

HashMap 底层维护了 Node 类型的数组 table,默认为 null

何时扩容

1、数组为空时 即tab = null 或者 tab.length = 0

2、元素个数超过数组长度*负载因子的时候

  • 负载因子默认值0.75;数组初始容量16

3、当链表长度大于8且数组长度小于64时

如何扩容

创建时如果没有给定初始容量,默认初始容量为16,每次扩充变为原来2倍。创建时如果给定了初始容量,则扩充为2的幂次方大小。插入元素后如果链表长度大于阈值(默认为8),先判断数组长度是否小于64,如果小于,则扩充数组,反之将链表转化为红黑树,以减少搜索时间。

具体:

HashMap 底层维护了 Node 类型的数组 table,默认为 null,当创建对象时,将加载因子(loadfactor)初始化为 0.75

第1次添加,则需要扩容 table 容量为 16,临界值(threshold)为12 (16*0.75)

以后再扩容,则需要扩容 table 容量为原来的 2 倍(32),临界值为原来的 2倍(32*0.75),即24,依次类推

在 Java8 中,如果一条链表的元素个数超过 TREEIFY_THRESHOLD(默认是8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)

为什么扩容是2的次幂

计算元素位置应该是哈希值对数组长度做取余操作( hash % n)但是 HashMap 通过 (n - 1) & hash 判断当前元素存放的位置。

  • hash % n == (n - 1) & hash的前提是 数组长度n 是 2 的次幂。
  • 采用二进制位操作 &,相对于%能够提高运算效率,并且能够充分的散列,减少hash碰撞

🌟HashMap中put操作如何实现的?

HashMap 底层维护了 Node 类型的数组 table,默认为 null,当创建对象时,将加载因子(loadfactor)初始化为 0.75

当添加 key-val 时通过 key 的哈希值得到在 table 的索引,然后判断该索引处是否有元素

  • 如果没有元素直接添加
  • 如果该索引处有元素,判断该元素的 key 和准备加入的 key 是否相等
    • 如果相等,则直接替换 val
    • 如果不相等需要判断是树结构还是链表结构,做出相应处理
      • 如果是链表结构,遍历链表,在尾部插入数据,如果链表长度大于8,判断链表长度是否大于64,进行扩容
      • 如果是树结构,直接插入红黑树
  • 如果添加时发现容量不够,则需要扩容。

🌟如何解决hash碰撞?

1、开放地址法:也称为线性探测法,就是从发生冲突的位置开始,按照一定次序(顺延)从hash表找到一个空闲位置,把发生冲突的元素存到这个位置。比如ThreadLocal

2、链地址法:就是把冲突的key,以单向链表来进行存储,比如HashMap

3、再哈希法:使用多个哈希函数,比如布隆过滤器

🌟HashMap为什么是线程不安全的?

为什么HashMap会产生死循环?

JDK 1.7 HashMap采用头插法,多线程扩容就会引起链表顺序倒置,形成死循环,数据丢失(用jstack命令定位线程死循环)

JDK 1.8 HashMap采用尾插法,死循环和数据丢失的问题已经解决。但是存在数据覆盖:HashMap在执行put操作时,因为没有加同步锁,多线程put可能会导致数据覆盖

如何解决HashMap线程不安全的问题?

  • 使用ConcurrentHashMap(推荐)
  • 使用HashTable(不推荐)
  • 使用synchronized或Lock加锁(不推荐)

🌟ConcurrentHashMap 和 Hashtable 的区别

底层数据结构:

JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表+红黑二叉树

Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式

🌟实现线程安全的方式:

  • JDK1.7 的时候,ConcurrentHashMap 对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
  • JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronizedCAS 来操作。(JDK1.6 以后 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap
  • Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

🌟JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?

  • 线程安全实现方式 :JDK 1.7 采用 Segment 分段锁来保证安全, Segment 是继承自 ReentrantLock。JDK1.8 放弃了 Segment 分段锁的设计,采用 Node + CAS + synchronized 保证线程安全,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点。
  • Hash 碰撞解决方法 : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
  • 并发度 :JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。

🌟ConcurrentHashMap 线程安全的具体实现方式/底层具体实现

ConcurrentHashMap底层实现原理

JDk 7

首先将数据分为一段一段( Segment)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。Segment 数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成

Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。

一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁。也就是说,对同一 Segment 的并发写入会被阻塞,不同 Segment 的写入是可以并发执行的。


JDK1.8

数据结构跟 HashMap 1.8 的结构类似,采用 数组+链表+红黑树。链表长度超过阈值(8),并且数组长度大于64,将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。

ConcurrentHashMap 取消了 Segment 分段锁,采用 Node + CAS + synchronized 来保证并发安全。

JDK1.8 中,锁粒度更细,synchronized锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。

并且引入了多线程并发扩容的实现,多个线程对原始数组进行分片,每个线程去负责一个分片的数据迁移,提升扩容效率

📚参考资料

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-03-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基础
    • 🌟Integer 和int区别
      • 🌟重载和重写的区别
        • 🌟静态变量和实例变量的区别
          • 🌟静态方法和实例方法有何不同
            • 🌟静态方法为什么不能调用非静态成员
              • 🌟成员变量与局部变量的区别?
                • 🌟String 为什么是不可变的?
                  • 🌟String、StringBuffer、StringBuilder区别
                    • 🌟为什么String要设计成不可变的呢
                      • 🌟Object 类的常见方法有哪些?
                        • 🌟== 和 equals() 的区别
                          • 🌟为什么重写 equals() 时必须重写 hashCode() 方法
                            • 🌟面向对象三大特性
                              • 🌟面向对象五大基本原则
                                • 🌟抽象类和接口的对比
                                  • 🌟BIO,NIO,AIO 有什么区别?
                                    • 泛型
                                      • 反射
                                        • 注解
                                          • 序列化
                                            • SPI
                                              • 语法糖
                                                • java8新特性
                                                • 集合
                                                  • 🌟集合的好处
                                                    • 🌟ArrayList、LinkedList和Vector的区别
                                                      • 🌟ArrayList扩容机制
                                                        • 🌟HashMap 和 Hashtable 的区别
                                                          • 🌟HashMap 和 TreeMap 区别
                                                            • 🌟HashMap 和 HashSet 区别
                                                              • 🌟HashSet、LinkedHashSet 和 TreeSet 的区别
                                                                • 🌟ArrayDeque 与 LinkedList 的区别
                                                                  • 🌟comparable 和 Comparator 的区别
                                                                    • 🌟HashSet 如何检查重复?
                                                                      • 🌟HashMap 的底层实现
                                                                        • 🌟HashMap的扩容机制
                                                                          • 🌟HashMap中put操作如何实现的?
                                                                            • 🌟如何解决hash碰撞?
                                                                              • 🌟HashMap为什么是线程不安全的?
                                                                                • 🌟ConcurrentHashMap 和 Hashtable 的区别
                                                                                  • 🌟JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?
                                                                                    • 🌟ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
                                                                                    • 📚参考资料
                                                                                    相关产品与服务
                                                                                    容器服务
                                                                                    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                                                                    领券
                                                                                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档