JAVA并发编程-7-并发容器

上一篇看这里:JAVA并发编程-6-ConcurrentHashMap

上一篇中我们介绍了最重要的一个并发容器ConcurrentHashMap,本章中我们简单介绍一下其他并发容器的原理和使用。

一、ConcurrentSkipListMap 和 ConcurrentSkipListSet

类似ConcurrentHashMap是HashMap的并发版本,ConcurrentSkipListMap 和 ConcurrentSkipListSet分别是有序的map–TreeMap和TreeSet的并发版本。

而skipList是一种跳表的结构。跳表可以理解为是链表的索引,我们知道链表如果非常长的话,由于链表的每个节点只保存对前后节点的引用,只能在链上一个一个查询值,这样搜索和查询的效率是很低的。而跳表是在原链表的基础上形成多层索引,如图:在这里插入图片描述
第一层只有20,第二层是20和40,第三层是20,40,70,90,最后一层是全部数据。如果现在要查询70,则在第一层中查询到是在20的右边,第二层中查询到是在40的右边,第3层中在40的右边查询到了70,则直接指向最后一层的70。

可见跳表是一种典型的以空间换时间的思想。跳表在某个节点在插入时,是否成为索引,随机决定,所以跳表又称为概率数据结构。

二、ConcurrentLinkedQueue

ConcurrentLinkedQueue是LinkedList的并发版本。

它是一个无界非阻塞队列,遵循先进先出原则,和LinkedList一样,底层就是一个链表,它实现了线程安全。

它的add()和offer()方法都是将元素插入到尾部,peek(拿头部的数据,但是不移除)和poll(拿头部的数据,但是移除)

三、写时复制容器 CopyOnWrite

写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器

这样做的好处是我们可以对容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以写时复制容器也是一种读写分离的思想,读和写不同的容器。

如果读的时候有多个线程正在向容器添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的正在写的容器,只能保证最终一致性。

CopyOnWriteArrayList和CopyOnWriteArraySet就分别是ArrayList和ArraySet的写时复制并发版本。

适用读多写少的并发场景,常见应用:白名单/黑名单, 商品类目的访问和更新场景。

另外,值得注意的一点是,它在写的时候又把容器复制了一份,如果容器已经比较大了,复制一份也会占用更高的内存空间,存在内存占用问题。

四、阻塞队列

阻塞队列是我们经常会用到的并发容器。

1、概念

1)当队列满的时候,插入元素的线程被阻塞,直达队列不满。
2)队列为空的时候,获取元素的线程被阻塞,直到队列不空。

2、生产者、消费者模式

生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,便有了生产者和消费者模式。生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

3、常用方法

方法 抛出异常 返回值 一直阻塞 超时退出
插入方法 add offer put offer(time)
移除方法 remove poll take poll(time)
检查方法 element peek
  • 抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalStateException(“Queuefull”)异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
  • 返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null。
  • 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
  • 超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。

4、常用阻塞队列

它们都继承自BlockingQueue接口。

  • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
    按照先进先出原则,要求设定初始大小
  • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。按照先进先出原则,可以不设定初始大小,默认Integer.Max_Value

ArrayBlockingQueue和LinkedBlockingQueue不同:

  • 锁上面:ArrayBlockingQueue只有一个锁,生产者和消费者用同一个锁。LinkedBlockingQueue用了两个锁,生产者使用putLock,消费者使用takeLock

  • 实现上:ArrayBlockingQueue直接插入元素,LinkedBlockingQueue在链表上插入需要转换。这是由底层数据结构决定的。

  • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
    默认情况下,按照自然顺序,要么实现compareTo()方法,指定构造参数Comparator

  • DelayQueue:一个使用优先级队列实现的无界阻塞队列。
    支持延时获取的元素的阻塞队列,元素必须要实现Delayed接口。适用场景:实现自己的缓存系统,订单到期,限时支付等等。

  • SynchronousQueue:一个不存储元素的阻塞队列。每一个put操作都要等待一个take操作

  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
    transfer(),必须要消费者消费了以后方法才会返回,tryTransfer()无论消费者是否接收,方法都立即返回。

  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
    可以从队列的头和尾都可以插入和移除元素,实现工作密取,方法名带了First对头部操作,带了last从尾部操作,另外:add=addLast; remove=removeFirst; take=takeFirst

5、使用DelayQueue实现延时订单

需要实现的业务功能是将订单放入队列,过了一定的时间后,再将订单取出,进行下一步的业务处理。

首先,存放到队列的元素必须实现delay接口

/**
 *
 *类说明:存放到队列的元素
 */
public class ItemVo<T> implements Delayed{
	
	private long activeTime;//到期时间,单位毫秒
	private T date;
	
	//activeTime是个过期时长
	public ItemVo(long activeTime, T date) {
		super();
		this.activeTime = TimeUnit.NANOSECONDS.convert(activeTime, 
				TimeUnit.MILLISECONDS)+System.nanoTime();//将传入的时长转换为超时的时刻
		this.date = date;
	}
	
	public long getActiveTime() {
		return activeTime;
	}

	public T getDate() {
		return date;
	}

	//按照剩余时间排序
	@Override
	public int compareTo(Delayed o) {
		long d = getDelay(TimeUnit.NANOSECONDS)-o.getDelay(TimeUnit.NANOSECONDS);
		return (d==0)?0:((d>0)?1:-1);
	}

	//返回元素的剩余时间
	@Override
	public long getDelay(TimeUnit unit) {
		long d = unit.convert(this.activeTime-System.nanoTime(),
				TimeUnit.NANOSECONDS);
		return d;
	}
}

需要定义一个订单类。

/**
 *
 *类说明:订单的实体类
 */
public class Order {
	private final String orderNo;//订单的编号
	private final double orderMoney;//订单的金额
	
	public Order(String orderNo, double orderMoney) {
		super();
		this.orderNo = orderNo;
		this.orderMoney = orderMoney;
	}

	public String getOrderNo() {
		return orderNo;
	}

	public double getOrderMoney() {
		return orderMoney;
	}
	
}

定义一个线程,执行将订单放入阻塞队列的任务。

/**
 *
 *类说明:将订单放入队列
 */
public class PutOrder implements Runnable {
	
	private DelayQueue<ItemVo<Order>> queue;
	
	public PutOrder(DelayQueue<ItemVo<Order>> queue) {
		super();
		this.queue = queue;
	}

	@Override
	public void run() {
		
		//5秒到期
		Order ordeTb = new Order("Tb12345",366);
		ItemVo<Order> itemTb = new ItemVo<Order>(5000,ordeTb);
		queue.offer(itemTb);
		System.out.println("订单5秒后到期:"+ordeTb.getOrderNo());
		
		//8秒到期
		Order ordeJd = new Order("Jd54321",366);
		ItemVo<Order> itemJd = new ItemVo<Order>(8000,ordeJd);
		queue.offer(itemJd);
		System.out.println("订单8秒后到期:"+ordeJd.getOrderNo());
	}	
}

定义一个线程,执行将订单从阻塞队列取出的任务。

/**
 *
 *类说明:取出到期订单的功能
 */
public class FetchOrder implements Runnable {
	
	private DelayQueue<ItemVo<Order>> queue;
	
	public FetchOrder(DelayQueue<ItemVo<Order>> queue) {
		super();
		this.queue = queue;
	}	

	@Override
	public void run() {
		while(true) {
			try {
				ItemVo<Order> item = queue.take();
				Order order = (Order)item.getDate();
				System.out.println("get from queue:"+order.getOrderNo());
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}	
}
/**
 *
 *类说明:测试延时订单
 */
public class Test {
    public static void main(String[] args) throws InterruptedException {
    	
    	DelayQueue<ItemVo<Order>> queue = new DelayQueue<>();
    	
    	new Thread(new PutOrder(queue)).start();
    	new Thread(new FetchOrder(queue)).start();

        //每隔500毫秒,打印个数字
        for(int i=1;i<15;i++){
            Thread.sleep(500);
            System.out.println(i*500);
        }
    }
}

来看执行结果:
在这里插入图片描述

本章介绍了许多并发容器的简单原理和方法使用。对于阻塞队列的实现原理并不复杂,都是基于之前几张讲的显示锁,condition等来实现的,大家可以自己研究下源码。了解这些并发容器可以帮助丰富我们写代码的方法和手段。

下一篇:JAVA并发编程-8-线程池

猜你喜欢

转载自blog.csdn.net/weixin_40292704/article/details/106182012
今日推荐