JUC之线程安全队列

常见问题

需要达到的水平:

  1. 哪些队列是有界的,哪些队列是无界的。
  2. 针对特定场景需求如何选择合适的队列实现。
  3. 从源码的角度常见的线程安全队列是如何实现的,并进行了哪些改进,以提高效率。

ConcurrentLinkedQueue 和 LinkedBlockingQueue区别:

  • Concurrent类似于lock-free ,在常见的多线程场景中,一般可提供较高吞吐量。
  • LinkedBlockingQueue 内部基于锁,并提供了BlockingQueue 的等待性方法
    有事我们把并发包下的所有容器都习惯叫做并发容器,但严格说,类似ConcurrentLinkedQueue这种才是真正代表并发。
    总结:
    JUC 包下的容器中,大致分为三类:Concurrent、CopyOnWrite、Blocking这三种,同样都是线程安全容器,可以简单认为:
  • Concurrent 型没有类似于CopyOnWrite容器相对较重的修改开销。
  • 凡是都有两面性,导致Concurrent 提供了较低的遍历一致性。你可以这样理解所谓的弱一致性:例如当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历。
  • 与弱一致性相对应的是同步容器的另一种行为:“fast - fail” ,也就是检测到容器在遍历时发生了修改,则抛出ConcurrentModificationException,不再继续遍历。
  • 弱一致性的另一个体现是,size等操作,未必100%准确。
  • 与此同时读写的性能具有一定的不一致性。

部分线程安全队列实现图:
在这里插入图片描述
从基本的数据结构角度分析:
ConcurrentLinkedDeque 和 LinkedBlockingDeque,Deque的特点是双端队列,支持头和尾都进行插入删除操作。如

  • 尾部插入时:addLast(e)、offerLast(e)
  • 尾部删除时:removeLast()、pollLast()

从行为特征分析:
绝大部分的Queue都实现了BlockingQueue接口,Blocking意味着提供了特定的等待操作,获取时(take)等待元素进队,插入时(put)等待队列出现空位

BlockingQueue中哪些队列是有界的哪些队列是无界的

  • ArrayBlockingQueue 是典型的有界队列,内部以final数组保护数据,数组的大小决定了队列的边界,所以我们在创建BlockingQueue时,都要指定容量如
public ArrayBlockingQueue(int capacity, boolean fair)
  • LinkedBockingQueue 通常容易被误解为无边界,但是其实其行为和内部代码都是基于有界的逻辑实现的,只不过我们在创建时如果没有指定容量,那么其容量限制就自动的被设置成Integer.MAX_VALUE,成了无界队列。
  • SynchronousQueue 是一个非常奇葩的队列实现,每一个删除操作都需要等待插入操作,反之一样,每个插入操作都要等待删除操作。且其内部容量是0
  • PriorityBlockingQueue 是无边界的优先队列,但大小总要受系统资源影响。
  • DelayedQueue 和LinkedTransferQueue 同样是无边界的队列。

注意:
对于无边界队列,put操作永远也不会出现BlockingQueue中的那种等待状态。

BlockingQueue底层实现

BlockingQueue基本是基于锁实现。下面通过LinkedBlockingQueue的源码来看

/*
*Lock held by take poll etc
*/
private final ReentrantLock takelock = new ReentrantLock();
/*
*wait queue for waiting takes
*/
private final Condition notEmpty = takeLock.newCondition();
/*
*Lock held by put offer etc
*/
private final ReentrantLock putlock = new ReentrantLock();
/*
*wait queue for waiting puts
*/
private final Condition notFull = putLock.newCondition();

ArrayBlockingQueue 的
notFull 和 notEmpty 都是同一个再入锁的条件变量,而LinkedBlocking则改进了锁操作的粒度,头尾操作使用不同的锁,所以在通用场景下,它的吞吐量相对要更好一些。

下面LinkedBlockingQueue的take方法实现和ArrayBlockingQueue 也是有不同的,由于其自身是链表,需要自己维护元素数量值。

public E take() throws InterruptedException {
	final E x;
	final int c;
	final AutomicInteger count = this.count;
	final ReentrantLock takeLock = this.takeLock;
	takeLock.lockInterruptibly();
	try {
		while (count,get() == 0) {
			notEmpty.await();
		}
		x = dequeue();
		c = count.getAndDecrement();
		if (c > 1)
			notEmpty.signal();
	} finally {
		takeLock.unLock();
	}
	if (c == capacity)
		singalNotFull();
	return x;
}

而类似ConcurrentLinkedQueue,则是基于CAS的无锁技术,不需要在每个操作时都使用锁,所以扩展性表现要更加优异。
相对比较另类的SynchronousQueue 在Java 6中发生了非常大的改变, 利用CAS替换掉了原本基于锁的逻辑,同步开销比较小,它也是Executors.newCacheThreadPool 的默认队列。

队列使用场景与典型用例

通过BlockingQueue来实现生产者消费者模式:

public class ProducerAndConsumer {
    public static final String EXIT_MSG = "GOOD ... Bye!";
    public static void main(String[] args) {
        //使用较小的队列,以便更好的在输出中展示其影响。
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        Producer producer = new Producer(queue);
        Consumer consumer = new Consumer(queue);
        new Thread(producer).start();
        new Thread(consumer).start();
    }
    
    static class Producer implements Runnable {
        private BlockingQueue<String> queue;
        public Producer(BlockingQueue<String> q) {
            this.queue = q;
        }
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                try {
                    Thread.sleep(5L);
                    String msg = "Message" + i;
                    System.out.println("Produced new item: " + msg);
                    queue.put(msg);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
            try {
                System.out.println("Time to say goodBye!");
                queue.put(EXIT_MSG);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    static class Consumer implements Runnable {
        private BlockingQueue<String> queue;
        public Consumer(BlockingQueue<String> q) {
            this.queue = q;
        }
        @Override
        public void run() {
            try {
                String msg;
                while (!EXIT_MSG.equalsIgnoreCase((msg = queue.take()))) {
                    System.out.println("Consumed item:" + msg);
                    Thread.sleep(10L);
                }
                System.out.println("Got exit massage, bye!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:
在这里插入图片描述
在这里插入图片描述
如果不使用Blocking队列,我们就需要自己实现轮询,条件判断(如检查poll返回值是否是null)等逻辑,如果没有特别的要求Blocking队列代码实现更加简单直观。

特定场景如何选择合适的队列实现

以LinkedBlockingqueue 和 ArrayBlockingQueue 和 SynchronousQueue为例。

  • 考虑应用场景对边界应用的要求。ArrayBlockingQueue 有明确的容量限制,而LinkedBlockingQueue取决于我们是否在创建时指定。SynchronousQueue不能缓存任何数据。
  • 从空间利用率的角度分析。ArrayBlockingQueue要比LinkedBlockingQueue紧凑,因为不需要创建节点。但初始分配阶段就需要一段连续的空间,所以初始内存需求更大。
  • 通用场景中,LinkedBlockingQueue 的吞吐量比 ArrayBlockingQueue 更紧凑,因为它实现了更加细粒度的锁操作。
  • ArrayBlockingQueue实现比较简单,性能更好预测,属于比较稳健的选手。
  • 如果我们要实现的是两个线程间接力性(handoff)的场景,你可能会选CountDownLatch,但是SynchronousQueue也完美符合这种场景,而且线程间协调和数据统一起来,代码更加规范。
  • 在队列元素较小的场景下,SynchronousQueue的性能表现,往往大大超过其他实现。
发布了40 篇原创文章 · 获赞 0 · 访问量 632

猜你喜欢

转载自blog.csdn.net/weixin_42610002/article/details/104665809
今日推荐