简析SynchronousQueue,LinkedBlockingQueue(两个locker,更快),ArrayBlockingQueue(一个locker,读写都竞争)
三者都是blockingQueue.
对于blockingQueue的阻塞和非阻塞方法对
注记方案:
* oppo(oppo手机)是一对,offer和poll不阻塞
* ppt是一对.put和take都阻塞.
解析源代码之前先实战看下
SynchronousQueue.
public
static
void
main(String[] args)
throws
InterruptedException {
final
SynchronousQueue<Long> workQueue =
new
SynchronousQueue<Long>();
boolean offer = workQueue.offer(2L); // offer不能放入,前面无阻塞线程,本身也不阻塞不会放入到阻塞线程队列中
System.out.println("main thread: offer=" + offer);
Long poll = workQueue.poll();
// 不能放poll出元素为null
,前面无阻塞线程,本身也不阻塞不会放入到阻塞线程队列中
System.out.println("main thread: poll=" + poll);
ExecutorService newCachedThreadPool = Executors.
newFixedThreadPool
(4);
// 内部源码实现是:new ThreadPoolExecutor(0,Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
System.out.println("/**=====================下面是队列是 take类型========*/");
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out
.println("take thread 1: begin take and thread will be blocked by call park(await) method");
Long take = workQueue.take();
System.out
.println("take thread 1: take suceffull take object="
+ take);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
});
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out
.println("take thread 2: begin take and thread will be blocked by call park(await) method");
Long take = workQueue.take();
System.out
.println("take thread 2: take suceffull take object="
+ take);
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
});
Thread.sleep(1000);
poll = workQueue.poll();
System.out.println("main thread: 队列是take类型. 同类型操作分两种:1.阻塞take会阻塞,加入队列中 2.非阻塞操作poll失败. Queue接口poll失败, poll=" + poll);
offer = workQueue.offer(2L); //
System.out.println("main thread: 队列是take类型. 非同类型不管阻塞不阻塞都成功. 非同类型非阻塞操作Queue接口 offer 成功? " + offer);
Thread.sleep(2000);
long object = 123L;
System.out.println("put thead: begin put " + object);
try {
workQueue.put(object);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out
.println("put thead: 队列是take类型. 非同类型不管阻塞不阻塞都成功. blockingQueue 阻塞接口 put调用未阻塞,调用成功 SynchronousQueue will unpark(notify) the take thread ");
System.out.println("/**=====================下面是 Put类型========*/");
System.out.println("/*先阻塞两个put*/");
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
long object = 123L;
System.out.println("put thead 1: begin put " + object);
try {
workQueue.put(object);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out
.println("put thead 1: finish put sucefully , SynchronousQueue will unpark(notify) the take thread ");
}
});
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
long object = 123L;
System.out.println("put thead 2: begin put " + object);
try {
workQueue.put(object);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out
.println("put thead 2: finish put sucefully , SynchronousQueue will unpark(notify) the take thread ");
}
});
Thread.sleep(2000);
System.out.println("/*试图offer*/");
offer = workQueue.offer(2L); //
System.out.println("main thread: 队列是put类型. 同类型操作分两种:1.阻塞put会阻塞,加入队列中 2.非阻塞操作offer失败. 非阻塞操作Queue接口 offer 成功? " + offer);
poll = workQueue.poll();
System.out.println("main thread: 队列是put类型. 非同类型不管阻塞不阻塞都成功. poll=" + poll);
Long take = workQueue.take();
System.out
.println("main thread: 队列是put类型. 非同类型不管阻塞不阻塞都成功. BlockingQueue阻塞接口take调用未阻塞, take suceffull take object="
+ take);
newCachedThreadPool.shutdown();
}
输出:
main thread: offer=false
main thread: poll=null
/**=====================下面是队列是 take类型========*/
take thread 1: begin take and thread will be blocked by call park(await) method
take thread 2: begin take and thread will be blocked by call park(await) method
main thread: 队列是take类型. 同类型操作分两种:1.阻塞take会阻塞,加入队列中 2.非阻塞操作poll失败. Queue接口poll失败, poll=null
main thread: 队列是take类型. 非同类型不管阻塞不阻塞都成功. 非同类型非阻塞操作Queue接口 offer 成功? true
take thread 2: take suceffull take object=2
put thead: begin put 123
take thread 1: take suceffull take object=123
put thead: 队列是take类型. 非同类型不管阻塞不阻塞都成功. blockingQueue 阻塞接口 put调用未阻塞,调用成功 SynchronousQueue will unpark(notify) the take thread
/**=====================下面是 Put类型========*/
/*先阻塞两个put*/
put thead 1: begin put 123
put thead 2: begin put 123
/*试图offer*/
main thread: 队列是put类型. 同类型操作分两种:1.阻塞put会阻塞,加入队列中 2.非阻塞操作offer失败. 非阻塞操作Queue接口 offer 成功? false
main thread: 队列是put类型. 非同类型不管阻塞不阻塞都成功. poll=123
main thread: 队列是put类型. 非同类型不管阻塞不阻塞都成功. BlockingQueue阻塞接口take调用未阻塞, take suceffull take object=123
put thead 2: finish put sucefully , SynchronousQueue will unpark(notify) the take thread
put thead 1: finish put sucefully , SynchronousQueue will unpark(notify) the take thread
根据上面的代码可以简单总结SynchronousQueue 的queue特点:
1.
queue有三种类型: 空类型,take以及put类型,
分别说明.
1.1. 空类型时, take和put都会被阻塞,
非阻塞offer和poll都不会阻塞. 无法成功操作.
1.2. 队列是某种类型时,
同类型操作:
分两种:
1.阻塞
method
会阻塞,加入阻塞队列中
2.非阻塞method操作失败 .
非同类型: 不管阻塞不阻塞
method
都成功.
具体说来:
1.2.1. 队列是take类型时,
.
同类型操作:阻塞take操作会阻塞,非阻塞 poll()失败,
非同类型:
异类的put和offer能够成功.
1.2.2. 队列是put类型.,
同类型操作:阻塞put操作会被阻塞,非阻塞offer操作失败.
非同类型: 异类的take,poll会成功2. 关于队列长度,界:
一个线程阻塞队列要么全是take线程,要么全是put线程. 后来的互补的操作将会匹配对头的线程.使退出队列.
1. 里面没有任何数据.调用offer()无法成功,返回flase,表示填充失败.不会被阻塞.
2. 先take,被阻塞,直到有一个线程来offer. 两个不同的互补碰撞发生匹配完成(fullfill). 之前的take的线程被唤醒获得offer的提供的数据.
再来解析
SynchronousQueue 源代码,
SynchronousQueue 源代码注释中关键术语解析:
Node类型: 一共分层两种. 一种是 isDate=true. (对应offer , put 函数) 一种是isDate=false (对应 take函数)
dual queue:dual的含义就好理解了,因为只有两类,可以当isDate=true和isDate=false遇到时会匹配.可翻译为成双的,对偶的. 对偶队列.
same mode: 相同的模式(isDate都=true,或者isDate都=false).比如take产生的Node和前面已经放入到队列中的take动作Node就属于同一个模式
complementary :互补的.比如先take,放到队列中.后面来一个offer动作就是complementary (互补).反之亦然.
fulfill: 字面英文翻译,完成.具体到算法里的含义是一个动作和之前的complementary(译为互补)的动作得到匹配.
=============
SynchronousQueue下面的一个部分注释部分翻译.
/*
* A dual queue (and similarly stack) is one that at any given
* time either holds "data" -- items provided by put operations,
* or "requests" -- slots representing take operations, or is
* empty. A call to "fulfill" (i.e., a call requesting an item
* from a queue holding data or vice versa) dequeues a
* complementary node. The most interesting feature of these
* queues is that any operation can figure out which mode the
* queue is in, and act accordingly without needing locks.
任何一个操作 put/take都会产生一个节点,抓住数据(如果是take,数据为null). 一个实现"匹配"的调用将会将互补的节点退出队列.
最有趣的是任何一个操作都能指出目前的队列处于何种类型(注:队列里要么全是take线程,要么全是put线程).并且不需要锁.
//
下面的注释比较了java实现的算法和借鉴的算法(见注释中网址)有何区别
* The algorithms here differ from the versions in the above paper
* in extending them for use in synchronous queues, as well as
* dealing with cancellation. The main differences include:
* 1. The original algorithms used bit-marked pointers, but
* the ones here use mode bits in nodes, leading to a number
* of further adaptations.
* 2. SynchronousQueues must block threads waiting to become
* fulfilled. 必须等待匹配的线程.
fulfilled-完成的意思.
* 3. Support for cancellation via timeout and interrupts,
* including cleaning out cancelled nodes/threads
* from lists to avoid garbage retention and memory depletion. //能够取消或者中断
*
=============再来看看SynchronousQueue.TransferQueue.transfer下面的注释.=========
/**
当队列是空,或者是同一种Mode时,直接放入到列队尾.不会完成(fullfill)
* 2. If queue apparently contains waiting items, and this
我一开始没理解的一点是:
当一个head是 isDate=false , tail是isDate=true时, 一个线程进来的操作是isDate=false时.
不会进入①,进入②后看代码又无法和head完成匹配(fullfill).
后来想明白了,这种情况不会发生.因为tail是isDate=true,这个会与head完成匹配(fullfill).换句话说.
队列里tail和head肯定是same mode.所以当①判断失败,进入②后,
/**
* Puts or takes an item.
*/
Object transfer(Object e, boolean timed, long nanos) {
/* Basic algorithm is to loop trying to take either of
* two actions:
*
* 1. If queue apparently empty or holding same-mode nodes,
* try to add node to queue of waiters, wait to be
* fulfilled (or cancelled) and return matching item.
* //
same-mode指的是take还是put.. 如果空或者是同类型,那么就放入队列阻塞等待
* 2. If queue apparently contains waiting items, and this
* call is of complementary mode, try to fulfill by CAS'ing
* item field of waiting node and dequeuing it, and then
* returning matching item.
*
// 如果是不同,刚好是互补(complementary)节点,那么刚好能够匹配(fulfill )
* In each case, along the way, check for and try to help
* advance head and tail on behalf of other stalled/slow
* threads.
*
* The loop starts off with a null check guarding against
* seeing uninitialized head or tail values. This never
* happens in current SynchronousQueue, but could if
* callers held non-volatile/final ref to the
* transferer. The check is here anyway because it places
* null checks at top of loop, which is usually faster
* than having them implicitly interspersed.
*/
QNode s = null; // constructed/reused as needed
boolean isData = (e != null);
for (;;) {
QNode t = tail;
QNode h = head;
if (t == null || h == null) // saw uninitialized value
continue; // spin
if (h == t || t.isData == isData) { // empty or same-mode 队列里如果都是take线程.此线程还是take调用,进入该分支
QNode tn = t.next;
if (t != tail) // inconsistent read
continue;
if (tn != null) { // lagging tail
advanceTail(t, tn);
continue;
}
if (timed && nanos <= 0) // can't wait
return null;
if (s == null)
s = new QNode(e, isData);
if (!t.casNext(null, s)) // failed to link in
continue;
advanceTail(t, s); // swing tail and wait
Object x = awaitFulfill(s, e, timed, nanos);
if (x == s) { // wait was cancelled
clean(t, s);
return null;
}
if (!s.isOffList()) { // not already unlinked
advanceHead(t, s); // unlink if head
if (x != null) // and forget fields
s.item = s;
s.waiter = null;
}
return (x != null)? x : e;
} else { // complementary-mode
QNode m = h.next; // node to fulfill//进入该分支,因为整个队列的节点类型都一样,肯定能和head完成匹配(fulfill)
if (t != tail || m == null || h != head)
continue; // inconsistent read
Object x = m.item;
if (isData == (x != null) || // m already fulfilled
x == m || // m cancelled
!m.casItem(x, e)) { // lost CAS //这一步很很重要,成功把被匹配线程的数据节点改成了自己的节点,实现了数据传输
advanceHead(h, m); // dequeue and retry
continue;
}
advanceHead(h, m); // successfully fulfilled // 完成匹配,将被匹配的线程退出原队列
LockSupport.unpark(m.waiter);
// 唤醒被匹配的线程
return (x != null)? x : e;
}
}
}
0.阻塞有几种?
1. lock获取不到锁阻塞. 2. 获取到锁但是await阻塞.然后又释放锁(见 <
[源码]
Condition的原理,简单案例(ArrayBlockingQueue),复杂案例(LinkedBlockingQueue).>)
1. LinkedBlockingQueue 和 SynchronousQueue是否有界,上界,下界范围是多少?
2. LinkedBlockingQueue 和 SynchronousQueue 的有几种阻塞线程? 前者一个线程阻塞队列(封装在reentrantLock内,使用者不知),一个条件队列(封装在ConditionObject内,使用者不知). 条件队列signal后把线程会放到阻塞队列里,见wiz<Condition的原理,简单案例(ArrayBlockingQueue),复杂案例(LinkedBlockingQueue).> . 后者只有一个线程阻塞队列. 其各自的阻塞队列数据结构有何不同?前者无数据,后者有数据.
2. LinkedBlockingQueue 和 SynchronousQueue如何在线程之间传递数据?前者获取锁后放置到数据队列中,然后unpark锁.后者将数据传递给线程节点上的数据引用.使读线程解锁后能读取到.虽然SynchronousQueue没有了数据队列,用每个线程持有一个数据替代了.
3. LinkedBlockingQueue.先offer,再poll 和 SynchronousQueue 先offer,再poll有何区别?
4. LinkedBlockingQueue 先put,再take 和 SynchronousQueue 先put,再take有何区别? 前者可以先放在数据队列上.后者没有地方来接受他的数据,必须等待到有一个take线程产生的数据节点来接受数据.
LinkedBlockingQueue,ArrayBlockingQueue:
有数据队列和阻塞线程队列
1. 数据队列有最大长度,有界,默认是Integer.Max;
2. 数据队列达到上界,下界时,对相应的阻塞方法有影响.
LinkedBlockingQueue 两个locker,更复杂. ArrayBlockingQueue一个locker,两个Condition.
SynchronousQueue :
最重要的区别:
外在: 一个有容量>=1,依赖缓冲区线程之间交换数据. 一个无容量需要线程时时产生数据节点来接受传递数据.
内部:数据读取匹配的方式不同:
BlockingQueue是
与已存放在队列上的
数据配对, SynchronousQueue是与已阻塞的线程配对(一个线程id对应着一个数据,这是 SynchronousQueue特有的特点)
对于LinkedBlockingQueue,ArrayBlockingQueue,
有数据队列,也有线程阻塞队列 .
数据配对就行
对于SynchronousQueue ,
无数据队列,只有线程阻塞队列/stack.
与已
阻塞的线程配对就行.
要理解上面这句话.这几个问题思考下.
1. LinkedBlockingQueue 和 SynchronousQueue是否有界? 前者有上界,下界>=1. 后者上界,下界都是0.(无缓冲层就无法传递数据,将数据巧妙地保存在了由线程调用时产生的节点上(
和线程同生共死).
)
2. LinkedBlockingQueue 和 SynchronousQueue 的有几种阻塞线程? 其各自的队列数据结构有何不同?
总结: 前者阻塞队列上无数据,后者阻塞队列上有数据和操作类型.两者在阻塞时都利用堆栈的局部变量来暂时保存数据和传递数据.
前者的利用排它锁,阻塞数据队列上无数据, AbstractQueueSyncronizer( 可能是公平也可能是非公平锁,插入瞬间非公平 ), 阻塞时将数据保存在
内存堆栈局部变量上,每次获得锁后将数据传递给数据队列,
后者获得匹配后,综合匹配的线程数据,返回非null数据. 并修改匹配线程的数据且唤醒被匹配的阻塞线程.
被匹配的阻塞
线程根据其阻塞队列上的新数据和原线程内存堆栈上的局部变量数据(重点,难点)综合返回非null数据. 具体
见源代码注释.
第二个问题引出的另外一个话题是由于
SynchronousQueue 已经没有了数据队列缓冲区,导致SynchronousQueue 中继承Queue的put方法的语义都量变到质变了.
BlockingQueue 继承自Queue的put javaDoc : Inserts the specified element into this queue, waiting if necessary for
spaceto become available.
SynchronousQueue 继承自Queue的put javaDoc : Adds the specified element to this queue, waiting if necessary for
another thread to receive it.
数据存放不同:
由于
LinkedBlockingQueue,ArrayBlockingQueue
代码实现上
通过数据队列转发数据的. 故这两者
不能设置queye长度的最大值为0.
两者通过堆内存传递,notify
All阻塞线程. 如果数据队列是0,数据就无法传递了.
SynchronousQueue 无数据队列,那么数据如何传输呢?
代码实现上其数据是直接通过堆内存直接传递给阻塞线程. 线程1被阻塞将数据同时存放在
线程堆栈
上的局部变量以及和
线程id绑定的
队列节点(是field属性,状态)上. 线程2来匹配,会改变原阻塞线程的堆内存的值,使得原阻塞线程能够获取两份数据.这两份数据中肯定有份是生产者提供的数据,一份是消费者伪造的假数据,通过标示为判断最终得到生产者得到的数据.
对于SynchronousQueue 最大值是0,也没有其他线程生成数据节点,put时无法存放数据, 让put一开始就进入了阻塞.
对于LinkedBlockingQueue,ArrayBlockingQueue, put只有在数据队列满了才会阻塞.
应用场景:
以
Executors.
newCachedThreadPool
()为例,CachedThreadPool特点: 有任务时能够无限制生成线程,无任务时也能够快速回收线程. 用线程不断生成替代了缓冲队列.
该javadoc上,明确说明了适合大批量的小任务. 不适合一下子大量,一下子又无数据. 不太适合生产者生产速率动荡变化,每个任务都很长的场景.
</pre><pre name="code" class="java">public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
利用了
SynchronousQueue.offer和take的调用配合实现了
CachedThreadPool
();相比blockingQueue优点: Syncronize一个阻塞队列.+ 死循环cas(compare and swap),不会频繁的线程挂起和唤醒(park和unpark)
和
new ThreadPoolExecutor时设置coreSize=0,linkedblockingQueue 容量=1 的区别是后者维护一个size=1的阻塞队列,队列经常在满和空之间切换,需要频繁的线程挂起和唤醒(park和unpark)
强烈建议打开eclipse对应的源代码,.jdk1.6 里的src.zip 源代码包
1 public void execute(Runnable command) {
2 if (command == null)
3 throw new NullPointerException();
4 if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
5 if (runState == RUNNING && workQueue.offer(command)) {
6 if (runState != RUNNING || poolSize == 0)
7 ensureQueuedTaskHandled(command);
8 }
9 else if (!addIfUnderMaximumPoolSize(command))
10 reject(command); // is shutdown or saturated
11 }
12 }
刚开始execute(runable),queue.offer()失败,产生新的线程执行任务.一直不停的offer,产生新的线程.到后面老的线程执行完毕会调用take()
第一次execute(runable),第5行queue.
offer()失败,进入第9行产生max配额的新线程执行任务runnable.后面不停的execute(),一直不停的offer失败,产生max配额的新线程去执行runnable.直到第一次的线程执行任务完毕后会调用SynchronousQueue.take(). 这样如果再有execute(),第5行就能匹配成功新的offer就能配对成功,runnable实例被老的线程获取执行,不会去新建线程.这个实现了动态的线程池.所以java才说适合大批量的小的异步任务(These pools will typically improve the performance of programs that execute many short-lived asynchronous tasks.)
Executors.固定大小的线程的好处是.线程资源是有限的,每个线程512k
-Xss 每个线程的Stack大小
,默认堆栈.避免有些无限制加入线程池的问题.没有提供可配置的BlockingQueue容量大小.