引言

JDK中除了上文提到的各种并发容器,还提供了丰富的阻塞队列。阻塞队列统一实现了BlockingQueue接口,BlockingQueue接口在java.util包Queue接口的基础上提供了put(e)以及take()两个阻塞方法。他的主要使用场景就是多线程下的生产者消费者模式,生产者线程通过put(e)方法将生产元素,消费者线程通过take()消费元素。除了阻塞功能,BlockingQueue接口还定义了定时的offer以及poll,以及一次性移除方法drainTo。

//插入元素,队列满后会抛出异常
boolean add(E e);
//移除元素,队列为空时会抛出异常
E remove(); //插入元素,成功反会true
boolean offer(E e);
//移除元素
E poll(); //插入元素,队列满后会阻塞
void put(E e) throws InterruptedException;
//移除元素,队列空后会阻塞
E take() throws InterruptedException; //限时插入
boolean offer(E e, long timeout, TimeUnit unit)
//限时移除
E poll(long timeout, TimeUnit unit); //获取所有元素到Collection中
int drainTo(Collection<? super E> c);

JDK1.8中的阻塞队列实现共有7个,分别是ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue、SynchronousQueue、LinkedTransferQueue以及LinkedBlockingDeque,下面就来一一对他们进行一个简单的分析。

ArrayBlockingQueue

ArrayBlockingQueue是一个底层用数组实现的有界阻塞队列,有界是指他的容量大小是固定的,不能扩充容量,在初始化时就必须确定队列大小。它通过可重入的独占锁ReentrantLock来控制并发,Condition来实现阻塞。

//通过数组来存储队列中的元素
final Object[] items; //初始化一个固定的数组大小,默认使用非公平锁来控制并发
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
} //初始化固定的items数组大小,初始化notEmpty以及notFull两个Condition来控制生产消费
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);//通过ReentrantLock来控制并发
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}

可以看到ArrayBlockingQueue初始化了一个ReentrantLock以及两个Condition,用来控制并发下队列的生产消费。这里重点看下阻塞的put以及take方法:

//插入元素到队列中
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); //获取独占锁
try {
while (count == items.length) //如果队列已满则通过await阻塞put方法
notFull.await();
enqueue(e); //插入元素
} finally {
lock.unlock();
}
} private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length) //插入元素后将putIndex+1,当队列使用完后重置为0
putIndex = 0;
count++;
notEmpty.signal(); //队列添加元素后唤醒因notEmpty等待的消费线程
} //移除队列中的元素
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); //获取独占锁
try {
while (count == 0) //如果队列已空则通过await阻塞take方法
notEmpty.await();
return dequeue(); //移除元素
} finally {
lock.unlock();
}
} private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length) //移除元素后将takeIndex+1,当队列使用完后重置为0
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal(); //队列消费元素后唤醒因notFull等待的消费线程
return x;
}

在队列添加和移除元素的过程中使用putIndex、takeIndex以及count三个变量来控制生产消费元素的过程,putIndex负责记录下一个可添加元素的下标,takeIndex负责记录下一个可移除元素的下标,count记录了队列中的元素总量。队列满后通过notFull.await()来阻塞生产者线程,消费元素后通过notFull.signal()来唤醒阻塞的生产者线程。队列为空后通过notEmpty.await()来阻塞消费者线程,生产元素后通过notEmpty.signal()唤醒阻塞的消费者线程。

限时插入以及移除方法在ArrayBlockingQueue中通过awaitNanos来实现,在给定的时间过后如果线程未被唤醒则直接返回。

public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout); //获取定时时长
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) {
if (nanos <= 0) //指定时长过后,线程仍然未被唤醒则返回false
return false;
nanos = notFull.awaitNanos(nanos); //指定时长内阻塞线程
}
enqueue(e);
return true;
} finally {
lock.unlock();
}
}

还有一个比较重要的方法:drainTo,drainTo方法可以一次性获取队列中所有的元素,它减少了锁定队列的次数,使用得当在某些场景下对性能有不错的提升。

public int drainTo(Collection<? super E> c, int maxElements) {
checkNotNull(c);
if (c == this)
throw new IllegalArgumentException();
if (maxElements <= 0)
return 0;
final Object[] items = this.items;
final ReentrantLock lock = this.lock; //仅获取一次锁
lock.lock();
try {
int n = Math.min(maxElements, count); //获取队列中所有元素
int take = takeIndex;
int i = 0;
try {
while (i < n) {
@SuppressWarnings("unchecked")
E x = (E) items[take];
c.add(x); //循环插入元素
items[take] = null;
if (++take == items.length)
take = 0;
i++;
}
return n;
} finally {
// Restore invariants even if c.add() threw
if (i > 0) {
count -= i;
takeIndex = take;
if (itrs != null) {
if (count == 0)
itrs.queueIsEmpty();
else if (i > take)
itrs.takeIndexWrapped();
}
for (; i > 0 && lock.hasWaiters(notFull); i--)
notFull.signal(); //唤醒等待的生产者线程
}
}
} finally {
lock.unlock();
}
}

LinkedBlockingQueue

LinkedBlockingQueue是一个底层用单向链表实现的有界阻塞队列,和ArrayBlockingQueue一样,采用ReentrantLock来控制并发,不同的是它使用了两个独占锁来控制消费和生产。put以及take方法源码如下:

public void put(E e) throws InterruptedException {
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
//因为使用了双锁,需要使用AtomicInteger计算元素总量,避免并发计算不准确
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await(); //队列已满,阻塞生产线程
}
enqueue(node); //插入元素到队列尾部
c = count.getAndIncrement(); //count + 1
if (c + 1 < capacity) //如果+1后队列还未满,通过其他生产线程继续生产
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0) //只有当之前是空时,消费队列才会阻塞,否则是不需要通知的
signalNotEmpty();
} private void enqueue(Node<E> node) {
//将新元素添加到链表末尾,然后将last指向尾部元素
last = last.next = node;
} public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await(); //队列为空,阻塞消费线程
}
x = dequeue(); //消费一个元素
c = count.getAndDecrement(); //count - 1
if (c > 1) // 通知其他等待的消费线程继续消费
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity) //只有当之前是满的,生产队列才会阻塞,否则是不需要通知的
signalNotFull();
return x;
} //消费队列头部的下一个元素,同时将新头部置空
private E dequeue() {
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}

可以看到LinkedBlockingQueue通过takeLock和putLock两个锁来控制生产和消费,互不干扰,只要队列未满,生产线程可以一直生产,只要队列不为空,消费线程可以一直消费,不会相互因为独占锁而阻塞。

看过了LinkedBlockingQueue以及ArrayBlockingQueue的底层实现,会发现一个问题,正常来说消费者和生产者可以并发执行对队列的吞吐量会有比较大的提升,那么为什么ArrayBlockingQueue中不使用双锁来实现队列的生产和消费呢?我的理解是ArrayBlockingQueue也能使用双锁来实现功能,但由于它底层使用了数组这种简单结构,相当于一个共享变量,如果通过两个锁,需要更加精确的锁控制,这也是为什么JDK1.7中的ConcurrentHashMap使用了分段锁来实现,将一个数组分为多个数组来提高并发量。LinkedBlockingQueue不存在这个问题,链表这种数据结构头尾节点都相对独立,存储上也不连续,双锁控制不存在复杂性。这是我的理解,如果你有更好的结论,请留言探讨。

PriorityBlockingQueue

PriorityBlockingQueue是一个底层由数组实现的无界队列,并带有排序功能,同样采用ReentrantLock来控制并发。由于是无界的,所以插入元素时不会阻塞,没有队列满的状态,只有队列为空的状态。通过这两点特征其实可以猜测它应该是有一个独占锁(底层数组)和一个Condition(只通知消费)来实现的。put以及take方法源码分析如下:

public void put(E e) {
offer(e);
} public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
//无界队列,队列长度不够时会扩容
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);
try {
//通过comparator来实现优先级排序
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftUpComparable(n, e, array);
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
notEmpty.signal(); //和ArrayBlockingQueue一样,每次添加元素后通知消费线程
} finally {
lock.unlock();
}
return true;
} public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
while ( (result = dequeue()) == null)
notEmpty.await(); //队列为空,阻塞消费线程
} finally {
lock.unlock();
}
return result;
}

DelayQueue

DelayQueue也是一个无界队列,它是在PriorityQueue基础上实现的,先按延迟优先级排序,延迟时间短的排在前面。和PriorityBlockingQueue相似,底层也是数组,采用一个ReentrantLock来控制并发。由于是无界的,所以插入元素时不会阻塞,没有队列满的状态。能想到的最简单的使用场景一般有两个:一个是缓存过期,一个是定时执行的任务。但由于是无界的,缓存过期上一般使用的并不多。简单来看下put以及take方法:

private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue<E>();//优先级队列 public void put(E e) {
offer(e);
} public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e); //插入元素到优先级队列
if (q.peek() == e) { //如果插入的元素在队列头部
leader = null;
available.signal(); //通知消费线程
}
return true;
} finally {
lock.unlock();
}
} public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek(); //获取头部元素
if (first == null)
available.await(); //空队列阻塞
else {
long delay = first.getDelay(NANOSECONDS); //检查元素是否延迟到期
if (delay <= 0)
return q.poll(); //到期则弹出元素
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay); //阻塞未到期的时间
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}

SynchronousQueue

SynchronousQueue相比较之前的4个队列就比较特殊了,它是一个没有容量的队列,也就是说它内部时不会对数据进行存储,每进行一次put之后必须要进行一次take,否则相同线程继续put会阻塞。这种特性很适合做一些传递性的工作,一个线程生产,一个线程消费。内部分为公平和非公平访问两种模式,默认使用非公平,未使用锁,全部通过CAS操作来实现并发,吞吐量非常高。这里只对它的非公平实现下的take和put方法做下简单分析:

//非公平情况下调用内部类TransferStack的transfer方法put
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
if (transferer.transfer(e, false, 0) == null) {
Thread.interrupted();
throw new InterruptedException();
}
}
//非公平情况下调用内部类TransferStack的transfer方法take
public E take() throws InterruptedException {
E e = transferer.transfer(null, false, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
} //具体的put以及take方法,只有E的区别,通过E来区别REQUEST还是DATA模式
E transfer(E e, boolean timed, long nanos) {
SNode s = null; // constructed/reused as needed
int mode = (e == null) ? REQUEST : DATA; for (;;) {
SNode h = head;
//栈无元素或者元素和插入的元素模式相匹配,也就是说都是插入元素
if (h == null || h.mode == mode) {
//有时间限制并且超时
if (timed && nanos <= 0) {
if (h != null && h.isCancelled())
casHead(h, h.next); // 重新设置头节点
else
return null;
}
//未超时cas操作尝试设置头节点
else if (casHead(h, s = snode(s, e, h, mode))) {
//自旋一段时间后未消费元素则挂起put线程
SNode m = awaitFulfill(s, timed, nanos);
if (m == s) { // wait was cancelled
clean(s);
return null;
}
if ((h = head) != null && h.next == s)
casHead(h, s.next); // help s's fulfiller
return (E) ((mode == REQUEST) ? m.item : s.item);
}
}
//栈不为空并且和头节点模式不匹配,存在元素则消费元素并重新设置head节点
else if (!isFulfilling(h.mode)) { // try to fulfill
if (h.isCancelled()) // already cancelled
casHead(h, h.next); // pop and retry
else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
for (;;) { // loop until matched or waiters disappear
SNode m = s.next; // m is s's match
if (m == null) { // all waiters are gone
casHead(s, null); // pop fulfill node
s = null; // use new node next time
break; // restart main loop
}
SNode mn = m.next;
if (m.tryMatch(s)) {
casHead(s, mn); // pop both s and m
return (E) ((mode == REQUEST) ? m.item : s.item);
} else // lost match
s.casNext(m, mn); // help unlink
}
}
}
//节点正在匹配阶段
else { // help a fulfiller
SNode m = h.next; // m is h's match
if (m == null) // waiter is gone
casHead(h, null); // pop fulfilling node
else {
SNode mn = m.next;
if (m.tryMatch(h)) // help match
casHead(h, mn); // pop both h and m
else // lost match
h.casNext(m, mn); // help unlink
}
}
}
} //先自旋后挂起的核心方法
SNode awaitFulfill(SNode s, boolean timed, long nanos) {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
//计算自旋的次数
int spins = (shouldSpin(s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
if (w.isInterrupted())
s.tryCancel();
SNode m = s.match;
//匹配成功过返回节点
if (m != null)
return m;
//超时控制
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel();
continue;
}
}
//自旋检查,是否进行下一次自旋
if (spins > 0)
spins = shouldSpin(s) ? (spins-1) : 0;
else if (s.waiter == null)
s.waiter = w; // establish waiter so can park next iter
else if (!timed)
LockSupport.park(this); //在这里挂起线程
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}

代码非常复杂,这里说下我所理解的核心逻辑。代码中可以看到put以及take方法都是通过调用transfer方法来实现的,然后通过参数mode来区别,在生产元素时如果是同一个线程多次put则会采取自旋的方式多次尝试put元素,可能自旋过程中元素会被消费,这样可以及时put,降低线程挂起的性能损耗,高吞吐量的核心也在这里,消费线程一样,空栈时也会先自旋,自旋失败然后通过线程的LockSupport.park方法挂起。

LinkedTransferQueue

LinkedTransferQueue是一个无界的阻塞队列,底层由链表实现。虽然和LinkedBlockingQueue一样也是链表实现的,但并发控制的实现上却很不一样,和SynchronousQueue类似,采用了大量的CAS操作,没有使用锁,由于是无界的,所以不会put生产线程不会阻塞,只会在take时阻塞消费线程,消费线程挂起时同样使用LockSupport.park方法。

LinkedTransferQueue相比于以上的队列还提供了一些额外的功能,它实现了TransferQueue接口,有两个关键方法transfer(E e)和tryTransfer(E e)方法,transfer在没有消费时会阻塞,tryTransfer在没有消费时不会插入到队列中,也不会等待,直接返回false。

private static final int NOW   = 0; // for untimed poll, tryTransfer
private static final int ASYNC = 1; // for offer, put, add
private static final int SYNC = 2; // for transfer, take
private static final int TIMED = 3; // for timed poll, tryTransfer //通过SYNC状态来实现生产阻塞
public void transfer(E e) throws InterruptedException {
if (xfer(e, true, SYNC, 0) != null) {
Thread.interrupted(); // failure possible only due to interrupt
throw new InterruptedException();
}
}
//通过NOW状态跳过添加元素以及阻塞
public boolean tryTransfer(E e) {
return xfer(e, true, NOW, 0) == null;
} //通过ASYNC状态跳过阻塞
public void put(E e) {
xfer(e, true, ASYNC, 0);
}
//通过SYNC状态来实现消费阻塞
public E take() throws InterruptedException {
E e = xfer(null, false, SYNC, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
} //生产消费调用同一个方法,通过e是否为空,haveData,how等参数来区分具体逻辑
private E xfer(E e, boolean haveData, int how, long nanos) {
if (haveData && (e == null))
throw new NullPointerException();
Node s = null; // the node to append, if needed retry:
for (;;) { // restart on append race
//找出第一个可用节点
for (Node h = head, p = h; p != null;) { // find & match first node
boolean isData = p.isData;
Object item = p.item;
//队列为空时直接跳过
if (item != p && (item != null) == isData) { // unmatched
//节点类型相同,跳过
if (isData == haveData) // can't match
break;
if (p.casItem(item, e)) { // match
for (Node q = p; q != h;) {
Node n = q.next; // update by 2 unless singleton
if (head == h && casHead(h, n == null ? q : n)) {
h.forgetNext();
break;
} // advance and retry
if ((h = head) == null ||
(q = h.next) == null || !q.isMatched())
break; // unless slack < 2
}
LockSupport.unpark(p.waiter);
return LinkedTransferQueue.<E>cast(item);
}
}
Node n = p.next;
p = (p != n) ? n : (h = head); // Use head if p offlist
}
//插入节点或移除节点具体逻辑
//tryTransfer方法会直接跳过并返回结果
if (how != NOW) { // No matches available
if (s == null)
s = new Node(e, haveData);
Node pred = tryAppend(s, haveData); //加入节点
if (pred == null)
continue retry; // lost race vs opposite mode
if (how != ASYNC)
//自旋以及阻塞消费线程逻辑,和SynchronousQueue类似,先尝试自选,失败后挂起线程
//transfer方法在没有消费线程时也会阻塞在这里
return awaitMatch(s, pred, e, (how == TIMED), nanos);
}
return e; // not waiting
}
}

LinkedBlockingDeque

LinkedBlockingDeque是一个有界的双端队列,底层采用一个双向的链表来实现,在LinkedBlockingQeque的Node实现多了指向前一个节点的变量prev。并发控制上和ArrayBlockingQueue类似,采用单个ReentrantLock来控制并发,这里是因为双端队列头尾都可以消费和生产,所以使用了一个共享锁。它实现了BlockingDeque接口,继承自BlockingQueue接口,多了addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast等方法,用来头尾生产和消费。LinkedBlockingDeque的实现代码比较简单,基本就是综合了LinkedBlockingQeque和ArrayBlockingQueue的代码逻辑,这里就不做分析了。

总结

文章对JDK1.8中的7种阻塞队列都做了简单分析,帮助大家大致梳理的这7个队列的基本原理。总的来说每种阻塞队列都有它自己的应用场景,使用时可以先根据有界还是无界,然后在根据各自的特性来进行选择。

有界阻塞队列包括:ArrayBlockingQueue、LinkedBlockingQueue以及LinkedBlockingDeque三种,LinkedBlockingDeque应用场景很少,一般用在“工作窃取”模式下。ArrayBlockingQueue和LinkedBlockingQueue基本就是数组和链表的区别。无界队列包括PriorityBlockingQueue、DelayQueue和LinkedTransferQueue。PriorityBlockingQueue用在需要排序的队列中。DelayQueue可以用来做一些定时任务或者缓存过期的场景。LinkedTransferQueue则相比较其他队列多了transfer功能。最后剩下一个不存储元素的队列SynchronousQueue,用来处理一些高效的传递性场景。

参考资料:

  • 《Java并发编程的艺术》
  • 《Java并发编程实战》

Java并发(10)- 简单聊聊JDK中的七大阻塞队列的更多相关文章

  1. java并发编程工具类JUC第一篇:BlockingQueue阻塞队列

    Java BlockingQueue接口java.util.concurrent.BlockingQueue表示一个可以存取元素,并且线程安全的队列.换句话说,当多线程同时从 JavaBlocking ...

  2. Java并发编程系列-(9) JDK 8/9/10中的并发

    9.1 CompletableFuture CompletableFuture是JDK 8中引入的工具类,实现了Future接口,对以往的FutureTask的功能进行了增强. 手动设置完成状态 Co ...

  3. 简单聊聊CSS中的3D技术之“立方体”

    简单聊聊CSS中的3D技术之“立方体” 大家好,我是今天的男一号,我叫小博主. 今天来聊一下我在前端“逆战班”学习中遇到的颇为有趣的3D知识.前端学习3周,见识稀疏,在下面的分享中如有不对的地方请大家 ...

  4. 【Java并发编程实战】----- AQS(四):CLH同步队列

    在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...

  5. 【Java并发编程实战】—– AQS(四):CLH同步队列

    在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形. 其主要从双方面进行了改造:节点的结构与节点等待机制.在结构上引入了 ...

  6. 简单聊聊TestNG中的并发

    前言 最近在做项目里的自动化测试工作,使用的是TestNG测试框架,主要涉及的测试类型有接口测试以及基于业务实际场景的场景化测试.由于涉及的场景大多都是大数据的作业开发及执行(如MapReduce.S ...

  7. Java并发指南14:JUC中常用的Unsafe和Locksupport

    本文转自网络,侵删 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutoria ...

  8. Java并发指南8:AQS中的公平锁与非公平锁,Condtion

    一行一行源码分析清楚 AbstractQueuedSynchronizer (二) 转自https://www.javadoop.com/post/AbstractQueuedSynchronizer ...

  9. Java并发原理层面:ReentrantLock中lock()、unlock()全解析

    一.前言 Java线程同步两种方式,synchronized关键字和Lock锁机制,其中,AQS队列就是Lock锁实现公平加锁的底层支持. 二.AQS源码对于lock.lock()的实现 2.1 AQ ...

随机推荐

  1. spark优化系列一:参数介绍

    1 spark on yarn常用属性介绍 属性名 默认值 属性说明 spark.yarn.am.memory 512m 在客户端模式(client mode)下,yarn应用master使用的内存数 ...

  2. C# Winform 实现屏蔽键盘的win和alt+F4的实现代码

    最近在做一个恶搞程序,就是打开后,程序获得桌面的截图然后,然后全屏显示在屏幕上,用户此时则不能进行任何操作. 此时希望用户不能通过键盘alt+F4来结束程序及通过Win的组合键对窗口进行操作.我在网上 ...

  3. 1698-Just a Hook 线段树(区间替换)

    Just a Hook Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total ...

  4. java Spring boot使用spring反射

    spring 反射 当你配置各种各样的bean时,是以配置文件的形式配置的,你需要用到哪些bean就配哪些,spring容器就会根据你的需求去动态加载,你的程序就能健壮地运行. 1.可以通过类名去实例 ...

  5. 从C到C++ (1)

    从C到C++ 一. bool类型 bool取值false和true,是0和1的区别: false可以代表0,但true有很多种,并非只有1. 二. const限定符 常量在定义后就不能修改,所以定义时 ...

  6. 雷哥带你走进Javascript

    javascript复习笔记--------------------------------------------1.概念2.面向对象思想3.作用认识4.引入方式5.执行顺序 变量 1)声明方式 x ...

  7. Java日志(二):log4j与XML配置文件

    [Java日志(一):log4j与.properties配置文件]一文列举的几个案例以.properties文件作为log4j的配置文件,本文简单看一下log4j与XML配置文件 (1)XML配置文件 ...

  8. 「学习记录」《数值分析》第二章计算实习题(Python语言)

    在假期利用Python完成了<数值分析>第二章的计算实习题,主要实现了牛顿插值法和三次样条插值,给出了自己的实现与调用Python包的实现--现在能搜到的基本上都是MATLAB版,或者是各 ...

  9. TensorFlow 调用预训练好的模型—— Python 实现

    1. 准备预训练好的模型 TensorFlow 预训练好的模型被保存为以下四个文件 data 文件是训练好的参数值,meta 文件是定义的神经网络图,checkpoint 文件是所有模型的保存路径,如 ...

  10. windows下git hub的GUI软件配置与使用

    转载自:http://www.cnblogs.com/haore147/p/3618930.html   1. 安装两个软件 1 2 1. git的命令行程序--git for windows:htt ...