《Java核心技术面试精讲–杨晓峰》学习笔记目录
线程中的基本概念以及代码演示如下
线程、并发中的基本概念
线程状态
多线程实践
本节大部分章节都用来介绍 ConcurrentHashMap 的实现原理,分析了 java 对与高并发场景的解决思路。日常处理使用synchronized 关键字即可。
文章目录
-
-
- 回答
- ConcurrentHashMap 的实现原理代码分析
- 补充
-
- ConcurrentHashMap和Hashtable的区别
-
回答
Java 提供了不同层面的线程安全支持。在传统集合框架内部,除了Hashtable
等同步容器,还提供了所谓的同步包装器(Synchronized Wrapper
),我们可以调用Collections
工具类提供的包装方法,来获取一个同步的包装容器(如Collections.synchronizedMap
),但是它们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较低下。
另外,更加普遍的选择是利用并发包提供的线程安全容器类,它提供了:
- 各种并发容器,比如
ConcurrentHashMap
、CopyOnWriteArrayList
。 - 各种线程安全队列(
Queue/Deque
),如ArrayBlockingQueue
、SynchronousQueue
。 - 各种有序容器的线程安全版本等。
具体保证线程安全的方式,包括有从简单的synchronize
方式,到基于更加精细化的,比如基于分离锁实现的ConcurrentHashMap
等并发实现等。具体选择要看开发的场景需求,总体来说,并发包内提供的容器通用场景,远优于早期的简单同步实现。
ConcurrentHashMap 的实现原理代码分析
1.7
put加锁
通过分段加锁segment,一个hashmap里有若干个segment,每个segment里有若干个桶,桶里存放K-V形式的链表,put数据时通过key哈希得到该元素要添加到的segment,然后对segment进行加锁,然后在哈希,计算得到给元素要添加到的桶,然后遍历桶中的链表,替换或新增节点到桶中
size
分段计算两次,两次结果相同则返回,否则对所以段加锁重新计算
1.8
put CAS 加锁
1.8中不依赖与segment加锁,segment数量与桶数量一致;
首先判断容器是否为空,为空则进行初始化利用volatile的sizeCtl作为互斥手段,如果发现竞争性的初始化,就暂停在那里,等待条件恢复,否则利用CAS设置排他标志(U.compareAndSwapInt(this, SIZECTL, sc, -1));否则重试
对key hash计算得到该key存放的桶位置,判断该桶是否为空,为空则利用CAS设置新节点
否则使用synchronize加锁,遍历桶中数据,替换或新增加点到桶中
最后判断是否需要转为红黑树,转换之前判断是否需要扩容
size
利用LongAdd累加计算
补充
ConcurrentHashMap和Hashtable的区别
ConcurrentHashMap和Hashtable的区别主要体现在实现线程安全的方式上不同。
- 底层数据结构: JDK1.8采用的数据结构跟HashMap1.8的结构- -样,数组+链表/红黑二叉树。Hashtable和JDK1.8之前的HashMap的底层数据结构类似都是采用数组+链表的形式,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的;
- 实现线程安全的方式(重要) :
JDK1.8
用Node
数组+链表+红黑树的数据结构来实现,并发控制使用synchronized
和CAS
来操作。(JDK1.6
以后对synchronized
锁做了很多优化)整个看起来就像是优化过且线程安全的HashMap,Hashtable
(同一把锁) :使用synchronized
来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put
添加元素,另一个线程不能使用put
添加元素,也不能使用get,
竞争会越来越激烈效率越低。