我们看一下容器的并发情况,首先我们介绍一下HashMap,HashMap并不是一个安全的集合,如果大家在多线程中使用
HashMap的话,那就会产生一些稀奇古怪的现象,最简单的把HashMap变成线程安全的方式呢,Collections.synchronizedMap
进行一个包装
同样对于其他的集合,List,Set,都提供了SynchronizedList方法,和SynchronizedSet方法,把任意非线程安全的集合,
变成线程安全的集合,这个可以作为一个实用的工具类,但是这种只适合于并发量比较小的情况,我们可以看一下Collection
内部是如何实现的,
/**
* Returns a synchronized (thread-safe) map backed by the specified
* map. In order to guarantee serial access, it is critical that
* <strong>all</strong> access to the backing map is accomplished
* through the returned map.<p>
*
* It is imperative that the user manually synchronize on the returned
* map when iterating over any of its collection views:
* <pre>
* Map m = Collections.synchronizedMap(new HashMap());
* ...
* Set s = m.keySet(); // Needn't be in synchronized block
* ...
* synchronized (m) { // Synchronizing on m, not s!
* Iterator i = s.iterator(); // Must be in synchronized block
* while (i.hasNext())
* foo(i.next());
* }
* </pre>
* Failure to follow this advice may result in non-deterministic behavior.
*
* <p>The returned map will be serializable if the specified map is
* serializable.
*
* @param <K> the class of the map keys
* @param <V> the class of the map values
* @param m the map to be "wrapped" in a synchronized map.
* @return a synchronized view of the specified map.
*/
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
他把这个map包装在SynchronizedMap里面,Map的每一个操作,他都会去做一个同步,put是同步,get也会做一个同步
/**
* @serial include
*/
private static class SynchronizedMap<K,V>
implements Map<K,V>, Serializable {
private static final long serialVersionUID = 1978198479659022715L;
private final Map<K,V> m; // Backing Map
final Object mutex; // Object on which to synchronize
SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
mutex = this;
}
SynchronizedMap(Map<K,V> m, Object mutex) {
this.m = m;
this.mutex = mutex;
}
public int size() {
synchronized (mutex) {return m.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return m.isEmpty();}
}
public boolean containsKey(Object key) {
synchronized (mutex) {return m.containsKey(key);}
}
public boolean containsValue(Object value) {
synchronized (mutex) {return m.containsValue(value);}
}
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
public V remove(Object key) {
synchronized (mutex) {return m.remove(key);}
}
从而使得map变得线程安全的一个包装,但是他的问题在哪里呢,就让put和ge都变得非常串行的一个实现,get和get
之间也得做一个等待,如果并发很高,其实是线程一个一个去做这个事情的,所以他的并发量并不会非常大,所以他只是一个
并行的解决方案,并不是一个高并发的解决方案,这几个也都是一样的,这里给大家介绍一个高并发的解决方案呢,是
ConcurrentHashMap,它是一个高性能的高并发的解决方案,那这里有关Map的使用,我就不介绍了,和普通的HashMap是
一样的,为什么ConcurrentHashMap是一个高并发的解决方案,而普通的HashMap做一个简单的包装呢,他只是一个简单的并发
方案呢,我想把普通的HashMap做一个介绍,内部实现其实是一个数组
每一个表象放的是Entry,每一个Entry里面放的key和value,当我一个key进来的时候,我要放到哪一个表项里面去呢,放到数组
的哪一个槽位当中呢,通过哈希算法得到的,映射到同一个槽位当中称之为哈希冲突,虽然你映射到同一个槽位里,那我就把你放到
同一个槽位里,一个entry数组槽位当中,我怎么放两个entry呢,我这个entry槽位你有一个next,这样我这里就变成了一个链表,
他基本实现是一个数组,数组里面放的是entry,每一个entry都是链表当中的一环,当哈希发生大量的哈希冲突的时候呢,他又退化
成一个链表,这个就是HashMap的一个基本实现,一般来说HashMap也不会放满,因为你放满之后必然会产生冲突,如果你所有的元素
都放满了,那你就冲突了,冲突是我们不想看见的,一旦HashMap发生冲突以后,后果性能就会降低,下面我们来看一下
ConcurrentHashMap怎么做的,我们来看put操作,当你put一个key进去的时候,
/**
* Maps the specified key to the specified value in this table.
* Neither the key nor the value can be null.
*
* <p>The value can be retrieved by calling the {@code get} method
* with a key that is equal to the original key.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with {@code key}, or
* {@code null} if there was no mapping for {@code key}
* @throws NullPointerException if the specified key or value is null
*/
public V put(K key, V value) {
return putVal(key, value, false);
}
他有一个Segment,因为现在要一个高并发的HashMap,并不是一个普通的HashMap,如果有大量线程产生的时候,
意味着我大量的线程,我可以把一个大的HashMap切分成若干个小的HashMap,然后我每一个线程进来的时候呢,
先把当前的key映射到小HashMap里面去,然后在小HashMap里面做一个HashMap做的事情,假如我有16个小HashMap,
意味着我可以接受16个线程的访问,相比于我之前只能接受一个线程去访问他,性能就提高了16倍了,就提高了
16倍,而小的HashMap就是Segment,所以Segment也是key里面的对,但是它是一个小HashMap,所以当你put一个key value
进去的时候呢,减少了多个线程的冲突,这个Segment继承了ReentrantLock重入锁,
static class Segment<K,V> extends ReentrantLock implements Serializable {
tryLock就是重入锁tryLock,tryLock做什么呢,就是做CAS操作,因为我使用的是tryLock,所以不会出现等待的情况,lock才会
等待,并没有简单的使用lock方法,而是使用tryLock尝试拿锁,拿不到我们在应用层做处理,tryLock在里面是一个CAS的实现,
我不停的try,我有一个try的次数,相当于是一个自循,rehash会把空间翻倍,rehash可能是一个比较耗时的操作,
/**
* {@inheritDoc}
*/
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
你有多个个段你就有多少个lock,有多少个段就做多少个unlock,size是把所有的segment数据都要加进来,如果还在进行修改的话,
那我是没有办法统计的,所以在统计之前我要把所有的segment锁都要拿到,然后再做这个数据的统计,看看到底有多少数据,然后再把
这个锁释放掉,这就是一个小小的弊端,当你要取得全局锁的时候你要把所有的锁都获取到,但是size并不是一个高频率调用的一个方法,
应该说也是可以接受的,我们再来看一个比较重要的东西,BlockQueue阻塞队列,阻塞队列在这里是一个接口,并不是一个实际的类,
阻塞队列首先明确一点,它是线程安全的,在并发容器当中,他的性能其实并不好,但为什么他重要呢,它是一个非常好的共享数据的,
共享数据的一个容器,他有什么好处呢,如果你的队列为空,然后你还试图从这里读一个数据的时候呢,读的线程就会做一个等待,等待
另外一个线程往里面写数据的时候,等待线程就会被唤醒,拿到数据,反过来如果数据已满,你还往队列里面写数据,那写的线程就会等待,
等待有人把这个数据拿掉之后呢,才能写进去,这就是BlockQueue阻塞队列,他就是取数据,拿数据,拿数据还可以设置一个时间,
拿不到可以抛出一个异常,
/**
* Retrieves and removes the head of this queue, waiting up to the
* specified wait time if necessary for an element to become available.
*
* @param timeout how long to wait before giving up, in units of
* {@code unit}
* @param unit a {@code TimeUnit} determining how to interpret the
* {@code timeout} parameter
* @return the head of this queue, or {@code null} if the
* specified waiting time elapses before an element is available
* @throws InterruptedException if interrupted while waiting
*/
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
/**
* Retrieves and removes the head of this queue, waiting if necessary
* until an element becomes available.
*
* @return the head of this queue
* @throws InterruptedException if interrupted while waiting
*/
E take() throws InterruptedException;
我们介绍ArrayBlockingQueue,他内部就是使用数组来实现的,LinkedBlockingQueue他就使用链表来实现,
ArrayBlockingQueue他用到了两个工具,
/** Main lock guarding all access */
final ReentrantLock lock;
一个是用来进行访问控制,我要保证他的线程安全,另外一个是用来做通知
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
如果你要拿数据,你这个数据为空,我就需要在我有数据的时候通知你,这个时候使用的就是notEmpty这个condition,
但反过来你要写数据,你需要等待,怎么通知你数据已经不满了呢,就通过notFull,
/**
* Inserts the specified element at the tail of this queue, waiting
* for space to become available if the queue is full.
*
* @throws InterruptedException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
首先做一个加锁,为什么这里不是一个高性能的并发呢,但凡你看到他毫无顾忌的做加锁操作的时候,他必然不是高性能的,
像ConcurrentHashMap这种写法才是高性能的写法,不会随随便便去做一个lock操作,它会在应用层面做自循等待,我lock的
时候会判断是否满了,如果我满了的话我该做什么呢,我就会等,我不能再插入了,我需要插入,但是满了我不需要插,
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
/**
* Extracts element at current take position, advances, and signals.
* Call only when holding lock.
*/
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
BlockingQueue适合生产者消费者模式,一个生产者往队列里放数据,往队列里take数据,拿不到数据我就等,
拿到数据我就处理,ConcurrentLinkedQueue,也是一个队列,它是个链表,这个是一个高性能的队列,在高并发的
情况下他可以保持一个高吞吐量,BlockQueue是不行的,它是一个高性能队列,他的接口和BlokingQueue是一样的,
因为他们都是队列,插入数据,拿取数据,知道有这么一个工具就可以了,如果需要在高并发的有一个性能表现的队列,
那么你就使用他就可以了,内部也是大量的使用了无锁的算法,没有把线程挂起的操作