上一篇 细说并发4:Java 阻塞队列源码分析(上) 我们了解了 ArrayBlockingQueue, LinkedBlockingQueuePriorityBlockingQueue,这篇文章来了解剩下的四种阻塞队列。

读完本文你将了解:

七种阻塞队列的后四种

DelayQueue

DelayQueue 是一个支持延时获取元素的、无界阻塞队列。

队列使用 PriorityQueue 实现,队列中的元素必须实现 Delayed 接口:

  1. public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
  2. implements BlockingQueue<E> {...}

Delayed 接口:

  1. public interface Delayed extends Comparable<Delayed> {
  2. //返回当前对象的剩余执行时间
  3. long getDelay(TimeUnit unit);
  4. }

可以看到,实现 Delayed 的类也需要实现 Comparable 接口,即实现 compareTo() 方法,保证集合中元素的顺序和 getDelay() 一致。

因此创建元素时可以指定多久才能从队列中获取当前元素。

DelayQueue 的关键属性

  1. private final transient ReentrantLock lock = new ReentrantLock();
  2. private final PriorityQueue<E> q = new PriorityQueue<E>();
  3. private Thread leader;
  4. /**
  5. * Condition signalled when a newer element becomes available
  6. * at the head of the queue or a new thread may need to
  7. * become leader.
  8. */
  9. private final Condition available = lock.newCondition();

可以看到,DelayQueue 的属性只有四个,却都不简单:

  1. ReentrantLock lock

    • 读写锁
  2. PriorityQueue q
    • 无界的、优先级队列
  3. Thread leader
    • Leader-Follower 模型中的 leader
  4. Condition available
    • 队首有新元素可用或者有新线程成为 leader 时触发的 condition

简单介绍下关键属性。

1 PriorityQueue 是一个用数组实现的,基于二叉堆(元素[n] 的子孩子是 元素[2*n+1] 和元素[2*(n+1)] )数据结构的集合。

  1. /**
  2. * Priority queue represented as a balanced binary heap: the two
  3. * children of queue[n] are queue[2*n+1] and queue[2*(n+1)]. The
  4. * priority queue is ordered by comparator, or by the elements'
  5. * natural ordering, if comparator is null: For each node n in the
  6. * heap and each descendant d of n, n <= d. The element with the
  7. * lowest value is in queue[0], assuming the queue is nonempty.
  8. */
  9. transient Object[] queue;

在添加元素时如果超出限制也会扩容:

  1. public boolean offer(E e) {
  2. if (e == null)
  3. throw new NullPointerException();
  4. modCount++;
  5. int i = size;
  6. if (i >= queue.length)
  7. grow(i + 1);
  8. size = i + 1;
  9. if (i == 0)
  10. queue[0] = e;
  11. else
  12. siftUp(i, e);
  13. return true;
  14. }

所以是无界的。

2.Leader-Follower 模型

这种模型中所有线程会有三种身份中的一种:leader、follower,以及一个干活中的状态:proccesser。

它的基本原则就是,永远最多只有一个 leader。而所有 follower 都在等待成为 leader。

线程池启动时会自动产生一个 Leader 负责等待事件,当有一个事件产生时,Leader 线程首先通知一个 Follower 线程将其提拔为新的 Leader,然后自己就去干活了,去处理这个事件。处理完毕后加入 Follower 线程等待队列,等待下次成为 Leader。

这种方法可以增强 CPU 高速缓存相似性,及消除动态内存分配和线程间的数据交换。这种模式是为了最小化任务等待时间,当一个线程成为 leader 后,它只需要等待下一个可执行任务的出现,而其他线程要无限制地等待。

这部分摘自:http://blog.csdn.net/goldlevi/article/details/7705180

实现 Delayed 接口

前面提到了,DelayQueue 的元素必须实现 Delayed 接口,我们以 JDK 中的 ScheduledFutureTask 为例,看下如何实现:

  1. private class ScheduledFutureTask<V>
  2. extends FutureTask<V> implements RunnableScheduledFuture<V> {
  3. //1.初始化
  4. ScheduledFutureTask(Runnable r, V result, long ns, long period) {
  5. super(r, result);
  6. this.time = ns;
  7. this.period = period;
  8. this.sequenceNumber = sequencer.getAndIncrement();
  9. }
  10. //...
  11. //2.
  12. public long getDelay(TimeUnit unit) {
  13. return unit.convert(time - now(), NANOSECONDS);
  14. }
  15. //3.
  16. public int compareTo(Delayed other) {
  17. if (other == this) // compare zero if same object
  18. return 0;
  19. if (other instanceof ScheduledFutureTask) {
  20. ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
  21. long diff = time - x.time;
  22. if (diff < 0)
  23. return -1;
  24. else if (diff > 0)
  25. return 1;
  26. else if (sequenceNumber < x.sequenceNumber)
  27. return -1;
  28. else
  29. return 1;
  30. }
  31. long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
  32. return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
  33. }
  34. //...
  35. }

可以看到,实现 Delayed 接口大概有三步:

  1. 构造函数中初始化基本数据,比如执行时间等数据
  2. 实现 getDelay() 方法,返回当前元素还需要延时多久执行
  3. 实现 compareTo() 方法,指定不同元素如何比较谁先执行

延时阻塞队列如何实现

DelayQueue 中只有延迟时间到了才能从队列中取出元素。

那这个是怎么实现的呢?我们看一下获取元素的实现,以 take() 为例:

  1. public E take() throws InterruptedException {
  2. final ReentrantLock lock = this.lock;
  3. lock.lockInterruptibly();
  4. try {
  5. for (;;) {
  6. E first = q.peek(); //先获取队首元素,不删除
  7. if (first == null) //如果为空就阻塞等待
  8. available.await();
  9. else {
  10. long delay = first.getDelay(NANOSECONDS);
  11. if (delay <= 0L) //比较元素延时时间是否到达
  12. return q.poll(); //如果是就移除并返回
  13. first = null; // don't retain ref while waiting
  14. if (leader != null) //如果有 leader 线程,依然阻塞等待
  15. available.await();
  16. else { //如果没有 leader 线程,指定当前线程,然后等待任务的待执行时间
  17. Thread thisThread = Thread.currentThread();
  18. leader = thisThread;
  19. try {
  20. available.awaitNanos(delay);
  21. } finally {
  22. if (leader == thisThread)
  23. leader = null;
  24. }
  25. }
  26. }
  27. }
  28. } finally { //最后等待时间到了后,就通知阻塞的线程
  29. if (leader == null && q.peek() != null)
  30. available.signal();
  31. lock.unlock();
  32. }
  33. }
  34. //PriorityQueue.peek()
  35. public E peek() {
  36. return (size == 0) ? null : (E) queue[0];
  37. }

可以看到,在取元素时,会根据元素的延时执行时间是否为 0 进行判断,如果延时执行时间已经没有了,就直接返回;否则就要等待执行时间到达后再返回。其中的 Leader-Follower 模型的调度过程这里就不分析了,越分析内容越多 - -。

DelayQueue 使用场景:

  • 缓存系统的设计

    • DelayQueue 保存元素的有效期,用一个线程来循环查询 DelayQueue ,能查到元素,就说明缓存的有效期到了
  • 定时任务调度
    • DelayQueue 保存定时执行的任务和执行时间,同样有一个循环查询线程,获取到任务就执行
    • TimerQueue 就是使用 DelayQueue 实现的

SynchronousQueue

SynchronousQueue 支持公平访问队列,根据构造函数的参数不同,有两种实现方式:TransferQueueTransferStack,默认情况下是 false:

  1. private transient volatile Transferer<E> transferer;
  2. public SynchronousQueue() {
  3. this(false);
  4. }
  5. public SynchronousQueue(boolean fair) {
  6. transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
  7. }

SynchronousQueue 是一个不存储元素的阻塞队列。

这里的“不存储元素”指的是,SynchronousQueue 容量为 0,每添加一个元素必须等待被取走后才能继续添加元素。

我们看下它的 put() 的实现:

  1. public void put(E e) throws InterruptedException {
  2. if (e == null) throw new NullPointerException();
  3. if (transferer.transfer(e, false, 0) == null) {
  4. Thread.interrupted();
  5. throw new InterruptedException();
  6. }
  7. }

可以看到,它的添加是调用的 transferer.transfer(),如果返回 null 就调用 Thread.interrupted() 将中断标志位复位(设为 false),然后抛出异常。

看下 TransferStack.transfer():

  1. /**
  2. * Puts or takes an item.
  3. */
  4. @SuppressWarnings("unchecked")
  5. E transfer(E e, boolean timed, long nanos) {
  6. SNode s = null;
  7. int mode = (e == null) ? REQUEST : DATA; //判断是添加还是获取
  8. for (;;) {
  9. SNode h = head; //获取栈顶节点
  10. if (h == null || h.mode == mode) { // empty or same-mode
  11. if (timed && nanos <= 0) { // can't wait
  12. if (h != null && h.isCancelled()) //如果头节点无法获取,就去获取下一个
  13. casHead(h, h.next); // pop cancelled node
  14. else
  15. return null;
  16. } else if (casHead(h, s = snode(s, e, h, mode))) {
  17. //设置头节点
  18. SNode m = awaitFulfill(s, timed, nanos);
  19. if (m == s) { // wait was cancelled
  20. clean(s);
  21. return null;
  22. }
  23. if ((h = head) != null && h.next == s)
  24. casHead(h, s.next); // help s's fulfiller
  25. return (E) ((mode == REQUEST) ? m.item : s.item);
  26. }
  27. } else if (!isFulfilling(h.mode)) { // try to fulfill
  28. if (h.isCancelled()) // already cancelled
  29. casHead(h, h.next); // pop and retry
  30. else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
  31. for (;;) { // loop until matched or waiters disappear
  32. SNode m = s.next; // m is s's match
  33. if (m == null) { // all waiters are gone
  34. casHead(s, null); // pop fulfill node
  35. s = null; // use new node next time
  36. break; // restart main loop
  37. }
  38. SNode mn = m.next;
  39. if (m.tryMatch(s)) {
  40. casHead(s, mn); // pop both s and m
  41. return (E) ((mode == REQUEST) ? m.item : s.item);
  42. } else // lost match
  43. s.casNext(m, mn); // help unlink
  44. }
  45. }
  46. } else { // help a fulfiller
  47. SNode m = h.next; // m is h's match
  48. if (m == null) // waiter is gone
  49. casHead(h, null); // pop fulfilling node
  50. else {
  51. SNode mn = m.next;
  52. if (m.tryMatch(h)) // help match
  53. casHead(h, mn); // pop both h and m
  54. else // lost match
  55. h.casNext(m, mn); // help unlink
  56. }
  57. }
  58. }
  59. }

逻辑比较复杂,主要就是三步:

  1. 栈是空的或者栈顶元素的模式和当前要进行的操作一致

    • 将节点推到堆栈上并等待匹配
    • 等待参数中的时间后返回
    • 如果取消就返回 null
  2. 如果栈不为空且栈顶元素模式与当前要进行的操作不一致,如果这个元素的模式是相反的模式(取对应放)
    • 尝试将栈中一个模式匹配要求的节点推到堆栈上,与相应的等待节点匹配并返回
  3. 如果栈顶已经拥有另一个模式 匹配的节点
    • 通过执行 POP 操作来找到匹配的元素,然后继续

看着有点晕,简单概括就是一个添加操作后必须等待一个获取操作才可以继续添加。

SynchronousQueue 的吞吐量高于 LinkedBlockingQueueArrayBlockingQueue,有位前辈做了测试,可以点击 这篇文章 查看。这里引用一下结论:

LinkedBlockingQueue 性能表现远超 ArrayBlcokingQueue,不管线程多少,不管 Queue 长短,LinkedBlockingQueue 都胜过 ArrayBlockingQueue。

SynchronousQueue 表现很稳定,而且在 20 个线程之内不管 Queue 长短,SynchronousQueue 性能表现是最好的,(其实SynchronousQueue 跟 Queue 长短没有关系),如果 Queue 的 capability 只能是 1,那么毫无疑问选择 SynchronousQueue,这也是设计 SynchronousQueue 的目的吧。

但大家也可以看到当超过 1000 个线程时,SynchronousQueue 性能就直线下降了,只有最高峰的一半左右,而且当 Queue 大于 30 时,LinkedBlockingQueue 性能就超过 SynchronousQueue。

相较于其他队列有缓存的作用,SynchronousQueue 适用于单线程同步传递性场景,比如:消费者没拿走当前的产品,生产者是不能再给产品的,这样可以控制生产者生产的速率和消费者一致。

LinkedTransferQueue

LinkedTransferQueue 实现了 TransferQueue 接口, 是一个由链表组成的、无界阻塞队列。

  1. public class LinkedTransferQueue<E> extends AbstractQueue<E>
  2. implements TransferQueue<E>, java.io.Serializable {...}

TransferQueue

TransferQueue 也是一种阻塞队列,它用于生产者需要等待消费者消费事件的场景,与前面一节的 SynchronousQueue 有相似之处。它定义的方法如下:

  1. public interface TransferQueue<E> extends BlockingQueue<E> {
  2. //尽可能快地转移元素给一个等待的消费者
  3. //如果在这之前有其他线程调用了 taked() 或者 poll(long,TimeUnit) 方法,就返回 true
  4. //否则返回 false
  5. boolean tryTransfer(E e);
  6. //转移元素给一个消费者,在有的情况下会等待直到被取走
  7. //
  8. void transfer(E e) throws InterruptedException;
  9. //在 timeout 时间内将元素转移给一个消费者,如果这段时间内传递出去了就返回 true
  10. //否则返回 false
  11. boolean tryTransfer(E e, long timeout, TimeUnit unit)
  12. throws InterruptedException;
  13. //如果至少有一个等待的消费者,就返回 true
  14. boolean hasWaitingConsumer();
  15. //返回等待获取元素的消费者个数
  16. //这个值用于监控
  17. int getWaitingConsumerCount();
  18. }

tryTransfer() 和 transfer()

相对于其他阻塞队列,LinkedTransferQueue 多了两个关键地方法:tryTransfer()transfer()

分别来看看它是如何实现的。

1.transfer()

transfer() 方法的作用是:如果有等待接收元素的消费者线程,直接把生产者传入的元素 transfer 给消费者;如果没有消费者线程,transfer() 会将元素存放到队列尾部,并等待元素被消费者取走才返回:

  1. public void transfer(E e) throws InterruptedException {
  2. if (xfer(e, true, SYNC, 0) != null) {
  3. Thread.interrupted(); // failure possible only due to interrupt
  4. throw new InterruptedException();
  5. }
  6. }
  7. private E xfer(E e, boolean haveData, int how, long nanos) {
  8. if (haveData && (e == null))
  9. throw new NullPointerException();
  10. Node s = null; // the node to append, if needed
  11. retry:
  12. for (;;) { // restart on append race
  13. for (Node h = head, p = h; p != null;) { // find & match first node
  14. boolean isData = p.isData;
  15. Object item = p.item;
  16. if (item != p && (item != null) == isData) { // unmatched
  17. if (isData == haveData) // can't match
  18. break;
  19. if (p.casItem(item, e)) { // match
  20. for (Node q = p; q != h;) {
  21. Node n = q.next; // update by 2 unless singleton
  22. if (head == h && casHead(h, n == null ? q : n)) {
  23. h.forgetNext();
  24. break;
  25. } // advance and retry
  26. if ((h = head) == null ||
  27. (q = h.next) == null || !q.isMatched())
  28. break; // unless slack < 2
  29. }
  30. LockSupport.unpark(p.waiter);
  31. @SuppressWarnings("unchecked") E itemE = (E) item;
  32. return itemE;
  33. }
  34. }
  35. Node n = p.next;
  36. p = (p != n) ? n : (h = head); // Use head if p offlist
  37. }
  38. if (how != NOW) { // No matches available
  39. if (s == null)
  40. s = new Node(e, haveData);
  41. Node pred = tryAppend(s, haveData); //尝试添加到队尾
  42. if (pred == null)
  43. continue retry; // lost race vs opposite mode
  44. if (how != ASYNC)
  45. return awaitMatch(s, pred, e, (how == TIMED), nanos);
  46. }
  47. return e; // not waiting
  48. }
  49. }

awaitMatch() 方法的作用是:CPU 自旋等待消费者取走元素,为了避免长时间消耗 CPU,在自旋一定次数后会调用 Thread.yield() 暂停当前正在执行的线程,改为执行其他线程。

2.tryTransfer()

tryTransfer() 的作用是:试探生产者传入的元素是否能 直接传递给消费者

  • 如果有等待接收的消费者,返回 true
  • 没有则返回 false
  1. public boolean tryTransfer(E e, long timeout, TimeUnit unit)
  2. throws InterruptedException {
  3. if (xfer(e, true, TIMED, unit.toNanos(timeout)) == null)
  4. return true;
  5. if (!Thread.interrupted())
  6. return false;
  7. throw new InterruptedException();
  8. }

可以看到,和 transfer() 必须等到消费者取出元素才返回不同的是,tryTransfer() 无论是否有消费者接收都会立即返回。

LinkedBlockingDeque

LinkedBlockingDeque 是一个由链表组成的、双向阻塞队列。

关键属性

  1. static final class Node<E> {
  2. E item;
  3. Node<E> prev;
  4. Node<E> next;
  5. Node(E x) {
  6. item = x;
  7. }
  8. }
  9. transient Node<E> first;
  10. transient Node<E> last;
  11. private transient int count;
  12. private final int capacity;
  13. final ReentrantLock lock = new ReentrantLock();
  14. private final Condition notEmpty = lock.newCondition();
  15. private final Condition notFull = lock.newCondition();

可以看到,LinkedBlockingDeque 中持有队列首部和尾部节点,每个节点也是双向的。

双向的作用是:可以从队列两端插入和移除元素。多了一个操作队列的方向,在多线程同时入队时,可以减少一半的竞争。

除了 remove(Object) 等移除操作,LinkedBlockingDeque 的大多数操作的时间复杂度都是 O(n)。

LinkedBlockingDeque 多了获取和查询的 XXXFirstXXXLast 的方法。

7 种阻塞队列的特点

这篇文章介绍的 4 种加上上一篇 细说并发4:Java 阻塞队列源码分析(上) 中 3 种,总共 7 种阻塞队列,这么多队列看的眼都花了。

这里简单总结下 Java 中 7 种阻塞队列的特点:

  1. ArrayBlockingQueue

    • 环形数组实现的、有界的队列,一旦创建后,容量不可变
    • 基于数组,在添加删除上性能还是不如链表
  2. LinkedBlockingQueue:
    • 基于链表、有界阻塞队列
    • 添加和获取是两个不同的锁,所以并发添加/获取效率更高些
    • Executors.newFixedThreadPool() 使用了这个队列
  3. PriorityBlockingQueue
    • 基于数组的、支持优先级的、无界阻塞队列
    • 使用自然排序或者定制排序指定排序规则
    • 添加元素时,当数组中元素大于等于容量时,会扩容(当前队列中元素个数小于 64 个,数组容量就乘 3;否则就乘 2 加 2),拷贝数组
  4. DelayQueue
    • 支持延时获取元素的、无界阻塞队列
    • 添加元素时如果超出限制也会扩容
    • Leader-Follower 模型
  5. SynchronousQueue
    • 容量为 0
    • 一个添加操作后必须等待一个获取操作才可以继续添加
    • 吞吐量高于 LinkedBlockingQueueArrayBlockingQueue
  6. LinkedTransferQueue
    • 由链表组成的、无界阻塞队列
    • 实现了 TransferQueue 接口
    • CPU 自旋等待消费者取走元素,自旋一定次数后结束
  7. LinkedBlockingDeque
    • 由双向链表组成的、双向阻塞队列
    • 可以从队列两端插入和移除元素
    • 多了一个操作队列的方向,在多线程同时入队时,可以减少一半的竞争

总结

在实际开发中可能接触不到阻塞队列,线程池或者其他池都将这些细节封装好了,但是在看一些开源框架的时候经常看到有使用它们,因此如果想要自己写牛逼的框架,这些底层的东西还是需要了解的。

我们结合源码和《Java 并发编程的艺术》相关章节分两篇文章介绍了 Java 中的阻塞队列,了解了 7 种阻塞队列的大致源码实现,后面遇到需要使用阻塞队列时心里应该有些底了。

学基础就是这样,不能指望立即有用,古话说得好:无用之用是为大用,不一定哪天就派上用场了!

Thanks

《Java 并发编程的艺术》

http://blog.csdn.net/goldlevi/article/details/7705180

http://stevex.blog.51cto.com/4300375/1287085/

细说并发5:Java 阻塞队列源码分析(下)的更多相关文章

  1. 细说并发4:Java 阻塞队列源码分析(上)

    上篇文章 趣谈并发3:线程池的使用与执行流程 中我们了解到,线程池中需要使用阻塞队列来保存待执行的任务.这篇文章我们来详细了解下 Java 中的阻塞队列究竟是什么. 读完你将了解: 什么是阻塞队列 七 ...

  2. JDK数组阻塞队列源码深入剖析

    JDK数组阻塞队列源码深入剖析 前言 在前面一篇文章从零开始自己动手写阻塞队列当中我们仔细介绍了阻塞队列提供给我们的功能,以及他的实现原理,并且基于谈到的内容我们自己实现了一个低配版的数组阻塞队列.在 ...

  3. 聊聊 JDK 非阻塞队列源码(CAS实现)

    正如上篇文章聊聊 JDK 阻塞队列源码(ReentrantLock实现)所说,队列在我们现实生活中队列随处可见,最经典的就是去银行办理业务,超市买东西排队等.今天楼主要讲的就是JDK中安全队列的另一种 ...

  4. Java split方法源码分析

    Java split方法源码分析 public String[] split(CharSequence input [, int limit]) { int index = 0; // 指针 bool ...

  5. 【JAVA】ThreadLocal源码分析

    ThreadLocal内部是用一张哈希表来存储: static class ThreadLocalMap { static class Entry extends WeakReference<T ...

  6. 【Java】HashMap源码分析——常用方法详解

    上一篇介绍了HashMap的基本概念,这一篇着重介绍HasHMap中的一些常用方法:put()get()**resize()** 首先介绍resize()这个方法,在我看来这是HashMap中一个非常 ...

  7. 【Java】HashMap源码分析——基本概念

    在JDK1.8后,对HashMap源码进行了更改,引入了红黑树.在这之前,HashMap实际上就是就是数组+链表的结构,由于HashMap是一张哈希表,其会产生哈希冲突,为了解决哈希冲突,HashMa ...

  8. Java并发编程笔记之PriorityBlockingQueue源码分析

    JDK 中无界优先级队列PriorityBlockingQueue 内部使用堆算法保证每次出队都是优先级最高的元素,元素入队时候是如何建堆的,元素出队后如何调整堆的平衡的? PriorityBlock ...

  9. Java并发编程笔记之ReentrantLock源码分析

    ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞后放入该锁的AQS阻塞队列里面. 首先我们先看一下ReentrantLock的类图结构,如下图所示 ...

随机推荐

  1. <linux/init.h>,<linux/module.h>头文件不存在等问题的解决方法

    这个问题真心是处理了一个下午,还自己去下载了个最新的内核拿来编译,其实是完全没必要的,因为ubuntu系统是可以直接下载新内核的. 你可以在/usr/src/文件夹下找到这些内核文件夹,比如说我自己的 ...

  2. Excel水平线画不直,图形对象对不齐,怎么办

    看够了千篇一律的数字报表,不妨添加些图形对象来调剂下,今天小编excel小课堂(ID:excel-xiaoketang 长按复制)给各位分享10个插入图形对象时简单实用的小技巧. 01课题 今天小编e ...

  3. mac下ssh到远程服务器时中文乱码

    前言:mac本地的语言环境为英文,远程是支持中文的, 问题: 一开始是在iterm2下登录远程服务器更新数据库时发现中文注释不能正常显示,以为是iterms2下设置有问题,使用系统自带的termina ...

  4. 比较好的Json 格式数据

    { "81040753986": [{ "order_info": { "unique_package_reference": " ...

  5. sprites.png雪碧图

    长时间不用把精灵图怎么用给忘了... 一.PC端 给所用到精灵图的元素设置background:url(sprites.png路径);  background-position: -x -y; 其中: ...

  6. Flutter新手第一个坑:Could not find com.android.tools.lint:lint-gradle:26.1.1.

    解决方法1:修改build.gradle,注释掉jcenter(),google().使用阿里的镜像.原因是jcenter google库无法访问到导致的问题.虽然我有万能的爬墙工具,开启全局代理依然 ...

  7. 一个很有用的树形控件----zTree

    演示地址 http://www.treejs.cn/v3/demo.php#_101

  8. 解决maven项目Cannot change version of project facet Dynamic web module to 3.0/3.1

    解决maven项目Cannot change version of project facet Dynamic web module to 3.0 1.打开项目所在目录下的.settings文件夹 打 ...

  9. 《高级Web应用程序设计》作业(20170904)

    作业1(类型-理论学习,上传ftp,截止日期9月20日) 1.请写出ASP.NET MVC的优点. 2.请写出默认项目模板中以下文件夹或文件的作用.App_Data文件夹.Content文件夹.Con ...

  10. UVA-10972 RevolC FaeLoN (边双连通+缩点)

    题目大意:将n个点,m条边的无向图变成强连通图,最少需要加几条有向边. 题目分析:所谓强连通,就是无向图中任意两点可互达.找出所有的边连通分量,每一个边连通分量都是强连通的,那么缩点得到bcc图,只需 ...