一、BlockingQueue阻塞队列
多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue
BlockingQueue的核心方法
1.放入数据
(1)offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前执行方法
的线程);
(2)offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
(3)put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.
2. 获取数据
(1)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;
(2)poll(long timeout, TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。
(3)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;
(4)drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。
常见BlockingQueue
- ArrayBlockingQueue基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁
public class ArrayBlockingQueueTest {
/**
* 创建容量大小为1的有界队列
*/
private BlockingQueue<Ball> blockingQueue = new ArrayBlockingQueue<Ball>(1);
/**
* 队列大小
* @return
*/
public int queueSize(){
return blockingQueue.size();
}
/**
* 将球放入队列当中,生产者
* @param ball
* @throws InterruptedException
*/
public void produce(Ball ball) throws InterruptedException{
blockingQueue.put(ball);
}
/**
* 将球从队列当中拿出去,消费者
* @return
*/
public Ball consume() throws InterruptedException {
return blockingQueue.take();
}
public static void main(String[] args){
final ArrayBlockingQueueTest box = new ArrayBlockingQueueTest();
ExecutorService executorService = Executors.newCachedThreadPool();
/**
* 往箱子里面放入乒乓球
*/
executorService.submit(new Runnable() {
public void run() {
int i = 0;
while (true){
Ball ball = new Ball();
ball.setNumber("乒乓球编号:"+i);
ball.setColor("yellow");
try {
System.out.println(System.currentTimeMillis()+
":准备往箱子里放入乒乓球:--->"+ball.getNumber());
box.produce(ball);
System.out.println(System.currentTimeMillis()+
":往箱子里放入乒乓球:--->"+ball.getNumber());
System.out.println("put操作后,当前箱子中共有乒乓球:--->"
+ box.queueSize() + "个");
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}
}
});
/**
* consumer,负责从箱子里面拿球出来
*/
executorService.submit(new Runnable() {
public void run() {
while (true){
try {
System.out.println(System.currentTimeMillis()+
"准备到箱子中拿乒乓球:--->");
Ball ball = box.consume();
System.out.println(System.currentTimeMillis()+
"拿到箱子中的乒乓球:--->"+ball.getNumber());
System.out.println("take操作后,当前箱子中共有乒乓球:--->"
+ box.queueSize() + "个");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
}
}
public class Ball {
/**
* 编号
*/
private String number ;
/**
* 颜色
*/
private String color ;
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
-
LinkedBlockingQueue基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
-
DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
public class DelayedQueueTest {
public static void main(String[] args) {
DelayQueue<MovieTiket> delayQueue = new DelayQueue<MovieTiket>();
MovieTiket tiket = new MovieTiket("电影票0",10000);
delayQueue.put(tiket);
MovieTiket tiket1 = new MovieTiket("电影票1",5000);
delayQueue.put(tiket1);
MovieTiket tiket2 = new MovieTiket("电影票2",8000);
delayQueue.put(tiket2);
System.out.println("message:--->入队完毕");
while( delayQueue.size() > 0 ){
try {
tiket = delayQueue.take();
System.out.println("电影票出队:"+tiket.getMsg());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class MovieTiket implements Delayed {
//延迟时间
private final long delay;
//到期时间
private final long expire;
//数据
private final String msg;
//创建时间
private final long now;
public long getDelay() {
return delay;
}
public long getExpire() {
return expire;
}
public String getMsg() {
return msg;
}
public long getNow() {
return now;
}
/**
* @param msg 消息
* @param delay 延期时间
*/
public MovieTiket(String msg , long delay) {
this.delay = delay;
this.msg = msg;
expire = System.currentTimeMillis() + delay; //到期时间 = 当前时间+延迟时间
now = System.currentTimeMillis();
}
/**
* @param msg
*/
public MovieTiket(String msg){
this(msg,1000);
}
public MovieTiket(){
this(null,1000);
}
/**
* 获得延迟时间 用过期时间-当前时间,时间单位毫秒
* @param unit
* @return
*/
public long getDelay(TimeUnit unit) {
return unit.convert(this.expire
- System.currentTimeMillis() , TimeUnit.MILLISECONDS);
}
/**
* 用于延迟队列内部比较排序 当前时间的延迟时间 - 比较对象的延迟时间
* 越早过期的时间在队列中越靠前
* @param delayed
* @return
*/
public int compareTo(Delayed delayed) {
return (int) (this.getDelay(TimeUnit.MILLISECONDS)
- delayed.getDelay(TimeUnit.MILLISECONDS));
}
@Override
public String toString() {
return "MovieTiket{" +
"delay=" + delay +
", expire=" + expire +
", msg='" + msg + '\'' +
", now=" + now +
'}';
}
}
-
PriorityBlockingQueue 基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。
-
SynchronousQueue 一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。
二、Condition机制
Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。简单说,他的作用是使得某些线程一起等待某个条件(Condition),只有当该条件具备(signal 或者 signalAll方法被调用)时,这些等待线程才会被唤醒,从而重新争夺锁。wait()、notify()这些都更倾向于底层的实现开发,而Condition接口更倾向于代码实现的等待通知效果。两者之间的区别与共通点也可以了解一下:
基本使用
package cn.memedai;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Lock与Condition接口示例
*/
public class LockConditionDemo {
//存储地方
class Depot {
private int capacity;
private int size;
private Lock lock;
private Condition fullCondition;
private Condition emptyCondition;
public Depot(int capacity) {
this.capacity = capacity;
this.size = 0;
this.lock = new ReentrantLock();
this.fullCondition = lock.newCondition();
this.emptyCondition = lock.newCondition();
}
//生产操作
public void produce(int newSize) throws InterruptedException {
lock.lock();
int left = newSize;
try {
while (left > 0) {
//代表超过了容量就不能再生产了
while (size >= capacity) {
fullCondition.await();//进行等待处理
}
//获取实际生产的数量(及库存中新增的数量)
//如果库存+要生产的大于了总的容量那么新增的就是总容量的数量相减
int inc = (size + left) > capacity ? (capacity - size) : left;
size += inc;
left -= inc;
System.out.println(Thread.currentThread().getName() + "------left剩余" + left + "------size容量" + size + "-------inc增长" + inc);
emptyCondition.signal();
}
} finally {
lock.unlock();//解锁
}
}
//消费操作
public void consume(int newSize) throws InterruptedException {
lock.lock();
try {
int left = newSize;
while (left > 0) {
//库存为0等待生产者进行生产的操作
while (size <= 0) {
emptyCondition.await();
}
int dec = (size < left) ? size : left;
size -= dec;
left -= dec;
System.out.println(Thread.currentThread().getName() + "-------left剩余" + left + "-------size容量" + size + "--------减少量dec" + dec);
fullCondition.signal();
}
} finally {
lock.unlock();
}
}
}
//生产者
class Producer{
private Depot depot;
public Producer(Depot depot) {
this.depot = depot;
}
//往存储地方生产
public void produce(final int newSize){
new Thread(){
@Override
public void run() {
try {
depot.produce(newSize);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
//消费者
class Customer{
private Depot depot;
public Customer(Depot depot) {
this.depot = depot;
}
//进行消费
public void consume(final int newSize){
new Thread(){
@Override
public void run() {
try {
depot.consume(newSize);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
public static void main(String[] args) {
Depot depot = new LockConditionDemo().new Depot(100);
Producer producer = new LockConditionDemo().new Producer(depot);
Customer customer = new LockConditionDemo().new Customer(depot);
producer.produce(60);
producer.produce(120);
customer.consume(90);
customer.consume(150);
producer.produce(110);
}
}
打印结果
Thread-1------left剩余20------size容量100-------inc增长100
Thread-2-------left剩余0-------size容量10--------减少量dec90
Thread-3-------left剩余140-------size容量0--------减少量dec10
Thread-4------left剩余10------size容量100-------inc增长100
Thread-3-------left剩余40-------size容量0--------减少量dec100
Thread-4------left剩余0------size容量10-------inc增长10
Thread-3-------left剩余30-------size容量0--------减少量dec10
Thread-1------left剩余0------size容量20-------inc增长20
Thread-3-------left剩余10-------size容量0--------减少量dec20
Thread-0------left剩余0------size容量60-------inc增长60
Thread-3-------left剩余0-------size容量50--------减少量dec10
通过简单的示例,使用Condition具备两个条件,首先线程一定需要获取到当前的同步状态,其次必须从锁中获取到Condition对象,而condition.await()方法就对应了Object.wait()方法使得当前线程在满足某种条件的时候就进行等待,condition.signal()就是在某种条件下唤醒当前线程。其配合lock接口的使用非常方便。
Condition等待/通知机制的实现原理
await():使当前线程进入等待状态直到被signal()、signalAll()方法唤醒或者被中断
signal():唤醒等待中的一个线程
signalAll():唤醒等待中的全部线程
Condition接口只是定义了相关的处理等待通知的方法,真正实现其等待通知效果的在AQS中的ConditionObject类,在了解源码之前先讲一下同步队列和等待队列:
前面的文章讲过当线程未获取到同步状态的时候,会创建一个Node节点并把这个节点放入同步队列的尾部,进入同步队列的中的线程都是阻塞的。
在AQS中同步队列和等待队列都复用了Node这个节点类,一个同步状态可以含有多个等待队列,同时等待队列只是一个单向的队列
await():使当前线程进入等待状态
public final void await() throws InterruptedException {
if (Thread.interrupted())//响应中断
throw new InterruptedException();
Node node = addConditionWaiter();//放入到等待队列中
int savedState = fullyRelease(node);//释放同步状态(同步队列头节点释放状态唤醒后继节点获取同步状态)
int interruptMode = 0; //判断是否在同步队列中
while (!isOnSyncQueue(node)) {
LockSupport.park(this);//存在等待队列中就阻塞该线程
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)//判断等待过程中是否被中断过
break;
} //自旋去获取同步状态【在AQS中了解】获取成功并且在退出等待时不抛出中断异常(抛出了异常就会立马被中断)
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;//在退出等待时重新中断
if (node.nextWaiter != null) //如果存在其他节点
unlinkCancelledWaiters();//移除所有不是等待状态的节点
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);//如果在等待过程中发现被中断,就执行中断的操作
}
addConditionWaiter():往等待队列中添加元素
private Node addConditionWaiter() {
Node t = lastWaiter;//等待队列中的最后一个元素
if (t != null && t.waitStatus != Node.CONDITION) {//如果尾节点部位null,并且尾节点不是等待状态中说明这个节点不应该待在等待队列中
unlinkCancelledWaiters();//从等待队列中移除
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);//创建一个等待状态的节点
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;//加入等待队列的尾部
return node;
}
unlinkCancelledWaiters():将不是等待状态的节点从等待队列中移除
private void unlinkCancelledWaiters() {
Node t = firstWaiter;//头节点
Node trail = null;
while (t != null) {//存在节点
Node next = t.nextWaiter;//下一个节点
if (t.waitStatus != Node.CONDITION) {//如果不是出于等待中的状态
t.nextWaiter = null;//t的后指针引用清除
if (trail == null)//前面是否存在节点
firstWaiter = next;//下一个节点就是头节点
else
trail.nextWaiter = next;//赋值给前节点的后指针引用
if (next == null)//代表不存在元素了
lastWaiter = trail;
}
else
trail = t;//将t赋值给trail
t = next;//next赋值给t
}
}
fullyRelease(Node node):释放当前状态值,返回同步状态
final int fullyRelease(Node node) {
boolean failed = true;//失败状态
try {
int savedState = getState();//获取当前同步状态值
if (release(savedState)) {//独占模式下释放同步状态,AQS独占式释放锁、前面文章讲过
failed = false;//失败状态为false
return savedState;//返回同步状态
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;//取消等待状态
}
}
isOnSyncQueue:判断线程是否在同步队列中
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)//如果等待状态为等待中,或者前继节点为null代表第一种情况该节点出于等待状态,第二种情况可能已经被唤醒不在等待队列中了
return false;
if (node.next != null) //如果后继节点不为null代表肯定在等待队列中
return true;
return findNodeFromTail(node);//从后往前找判断是否在等待队列中
}
总结一下等待操作:
首先等待操作没有进行CAS或者任何的同步操作,因为调用await()方法的是获取当前lock锁对象的线程,也就是同步队列中的首节点,当调用await()方法后,将同步队列的首节点创建一个等待节点放入等待队列的尾部,然后释放出同步状态(不释放同步状态就会造成死锁),唤醒同步队列中的后继节点,然后当前线程进入等待的状态
signal():唤醒等待队列中的一个线程
public final void signal() {
if (!isHeldExclusively())//判断当前线程是否已经获取同步状态
throw new IllegalMonitorStateException();
Node first = firstWaiter;//等待队列头节点
if (first != null)
doSignal(first);//具体实现方法唤醒第一个node
}
doSignal(Node node):具体处理唤醒节点的操作
private void doSignal(Node first) {
do {
if((firstWaiter = first.nextWaiter) == null)//执行移除头节点的操作
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
transferForSignal(Node node):唤醒的具体实现方式
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))//将节点的等待状态设置更改为初始状态如果改变失败就会被取消
return false;
Node p = enq(node);//往同步队列中添加节点【死循环方式】
int ws = p.waitStatus;//获取节点的等待状态
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))//如果该结点的状态为cancel 或者修改waitStatus失败,则直接唤醒(这一步判断是为了不立刻唤醒脱离等待中的线程,因为他要等同步队列中的头节点释放同步状态再去竞争)
LockSupport.unpark(node.thread);//具体的唤醒操作
return true;
}
总结一下唤醒操作的流程:
当调用signal()方法时,将等待队列中的首节点拿出来,加入到同步队列中,此时该节点不会立刻被唤醒因为就算被唤醒也是需要重新去获取同步状态的,而是在调用lock.unlock()方法释放锁以后将其唤醒获取同步状态
到现在为止,基本的Condition的等待通知机制已经讲解完毕,至于附加功能的比如超时等待或者唤醒全部的功能在源码上都差不了多少稍微新增一些功能需要,在原有的await()方法上增加了一些处理逻辑,真正的原理还是相差无几的