Java高并发容器类

一、同步容器(线程安全还不够)

同步容器保证了装入容器数据访问的线程安全,也就是说无论多少个线程同时调用,都不会破坏容器、容器内数据的安全性。

虽然做到了线程安全,容器的复合操作:迭代、跳转、运算仍会带来新问题。因为同步容器保证了容器内数据的安全,可不能保证反馈数据的正确性。因为多线程中的调用者不合时宜的调用,经常会导致调用异常(然而仍然是线程安全,并没有破坏vector)。

这就像什么呢,容器虽然是安全的,人为操作容器仍需要安全规范

例如查找index来遍历线程安全容器Vector的操作:

public class VectorExample {
	
	private static Vector list = new Vector();
	
	public static void initVector() {
		for(int i=0; i<10; i++) {
			list.addElement(new Integer(i));
		}
	}
	
	public static Object getLast() {
		return list.get(list.size()-1);
	}
	
	public static void deleteLast() {
		list.remove(list.size()-1);
	}

}

此时,如果线程A调用deleteLast( ),线程B同时getLast( )。线程B调用list.size的索引值将不再有效(过时),这就势必抛出ArrayIndexOutOfBoundsException异常。

若用迭代器Iterator遍历Vector则实现了改良,引入了“fail-fast”机制。fail-fast即在遍历某个集合时,不得同时修改该集合,否则马上抛出异常:

public class VectorExample {
	
	private static Vector list = new Vector();
	
	public static void main(String[] args) {
		for(int i=0; i<10; i++) {
			list.addElement(new Integer(i));
		}
		
		Iterator listIte = list.iterator();
		int count = 0;
		
		while(listIte.hasNext()) {
			if(count == 2)
				list.remove(2);
			System.out.println(listIte.next());
			count++;
		}
	}

}

遍历第三个元素,抛出ConcurrentModificationException异常:

如何保证不抛出异常的安全遍历呢?加锁是一个看似不错的机制:

public class VectorExample {
	
	private static Vector list = new Vector();
	
	public static void initVector() {
		synchronized(list) {
			for(int i=0; i<10; i++) {
				list.addElement(new Integer(i));
			}
		}
	}
	
	public static Object getLast() {
		synchronized(list) {
			return list.get(list.size()-1);
		}
	}
	
	public static void deleteLast() {
		synchronized(list) {
			list.remove(list.size()-1);
		}
	}

}

 这样,不管有几个线程,getLast( )和deleteLast( )函数执行前都必须先申请得到容器list上的锁。也就保证了,同一时刻遍历、修改操作只能有一个能发生。

然而实际情况是,如果对于list容器的操作很多,每个操作都需要等待list锁会造成两个问题:

(1)饿死,某个线程持有锁的时间很长,许多线程竞争list锁就更激烈。导致多线程其他操作不能执行,程序“假死”。

(2)吞吐量降低,许多线程都只等待一个锁被释放,将极大降低吞吐量、CPU利用率,其执行效率甚至远低于单线程。

那么既然加锁不是适合方案,另一个替代方案是“克隆容器”。也就是说,我们复制list容器。每个线程保有一份list容器的副本,在副本上进行迭代、修改、提交。这样虽然大大减少了线程间竞争,却显著提高了内存的开销,也不是一个非常可取的方案。


 

二、并发容器(加速同步容器的遍历访问)

因为修改、遍历元素的原子操作,也就是同一时刻只能有一个线程操作容器,同步容器对容器状态的操作“串行”。串行提高了线程安全性,却严重降低了并发性。

例如HashMap,通过hashcode取余来分布散列值。因为hashcode的随机性,糟糕的散列函数会导致元素并不是在Map容器里均匀分布。最严重的,所有元素都发生碰撞冲突,一个散列表变成了活生生的线性链表。那么这时候原始的同步容器访问会花费很长时间。

ConcurrentHashMap

HashMap是java.util.包下的集合类,ConcurrentHashMap是java.util.concurrent包下的。它们都同样是扩展了Map接口,并用Hash来进行散列分布。

不同的是,ConcurrentHashMap利用了粒度更细的“分段锁”。Java 1.8及以前,ConcurrentHashMap使用了包含了默认16个私有锁对象的数组,每个锁保护所有散列桶的1/16, 第N个散列桶由第N mod 16的锁来保护。因而,ConcurrentHashMap能够支持16个并发的写入器。

根据分段锁的概念,我们很容易写出StripedMap(ConcurrentHashMap实现简化版):

public class StripedMap {
	private static final int N_LOCKS = 16;
	private final Node[] buckets;
	private final Object[] locks;
	
	
	private static class Node{ }
	
	public StripedMap(int numBuckets) {
		buckets = new Node[numBuckets];
		locks = new Object[N_LOCKS];
		for(int i=0; i<N_LOCKS; i++) {
			locks[i] = new Object();
		}
	}
	
	private final int hash(Object key) {
		return Math.abs(key.hashCode() % buckets.length);
	}
	
	public Object get(Object key) {
		int hash = hash(key);
		synchronized(locks[hash % N_LOCKS]) {
			for(Node m = buckets[hash]; m != null; m = m.next) {
				if(m.key.equals(key))
					return m.value;
			}
		}
	}
}

分段锁也有其天然的缺点:当分段锁数量变多,访问获取锁的线程也大量增加时,获取多个锁来实现独占访问会更困难,且开销更高。

 

CopyOnWriteArrayList

CopyOnWriteArrayList即“写入时复制”,印证了之前所提的“克隆容器”的概念。此种方案避免了对容器的加锁机制,也取消了迭代遍历时的复制,而只对容器发生修改操作才创建并重新发布一个新的容器副本,并发性能大大提高。

但是这样的应用范围也相当有限,因为只有迭代访问操作远远多于修改操作时,才适用“写入时复制”容器。

 

BlockingQueue

BlockingQueue即阻塞队列,它是一个接口。其中主要提供了6种抽象函数,分别满足阻塞、非阻塞的读写操作。

offer(E e):非阻塞入队,若Queue没满,立即返回true; 如果Queue已满,立即返回false;

put(E e):阻塞入队,若Queue已满,则一直阻塞至队列不满/线程被中断(interrupted)

offer(E e, long timeout, TimeUnit unit):阻塞入队,若Queue已满则进入等待,直到出现以下三种情况之一中断:

      a. 其他线程被唤醒;

      b. 等待时间超时,Queue仍满;注意timeout为时间单位个数,unit为单位时间度量。例如timeout = 100, unit = 0.02s,那么等待时间为2s;

      c. 当前线程被中断;

 

poll():非阻塞出队,若Queue为空直接返回null;如果有元素则出队;

take():阻塞出队,若Queue为空一直阻塞至Queue不为空/线程被中断;

poll(long timeout, TimeUnit unit):阻塞出队,若Queue为空则进入等待,直到出现以下三种情况:

       a. 其他线程被唤醒;

       b. 等待时间超时;

       c. 当前线程被中断;

BlockingQueue体现的是设计模式中的“生产者消费者模式” ,其场景主要是一个线程生产对象,而另外一个线程消费这些对象。负责生产的线程将会持续生产新对象并将其插入到BlockingQueue中,直到阻塞队列达到队长上限。如果该BlockingQueu e达到了队长上限(临界点),生产线程插入新对象时发生阻塞。它会一直处于阻塞之中,直到负责消费的线程从队列中拿走一个对象;同理,负责消费的线程一直从该BlockingQueue中拿出对象。如果消费线程尝试去从一个空队列中提取对象的话,这个消费线程将会处于阻塞之中,直到生产线程把一个对象丢进队列。

例如,我们可以手写一个BlockingQueue的应用案例:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class BlockingQueueExample {
	
	public static void main(String[] args) throws InterruptedException {
		BlockingQueue queue = new ArrayBlockingQueue(1000);
		
		Producer producer = new Producer(queue);
		Consumer consumer = new Consumer(queue);
		
		new Thread(producer).start();
		new Thread(consumer).start();
		
		Thread.sleep(3000);
	}

}

class Producer implements Runnable{
	
	protected BlockingQueue mQueue = null;
	
	Producer(BlockingQueue queue){
		mQueue = queue;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		try {
			mQueue.put("1");
			Thread.sleep(1000);
			mQueue.put("2");
			Thread.sleep(1000);
		}catch(InterruptedException e) {
			e.printStackTrace();
		}
	}
}

class Consumer implements Runnable{

	protected BlockingQueue mQueue = null;
	
	Consumer(BlockingQueue queue){
		mQueue = queue;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
		try {
			System.out.println(mQueue.take().toString());
			System.out.println(mQueue.take().toString());
		}catch(InterruptedException e) {
			e.printStackTrace();
		}		
	}
}

 

ArrayBlockingQueue

在案例中,我们用到了ArrayBlockingQueue。顾名思义,这是用数组形式实现的BlockingQueueArrayBlockingQueu e是一种有界缓存区。因为数组初始化需要指定其容量,一旦创建就不能扩大容量。

也许ArrayBlockingQueue因为不能扩容,所以设计初衷便是特定的高并发需求。ArrayBlockingQueue所有阻塞入队、出队操作都共用一把ReentrantLock。申请lock前需要满足两个条件:

(1)集合不为空

(2)集合不为满

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ArrayBlockingQueueDemo<E> {
	private final E[] items;
	private int takeIndex;
	private int putIndex;
	private int count;
	
	private final ReentrantLock lock;
	private final Condition notEmpty;
	private final Condition notFull;
	
	public ArrayBlockingQueueDemo(int capacity) {
		this(capacity,false);
	}
	
	public ArrayBlockingQueueDemo(int capacity, boolean fair) {
		if(capacity <= 0)
			throw new IllegalArgumentException();
		this.items = (E[])new Object[capacity];
		this.lock = new ReentrantLock(fair);
		notEmpty = lock.newCondition();
		notFull = lock.newCondition();
	}
	
	private void insert(E x) {
		items[putIndex] = x;
		putIndex = putIndex++;
		++count;
		notEmpty.signal();
	}
	private E extract() {
		final E[] items = this.items;
		E x = items[takeIndex];
		items[takeIndex] = null;
		takeIndex = takeIndex++;
		--count;
		notFull.signal();
		return x;
		
	}
	
	public void put(E e)throws InterruptedException{
		if(e == null)
			throw new NullPointerException();
		final E[] items = this.items;
		final ReentrantLock lock = this.lock;
		lock.lockInterruptibly();                  //获取可中断锁
		try {
			try {
				while(count == items.length)
					notFull.await();
			}catch(InterruptedException exception) {
				notFull.signal();
				throw exception;
			}
			insert(e);
		}finally {
			lock.unlock();
		}
	}
	
	public E take() throws InterruptedException{
		final ReentrantLock lock = this.lock;
		lock.lockInterruptibly();
		try {
			try {
				while(count == 0)
					notEmpty.await();
			}catch(InterruptedException exception) {
				notEmpty.signal();
				throw exception;
			}
			E x = extract();
			return x;
		}finally {
			lock.unlock();
		}
		
	}
	
    ...      //offer操作和poll操作在此不作赘述
}

自然,ArrayBlockingQueue的特性不适合大数据量、入队出队都需要高并发的场景。共用一把ReentrantLock锁会导致生产线程和消费线程的竞争,数组实现会让容量比较有限。于是,另一种BlockingQueue的形式应运而生。

 

LinkedBlockingQueue

LinkedBlockingQueue,顾名思义,即链表形式实现的BlockingQueue。对比链表和数组的插入、删除速度,加上链表不用指定初始化容量(理论上属于无界缓存区,但是容量终究是一个有限值),LinkedBlockingQueue往往更受项目使用者青睐。

此外,LinkedBlockingQueue用两把ReentrantLock(文中的takeLock和putLock)分别管理入队、出队,减少了生产者、消费者之间的竞争,高并发的性能要大大优于ArrayBlockingQueue。

以下便是LinkedBlockingQueue的源码实现:

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class LinkedBlockingQueueDemo<E> {
	private final int capacity;
	private final AtomicInteger count = new AtomicInteger(0);
	private transient Node<E> head;
	private transient Node<E> last;
	
	private final ReentrantLock takeLock = new ReentrantLock();  
	private final ReentrantLock putLock = new ReentrantLock();
	
	private final Condition notEmpty = takeLock.newCondition();
	private final Condition notFull = putLock.newCondition();
	
	static class Node<E>{
		volatile E item;
		Node<E> next;
		
		Node(E x){
			item = x;
		}
	}
	
	public LinkedBlockingQueueDemo() {
		this(Integer.MAX_VALUE);
	}
	public LinkedBlockingQueueDemo(int capacity) {
		if(capacity <= 0)
			throw new IllegalArgumentException();
		this.capacity = capacity;
		last = head = new Node<E>(null);
	}
	
	
	private void insert(E x) {
		last = last.next = new Node<E>(x);
	}
	private E extract() {
		Node<E> first = head.next;
		head = first;
		E x = first.item;
		first.item = null;
		return x;
	}
	private void signalNotFull() {
		final ReentrantLock putLock = this.putLock;
		putLock.lock();
		try {
			notFull.signal();
		}finally {
			putLock.unlock();
		}
	}
	private void signalNotEmpty() {
		final ReentrantLock takeLock = this.takeLock;
		takeLock.lock();
		try {
			notEmpty.signal();
		}finally {
			takeLock.unlock();
		}
	}

	
	public void put(E e)throws InterruptedException{
		if(e == null)
			throw new NullPointerException();
		int c = -1;
		final ReentrantLock putLock = this.putLock;
		final AtomicInteger count = this.count;
		putLock.lockInterruptibly();
		try {
			try {
				while(count.get() == capacity)
					notFull.await();
			}catch(InterruptedException exception) {
				notFull.signal();
				throw exception;
			}
			insert(e);
			c = count.getAndIncrement();
			if(c+1 < capacity)
				notFull.signal();
		}finally {
			putLock.unlock();
		}
		if(c == 0)
			signalNotEmpty();
	}
	
	public E take() throws InterruptedException{
		E x;
		int c = -1;
		final AtomicInteger count = this.count;
		final ReentrantLock takeLock = this.takeLock;
		takeLock.lockInterruptibly();
		try {
			try {
				while(count.get() == 0)
					notEmpty.await();
			}catch(InterruptedException exception) {
				notEmpty.signal();
				throw exception;
			}
			x = extract();
			c = count.getAndIncrement();
			if(c>1)
				notEmpty.signal();
		}finally {
			takeLock.unlock();
		}
		if(c == capacity)
			signalNotFull();
		return x;
	}
	
    ...
}

 

BlockingDeque

自Java1.6开始,Deque和BlockingDeque也被加入容器类中。Deque/BlockingDeque实际是对Queue/BlockingQueue的一个扩展。Deque是一个双端队列,以外普通Queue是队尾插入、队头取出满足FIFO原则(First In First Out);Deque则均可在队头、队尾进行高效插入、移除。

阻塞双端队列在阻塞队列的生产者/消费者模式上,新增加了一条特性叫”工作密取“。传统的生产者/消费者模式,消费者、生产者共用一条工作队列,相比来说生产线就很繁忙。”工作密取“模式为,每个消费者维系自己的一个双端工作队列,减少了竞争机制。而且,如果当前消费者完成了自己的任务,可以访问其他消费者双端队列的队尾元素(访问队头则会发生碰撞冲突),提高CPU利用率。

此外,”工作密取“非常适合生产者/消费者为同一线程的问题,因为强制分开生产线程、消费线程不一定能把问题简单化。例如:当执行某个工作,会导致更多工作出现;网页爬虫处理一个网页过程时,需要先处理其他更多页面。工作密取”模式背后体现出的“享元模式”也值得继续深入探究。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值