消息队列
消息丢失;重复消费;顺序消费;消息堆积;幂等性
jps
(JVM Process Status): 类似 UNIX 的 ps
命令。用于查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息;
jstat(JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据;gc情况、垃圾回收统计
jinfo
(Configuration Info for Java) : Configuration Info for Java,显示虚拟机配置信息;
jmap (Memory Map for Java) : 生成堆转储快照;内存布局、堆信息
jhat
(JVM Heap Dump Browser) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果;
jstack (Stack Trace for Java) : 生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。
初始堆(memory start)大小、最大堆大小 |
jstack
(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合.
生成线程快照的目的主要是定位线程长时间出现停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的原因。线程出现停顿的时候通过jstack
来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事情,或者在等待些什么资源。
虚拟机栈:
固定大小:申请不到栈帧报 StackOverflow
无限大小:申请不到栈帧报 OutOfMemory
堆:忘记是不是能固定大小了
jvm内存满了,对象分配不到堆了就会报OutOfMemory
方法区:(面试官提示,感谢)
方法区里面的常量池也是会不断加入数据的
一直调用string.intern()方法会不断加字符串到字符串常量池导致OOM
OOM 全称 “Out Of Memory”,表示内存耗尽。当 JVM 因为没有足够的内存来为对象分配空间,并且垃圾回收器也已经没有空间可回收时,就会抛出这个错误。产生原因:
- 分配过少:JVM 初始化内存小,业务使用了大量内存;或者不同 JVM 区域分配内存不合理
- 代码漏洞:某一个对象被频繁申请,不用了之后却没有被释放,导致内存耗尽
内存泄漏:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了。因为申请者不用了,而又不能被虚拟机分配给别人用
内存溢出:申请的内存超出了 JVM 能提供的内存大小,此时称之为溢出
java.lang.OutOfMemoryError: PermGen space
Java7 永久代(方法区)溢出,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。每当一个类初次加载的时候,元数据都会存放到永久代
一般出现于大量 Class 对象或者 JSP 页面,或者采用 CgLib 动态代理技术导致
我们可以通过 -XX:PermSize
和 -XX:MaxPermSize
修改方法区大小
Java8 将永久代变更为元空间,报错:java.lang.OutOfMemoryError: Metadata space,元空间内存不足默认进行动态扩展
java.lang.StackOverflowError
虚拟机栈溢出,一般是由于程序中存在 死循环或者深度递归调用 造成的。如果栈大小设置过小也会出现溢出,可以通过 -Xss
设置栈的大小
虚拟机抛出栈溢出错误,可以在日志中定位到错误的类、方法
java.lang.OutOfMemoryError: Java heap space
Java 堆内存溢出,溢出的原因一般由于 JVM 堆内存设置不合理或者内存泄漏导致
如果是内存泄漏,可以通过工具查看泄漏对象到 GC Roots 的引用链。掌握了泄漏对象的类型信息以及 GC Roots 引用链信息,就可以精准地定位出泄漏代码的位置
如果不存在内存泄漏,就是内存中的对象确实都还必须存活着,那就应该检查虚拟机的堆参数(-Xmx 与 -Xms),查看是否可以将虚拟机的内存调大些
tasklist | findstr "java" |
Heap Space Size = Young Space Size + Old Space Size
,而-Xmn
参数控制的正是 Young 区的大小,当堆区被 Young Gen 完全挤占,又有对象想要升代到 Old Gen 时,发现 Old 区空间不足,于是触发 Full GC,触发 Full GC 以后呢,通常又会面临两种情况:
Young 区又刚好腾出来一点空间,对象又不用放到 Old 区里面了,皆大欢喜
Young 区空间还是不够,对象还是得放到 Old 区,Old 区空间不够,卒,喜提OOM
诶,就是奔着 Old 区去的,管你 Young 不 Young,Old 区空间不够,卒,喜提OOM
这个就解释了为什么系统刚刚启动时,会有一个短时间正常工作的现象,随后,当某段程序触发 Old Gen 升代时,就会发生随机的OOM
错误。那么什么时候对象会进入老年代呢?这里也很有意思,不妨结合日志里面出现OOM
的地方,对号入座:
经历足够多次数 GC 依然存活的对象
申请一个大对象(比如超过 Eden 区一半大小)
GC 后 Eden 区对象大小超过 S 区之和
Eden 区 + S0 区 GC 后,S1 区放不下
换言之,正常情况下,-Xmn
参数总是应当小于-Xmx
参数,否则就会触发OOM
错误。
在Linux环境下,项目出现CPU占用过高的情况时,可以按照以下步骤进行排查和处理:
定位高CPU占用的进程:
top
命令查看系统中CPU占用率最高的进程。分析进程中的线程:
top -H -p [PID]
来查看该进程中各个线程的CPU占用情况。转换线程ID为16进制:
printf "%x\n" [线程ID]
命令将线程ID转换为16进制格式。获取线程堆栈信息:
jstack [进程PID] | grep [线程ID的16进制] -A 30
命令获取该线程的Java堆栈信息(如果是Java进程)。这可以帮助定位到具体的代码行或方法调用。gdb
或其他相应的调试工具来获取线程的堆栈信息。分析代码和日志:
性能剖析
- 这些工具可以提供热点(hot spots)功能,显示哪些方法占用最多的CPU时间。
处理措施:
CPU飙高的排查方案及思路_cpu冲高排查思路-CSDN博客
JVM参数最重要的JVM参数总结 | JavaGuide
美团面试:熟悉哪些JVM调优参数,幸好我准备过!-CSDN博客
JDK监控和故障处理工具总结 | JavaGuide
生产事故-记一次特殊的OOM排查 - 程语有云 - 博客园 (cnblogs.com)
YGC问题排查,又让我涨姿势了! | HeapDump性能社区
1. GC触发时机 2. 死亡对象判断方法 3. 垃圾收集算法 4. 垃圾收集器
序列化是将数据结构或对象转换成一种可存储或可传输格式的过程,通常涉及将数据转换成字节流或类似的格式,以便在不同平台和编程语言之间进行传输和交换。反序列化是将序列化后的数据重新还原成原始的数据结构或对象,从文件、网络数据或数据库中读取序列化的数据,并将其转换回原始形式,以便在程序中进行使用和操作。这两个概念在编程中与数据存储、传输和处理密切相关,为开发工作带来便利1
Exception
:程序本身可以处理的异常,可以通过 catch
来进行捕获。Exception
又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
Error
:Error
属于程序无法处理的错误
RuntimeException
及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):
NullPointerException
(空指针错误)
IllegalArgumentException
(参数错误比如方法入参类型错误)
NumberFormatException
(字符串转换为数字格式错误,IllegalArgumentException
的子类,s包含非数字字符,或者为null)
ArrayIndexOutOfBoundsException
(数组越界错误)
ClassCastException
(类型转换错误)
ArithmeticException
(算术错误)
SecurityException
(安全错误比如权限不够)
UnsupportedOperationException
(不支持的操作错误比如重复创建同一用户)
try-with-resources
代替try-catch-finally
?//读取文本文件的内容 |
使用 Java 7 之后的 try-with-resources
语句改造上面的代码:
try (Scanner scanner = new Scanner(new File("test.txt"))) { |
在 java 中泛型只是一个占位符,必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。只能使用自己声明的
<E>
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
Linux系统有七个运行级别,分别用数字0-6表示。每个运行级别都会启动或停止不同的系统服务和进程,以满足不同的系统需求。
1.我们可以使用 runlevel 命令,进行查看
2.我们可以使用 who -r 命令进行查看
运行等级 | 描述 | 命令参数 |
0 | 关机模式,系统默认运行级别不能设置为0,否则不能正常启动,一开机就自动关机 | shutdown.target |
1 | 单用户模式,root权限,用于系统维护,禁止远程登录,就像Windows下的安全模式 | emergency.target |
2 | 多用户模式,没有 NFS 网络支持 | rescure.target |
3 | 完整的多用户文本模式,有 NFS,登录后进入控制台命令模式 | multi-user.target |
4 | 系统未使用,保留一般不用 | 无 |
5 | 图形化模式,登陆后进入图形 GUI 模式 | graphical.target |
6 | 重启模式,默认运行级别不能设为6,否则不能设为6,否则不能正常启动,就会一直开机重启 | 无 |
我们怎么查看系统在启动时默认的运行等级呢?
可以使用 systemctl get-default 命令进行查看
systemctl set-default multi-user.target
要改变Linux的运行级别,可以使用以下方法:使用runlevel命令查看当前系统的运行级别。使用init命令切换运行级别。修改/etc/inittab文件中的运行级别。使用systemctl命令设置默认运行级别。使用systemctl命令立即切换到指定的运行级别。
chmod:更改文件或目录的权限。
cat:查看文件内容
top:inux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况.该命令可以提供实时的对系统处理器的状态监视,它会显示系统中CPU最“敏感”的任务列表,并且可以按CPU使用、内存使用和执行时间对任务进行排序
分析一行信息
命令 | 说明 |
---|---|
-v | 排除选项 |
grep -v ‘^$’ filename | ^:行首,$:行尾,去掉空行 |
grep -v ‘#|$’ filename | ^#:以#开头,去掉空行和以#开头的行 |
压缩命令 |
top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况
ps命令用于报告当前系统的进程状态。ps命令是最基本同时也是非常强大的进程查看命令,使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵死、哪些进程占用了过多的资源等等,总之大部分信息都是可以通过执行该命令得到的。
free命令可以显示当前系统未使用的和已使用的内存数目,还可以显示被内核使用的内存缓冲区。
面试官常考的 21 条 Linux 命令 - 简书 (jianshu.com)
解析:: 文件类型很多,能回答几种常见的就行,例如普通文件,目录文件,块设备文件,套接字文件。 参考回答:
1.普通文件(-):这是最常见的文件类型,包括纯文本文件、二进制文件、数据文件等。它们不包含文件系统的结构信息,只是用户所接触到的文件。例如,.c文件、可执行的二进制文件等都是普通文件。
2.目录文件(d):目录文件是用于存放文件名及其相关信息的文件。它们可以包含下一级文件目录或普通文件,是内核组织文件系统的基本节点。通过目录文件,用户可以轻松地浏览和管理文件系统。
3.字符设备文件(c):这类文件提供了对设备不带缓冲区的访问,每次访问长度可变。它们通常用于表示系统中的字符设备,如控制台、串口等。
4.块设备文件(b):块设备文件提供对设备(如磁盘)带缓冲的访问,每次访问以固定的长度单位进行。它们用于表示系统中的块设备,如硬盘、U盘等。
5.FIFO(p):FIFO文件也称为命名管道,用于进程间的通信。它们允许一个进程向另一个进程发送数据,而不需要通过中间的文件或网络连接。
6.套接字(s):套接字文件用于进程间的网络通信。它们提供了一种在不同进程之间传输数据的方式,通常用于实现网络服务和客户端之间的通信。
7.链接文件(l):链接文件是指向另一个文件的指针。它们可以分为硬链接和符号链接两种。硬链接指向文件的inode节点,而符号链接则指向另一个文件的路径名。通过链接文件,用户可以方便地访问其他文件或目录。
除了以上七种常见的文件类型外,Linux系统中还有其他一些特殊的文件类型,如特殊文件、门文件等,但这些类型在日常使用中相对较少见。
学习指引: 推荐学习:小林 coding|图解系统|文件系统
解析:: 考察linux文件系统相关问题,比较细,推荐大家系统学习后理解掌握 参考回答:
1.inode的作用?: inode,即索引节点,在Linux文件系统中用于存储文件或目录的元数据信息。它是文件系统的一个基本组成部分,允许系统通过inode号而非完整的文件路径快速访问到文件数据。
2.inode包含哪些内容?: inode包含文件的元数据信息,如文件大小、文件所有者、文件权限、文件类型、文件的创建/访问/修改时间等。此外,inode还包含指向文件数据块的指针,这些指针指示了文件内容在磁盘上的实际存储位置。
3.给出一个文件名,Linux是如何根据该文件名打开文件的? (文件名->inode->block): 当给出一个文件名时,Linux首先会根据文件路径在目录结构中查找该文件对应的目录项。目录项中包含了文件的inode号。然后,系统会使用这个inode号在文件系统中找到对应的inode结构。一旦找到inode,系统就可以通过inode中的指针找到文件数据所在的磁盘块(block)。最后,系统将这些磁盘块加载到内存中,从而打开并访问文件。
4.文件的访问时间是如何记录的?: 文件的访问时间是通过inode中的访问时间戳(atime)来记录的。每当文件被读取时,其inode中的atime就会被更新为当前时间。这个机制允许系统跟踪文件的访问历史,以便进行各种管理和维护操作。需要注意的是,为了优化性能,某些文件系统可能会延迟更新atime或仅在文件内容实际被读取时更新它。
阿里云 实习面经(已OC) 一面面经|讲解_牛客网 (nowcoder.com)
解析::
参考回答:
1.是什么?:零拷贝是一种IO操作优化技术,旨在减少数据在内核空间和用户空间之间的冗余拷贝,从而解放CPU、减少上下文切换并降低系统资源消耗。它主要用来解决传统IO操作中不必要的数据拷贝问题,提高数据传输效率。
2.应用场景:零拷贝技术广泛应用于需要高性能数据传输的场景,如网络传输、文件传输、数据库操作等。在这些场景中,大量的数据需要在内核空间和用户空间之间传输,传统的IO操作会导致不必要的数据拷贝和性能损失。
3.实现方式有哪些?:实现零拷贝的方式主要有mmap、sendfile、splice和tee等。其中,mmap通过内存映射将内核缓冲区与用户空间共享,避免了数据拷贝;sendfile直接将数据从内核缓冲区发送到网络缓冲区,减少了CPU拷贝;splice和tee则在内核空间内实现数据的传输和复制,避免了用户空间的参与。这些技术根据具体的应用场景和需求选择使用,可以有效地提高数据传输效率和系统性能。
学习指引: 图解系统:什么是零拷贝?如何实现零拷贝?
/** |
硬核干货:4W字从源码上分析JUC线程池ThreadPoolExecutor的实现原理 | Throwable (throwx.cn)
ArrayList: 动态数组,实现了List接口,支持动态增长。
LinkedList: 双向链表,也实现了List接口,支持快速的插入和删除操作。
HashMap: 基于哈希表的Map实现,存储键值对,通过键快速查找值。
HashSet: 基于HashMap实现的Set集合,用于存储唯一元素。
TreeMap: 基于红黑树实现的有序Map集合,可以按照键的顺序进行排序。
LinkedHashMap: 基于哈希表和双向链表实现的Map集合,保持插入顺序或访问顺序。
PriorityQueue: 优先队列,可以按照比较器或元素的自然顺序进行排序。
Java 集合,也叫作容器,主要是由两大接口派生而来:一个是 Collection
接口,主要用于存放单一元素;另一个是 Map
接口,主要用于存放键值对。对于Collection
接口,下面又有三个主要的子接口:List
、Set
、 Queue
。
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。
Set
(注重独一无二的性质): 存储的元素不可重复的。
Queue
(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
Map
(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),“x” 代表 key,“y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
集合 | 实现 | 特点 |
---|---|---|
List | ArrayList | |
vector | 线程安全 | |
LinkedList | ||
Set | HashSet | |
LinkedHashSet | ||
TreeSet | ||
Map | HashMap | 线程不安全,key、value都可为null |
LinkedHashMap | ||
HashTable | 线程安全,数组+链表组成的,数组是 HashTable 的主体,链表则是主要为了解决哈希冲突而存在的。HashTable 的加锁方法是给每个方法加上synchronized 关键字,不支持null键和值 | |
TreeMap | 红黑树 | |
ConcurrentHashMap | Node数组+链表+红黑树实现,线程安全的(jdk1.8以前Segment锁,1.8以后volatile + CAS 或者 synchronized) |
Queue
PriorityQueue
DelayQueue
ArrayDeque
HashSet
,TreeSet
,ArrayList
,LinkedList
,HashMap
,TreeMap
都是线程不安全的。
底层数据结构:
ArrayList
使用动态数组实现。它的内部是一个数组,当数组容量不足时,会自动进行扩容。LinkedList
使用双向链表实现。每个元素都包含一个指向前一个元素和一个指向后一个元素的引用。随机访问性能:
ArrayList
支持快速的随机访问,因为它是基于数组的,可以通过索引直接访问元素。LinkedList
在随机访问时性能较差,因为必须从链表的头部或尾部开始遍历,直到找到目标元素。插入和删除操作性能:
ArrayList
在中间插入或删除元素时性能较差,因为需要移动数组中的元素。LinkedList
在插入和删除元素时性能较好,因为只需要改变相邻元素的引用。空间复杂度:
ArrayList
相对较省空间,因为它只需要存储元素值和数组容量。LinkedList
相对较耗费空间,因为每个元素都需要额外的两个引用字段。迭代器性能:
ArrayList
上的迭代器性能较好,因为它可以通过索引直接访问元素。LinkedList
上的迭代器性能较差,因为必须沿着链表一个一个地移动。适用场景:
ArrayList
更为合适。LinkedList
更为合适。Comparable
接口和 Comparator
接口都是 Java 中用于排序的接口,它们在实现类对象之间比较大小、排序等方面发挥了重要作用:
Comparable
接口实际上是出自java.lang
包 它有一个 compareTo(Object obj)
方法用来排序
Comparator
接口实际上是出自 java.util
包它有一个compare(Object obj1, Object obj2)
方法用来排序
HashSet
、LinkedHashSet
和 TreeSet
的主要区别在于底层数据结构不同。HashSet
的底层数据结构是哈希表(基于 HashMap
实现)。LinkedHashSet
的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。
底层数据结构不同又导致这三者的应用场景不同。HashSet
用于不需要保证元素插入和取出顺序的场景,LinkedHashSet
用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet
用于支持对元素自定义排序规则的场景。
相比于HashMap
来说, TreeMap
主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。
[!note]
JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突),JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间
在 JDK 1.7 版本之前, HashMap 数据结构是数组和链表,HashMap通过哈希算法将元素的键(Key)映射到数组中的槽位(Bucket)。如果多个键映射到同一个槽位,它们会以链表的形式存储在同一个槽位上,因为链表的查询时间是O(n),所以冲突很严重,一个索引上的链表非常长,效率就很低了。
链接法:使用链表或其他数据结构来存储冲突的键值对,将它们链接在同一个哈希桶中。
开放寻址法:在哈希表中找到另一个可用的位置来存储冲突的键值对,而不是存储在链表中。常见的开放寻址方法包括线性探测、二次探测和双重散列。
再哈希法(Rehashing):当发生冲突时,使用另一个哈希函数再次计算键的哈希值,直到找到一个空槽来存储键值对。
哈希桶扩容:当哈希冲突过多时,可以动态地扩大哈希桶的数量,重新分配键值对,以减少冲突的概率。
equals相等的两个对象,hashCode一定相等。hashCode相等的两个对象,equals不一定相等
初始容量和负载因子:
HashMap
有一个初始容量和一个负载因子。初始容量是哈希表在创建时的容量,默认为 16。负载因子是一个在哈希表大小超过其容量乘以负载因子时,哈希表将进行扩容的阈值,默认为 0.75。扩容操作:
HashMap
将进行扩容操作。当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法
可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。
疫苗:Java HashMap的死循环 | 酷 壳 - CoolShell
多线程环境下,HashMap
扩容时会造成死循环
和数据丢失
的问题。在 HashMap
中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对HashMap
的put操作会导致线程不安全,可能产生数据覆盖.
数据不一致
两个线程同时进行put操作,并且存在哈希冲突
由于线程首先都执行完了hash碰撞的判断,桶为空
每个线程再向空桶中插入元素
public V put(K key, V value) { |
两个线程都先获取size,在++size
添加两次元素,而size只增加1
public V put(K key, V value) { |
基于JDK8的HashMap实现(万字详解) - 知乎 (zhihu.com)
JDK1.8 之前 HashMap
底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashcode
经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash
判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash
方法。使用 hash
方法也就是扰动函数是为了防止一些实现比较差的 hashCode()
方法 换句话说使用扰动函数之后可以减少碰撞。
相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
计算哈希码(Hash Code):
HashSet
中添加一个元素时,首先会调用该元素的 hashCode()
方法,得到元素的哈希码。null
,则它的哈希码为 0。映射到桶位置(Bucket Position):
处理哈希冲突:
HashSet
使用链表或红黑树(在JDK 8之后)来存储相同桶位置上的元素。检查元素唯一性:
HashSet
会通过调用元素的 equals()
方法来检查元素的唯一性。equals()
判断),新元素不会被加入。使用Collections.synchronizedMap
方法:
Map<K, V> synchronizedMap = Collections.synchronizedMap(new HashMap<K, V>());
这将返回一个线程安全的Map
,它在每个方法上都使用同步机制来确保线程安全。但请注意,虽然这确保了每个方法的原子性,但在多个操作之间,仍然可能需要额外的同步。
使用ConcurrentHashMap
: ConcurrentHashMap
是Java提供的线程安全的Map
实现。它使用分段锁机制,每个段相当于一个小的HashMap
,不同的段之间互不影响,这样可以提高并发性能。
Map<K, V> concurrentMap = new ConcurrentHashMap<K, V>();
[!note]
ConcurrentHashMap:Node数组+链表+红黑树实现,线程安全的(jdk1.8以前Segment锁,1.8以后volatile + CAS 或者 synchronized)
Segment数组,通过分段锁(Segmentation)实现线程安全。它将整个哈希表分成多个段(Segment),每个段都是一个小的哈希表,并且拥有自己的锁。这样,多个线程可以并发地访问不同的段,从而减少了锁的竞争,提高了并发性能。
在 JDK 1.7 中它使用的是数组加链表的形式实现的,而数组又分为:大数组 Segment 和小数组 HashEntry。 Segment 是一种可重入锁(ReentrantLock),在 ConcurrentHashMap 里扮演锁的角色;HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组,一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素。
直接在table元素上加锁,实现对每一行进行加锁,进一步减小了并发冲突的概率。
对于put操作,如果Key对应的数组元素为null,则通过CAS操作(Compare and Swap)将其设置为当前值。
如果Key对应的数组元素(也即链表表头或者树的根元素)不为null,则对该元素使用 synchronized 关键字申请锁,然后进行操作。
如果该 put 操作使得当前链表长度超过一定阈值,则将该链表转换为红黑树,从而提高寻址效率。
使用了一种更细粒度的锁策略,结合CAS(Compare-and-Swap)无锁算法来实现线程安全。在JDK 1.8中,ConcurrentHashMap将整个哈希表看作一个整体,不再进行分段。而是通过Node数组+链表+红黑树的结构来存储数据,并使用Synchronized和CAS来协调并发访问。
锁粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。
线程安全实现方式: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 源码分析 | JavaGuide
一文彻底搞懂CAS实现原理 & 深入到CPU指令 - 知乎 (zhihu.com)
Java集合常见面试题总结(下) | JavaGuide
java.util.concurrent.atomic
包提供了一组用于在多线程环境中进行原子操作的类。这些类通过使用硬件级别的原子性操作或者利用 sun.misc.Unsafe
提供的 CAS(Compare-And-Swap)操作来确保对变量的操作是原子的。这些类大多数都是基于原始数据类型的,例如 int
、long
,还有一些是引用类型。
以下是 java.util.concurrent.atomic
包中一些主要的类以及它们的用途:
AtomicInteger: 用于对整数进行原子操作,支持原子的自增(incrementAndGet()
)、自减(decrementAndGet()
)等操作。
AtomicLong: 用于对长整型进行原子操作,同样支持原子的自增、自减等操作。
AtomicBoolean: 用于对布尔类型进行原子操作,支持原子的设置和获取操作。
AtomicReference: 用于对引用类型进行原子操作,支持原子的获取和设置引用对象。
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray: 用于对数组中的元素进行原子操作,提供了一些原子性的数组操作。
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater: 用于对类的字段进行原子更新,允许在并发环境中对对象的字段进行原子性操作。
这些原子类提供了一种比使用 synchronized
关键字更轻量级的线程安全机制,特别适用于一些简单的计数器、状态标志等场景。在需要进行原子操作而又不需要全局的锁的情况下,这些类可以提供更好的性能。
虽然这些类提供了原子性的操作,但并不是所有的操作都可以用原子方式完成,因此在使用时仍然需要注意保证原子性的操作是否符合预期。