订阅博客
收藏博客
微博分享
QQ空间分享

蒸汽洗车机,Java多线程:并发容器CopyOnWriteArrayList和ConcurrentHashMap,ditu

频道:全民彩票官方版 标签:陈楚生重庆市天气 时间:2019年11月01日 浏览:135次 评论:0条

前面我们讲了很多关于多线程设计和开发的内容,今天我们继续来讲解多线程中的并发容器。我们分2类来讲,一种是Copy-On-Write的思路,一种是jdk1.5引进的Concurrent包下面的并发容器。

一、CopyOnWriteArrayList

Copy-On-Write简称COW,是一种用于程序设计中的优化策略。简单的说,就是容器在写数据时先复制一份出来,写操作是针对复制出来的容器进行,读数据是针对原先的老容器进行。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,这也是早期读写分离的一种方案,读和写不同的容器,然后把原先指向老容器的引用指向新的容器。注意:是改变了引用,并非改变原先的老容器。

下面我们来看看CopyOnWriteArrayList的实现:

下面是添加元素的方法:

public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.le一键rootngth;
Object[] newElements age= Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

从add方法可以看出,在复制时是需要加锁的,不然多个线程写的时候会拷贝出多个副本出来。原理是拷贝一个新的数组,然后往新的数组添加元素,再将引用指向新拷贝出来的数组。

下面我们来看看读方法:

public E get(i三体三死神永生nt index) {
return get(getArray(), index);
}

读方法不需要加锁,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。

JDK中并没用CopyOnWriteM莎菲宝ap的实现,不过只要我们理解了COPY-ON-WRITE的思想,我们可以自己写一个CopyOnWriteMap的不用谢用英语怎么说工具类。

下面我们看看CopyOnWriteMap是如何实现的:

public class CopyOnWriteMap implements Map, Cloneable {
private航椒4号 volatile Map internalMap;

public CopyOnWrite彪言彪语Map() {
internalMap = new HashMap();
}

public V put(K key, V value) {

synchronized (this) {
Map newMap = new HashMap(internalMap);
V val = newMap.put(key, value);
intern精尽alMap = newMap;
return val;
}
}

public V get(Object key) {
return internalMap.get(key);
}

public void putAll(Map
synchronized (this) {
Map newMap = new HashMap(internalMap);
newMap.putAll(newData);
internalMap = newMap;
}
}
}

很简单,只要我们理解了COPY-ON-WRITE的机制,我们就可以开发出各种我们需要的容器,并在不同的场景中应用。

CopyOnWrite的缺点:

当然,copyOnWrite的缺点也是很明显的。

  • 内存占用问题。

copyOnWrite的写时复制机制,会存在2份内存对象,这个时候很有可能造成频繁的Yong GC和Full GC,消耗大量的资源。

  • 数据一致性问题。

CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

二、ConcurrentHashMap

在JDK1.5开始,对并发容器进行设计,java.util.concurrent包。与传统的Vector和Hashtable、Collections.synchronizedXxx()同步容器等相韦德磊比,主要解决了2个问题。

1)根据具体场景进行设计,尽量避免synchronized,提高10016并发性。

2)定义了一些并发安全的复合操作,并且保证并发环境下的迭代操作不会出错。

ConcurrentHashMap代替同步的Map。HashMap是根据散列值分段存储的,同步Map在同步的时候锁住了所有的段,而ConcurrentHashMap加锁的时候根据散列值锁住了散列值锁对应的那段,因此提高了并发性能。ConcurrentHashMap也增加了对常用复合操作的支持,比如"若没有则添加":putIfAbsent(),替换:replace()。这2个操作都是原子操作。

大家都知道HashMap是非线程安全的,Hashtable是线程安全的,但是由于Hashtable是采用synchronized进行同步,相当于所有线程进行读写时都去竞争一把锁,导致效率非常低下。

ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁。

下面我们来看看ConcurrentHashMap的初始化:

public ConcurrentHashMaweixinwangyebanp(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
thr我的逼ow new IllegalArgumentException();

if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;

// Find power-of-嫁妆two sizes best matching arguments
int sshift =艾草泡脚 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
竹鸡segmentShift = 32 - sshift;
segmentMask = ssize - 1;
this.segments = Segment.newArray(ssize);

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.segments.length; ++i)
this.segments[i] = new Segment(cap, loadFactor);
}

CurrentHashMap的初始化一共有三个参数,一个initialCapac猪肚怎么洗ity,表示初始的容量,一个loadFactor,表示负载参数,最后一个是concurrentLevel,代表ConcurrentHashMap内部的Segment的数量,ConcurrentLevel一经指定,不可改变,后续如果ConcurrentHashMap的元素数量增加导致ConrruentHashMap需要扩容,ConcurrentHashMa调侃p不会增加Segment的数量,而只会增加Segment中链表数组的容量大小,这样的好处是蒸汽洗车机,Java多线程:并发容器CopyOnWriteArrayList和ConcurrentHashMap,ditu扩容过程不需要对整个ConcurrentHashMap做rehash,而只需要对Segment里面的元素做一次rehash就可以了。

整个ConcurrentHashMap的初始化方法还是非常简单的,先是根据concurrentLevel来new出Segment,这蒸汽洗车机,Java多线程:并发容器CopyOnWriteArrayList和ConcurrentHashMap,ditu里Segment的数量是不大于concurrentLevel的最大的2的指数,就是说Segment的数量永远是2的指数个,这样的好处是方便采用移位操作来进行hash,加快hash的过程。接下来就是根据intialCapacity确定Segment的容量的大小,每一个Segment的容量大小也是2的指数,同样使为了加快hash的过程。

这边需要特别注意一下两个变量,分别是segmentShift和segmentMask蒸汽洗车机,Java多线程:并发容器CopyOnWriteArrayList和ConcurrentHashMap,ditu,这两个变量在后面将会起到很大的作用,假设构造函数确定了Segment的数量是2的n次方,那么segmentShift就等于32减去n,而segmentMask就等于2的n次方减一。

下面我们来看看get方法:

public V get(Object key) {
int蒸汽洗车机,Java多线程:并发容器CopyOnWriteArrayList和ConcurrentHashMap,ditu hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);
}

get方法是不加锁的,segmentFor这个函数用于确定操作应该在哪一个segment中进行,几乎对ConcurrentHashMap的所有操作都需要用到这个函数,我们看下这个函数的实现:

final Segment segmentFor(int hash) {
return segments[(hash >>> segmentShift) & segmentMask];
}

这个函数用了位操作来确定Segment,根据传入的hash值向右无符号右移segmentShift位,然后和segmentMask进行与操作,结合我们之前说的segmentShift和segmentMask的值,就可以得出以下结论:假设Segment的数量是2的n次方,根据元素的hash值的高n位就可以确定元素到底在哪一个Segment中。

下面我们来看看put方法:

V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();
try {
int c = count;
if (c++ > thresh蒸汽洗车机,Java多线程:并发容器CopyOnWriteArrayList和ConcurrentHashMap,dituold) // ensure capacity
rehash();
HashEntry[] tab = table;
int index = hash & (tab.length - 1);
HashEntry first = tab[index];
HashEntry e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;

V oldValue;
if (e != null) {
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value;
}
else {
oldValue = null;
++modCount;
tab[index] = new HashEntry(key, hash, first, value);
count = c; // write-volatile
}
return oldValue;
} finally {
unlock();蒸汽洗车机,Java多线程:并发容器CopyOnWriteArrayList和ConcurrentHashMap,ditu
}
}

put操作的前面也是确定Segment的过程。首先对Segment的put操作是加锁完成的,然toto后在第五行,如果Segment中元素的数量超过了阈值(由构造函数中的loadFactor算出)这需要进行对Segment扩容,并且要进行rehash,关于rehash的过程大家可以自己去蒸汽洗车机,Java多线程:并发容器CopyOnWriteArrayList和ConcurrentHashMap,ditu了解,这里不详细讲了。

第11行这里的这个while循环是在链表中寻找和要put的元素相同key的元素,如果找到,就直接更新更新key的value,如果没有找到,则进入21行这里,生成一个新的HashEntry并且把它加到整个Segment的头部,然后再更新count的值。

Remove操作的前面一部分和前面的get和put操作一样,都是定位Segment的过程,然后再调用Segment的remove方法,有兴趣的自己去看看源码实现。

喜欢的加个关注,这该死的Bug。

空间中已经讲了很多关于Java多线程的知识点,感兴趣的自己去查看。

Java多线程:CountDownLatch、CyclicBarrier和Semaphore

Java多线程:wait、notify、notifyAll和 Condition

Java多线程:线程池的原理及应用

Java多线程:Callable、Future和FutureTask

接下来我们kuaib还会继续讲解更多关于多线程的知识,后续我们会讲解一些流行的架构。