Java并发编程的相关知识(6)--HashMap HashTable ConcurrentMap ConcurrentLinkedQueue

HashMap(线程不安全)

HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,Entry的next节点永远不为空,就会成产生死循环获取Entry。

HashTable(效率低下)

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下hashtable效率非常低下,当多个线程访问hashtable时,会进入阻塞或轮询状态

ConcurrenHashtMap(锁分段)

hashTable容器竞争激烈的并发环境表现效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,而ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其它段的数据也能被其他线程访问。ConcurrentHashMap的类图如下:
在这里插入图片描述
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁。一个Segment里包含一个HashEntry数组,当对HashEntry数组的数据进行修改时,必须先获取与它对应的Segment锁。

ConcurrentHashMap的初始化

通过initialCapacity.loadFactor和concurrentLevel等几个参数来初始化

初始化segments数组

if(concurrencyLevel>MAX_SEGEMNTS)
	concurrencyLevel=MAX_SEGMENTS;
int sshift=0;
int ssize=1;
while(ssize<concurrencyLevel){
++sshift;
ssize<<=1;
}
segmentShift=32-sshift;
segmentMask=ssize-1;
this.segments=Segment.newArray(ssize):

segments数组的长度必须是2的N次方,concurrencyLevel的最大值65535,也就是segments数组的最大长度为65536,对应的二进制是16位

初始化segmentShift段偏移量和segmentMask段掩码

segmentShift=32-sshift;
segmentMask=ssize-1;
32是因为ConcurrentHashMap里的hash()方法输出的最大数是32

初始化每个segment

initialCapacity是ConcurrentHashMap的初始化容量,loadfactor是每个segment的负载因子,在构造方法里需要通过这两个参数来初始化数组中的每个segment。cap是segment里的HashEntry数组的长度,cap不是1,就是2的n次方。默认情况下initialCapacity等于16,loadfactor等于0.75,cap为1,segmentShift为28,segmentMask为15.

if(initialCapacity>MAXIMUM_CAPACITY)
	initialCapacity=MAXIMUM_CAPACITY;
int c=initialCapacity/ssize;
if(c*ssize<initialCapacity)
	++c;
int cap=1;
while(cap<c)
	cap<<=1;
for(int i=0;i<this.segements.length;++i)
	this.segments[i]=new Segments<K,V>(cap,loadFactor);

ConcurrentHashMap的get,put,size操作

get操作先经过一次散列,然后使用整个散列值通过散列运算定位到Segment,再通过散列算法定位到元素。
get的方法不需要加锁,因为它的get方法将要使用的共享变量都定义成volatile类型
定位Segment使用的是元素hashcode通过再散列后得到的值得高位,而定位HashEntry直接使用的是再散列后的值。避免两次散列后的值一样,使元素在hashEntry中散开。
put的方法首先定位到Segment,然后在Segment里进行插入操作。插入人操作需要经历两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二部定位到添加元素的位置,然后将其放在HashEntry的数组里。
在扩容的时候,只对某个segmen进行扩容。
size的方法先尝试2次通过不锁住segment的方法是来统计各个segment大小(通过volatile变量count),如果统计的过程中,容器的count发生了变化,(使用modCount变量,在put,remove,和clean方法里操作元素前都会将变量modcount进行加1,比较前后modCount是否发生变化,从而知道容器的大小是否发生变化)则再采用加锁的方式来统计所有Segment的大小。

ConcurrentLinkedQueue

ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部;当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法(CAS算法)来实现。(非阻塞方式实现线程安全)

入队过程

定位尾结点

判断tail是否有next节点,有则表示next节点可能是尾结点

使用CAS算法将入队节点设置为next节点

p.casNext(null,n)方法用于将入队节点设置为当前队列尾结点的next节点,如果p是null,表示p是当前队列的尾结点,如果p不为null,表示有其他线程更新了尾结点,则需要重新获取当前队列的尾结点。

public boolean offer(E e){
	if(e==null) throw new NullPointException();
	Node<E> n=new Node<E>(e);
	retry://死循环,入队不成功,反复入队
	for;;){
		Node<E> t=tail;
		Node<E> p=t;
		for(int hops=0; ; hops++){
			Node<E> next=succ(p);
			if(next!=null){
			//循环两次及其以上,并且当前节点还是不等于尾结点
				if(hops>HOPS&&t!=tail)
					continue retry;
				p=next;
			}
			//如果p是尾结点,则设置p节点的next节点为入队节点
			else if(p.casNext(null,n){
			//如果tail节点有大于等于1个next节点,则将入队节点设置成tail节点,
			//若更新失败,表示其他线程更新成功。
			if(hops>=HOPS)
				casTail(t,n);//更新tail节点,允许失败
			return true;
			}
			//p有next节点,表示p的next节点是尾结点。则重新设置p节点。
			else{
			p=succ(p);
			}
		}
	}
}

通过hops变量来控制并减少tail节点的更新频率,并不是每次节点入队后都将tail节点更新成尾结点,而是当tail节点和尾结点的举例大于等于常量HOPS的值时才更新tail节点,tail和尾结点距离越长,使用CAS更新tail节点的 次数就会越少,定位尾结点时间就越长,但仍然能提高入队的效率,因为本质上来看它通过增加对volatile变量的读操作来减少volatile变量的写操作,写操作开销远远大于读操作,所以入队效率会提升。

出队列

从队列里返回一个节点元素,并清空该节点对元素的引用。
并不是每次出队时都更新head节点,当head节点里有元素,直接弹出head节点里的元素,而不是更新head节点。只有当head节点没有元素时,出队操作才会更新head节点。
这种做法也是通过hops变量来减少使用CAS更新head节点的消耗,从而提高出队效率。

public E poll(){
	Node<E> h=head;
	//p表示头结点,需要出队节点
	Node<E> p=h;
	for(int hops=0; ;hops++){
		E item=p.getItem();//获取p节点的元素
		//如果p节点的元素不为空,则使用CAS设置p节点引用的元素为null,成功则返回p节点的元素
		if(item!=null&&p.casItem(item,null){
			if(hops>=HOPS){
			//将p节点的下一个节点设置成head节点
				Node<E> q=p.getNext();
				updateHead(h,(q!=null)?q:p);
			}
			return item;
		}
		//如果头结点的元素为空或头结点发生变化,这说明头结点已经被另外一个线程修改了。则获取p节点的下一个节点
		Node<E> next=succ(p);
		//如果p的下一个及诶单也为空,说明这个队列已经空了
		if(next==null){
			updateHead(h,p);
			break;
		}
		//如果下一个节点不为空,则将头结点的下一个节点设置成头结点。
		p=next;
	}
	return null;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值