java队列

  • 面试题:

     说一些线程安全的集合类:
         HashTable  ConcurrentHashMap
         CopyOnWriteArrayList  Vector(List的前身)  
  • 集合相关面试题

     考点:在对集合进行遍历过程中,想删除集合中的元素,只能使用迭代器的删除方法,若在迭代过程中,使用集合的删除方法,会抛出并发修改异常
     笔试题:编程题
     1. 对一个list集合去重  
         eg:list: tom  amy jack tom amy rose   
            去重后:tom  amy  jack  rose
                
     思路:遍历原集合,对遍历到的元素进行判断,判断元素是否已经出现,若已经出现,说明是重复元素,从原集合中删除.
      
     
    List<String> firstList = new ArrayList<>();
     ​
     //获取迭代器对象,对list进行遍历
     Iterator<String> it = list.iterator();
     while (it.hasNext()){
         String ele = it.next();
         if (firstList.contains(ele)){
             it.remove();
         }else{
             firstList.add(ele);
         }
     }
     System.out.println(list);

image

Queue队列(FIFO-先进先出):

1.offer(E) :返回boolean数值表示是否成功, 入队操作,元素放到队列末尾。

2.poll() 返回队首元素并从队首中删除此元素, 如果队列为空则返回空

3.peek() 返回队首元素不会从队首中删除此元素, 如果队列为空则返回空

其他:

4.add(E): 添加 返回boolean数值表示是否成功, 入队操作,元素放到队列末尾。 和offer方法作用相同,add来自于Collection接口中的方法。但一般建议用offer,因为add添加不成功会抛异常。

5.element:返回队首元素不会从队首中删除此元素, 如果队列为空则抛出异常,和peek方法作用相同。element来自于Collection接口中的方法。但一般建议用peek,因为element添加不成功会抛异常。

6.remove(): 返回队首元素并从队首中删除此元素, 如果队列为空则抛出异常,和poll方法作用相同。remove来自于Collection接口中的方法。但一般建议用poll,因为remove添加不成功会抛异常。

建议使用offer插入, 用poll移除, 用peek检查(获取/查询)。 因为他们不会抛出异常。

从队尾放元素,从队首取元素。

用法案例:

 import java.util.LinkedList;
 import java.util.Queue;
  
 public class Main {
     public static void main(String[] args) {
         //add()和remove()方法在失败的时候会抛出异常(不推荐)
         Queue<String> queue = new LinkedList<String>();
         //添加元素
         queue.offer("a");
         queue.offer("b");
         queue.offer("c");
         queue.offer("d");
         queue.offer("e");
         for(String q : queue){
             System.out.println(q);
         }
         System.out.println("===");
         System.out.println("poll="+queue.poll()); //返回第一个元素,并在队列中删除
         for(String q : queue){
             System.out.println(q);
         }
         System.out.println("===");
         System.out.println("element="+queue.element()); //返回第一个元素 
         for(String q : queue){
             System.out.println(q);
         }
         System.out.println("===");
         System.out.println("peek="+queue.peek()); //返回第一个元素 
         for(String q : queue){
             System.out.println(q);
         }
     }
 }

Dequeue双端队列:

b411610ce846a39808a7df910873da4a.png

Queue是一个接口,LinkedList是实现了Queue的一个类。 Deque也是一个接口,LinkedList也实现了Deque接口。

虽然Deque继承自Queue,但是使用Deque时,最好不要调用offer( ),而用offerLast( )。

如果直接写deque.offer( ),就需要思考,而用offerLast( )就能直接看出是添加到队尾。

因此使用Deque时,推荐总是明确使用offerLast、offerFirst、pollFirst、pollLast。

 public class Test {
     public static void main(String[] args) {
         Deque<Integer> deque = new LinkedList<>();
         deque.add(1);
         deque.add(2);
         deque.add(3);
         System.out.println("原始队列:");
         System.out.println(deque);
 ​
         System.out.println("队列头添加元素:");
         deque.addFirst(4);
         System.out.println(deque);
         System.out.println("队列尾添加元素:");
         deque.addLast(5);
         System.out.println(deque);
 ​
         System.out.println("队列头删除元素:");
         deque.removeFirst();
         System.out.println(deque);
         System.out.println("队列尾删除元素:");
         deque.removeLast();
         System.out.println(deque);
     }
 }

栈Stack(FILO-先进后出)

当用LinkedLists时调用它的双端队列或队列对应的方法时时它就是双端队列或队列,调用它的栈对应的方法时它就是栈。

Java中Stack类从Vector类继承,底层是用数组实现的线程安全的栈。栈是一种后进先出(LIFO)的容器,常用的操作push/pop/peek。

不过Java中用来表达栈的功能(push/pop/peek),更适用的是使用双端队列接口Deque,并用实现类ArrayDeque/LinkedList来进行初始化。

 Deque<Integer> stack = new ArrayDeque<>();
 Deque<Integer> stack = new LinkedList<>();

不用Stack至少有以下两点原因 1、从性能上来说应该使用Deque代替Stack。

Stack和Vector都是线程安全的,其实多数情况下并不需要做到线程安全,因此没有必要使用Stack。毕竟保证线程安全需要上锁,有额外的系统开销。

2、Stack从Vector继承是个历史遗留问题,JDK官方已建议优先使用Deque的实现类来代替Stack。

Stack从Vector继承的一个副作用是,暴露了set/get方法,可以进行随机位置的访问,这与Stack只能从尾巴上进行增减的本意相悖。

此外,Deque在转成ArrayList或者stream的时候保持了“后进先出”的语义,而Stack因为是从Vector继承,没有这个语义。

 Stack<Integer> stack = new Stack<>();
 Deque<Integer> deque = new ArrayDeque<>();
 ​
 stack.push(1);
 stack.push(2);
 deque.push(1);
 deque.push(2);
 ​
 System.out.println(new ArrayList<>(stack)); // [1,2]
 List<Integer> list1 = stack.stream().collect(Collectors.toList());//[1,2]
 ​
 // deque转成ArrayList或stream时保留了“后进先出”的语义
 System.out.println(new ArrayList<>(deque)); // [2,1]
 List<Integer> list2 = deque.stream().collect(Collectors.toList());//[2,1]
 ​

该用ArrayDeque还是LinkedList? ArrayDeque和LinkedList这两者底层,一个采用数组存储,一个采用链表存储;

数组存储,容量不够时需要扩容和数组拷贝,通常容量不会填满,会有空间浪费;

链表存储,每次push都需要new Node节点,并且node节点里面有prev和next成员,也会有额外的空间占用。

那么问题来了,在用作栈时到底用ArrayDeque好还是LinkedList好呢?

注意到ArrayDeque源码注释中有一句话: This class is likely to be faster than {@link Stack} when used as a stack, and faster than {@link LinkedList} when used as a queue.

ArrayDeque用作栈时比Stack快没有疑问,用作队列的时候似乎也会比LinkedList快!

笔者经过50W数据量的测试,发现两者性能基本接近,ArrayDeque平均耗时在18-24ms,LinkedList耗时平均在20-28ms。

如果数据量上升到100W的话,ArrayDeque的优势会更明显。

结论:ArrayDeque会略胜一筹,不过差别通常可以忽略

 
public static void main(String[] args) {
     int length = 500000;
     int max = length;
     // 生成一个长度为length,值从1~max的随机数组
     int[] data = new RandomIntArray(length,1,length,max).next();
     int loopCount = 10;
     long t1, t2;
 ​
     t1 = System.currentTimeMillis();
     for (int i = 0; i < loopCount; i++) {
         // testArrayDeque(data);
         testLinkedList(data);
     }
     t2 = System.currentTimeMillis();
 ​
     // 测试loopCount次取平均结果
     System.out.println("timeTaken: " + String.format("%.1f", (t2-t1)/(double)loopCount));
 }
 ​
 public static void testArrayDeque(int[] data) {
     int length = data.length;
     Deque<Integer> stack = new ArrayDeque<>();
     for (int i = 0; i < length/2; i++) {
         stack.push(data[i]);
         stack.push(data[i+1]);
         stack.pop();
         stack.push(stack.peek()+1);
     }
 }
 ​
 public static void testLinkedList(int[] data) {
     int length = data.length;
     Deque<Integer> stack = new LinkedList<>();
     for (int i = 0; i < length/2; i++) {
         stack.push(data[i]);
         stack.push(data[i+1]);
         stack.pop();
         stack.push(stack.peek()+1);
     }
 }
 ​

阻塞式队列BlockingQueue接口:

阻塞式队列的put和take方法是线程安全的。

API:

 接口: BlockingQueue<E>
 实现类: LinkedBlockingQueue<E>
     -通过该类的有参构造方法可以设置阻塞队列的容量
 带有阻塞功能的方式:
    put(E):void   存入元素,若队列满,则让生产者线程阻塞
    take():E      出队元素,若队列空,则让消费者线程阻塞

 public LinkedBlockingDeque()
 public LinkedBlockingDeque(int capacity)
 public LinkedBlockingDeque(Collection<? extends E> c)

阻塞式队列的作用:实现生产者和消费者线程的分离解耦,从而提高二者各自的执行效率.

举例1:服务器处理消息

举例2:日志记录

总结:LinkedBlockingQueue和LinkedBlockingDeque区别 两个都是队列,只不过前者只能一端出一端入,后者则可以两端同时出入,并且都是结构改变线程安全的队列。

其实两个队列从实现思想上比较容易理解,有以下特点: ①、链表结构(动态数组) ②、通过ReentrantLock实现锁 ③、利用Condition实现队列的阻塞等待,唤醒

 
测试1:队列空时,消费者线程阻塞
     BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
     Integer i = queue.take();  //此处阻塞
     System.out.println("取出元素");
     System.out.println(i);
 测试2:队列满时,生产者线程阻塞
     //生产者阻塞
     queue.put(1);
     queue.put(2);
     queue.put(3);
     queue.put(4);
     queue.put(5);
     System.out.println(queue);
     queue.put(6);  //此处阻塞

场景1:阻塞式队列为空,线程1不停的存,一会儿就把队列填满了。 线程2每隔2秒取出一个元素,就会发现线程2每2秒取出一个元素后,队列就会空出一个空间,就会立即被线程1存入一个新元素

 //场景:生产者快速的产生数据并存入队列,消费者每隔2s取出一个数据,最终造成的结果是程序启动,
 //在不到2s的时间,生产者就可以将队列填满,之后生产者阻塞,直到2s到,消费者取出一一个数据后,
 //生产者才会接触阻塞,继续存入一- 个数据,之后就绪进入阻塞状态.
 ​
 
import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingDeque;
 ​
 public class TestBlockingQueue {
     public static void main(String[] args) {
         BlockingQueue<Integer> queue=new LinkedBlockingDeque<Integer>(10);
         Thread productor=new Thread(){
             public void run(){
                 while(true){
                     try {
                         //Thread.sleep(2000);
                         int ran=(int)(Math.random()*10000);
                         queue.put(ran);
                         System.out.println(queue);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
             }
         };
         productor.start();
 ​
         Thread consumer=new Thread(){
             public void run(){
                 while(true){
                     try {
                         Thread.sleep(3000);
                         Integer i=queue.take();
                         //System.out.println(i);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
             }
         };
         consumer.start();
     }
 }
 //        [2318]
 //        [2318, 8661]
 //        [2318, 8661, 5268]
 //        [2318, 8661, 5268, 7060]
 //        [2318, 8661, 5268, 7060, 446]
 //        [2318, 8661, 5268, 7060, 446, 7435]
 //        [2318, 8661, 5268, 7060, 446, 7435, 9730]
 //        [2318, 8661, 5268, 7060, 446, 7435, 9730, 4729]
 //        [2318, 8661, 5268, 7060, 446, 7435, 9730, 4729, 5055]
 //        [2318, 8661, 5268, 7060, 446, 7435, 9730, 4729, 5055, 7685]
 //-------------------------------------------------------------------------
 //        [8661, 5268, 7060, 446, 7435, 9730, 4729, 5055, 7685, 308]
 //        [5268, 7060, 446, 7435, 9730, 4729, 5055, 7685, 308, 1178]
 //        [7060, 446, 7435, 9730, 4729, 5055, 7685, 308, 1178, 3078]
 //        [446, 7435, 9730, 4729, 5055, 7685, 308, 1178, 3078, 7418]
 //        [7435, 9730, 4729, 5055, 7685, 308, 1178, 3078, 7418, 6566]
 //        [9730, 4729, 5055, 7685, 308, 1178, 3078, 7418, 6566, 8209]
 //        [4729, 5055, 7685, 308, 1178, 3078, 7418, 6566, 8209, 9649]
 //        [5055, 7685, 308, 1178, 3078, 7418, 6566, 8209, 9649, 1717]

场景2: 阻塞式队列为空,线程2不停的取,因为队列为空取不出来,所以一直阻塞。 线程1每隔2秒存入一个元素,就会发现线程1每2秒存入一个元素就会立即被线程2取出

 
场景:生产者每隔2s产生-一个数据并存入队列,消费者一直不停的取数据,则没
 //到2s期间,消费者线程就是阻塞的,当2s-到, 生产者存入-一个数据,消费者阻
 //塞解除,取出数据,然后继续进入阻塞状态,以此类推
 ​
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingDeque;
 ​
 public class TestBlockingQueue2 {
     public static void main(String[] args) {
         BlockingQueue<Integer> queue=new LinkedBlockingDeque<Integer>(10);
         Thread productor=new Thread(){
             public void run(){
                 while(true){
                     try {
                         Thread.sleep(2000);
                         int ran=(int)(Math.random()*10000);
                         queue.put(ran);
                         System.out.println(queue);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
             }
         };
         productor.start();
 ​
 ​
 ​
         Thread consumer=new Thread(){
             public void run(){
                 while(true){
                     try {
                         Integer i=queue.take();
                         //System.out.println(i);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
             }
         };
         consumer.start();
 ​
 ​
     }
 }
 ​

本题中两个消费者线程执行的任务是一样的,不能创建2个匿名内部类来表示2。个消费者,代码完全重复,解决办法是:

答案

 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingDeque;
 ​
 public class TrainBlockingQueue {
 ​
     public static void main(String[] args) {
         BlockingQueue<Integer> queue=new LinkedBlockingDeque<Integer>();
         Runnable r=new Runnable() {
             public void run() {
                 try {
                     while(true){
                         if(queue.size()>=10){
                             System.out.println(
                                 Thread.currentThread().getName()+"取出元素"+queue.take());
                         }
                     }
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         };
         Thread t1=new Thread(r);
         Thread t2=new Thread(r);
         t1.start();
         t2.start();
 ​
         Thread productor=new Thread(){
             public void run() {
                 try {
                     while(true) {
                         Thread.sleep(1000);
                         int ran=(int)(Math.random()*10000);
                         queue.put(ran);
                         System.out.println(queue);
                     }
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         };
         productor.start();
 ​
 ​
     }
 ​
 ​
 }
 ​
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值