HashMap、ConcurrentHashMap、ReentrantLock和ReentrantReadwriteLock等常见集合和锁的知识点总结
ArrayList
结构:基于数组
主要方法:
add
remove
set
get
ensureCapacityInternal 判断容量,扩容
扩容机制:
jdk1.7之前:
创建list即使没有传入容量,其容量也会被设为10
jdk1.7之后:
如果创建list时没有指定容量,则其初始容量为0,当集合第1次添加元素的时候,集合扩容为10
如果创建list时指定了容量,则其容量为指定的容量
add数据时,当list需要的容量大于原容量时,扩容的大小 = 原容量+原容量/2
所谓的容量就是数组length,ArrayList是基于数组的
CopyOnWriteArrayList
结构:并发版ArrayList,基于数组,使用ReentrantLock保证线程安全
主要方法:
add
remove
set
get
CopyOnWriteArrayList的add方法:
1、ReentrantLock加锁
2、根据旧数组复制出一个新数组,长度是旧数组长度+1
3、把元素插入到新数组
4、让数组引用指向新数组
5、解锁
缺点:
CopyOnWriteArrayList基于数组,在add,remove,set时会复制一个新数组出来进行操作,
操作完成后再把引用指向新的数组,这种机制可能造成大量的内存空间耗费,
适合读多写少的场景(get操作不加锁,也不需要新建数组)
读时比synchronizedXXX性能好,写时比不上
加锁时机:
add,remove,set时加ReentrantLock锁,get时不需加锁
HashMap
结构:数组+单向链表(1.7),数组+单向链表+红黑树(1.8),允许一个key为null
方法:
put/putValue
resize 扩容
get/getNode
remove/removeNode
containsKey
containsValue
values
entrySet
根据key的hash值分配数组下标:int index = hash & (capacity - 1)
扩容机制:
HashMap要第一次put时才会初始化Node数组,刚创建hashMap时Node数组是null的
如果创建hashmap时没有指定容量:
刚初始化时数组长度为16
当map中的元素数量大于16*0.75=12时,则进行数组扩容,扩容为16*2=32
如果创建hashmap时指定了容量:
会先计算一个最接近的大于等于指定容量的值(2的n次幂),作为threshold,
在第一次put时把数组容量Capacity设为threshold,然后再计算一个新的threshold
当map中的元素数量大于Capacity*0.75时,则进行数组扩容,扩容为2Cap
当链表容量大于8且桶的容量大于64会将链表转化为红黑树,当某棵树节点数小于6时会把树转为链表
HashMap和HashTable的区别:
1、HashTable是线程安全的,且不允许key、value是null。
2、HashTable默认容量是11。
3、HashTable是直接使用key的hashCode(key.hashCode())作为hash值,
不像HashMap内部使用static final int hash(Object key)扰动函数对key的hashCode进行扰动后作为hash值。
4、HashTable取哈希桶下标是直接用模运算%.(因为其默认容量也不是2的n次方。所以也无法用位运算替代模运算)扩容时,
新容量是原来的2倍+1。int newCapacity = (oldCapacity << 1) + 1;
5、Hashtable是Dictionary的子类同时也实现了Map接口,HashMap是Map接口的一个实现类;
ConcurrentHashMap
结构:数组+单向链表+Segment(1.7),数组+单向链表+红黑树+CAS+synchronized(1.8),key和value都不可为null
方法:
put/putValue hash = spread(key.hashCode()) --》index = (table.length - 1) & hash)
initTable 初始化table
transfer 扩容方法
helpTransfer 要插入的key对应的数组上的节点的hash等于MOVED(-1),证明数组正在扩容,则让当前线程帮助把table的元素复制到新的table中
treeifyBin 尝试把链表转为红黑树
addCount 统计ConcurrentHashMap的size,如果需要扩容则进行扩容
get
put的流程:
当添加一对键值对的时候,首先会去判断保存这些键值对的数组是不是初始化了,
* 如果没有初始化就先调用initTable()方法来进行初始化过程
* 然后通过计算hash值来确定放在数组的哪个位置
* 如果没有hash冲突就直接CAS插入,如果hash冲突的话,则取出这个节点来*
* 如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新的数组,则当前线程也去帮助复制
* 最后一种情况就是,如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作
* 然后判断当前取出的节点位置存放的是链表还是树
* 如果是链表的话,则遍历整个链表,直到取出来的节点的key来个要放的key进行比较,如果key相等,并且key的hash值也相等的话,
* 则说明是同一个key,则覆盖掉value,否则的话则添加到链表的末尾
* 如果是树的话,则调用putTreeVal方法把这个元素添加到树中去
* 最后在添加完成之后,调用addCount()方法统计size,判断在该节点处共有多少个节点(注意是添加前的个数),如果达到8个以上了的话,
* 则调用treeifyBin方法来尝试将该处的链表转为树,或者扩容数组
sizeCtl各种值的含义
sizeCtl为0,代表数组未初始化, 且数组的初始容量为16
sizeCtl为正数,如果数组未初始化,那么其记录的是数组的初始容量,如果数组已经初始化,那么其记录的是数组的扩容阈值
sizeCtl为-1,表示数组正在进行初始化
sizeCtl小于0,并且不是-1,表示数组正在扩容, -(1+n),表示此时有n个线程正在共同完成数组的扩容操作
扩容机制:和hashmap差不多,刚创建currenthashmap时不会初始化table
当第一次put时,会初始化一个长度为16的table,扩容时扩容为原来容量的2倍
map的size大于阈值(sizeCtl=capacity * loadfactor)---扩容
某个链表的长度大于8且table长度小于64---扩容
某个链表的长度大于8且table长度大于64---把该链表转为红黑树
扩容后如果某个红黑树的节点数小于6--把该红黑树转为链表
加锁时机:在put,扩容,链表转树,树转链表时,对链表或红黑树的首节点加synchronized锁,get时不需加锁
AQS
结构:
AQS为各种同步器提供底层支持,内部维护这一个用于标记锁状态的int值state,和一个用于存放等待获取锁的线程的fifo同步队列(双链表) 以及ConditionObject条件队列
Node--用于封装取锁失败的线程,进行入队
head--指向等待队列的头结点,该节点要么是刚初始化队列时的空节点,要么是已获得锁的不再参与排队的节点
tail--指向队列的尾节点
Unsafe--CAS --- 用于原子更新head和tail引用的指向,或者原子更新节点状态waitStatus
自旋--节点入队时,死循环保证入队成功,节点入队后,死循环,只有获取锁成功或者报错才会跳出循环
支持模式:独占模式(ReentrantLock) 共享模式(Semaphore/CountDownLatch) 组合模式(ReentrantReadwriteLock)
方法eg:
acquireQueued
addWaiter
enq
AQS重要的提供给子类实现的方法:
1、 boolean tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
2、 boolean tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
3、 int tryAcquireShared(int):共享方式。尝试获取资源。返回负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
4、 boolean tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回 true,否则返回false。
5、 boolean isHeldExclusively():判断当前线程是否持有独占锁的那个线程。只有用到condition才需要去实现它,例如ReentrantLock的Sync中。
ReentrantLock
ReentrantLock分公平锁和非公平锁,它是独占锁,可重入锁,悲观锁
加锁--state+1,解锁--state-1
体系结构:
ReentrantLock是Lock的子类
ReentrantLock有3个内部类:Sync,FairSync,NonFairSync
Sync是AQS的子类
FairSync和NonFairSync是Sync的子类
ReentrantLock有一个属性Sync,这个属性指向一个Sync对象
当创建ReentrantLock对象时,传入true参数则会实例化一个FairSync对象,让Sync属性指向它
传入false或者不传则会实例化一个NonFairSync对象,让Sync属性指向它
当调用方法时,通过Sync就能调用到对应公平/非公平的方法,或者祖父类AQS里的方法
公平锁与非公平锁的区别是:非公平锁一上来就直接CAS尝试加锁,而公平锁需要先判断state是否为0和自己是否需要排队,但两者入队后都得乖乖排队
公平锁获取锁的过程:
1个线程执行lock()方法
boolean tryAcquire(int acquires)
1、判断state是否等于0
2、判断自己是不是需要排队
3、state=0且自己不需要排队,则尝试CAS抢锁,把state设置为1,把占有锁的线程设为自己,返回true结束方法
4、假如state不等于0,证明已有线程持有独占锁,则判断持有锁的线程是不是自己,是自己的话就把state+1,重入,返回true结束方法
5、如果3和4不成立,则返回false结束方法
如果tryAcquire返回true,则lock方法结束
如果tryAcquire返回false,则执行
Node addWaiter(Node mode),把线程包装成Node节点插入队列尾部,并返回该Node
然后执行
boolean acquireQueued(final Node node, int arg) 自旋排队
1、如果该线程Node处于队列的第二个节点,则再次tryAcquire尝试抢锁,成功的话就把自己设为头结点,返回线程中断状态,结束方法
2、如果该节点不是队列第二个节点,或者抢锁失败,则检查该节点是否需要挂起,如果需要挂起,则挂起该线程,线程进入WATTING状态
3、等该线程被唤醒重新running后,会重复步骤1尝试抢锁,成功就返回线程中断状态,结束方法,失败就继续重复步骤2
4、自旋过程如果报错,则线程节点会进行出队操作,取消排队,不再自旋获取锁
非公平锁获取锁的过程:
1个线程执行lock()方法
1、直接CAS尝试把state的值修改为1,成功的话即抢锁成功,把持锁线程设为自己
2、假如抢锁失败,则执行
nonfairTryAcquire(int acquires)
1、判断state是否等于0,如果等于0直接CAS,成功则把持锁线程设为自己,返回true
2、假如state不等于0,则判断自己是不是持锁线程,是的话就state+1,重入,返回true
3、如果1和2都不通过,则返回false
如果nonfairTryAcquire返回true,则lock方法结束
如果nonfairTryAcquire返回false,则执行下面
Node addWaiter(Node mode),把线程包装成Node节点插入队列尾部,并返回该Node
然后执行
boolean acquireQueued(final Node node, int arg) 自旋排队
1、如果该线程Node处于队列的第二个节点,则再次tryAcquire尝试抢锁,成功的话就把自己设为头结点,返回线程中断状态,结束方法
2、如果该节点不是队列第二个节点,或者抢锁失败,则检查该节点是否需要挂起,如果需要挂起,则挂起该线程,线程进入WATTING状态
3、等该线程被唤醒重新running后,会重复步骤1尝试抢锁,成功就返回线程中断状态,结束方法,失败就继续重复步骤2
4、自旋过程如果报错,则线程节点会进行出队操作,取消排队,不再自旋获取锁
公平锁和非公平锁解锁的过程是一样的:
1个线程执行unlock()
boolean release(int arg)
boolean tryRelease(int releases)
1、把state-1
2、如果state-1后不等于0,则结束unlock方法
3、如果state-1后等于0,表示该线程持锁的全部锁都释放了,则把持有独占锁的线程设为null,
如果队列中有需要唤醒的等待线程,则唤醒队列中应该被唤醒的那一个(一般为第二个)
ReentrantReadwriteLock
ReentrantReadwriteLock基于AQS,分公平锁和非公平锁, 读锁是共享锁, 写锁是独占锁, 支持锁降级,读读共享,读写互斥,写写互斥---提高吞吐量
使用state高16位表示读锁,低16位表示写锁
体系结构:
ReentrantReadWriteLock是ReadWriteLock的子类
ReentrantReadWriteLock和ReentrantLock一样,也是基于AQS的
ReentrantReadWriteLock有ReadLock,WriteLock,Sync,FairSync,NonFairSync 4个静态内部类
ReadLock和WriteLock是Lock的子类
Sync是AQS的子类
FairSync和NonFairSync是Sync的子类
特点:
1、读写锁接口ReadWriteLock接口的一个具体实现,实现了读写锁的分离,
2、⽀持公平和非公平,底层也是基于AQS实现
3、允许从写锁降级为读锁 流程:先获取写锁,然后获取读锁,最后释放写锁;但不能从读锁升级到写锁
4、重入:读锁后还可以获取读锁;获取了写锁之后既可以再次获取写锁又可以获取读锁
ReentrantReadWriteLock相比ReentrantLock的优势:
ReentrantLock是独占锁且可重入的,相比synchronized而言功能更加丰富也更适合复杂的并发
场景,但是也有弊端,假如有两个线程A/B访问数据,加锁是为了防止线程A在读数据, 线程B在写
数据而造成的数据不一致; 但线程A在读数据,线程C也在读数据,读数据是不会改变数据的所以没必要加
锁,但是还是加锁了,降低了程序的性能,所以就有了ReadWriteLock读写锁接口
--------------------------------------------------------------------------
读锁lock时只在这个方法区分公不公平:
公平:readerShouldBlock() --- hasQueuedPredecessors() (此方法和ReentrantLock共用)
队列有2个或2个以上节点,且第二个不是我则要阻塞
非公平:readerShouldBlock()--apparentlyFirstQueuedIsExclusive()--队列第二个线程是写线程则需要阻塞(防止写线程饥饿)
写锁lock时公平非公平在这个方法区分:writerShouldBlock()
公平:readerShouldBlock() --- hasQueuedPredecessors() --- 和ReentrantLock共用
队列有2个或2个以上节点,且第二个不是我则要阻塞
非公平:不需阻塞
读锁unlock时公平和不公平是一样的,写锁unlock时公平和不公平也是一样的
读线程入队的原因:第一次抢锁失败,然后就入队了
读线程和写线程共用一个队列,共用一个state(高16位--读锁计数 低16位--写锁计数)
读锁unlock--假如所有读锁都解完了--读锁state为0--唤醒队列第二个节点--队列第二个节点如果醒来后抢锁成功--成为头节点--又会去唤醒队列第二个节点
---------------------------------------------------------------------------------------------
获取读锁--公平方式
1、hasQueuedPredecessors--如果队列有2个或2个以上节点,且第二个不是当前线程---要block
2、如果已经有别人持有写锁--失败
3、其他情况---尝试CAS
获取读锁--非公平方式:
1、apparentlyFirstQueuedIsExclusive--如果队列第二个节点为写线程--要block
2、如果已经有别人持有写锁--失败
3、其他情况---尝试CAS
----------------------------------------------------------------------
写线程入队的原因:第一次抢锁失败,然后就入队了
-------------------------------------------------------
获取写锁--公平方式
1、hasQueuedPredecessors--如果队列有2个或2个以上节点,且第二个不是当前线程---要block
2、如果有人持有读锁,或者有别人持有写锁---失败
3、其他情况---尝试CAS
获取写锁--非公平
1、writerShouldBlock--直接不需要block
2、如果有人持有读锁,或者有别人持有写锁---失败
3、其他情况---尝试CAS
---------------------------------------------
ReentrantReadwriteLock中的一些变量:
firstReader:第一个获取读锁的线程
cachedHoldCounter :离当前最近的获取读锁的线程的计数器(记录此线程的id和获取锁的次数)
readHolds:ThreadLocalHoldCounter(ThreadLocal的子类)的实例,
用于为每个线程存储各自的本地数(记录此线程的id和获取锁的次数),一个线程对应一个ThreadLocal实例(key),
通过key就能获取到线程存储的数据,键值对(entry)存于ThreadLocal的内部类ThreadLocalMap中
thread--ThreadLocal实例 :thread数据 --- Entry --- ThreadLocalMap