架构师集合之队列篇

从今天开始,每两天学习一个知识,此博客专栏完全是为了记录自己学习过程,往架构师方向努力。

java队列——queue详细分析

Queue接口与List、Set同一级别,都是继承了Collection接口。LinkedList实现了Deque接口,上一张关系图。图中存在些问题,阻塞队列继承了AbstractQueue,还实现了BlockingQueue。

queue

Deque 双端队列

Deque(java.util.Deque)接口代表着双向队列,意思就是可以从队列的两端增加或者删除元素,Deque就是双向Queue的意思。

既然Deque是个接口所以初始化时就要用到其具体的实现,在 Collections API中有下面两种实现:
java.util.LinkedList
java.util.ArrayDeque
LinkedList类是非常标准的Deque和Queue的实现,它在内部使用链接列表来建模queue或deque。
ArrayDeque类内部存储元素是数组,如果元素数超过数组中的空间,则分配一个新的数组,并移动所有元素。

链表和数组的区别这里不讲,之后讲集合list和set的时候会详细讲解。

Deque实例化:
Deque linkedDeque = new LinkedList();
Deque arrayDeque = new ArrayDeque();
Deque中添加元素:

前面讲到可以在Deque 的两端增加元素,Deque 中有下面几种添加元素的方法:
add() 在Deque 的尾部添加元素,如果元素不能插入到Deque,那么add()方法将抛异常。
addLast() 在 Deque的尾部添加元素,如果元素不能插入到Deque,那么addLast()方法也将抛异常。
addFirst() 在Deque的头部添加元素,如果元素不能插入到Deque,那么addFirst()方法将抛异常。
offer() 在Deque的尾部添加元素,如果元素没满则添加成功返回true,否则返回false。
offerLast() 在Deque的尾部添加元素,和offer()类似,如果添加成功返回true,否则返回false。
offerFirst() 在 Deque 的头部添加元素,如果添加成功返回true,否则返回false。
push() 在Deque的头部添加元素,如果Deque中的元素满了,则会抛异常。

Deque中查看元素:

peek() 返回Deque中的第一个元素,如果Deque是空则返回null。
peekFirst() 返回Deque的第一个元素,如果Deque是空则返回null。
peekLast() 返回Deque的最后一个元素,如果Deque是空则返回null。
getFirst() 返回Deque的第一个元素,如果Deque是空则抛异常。
getLast() 返回Deque的最后一个元素,如果Deque是空则返回null。

Deque中移除元素:

remove() 移除Deque中的第一个元素并返回,如果Deque 是空则抛异常。
removeFirst() 移除Deque中的第一个元素并返回,如果Deque 是空则抛异常。
removeLast() 移除Deque中的最后一个元素并返回,如果Deque 是空则抛异常。
poll() 移除Deque中的第一个元素并返回,如果Deque 是空则返回null。
pollFirst() 移除Deque中的第一个元素并返回,如果Deque 是空则返回null。
pollLast() 移除Deque中的最后一个元素并返回,如果Deque 是空则返回null。
pop() 移除Deque中的第一个元素并返回,如果Deque 是空则抛异常。

Deque中判断包含元素:

contains() 检查Deque中是否包含某个元素,如果包含返回true否则返回false。

Deque的大小:

size() 可以返回Deque中存储的元素个数。

迭代Deque中的元素:

使用Iterator。
使用for-each循环。

非阻塞队列

继承了java.util.AbstractQueue

非阻塞队列子类:

PriorityQueue 类实质上维护了一个有序列表。加入到 Queue 中的元素根据它们的天然排序(通过其 java.util.Comparable 实现)或者根据传递给构造函数的 java.util.Comparator 实现来定位,线程不安全。

ConcurrentLinkedQueue 是基于链接节点的、线程安全的队列。并发访问不需要同步。因为它在队列的尾部添加元素并从头部删除它们,所以只要不需要知道队列的大小,ConcurrentLinkedQueue 对公共集合的共享访问就可以工作得很好。收集关于队列大小的信息会很慢,需要遍历队列。线程安全,因为他是通过其插入、删除时采取CAS操作来保证的。不会出现同一个tail结点的next指针被多个同时插入的结点所抢夺的情况出现。CAS无锁机制这里暂时不讲,后面讲多线程的时候会讲。

PriorityQueue 实例:
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Random;
//用Priority Queue保存学生信息,学生类含有姓名和成绩,当把学生保存在Priority Queue里时,成绩最低的学生放在最前面。
public class PriorityQueueTutorial{
	
	public static void main(String args[]){
		PriorityQueue<Student> queue = new PriorityQueue<Student>(11,
		        new Comparator<Student>() {
		          public int compare(Student s1, Student s2) {
		            return s1.grade - s2.grade;
		          }
		        });	    
			
		for (int i = 1; i <= 100; i++) {
			queue.add(new Student("s" + i, (new Random().nextInt(1000))));
		}
		while (!queue.isEmpty()) {
		      System.out.println(queue.poll().toString());
	    }
	}
}
 
class Student {	
	String name;
	int grade;
	public Student(String name, int grade)
	{
		this.name = name;
	    this.grade = grade;
	}
	
	public String toString() {
		return name + " " + grade;
	}
}

ConcurrentLinkedQueue 实例:
public class Demo {
	public static void main(String[] args) {
	        // 线程操作安全队列,装载数据
	        Queue<String> queue = new ConcurrentLinkedQueue<String>();
	
	        // 消费者线程:不断的消费队列中的数据
	        // 该线程不停的从队列中取出队列中最头部的数据
	        new Thread(new Runnable() {
	            @Override
	            public void run() {
	                while (true) {
	                     //这里有个知识,就是queue判断size的时候会非常耗时,所以建议用isEmpty方法
	                    if (!queue.isEmpty()) {
	                        // 从队列的头部取出并删除该条数据
	                        String s = queue.poll();
	                        System.out.println(System.currentTimeMillis() + "取出数据:" + s);
	                    }
	                }
	            }
	
	        }).start();
	
	        // 生产者线程A:不断的生产单个数据并装入队列中
	        // 该线程模拟不停的在队列中装入一个数据
	        new Thread(new Runnable() {
	            private int count = 0;
	            private int number = 0;
	
	            @Override
	            public void run() {
	                while (true) {
	                    number = count++;
	                    System.out.println("装载数据:" + number);
	                    queue.add(String.valueOf(number));
	
	                    try {
	                        Thread.sleep(1000);
	                    } catch (InterruptedException e) {
	                        e.printStackTrace();
	                    }
	                }
	            }
	
	        }).start();
	
	        // 生产者线程B:不断的生产批量数据并装入队列中
	        // 该线程模拟不停的在队列中装入一批数据
	        new Thread(new Runnable() {
	            private List<String> lists = new ArrayList<String>();
	            private int count = 0;
	
	            @Override
	            public void run() {
	                while (true) {
	                    // 一批数据的数量,不定长
	                    count = (int) (Math.random() * 5);
	                    for (int i = 0; i < count; i++) {
	                        lists.add("批量数据-" + i + "," + Math.random());
	                    }
	
	                    queue.addAll(lists);
	                    lists.clear();
	
	                    try {
	                        Thread.sleep(1000);
	                    } catch (InterruptedException e) {
	                        e.printStackTrace();
	                    }
	                }
	            }
	
	        }).start();
	  }

}

阻塞队列

实现了java.util.BlockingQueue

阻塞队列子类:

ArrayBlockingQueue:是一个用数组实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。支持公平锁和非公平锁。【注:每一个线程在获取锁的时候可能都会排队等待,如果在等待时间上,先获取锁的线程的请求一定先被满足,那么这个锁就是公平的。反之,这个锁就是不公平的。公平的获取锁,也就是当前等待时间最长的线程先获取锁】

LinkedBlockingQueue:一个由链表结构组成的有界队列,此队列的长度为Integer.MAX_VALUE。此队列按照先进先出的顺序进行排序。

PriorityBlockingQueue: 一个支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证同优先级元素的顺序。

DelayQueue: 一个实现PriorityBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。(DelayQueue可以运用在以下应用场景:1.缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。2.定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。)

SynchronousQueue: 一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。

LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列,相当于其它队列,LinkedTransferQueue队列多了transfer和tryTransfer方法。

LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。

方法介绍:
//相比Deque,添加多了put方法,移除多了take方法。
public interface BlockingQueue<E> extends Queue<E> {

    //将给定元素设置到队列中,如果设置成功返回true, 否则抛出异常。如果是往限定了长度的队列中设置值,推荐使用offer()方法。
    boolean add(E e);

    //将给定的元素设置到队列中,如果设置成功返回true, 否则返回false. e的值不能为空,否则抛出空指针异常。
    boolean offer(E e);

    //将元素设置到队列中,如果队列中没有多余的空间,该方法会一直阻塞,直到队列中有多余的空间。
    void put(E e) throws InterruptedException;

    //将给定元素在给定的时间内设置到队列中,如果设置成功返回true, 否则返回false.
    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

    //从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。
    E take() throws InterruptedException;

    //在给定的时间里,从队列中获取值,如果没有取到会抛出异常。
    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;

    //获取队列中剩余的空间。
    int remainingCapacity();

    //从队列中移除指定的值。
    boolean remove(Object o);

    //判断队列中是否拥有该值。
    public boolean contains(Object o);

    //将队列中值,全部移除,并发设置到给定的集合中。
    int drainTo(Collection<? super E> c);

    //指定最多数量限制将队列中值,全部移除,并发设置到给定的集合中。
    int drainTo(Collection<? super E> c, int maxElements);
}
案例:

这里因为有七个类,根据你需求不同去选择。
你需要一个指定最大长度的,使用中需要知道大小的队列就用ArrayBlockingQueue。
你需要一个指定最大长度的,使用中不需要知道大小的队列就用LinkedBlockingQueue。
你需要一个指定优先级的队列就用PriorityBlockingQueue。
你需要一个指定基于时间的队列就用DelayQueue。
你只需要一个队列就用SynchronousQueue。
你需要一个消费完立马知道的就用LinkedTransferQueue。
你需要一个双端队列就用LinkedBlockingDeque。
这里只写一种案例,一通百通,知道什么时候用什么队列就行了。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BlockingQueueTest {
 /**
 定义装苹果的篮子
  */
 public static class Basket{
  // 篮子,能够容纳3个苹果
  BlockingQueue<String> basket = new ArrayBlockingQueue<String>(3);

  // 生产苹果,放入篮子
  public void produce() throws InterruptedException{
   // put方法放入一个苹果,若basket满了,等到basket有位置
   basket.put("An apple");
  }
  // 消费苹果,从篮子中取走
  public String consume() throws InterruptedException{
   // get方法取出一个苹果,若basket为空,等到basket有苹果为止
   String apple = basket.take();
   return apple;
  }

  public int getAppleNumber(){
   return basket.size();
  }

 }
 // 测试方法
 public static void testBasket() {
  // 建立一个装苹果的篮子
  final Basket basket = new Basket();
  // 定义苹果生产者
  class Producer implements Runnable {
   public void run() {
    try {
     while (true) {
      // 生产苹果
      System.out.println("生产者准备生产苹果:" 
        + System.currentTimeMillis());
      basket.produce();
      System.out.println("生产者生产苹果完毕:" 
        + System.currentTimeMillis());
      System.out.println("生产完后有苹果:"+basket.getAppleNumber()+"个");
      // 休眠300ms
      Thread.sleep(300);
     }
    } catch (InterruptedException ex) {
    }
   }
  }
  // 定义苹果消费者
  class Consumer implements Runnable {
   public void run() {
    try {
     while (true) {
      // 消费苹果
      System.out.println("消费者准备消费苹果:" 
        + System.currentTimeMillis());
      basket.consume();
      System.out.println("消费者消费苹果完毕:" 
        + System.currentTimeMillis());
      System.out.println("消费完后有苹果:"+basket.getAppleNumber()+"个");
      // 休眠1000ms
      Thread.sleep(1000);
     }
    } catch (InterruptedException ex) {
    }
   }
  }

  ExecutorService service = Executors.newCachedThreadPool();
  Producer producer = new Producer();
  Consumer consumer = new Consumer();
  service.submit(producer);
  service.submit(consumer);
  // 程序运行10s后,所有任务停止
  try {
   Thread.sleep(10000);
  } catch (InterruptedException e) {
  }
  service.shutdownNow();
 }
 public static void main(String[] args) {
  BlockingQueueTest.testBasket();
 }
}

一些奇奇怪怪的知识点

队列可以放null么?答案是不可以,会报空指针异常。

remove和poll的区别是如果队列为空会不会报异常。remove会报异常,而poll不会。另一个区别是时间复杂度不一样,poll要优于remove,poll是O(log),而remove是O(n)。

LinkedBlockingQueue和ConcurrentLinkedQueue的最大容量都是Integer.MAX_VALUE,趋近于无限,并不是真的无限。

链表结构的队列,判断空是记得不能用size方法,而要用isEmpty方法,性能会快很多。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小明程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值