ConcurrentHashMap和ReentrantLock等常见集合和锁的知识点总结(面试)

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值