并发编程07--队列

队列

在java中队列是一个先进先出的数据结构,属于集合Collection的一个子类,如下图

                    

在并发队列上JDK提供了两套实现,一个是以ConcurrentLinkedQueue为代表的高性能队列非阻塞,一个是以BlockingQueue接口为代表的阻塞队列,具体如下:

  1. ArrayDeque, (非阻塞数组双端队列) 线程不安全,不支持null (https://www.cnblogs.com/lxyit/p/9080590.html
  2. PriorityQueue, (非阻塞优先级队列) 线程不安全(https://www.cnblogs.com/demingblog/p/6485193.html
  3. ConcurrentLinkedQueue, (非阻塞基于链表的并发队列) 线程安全(http://ifeve.com/concurrentlinkedqueue/
  4. DelayQueue, (延期阻塞队列)(阻塞队列实现了BlockingQueue接口) 
  5. ArrayBlockingQueue, (基于数组的并发阻塞队列) 
  6. LinkedBlockingQueue, (基于链表的FIFO阻塞队列) 
  7. LinkedBlockingDeque, (基于链表的FIFO双端阻塞队列) 
  8. PriorityBlockingQueue, (带优先级的无界阻塞队列) 
  9. SynchronousQueue (并发同步阻塞队列)

阻塞队列与非阻塞队

阻塞队列与普通队列的区别在于

  • 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素,非阻塞直接返回null;
  • 试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列,非阻塞直接返回false或抛异常;

 

ConcurrentLinkedDeque

ConcurrentLinkedQueue : 是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常其性能好于BlockingQueue。

  • 它是一个基于链接节点(Node)的无界线程安全队列,以CAS自旋锁方式保证线程安全。
  • 该队列的元素遵循先进先出的原则。头是最先加入的,尾是最近加入的,该队列不允许null元素。

源码如下:

//head 和 tail 全为volatile修饰的Node,一线程对一个volatile变量的读,总是
//能看到(任意线程)对这个volatile变量最后的写入”
transient volatile Node<E> head;
private transient volatile Node<E> tail;

// 构造器,Node结构的链表
public ConcurrentLinkedQueue() {
   head = tail = new Node<E>();
}

// 维持一个链表且传入初始值
public ConcurrentLinkedQueue(Collection<? extends E> c) {
   Node<E> h = null, t = null;
   for (E e : c) {
     // 加入非空元素
       Node<E> newNode = new Node<E>(Objects.requireNonNull(e));
       if (h == null)
           h = t = newNode;
       else
           t.appendRelaxed(t = newNode);
   }
   // 全为null时,
   if (h == null)
      h = t = new Node<E>();
   head = h;
   tail = t;
}

其他源码可参考:https://blog.youkuaiyun.com/qq_22798455/article/details/81637397

ConcurrentLinkedQueue重要方法:

  • add 和offer() 都是加入元素的方法(在ConcurrentLinkedQueue中这俩个方法没有任何区别)
  • poll() 和peek() 都是取头元素节点,区别在于前者会删除元素,后者不会。
ConcurrentLinkedDeque q = new ConcurrentLinkedDeque();
    q.offer("阿里");
    q.offer("码云");
    q.offer("腾讯");
    //从头获取元素,删除该元素
    System.out.println(q.poll());
    //从头获取元素,不刪除该元素
    System.out.println(q.peek());
    //获取总长度
    System.out.println(q.size());

BlockingQueue

常用的队列主要有以下两种:(当然通过不同的实现方式,还可以延伸出很多不同类型的队列,DelayQueue就是其中的一种)

  • 先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。
  • 后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种:

  1. 当队列满了的时候进行入队列操作,存储元素的线程会等待队列可用
  2. 当队列空了的时候进行出队列操作,获取元素的线程会等待队列变为非空

因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空队列进行出队列操作时,它将会被阻塞,除非有另一个线程进行了入队列操作。阻塞队列是线程安全的。

API

               

ArrayBlockingQueue

ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。

源码实例:

 /**
  *capacity指定队列的大小
  *fair指定是否公平
  */
public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    // 这两个notEmpty和notFull参数实际上是Condition,而Condition可以把它看做一个阻塞信号
    // Condition的子类ConditionObject(是AbstractQueuedSynchronizer的内部类)拥有两个方法signal和
    // signalAll方法,前一个方法是唤醒队列中得第一个线程,而signalAll是唤醒队列中得所有等待线程,
    // 但是只有一个等待的线程会被选择,这两个方法可以看做notify和notifyAll的变体。
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

ArrayBlockingQueue是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。下面是一个初始化和使用ArrayBlockingQueue的例子:

<String> arrays = new ArrayBlockingQueue<String>(3);
arrays.add("李四");
arrays.add("张军");
arrays.add("张军");
// 添加阻塞队列
arrays.offer("张三", 1, TimeUnit.SECONDS);

总结:

  1. 线程安全的,并且迭代器也是线程安全的,使用Lock加Condition控制并发;
  2. 在创建队列时需要指定容量,则不能再改动; 
  3. 只有1个锁,添加数据和删除数据的时候只能有1个被执行,不允许并行执行;
  4. 队列锁是不公平策略,即唤醒线程的顺序是随机的;
     

LinkedBlockingQueue

LinkedBlockingQueue是链表式阻塞队列,队列大小的配置是可选的,可以在初始化时指定一个大小,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。

和ArrayBlockingQueue一样,LinkedBlockingQueue 也是以先进先出的方式存储数据,最新插入的对象是尾部,最新移出的对象是头部。

源码如下:

/** 拿锁,同时一个线程可以取数据 */
private final ReentrantLock takeLock = new ReentrantLock();
/** 无数据时拿线程等待 */
private final Condition notEmpty = takeLock.newCondition();
/** 放锁,同时一个线程存数据 */
private final ReentrantLock putLock = new ReentrantLock();
/** 数据满时存线程等待 */
private final Condition notFull = putLock.newCondition();

public LinkedBlockingQueue() {
   this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
   if (capacity <= 0) throw new IllegalArgumentException();
   this.capacity = capacity;
   last = head = new Node<E>(null);
}
LinkedBlockingQueue入队方法:add、offer、put方法。
   public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    final AtomicInteger count = this.count;
    if (count.get() == capacity)            // 如果容量满了,返回false
        return false;
    int c = -1;
    Node<E> node = new Node(e); 
    final ReentrantLock putLock = this.putLock;
    putLock.lock(); // 放锁加锁,保证调用offer方法的时候只有1个线程
    try {
        if (count.get() < capacity) { // 再次判断容量是否已满,因为可能拿锁在进行消费数据,没满的话继续执行
            enqueue(node);            // 节点添加到链表尾部
            c = count.getAndIncrement(); 
            if (c + 1 < capacity) // 如果容量还没满
                notFull.signal(); // 在放锁的条件对象notFull上唤醒正在等待的线程,表示可以再次往队列里面加数据了,队列还没满
        }
    } finally {
        putLock.unlock(); 
    }
    if (c == 0) // 只有等于0时对拿锁进行激活,大于0是不需要,因为大于0时拿锁不会wait
        signalNotEmpty(); //可以进行消费
    return c >= 0; // 添加成功返回true,否则返回false
}
public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    int c = -1;
    Node<E> node = new Node(e); 
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly(); // 放锁加锁,保证调用put方法的时候只有1个线程
    try {
        while (count.get() == capacity) { 
            notFull.await(); // 阻塞并挂起当前线程
        }
        enqueue(node); 
        c = count.getAndIncrement(); 
        if (c + 1 < capacity) 
            notFull.signal(); // 在放锁的条件对象notFull上唤醒正在等待的线程,表示可以再次往队列里面加数据了,队列还没满
    } finally {
        putLock.unlock(); // 释放放锁,让其他线程可以调用put方法
    }
    if (c == 0) // 只有等于0时对拿锁进行激活,大于0是不需要,因为大于0时拿锁不会wait
        signalNotEmpty(); // 
}

下面是一个初始化和使LinkedBlockingQueue的例子:

LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue(3);
linkedBlockingQueue.add("张三");
linkedBlockingQueue.add("李四");
linkedBlockingQueue.add("李四");
System.out.println(linkedBlockingQueue.size());

总结:

  1. 线程安全;
  2. 存和取有独立的锁,可以同时存数据和取数据,但每种任务同时刻只能有一个;

PriorityBlockingQueue

PriorityBlockingQueue是一个没有边界的队列,它的排序规则和 java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中允许插入null对象。

所有插入PriorityBlockingQueue的对象必须实现 java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的。

另外,我们可以从PriorityBlockingQueue获得一个迭代器Iterator,但这个迭代器并不保证按照优先级顺序进行迭代。

总结:

  1. 无存储数量上限,内部使用数组保存数据,数据会自动扩容,<64扩大2倍,>=64则扩大1.5倍;
  2. 线程安全,存取使用一个锁;
  3. 内部数据可以排序;

SynchronousQueue

SynchronousQueue队列内部仅允许容纳一个元素。当一个线程插入一个元素后会被阻塞,除非这个元素被另一个线程消费。

总结:

  1. 内部不存储数据,put后直接将生产的数据直接传递给消费者;
  2. 存取同一个锁,一次put后只能等到消费后再次put;
  3. 效率高于LinkBlockingQueue

 

生产者消费者实例

class ProducerThread implements Runnable {
	private BlockingQueue<String> blockingQueue;
	private AtomicInteger count = new AtomicInteger();
	private volatile boolean FLAG = true;
	public ProducerThread(BlockingQueue<String> blockingQueue) {
		this.blockingQueue = blockingQueue;
	}
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + "生产者开始启动....");
		while (FLAG) {
			String data = count.incrementAndGet() + "";
			try {
				boolean offer = blockingQueue.offer(data, 2, TimeUnit.SECONDS);
				if (offer) {
					System.out.println(Thread.currentThread().getName() + ",生产队列" + data + "成功..");
				} else {
					System.out.println(Thread.currentThread().getName() + ",生产队列" + data + "失败..");
				}
				Thread.sleep(1000);
			} catch (Exception e) {

			}
		}
		System.out.println(Thread.currentThread().getName() + ",生产者线程停止...");
	}
	public void stop() {
		this.FLAG = false;
	}
}
class ConsumerThread implements Runnable {
	private volatile boolean FLAG = true;
	private BlockingQueue<String> blockingQueue;

	public ConsumerThread(BlockingQueue<String> blockingQueue) {
		this.blockingQueue = blockingQueue;
	}
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + "消费者开始启动....");
		while (FLAG) {
			try {
				String data = blockingQueue.poll(2, TimeUnit.SECONDS);
				if (data == null || data == "") {
					FLAG = false;
					System.out.println("消费者超过2秒时间未获取到消息.");
					return;
				}
				System.out.println("消费者获取到队列信息成功,data:" + data);

			} catch (Exception e) {
				// TODO: handle exception
			}
		}
	}
}
public class Test0008 {
	public static void main(String[] args) {
		BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>(3);
		ProducerThread producerThread = new ProducerThread(blockingQueue);
		ConsumerThread consumerThread = new ConsumerThread(blockingQueue);
		Thread t1 = new Thread(producerThread);
		Thread t2 = new Thread(consumerThread);
		t1.start();
		t2.start();
		//10秒后 停止线程..
		try {
			Thread.sleep(10*1000);
			producerThread.stop();
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
}

参考:

https://blog.youkuaiyun.com/fuyuwei2015/article/details/72716753

https://blog.youkuaiyun.com/qq_22798455/article/details/82719096

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值