JDK并发包1-2

我们看一下容器的并发情况,首先我们介绍一下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是一样的,

因为他们都是队列,插入数据,拿取数据,知道有这么一个工具就可以了,如果需要在高并发的有一个性能表现的队列,

那么你就使用他就可以了,内部也是大量的使用了无锁的算法,没有把线程挂起的操作

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值