Java并发编程之阻塞队列

Java并发编程之阻塞队列

实现线程安全的队伍有2种方式:

  1. 阻塞式的, 也就是加锁
  2. 非阻塞式的, 使用CAS, ConcurrentLinkedQueue就是使用的这种方式

阻塞队列提供两个附加的操作, 阻塞添加和阻塞移除:

  • 阻塞添加: 当队列满时, 队列会阻塞添加元素的线程, 直到队列不满.
  • 阻塞移除: 当队列空时, 队列会阻塞移除元素的线程, 直到队列不空.
操作/处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
入队方法 add(e) offer(e) put(e) offer(e, time, unit)
出队方法 remove() poll() take() poll(time, unit)
检查方法 element() peek()

offer()方法返回值类型为boolean, 入队失败返回false;
poll()方法若出队失败, 则返回null;
peek()方法只获取元素, 并不执行出队, 并获取不到元素时返回null;

使用场景: 生产者/消费者, 生产者向队列中添加元素, 消费者从队列中获取元素.

本文介绍的是Java中的7个阻塞式的队列.

ArrayBlockingQueue

ArrayBlockingQueue底层实现为数据, 是一个有界的阻塞队列, 此队列以FIFO的原则对元素进行排序.

public class TestArrayBlockingQueue {

    // 创建一个容量为5的ArrayBlockingQueue
    static BlockingQueue<String> queue = new ArrayBlockingQueue<String>(5);
    
    public static void main(String[] args) throws Exception {
        // 启动10个生产者线程, 每个生产者向queue中put自己的线程名称
        // 并且put之后打印自己的名称
        for(int i=0; i<10; i++) {
            new Thread(new Runnable(){
                public void run() {
                    try {
                        queue.put(Thread.currentThread().getName());
                        System.out.println(Thread.currentThread().getName() + " put");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "producer-"+i).start();
        }
        Thread.sleep(5000); // 睡5秒, 让10个生产者都开始执行
        // 打印队列的实际长度, 为5, 
        // 因为没有被消费, 所以只有5个生产者线程put成功, 
        // 其他线程被阻塞
        System.out.println("queue size: " + queue.size());
        // 创建10个消费者线程, 每个线程去queue中做一次take操作
        // 每take一个元素, 就会有一个生产者线程被唤醒, 向queue中put元素
        for(int i=0; i<10; i++) {
            new Thread(new Runnable(){
                public void run() {
                    try {
                        queue.take();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "concumer-"+i).start();
        }
        Thread.sleep(5000);
        System.out.println("queue size: " + queue.size());
    }
}

ArrayBlockingQueue默认是非公平队列;
公平队列是指被阻塞的线程, 可以按照被阻塞的顺序访问队列, 即先被阻塞的线程先访问队列. 非公平队列是指被阻塞的队列, 在队列可以用时, 都有同等的资格来争夺资源. 创建公平队列可以借助ArrayBlockingQueue的另一个构造方法:

BlockingQueue<String> queue = new ArrayBlockingQueue<String>(5, true);

LinkedBlockingQueue

LinkedBlockingQueue的底层实现为链表, 与ArrayBlockingQueue类似, 是一个有界的阻塞队列; 不同的是LinkedBlockingQueue可以不指定队列长度, 此时长度为Integer.MAX_VALUE; LinkedBlockingQueue也是以FIFO原则对元素进行排序.

PriorityBlockingQueue

PriorityBlockingQueue从命名上可以看出, 它是支持优先级排序的阻塞队列.
PriorityBlockingQueue的底层实现为数据, 是一个无界的阻塞队列. 它并不阻塞生产者线程, 只阻塞消费者线程, 所以当生产者的生产速度快于消费者的消费速度时, 随着时间推移, 会最终消耗所有可用内存.

PriorityBlockingQueue支持通过自定义compareTo()方法进行排序, 也可以通过指定Comparator进行排序.

public class TestPriorityBlockingQueue {

    static BlockingQueue<RegisterInfo> queue = new PriorityBlockingQueue<RegisterInfo>();
    
    private static class RegisterInfo implements Comparable<RegisterInfo> {
        
        private String name;
        
        private int priority;
        
        public RegisterInfo(String name, int priority) {
            this.name = name;
            this.priority = priority;
        }

        @Override
        public int compareTo(RegisterInfo registerInfo) {
            if(this.priority > registerInfo.getPriority()) {
                return 1;
            } else if (this.priority < registerInfo.getPriority()){
                return -1;
            } else {
                return 0;
            }
        }

        public String getName() {
            return name;
        }
        
        public int getPriority() {
            return priority;
        }
        
    }
    
    public static void main(String[] args) throws Exception {
        RegisterInfo info1 = new RegisterInfo("张三", 5);
        RegisterInfo info2 = new RegisterInfo("李四", 7);
        RegisterInfo info3 = new RegisterInfo("王五", 3);
        queue.add(info1);
        queue.add(info2);
        queue.add(info3);
        System.out.println(queue.take().getName());
        System.out.println(queue.take().getName());
        System.out.println(queue.take().getName());
    }
}

DelayQueue

DelayQueue是一个支持可以延时获取元素的无界队列, 内部使用PriorityQueue实现. 放入DelayQueue队列中的元素必须实现Delayed接口, 创建元素时指定延时多长时间才能从队列中获取到此元素.

DelayQueue可以用于缓存的设计, 使用DelayQueue保存缓存的有效时间, 使用一个线程循环队列, 能从队列中取出元素时, 表示缓存的有效时间到了.

public class TestDelayQueue {

    static BlockingQueue<CacheTime> queue = new DelayQueue<CacheTime>();
    
    private static class CacheTime implements Delayed {
        
        private String id;
        
        private long time;
        
        public CacheTime(String id, long time) {
            this.id = id;
            this.time = time;
        }

        @Override
        public int compareTo(Delayed delayed) {
            long diff = 0L;
            if(delayed instanceof CacheTime) {
                CacheTime other = (CacheTime) delayed;
                diff = this.time - other.time;
            } else {
                diff = this.time - delayed.getDelay(TimeUnit.MILLISECONDS);
            }
            if(diff > 0) {
                return 1;
            } else if(diff < 0) {
                return -1;
            } else {
                return 0;
            }
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return time - System.currentTimeMillis();
        }
        
        public String getId() {
            return this.id;
        }
    }
    
    public static void main(String[] args) throws Exception {
        long now = System.currentTimeMillis();
        CacheTime ct1 = new CacheTime("001", now + 3000);
        CacheTime ct2 = new CacheTime("002", now + 5000);
        CacheTime ct3 = new CacheTime("003", now + 1000);
        queue.add(ct1);
        queue.add(ct2);
        queue.add(ct3);
        for(int i=0; i<3; i++) {
            System.out.println(queue.take().getId());
        }
    }
}

CacheTime类用于模拟缓存有效时间, id为缓存id, time为缓存有效时间.
CacheTime实现了Delayed接口:
getDelay()方法用于返回此元素剩余的延时时间
compareTo()方法用于排序

在main()方法, 创建了3个CacheTime对象, 并设定其延时时间为3秒后, 5秒后, 1秒后; 将其加入到DelayQueue中, 循环DelayQueue, 元素将根据延时时间由少到多依次延时出队.

SynchronousQueue

SynchronousQueue是一个不存元素的阻塞队列, 它的容量为0. 每次执行put操作必须等待一个take操作.
SynchronousQueue支持公平锁. 其内部分别使用TransferQueue和TransferStack保存被阻塞线程的引用.

这里的TransferQueue是SynchronousQueue的内部类, 不是那继承了BlockingQueue的TransferQueue接口.

public class TestSynchronousQueue {

    static BlockingQueue<String> queue = new SynchronousQueue<String>();
    
    public static void main(String[] args) throws Exception {
        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    queue.put("hello");
                    System.out.println("put complete...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "producer").start();
        Thread.sleep(1000);
        System.out.println("queue size: " + queue.size());
        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    System.out.println("take: " + queue.take());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "consumer").start();
    }
}

示例中, producer线程put之后将一直阻塞, 直到consumer线程执行take操作.
但是, 由于add方法非阻塞, 所以, 如果没有consumer线程等待消费数据, add方法则抛出异常.

SynchronousQueue的一个使用场景就是在线程池中. Executors.newCachedThreadPool()内部就是使用了SynchronousQueue.

LinkedTransferQueue

LinkedTransferQueue是一个用链表实现的无界阻塞队列.
LinkedTransferQueue实现了TransferQueue接口, 扩展了transfer和tryTransfer两个方法.

  • transfer(): 生产者线程向队列中添加元素时, 如果正有消费者线程等待消费, 则直接将元素传输给生产者线程; 如果没有消费者等待消费, 则将元素存放在队列中, 并一直等到消费者线程消费之后才返回.
  • tryTransfer(): 试着将元素传输给消费者线程, 如果有消费者线程等待接收元素返回true, 否则返回false. 也就是说, tryTransfer()方法无论有没有消费者线程等待接收元素都会立即返回. 如果是tryTransfer(E e, long timeout, TimeUnit unit)方法, 如果没有消费者线程则等待指定时间后再返回.
public class TestLinkedTransferQueue {

    static LinkedTransferQueue<String> queue = new LinkedTransferQueue<String>();
    
    public static void main(String[] args) {
        
        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    queue.transfer("hello world");
                    System.out.println(Thread.currentThread().getName() + " end.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "producer").start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("queue size: " + queue.size());
        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    System.out.println(queue.take());
                    System.out.println(Thread.currentThread().getName() + " end.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "consumer").start();
    }
}

一般情况下, 使用LinkedTransferQueue, 调用transfer方法传输元素时, 都会有一个消费者等在那儿, 准备接收这个元素.

posted @ 2018-04-27 21:09  第七狙击手  阅读(186)  评论(0编辑  收藏  举报