zoukankan      html  css  js  c++  java
  • Java并发之ArrayBlockingQueue

     ArrayBlockingQueue是一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部是在队列中存在时间最长的元素。队列的尾部是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。 

            ArrayBlockingQueue继承自 AbstractQueue并实现 BlockingQueue接口。

     

            ArrayBlockingQueue是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞,试图从空队列中提取元素将导致类似阻塞。 

            ArrayBlockingQueue支持对等待的生产者线程和使用者线程进行排序的可选公平策略。默认情况下,不保证是这种排序。然而,通过将公平性 (fairness) 设置为 true 而构造的队列允许按照 FIFO 顺序访问线程。公平性通常会降低吞吐量,但也减少了可变性和避免了“不平衡性”。 公平性通过创建 ArrayBlockingQueue实例时指定。

            1.成员变量

    Java代码  收藏代码
    1. /** 队列数组实现 */  
    2. private final E[] items;  
    3. /** 已取出元素索引,用于下一个元素的 take, poll or remove */  
    4. private int takeIndex;  
    5. /** 已插入元素索引,用于下一个元素的 put, offer, or add */  
    6. private int putIndex;  
    7. /** 队列中项目数 */  
    8. private int count;  
    9. /** 保护所有访问的主锁 */  
    10. private final ReentrantLock lock;  
    11. /** Condition 实例,用于等待中 take */  
    12. private final Condition notEmpty;  
    13. /** Condition 实例,用于等待中 put*/  
    14. private final Condition notFull;  

            之前我们已经学习了锁相关的知识,所以几个成员变量不难理解。

            1)其中E[] items;是数组式的队列实现;

            2)takeIndex 用于记录 take操作的次数;

            3)putIndex 用于记录 put操作的次数;

            4)count 用于记录队列中元素数目;

            5)ReentrantLock lock 是用于控制访问的主锁;

            6)Condition notEmpty 获取操作时的条;

            7)Condition notFull 插入操作时的条件;

            2.构造方法

            ArrayBlockingQueue的构造方法有3个。

            1)最简单的构造方法:

    Java代码  收藏代码
    1. //指定队列大小  
    2. public ArrayBlockingQueue(int capacity) {  
    3.     this(capacity, false);  
    4. }  

            此种构造方法最为简单也最为常用,在创建 ArrayBlockingQueue实例时只需指定其大小即可。

    Java代码  收藏代码
    1. BlockingQueue<Object> q = new ArrayBlockingQueue<Object>(10);  

            2)增加访问策略的构造方法,除了指定队列大小外还可指定队列的访问策略:

    Java代码  收藏代码
    1. //指定队列大小、访问策略  
    2. public ArrayBlockingQueue(int capacity, boolean fair) {  
    3.     if (capacity <= 0)  
    4.         throw new IllegalArgumentException();  
    5.     this.items = (E[]) new Object[capacity];  
    6.     lock = new ReentrantLock(fair);  
    7.     notEmpty = lock.newCondition();  
    8.     notFull = lock.newCondition();  
    9. }  

            fair如果为 true,则按照 FIFO 顺序访问插入或移除时受阻塞线程的队列;如果为 false,则访问顺序是不确定的。

            构造方法中首先初始化了 items数组,然后根据fair创建 ReentrantLock实例,最后返回 notEmpty与 notFull两个 Condition实例,分别用于等待中的获取与添加操作。

            3)带初始元素的构造方法:

    Java代码  收藏代码
    1. //指定队列大小、访问策略、初始元素  
    2. public ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c) {  
    3.     this(capacity, fair);  
    4.     if (capacity < c.size())  
    5.         throw new IllegalArgumentException();  
    6.   
    7.     for (Iterator<? extends E> it = c.iterator(); it.hasNext();)  
    8.         add(it.next());  
    9. }  

            除了可以指定容量和访问策略外,还可以包含给定 collection 的元素,并以 collection 迭代器的遍历顺序添加元素。

            代码也可以观察到,在实例化队列之后还使用了add方法将 collection 中的元素按原有顺序添加到实例中。

            3.添加元素

            1)add方法

            ArrayBlockingQueue的add方法调用的是父类方法,而父类 add方法则调用的是 offer方法,以下是add方法的源代码:

    Java代码  收藏代码
    1. public boolean add(E e) {  
    2.     //调用父类add方法  
    3.     return super.add(e);  
    4. }  

            2)offer方法

            offer方法将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回 true,如果此队列已满,则返回 false。此方法通常要优于 add(E) 方法,后者可能无法插入元素,而只是抛出一个异常。 

    Java代码  收藏代码
    1. /** 
    2.  * 将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量), 
    3.  * 在成功时返回 true,如果此队列已满,则返回 false。 
    4.  * 此方法通常要优于 add(E) 方法,后者可能无法插入元素,而只是抛出一个异常。 
    5.  */  
    6. public boolean offer(E e) {  
    7.     //判断e是否为null  
    8.     if (e == null)  
    9.         throw new NullPointerException();  
    10.     final ReentrantLock lock = this.lock;  
    11.     //获取锁  
    12.     lock.lock();  
    13.     try {  
    14.         //判断队列是否已满  
    15.         if (count == items.length)  
    16.             return false;  
    17.         else {  
    18.             //如果未满则插入  
    19.             insert(e);  
    20.             return true;  
    21.         }  
    22.     } finally {  
    23.         //释放锁  
    24.         lock.unlock();  
    25.     }  
    26. }  

            因为 add方法在队列已满时会抛出异常,所以 offer方法一般优于 add方法使用。

            首先,判断要添加的元素是否为 null,如果为null则抛出空指针异常。

            接着,创建一个 ReentrantLock实例,ReentrantLock是可重入锁实现。更详细介绍参考http://286.iteye.com/blog/2296191

            然后,获取锁。

            最后,判断队列是否已满,如果已满则返回 false;如果未满则调用insert方法插入元素,返回true。

            所有操作完成后释放锁。

            offer方法的处理流程可以参照以下流程图:



            从代码中就可以看到,offer方法利用了ReentrantLock来实现队列阻塞的功能,所以多线程操作相同队列时会排队等待。

            offer的另一个重载方法是 offer(E e, long timeout, TimeUnit unit),此重载方法将指定的元素插入此队列的尾部,如果该队列已满,则在到达指定的等待时间之前等待可用的空间。其源代码为:

    Java代码  收藏代码
    1. /** 
    2.  * 将指定的元素插入此队列的尾部,如果该队列已满,则在到达指定的等待时间之前等待可用的空间。 
    3.  */  
    4. public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {  
    5.   
    6.     if (e == null)  
    7.         throw new NullPointerException();  
    8.     long nanos = unit.toNanos(timeout);  
    9.     final ReentrantLock lock = this.lock;  
    10.     lock.lockInterruptibly();  
    11.     try {  
    12.         for (;;) {  
    13.             if (count != items.length) {  
    14.                 insert(e);  
    15.                 return true;  
    16.             }  
    17.             if (nanos <= 0)  
    18.                 return false;  
    19.             try {  
    20.                 nanos = notFull.awaitNanos(nanos);  
    21.             } catch (InterruptedException ie) {  
    22.                 notFull.signal(); // propagate to non-interrupted thread  
    23.                 throw ie;  
    24.             }  
    25.         }  
    26.     } finally {  
    27.         lock.unlock();  
    28.     }  
    29. }  

            与普通offer方法不同之处在于:offer(E e, long timeout, TimeUnit unit)方法利用循环在指定时间内不断去尝试添加元素,如果成功则返回true,如果指定时间已到则退出返回 false。

            3)insert方法

            insert方法在当前位置(putIndex)插入元素。

    Java代码  收藏代码
    1. /** 
    2.  * 在当前位置(putIndex)插入元素(在获得锁的情况下调用) 
    3.  */  
    4. private void insert(E x) {  
    5.     //设置 putIndex位置 items数组元素为x  
    6.     items[putIndex] = x;  
    7.     //返回putIndex新值,如果已满则返回0,未满则+1  
    8.     putIndex = inc(putIndex);  
    9.     //增加元素数量  
    10.     ++count;  
    11.     //唤醒获取线程  
    12.     notEmpty.signal();  
    13. }  

            因为 ArrayBlockingQueue内部队列实现为数组items,而 putIndex则记录了队列中已添加元素的位置,所以新添加的元素就直接被添加到数组的指定位置。随后修改 putIndex值,如果队列未满则+1,如果已满则从0重新开始。最后唤醒 notEmpty中的一个线程。

            4)put方法

            将指定的元素插入此队列的尾部,如果该队列已满,则等待可用的空间。

            以下是put方法的源代码:

    Java代码  收藏代码
    1. /** 
    2.  * 将指定的元素插入此队列的尾部,如果该队列已满,则等待可用的空间 
    3.  */  
    4. public void put(E e) throws InterruptedException {  
    5.     //判断e是否为null  
    6.     if (e == null)  
    7.         throw new NullPointerException();  
    8.     final E[] items = this.items;  
    9.     final ReentrantLock lock = this.lock;  
    10.     //获取中断锁  
    11.     lock.lockInterruptibly();  
    12.     try {  
    13.         try {  
    14.             //利用循环判断队列是否已满  
    15.             while (count == items.length)  
    16.                 //如果已满则调用await方法阻塞等待  
    17.                 notFull.await();  
    18.         } catch (InterruptedException ie) {  
    19.             notFull.signal(); // propagate to non-interrupted thread  
    20.             throw ie;  
    21.         }  
    22.         //队列未满则插入  
    23.         insert(e);  
    24.     } finally {  
    25.         //释放锁  
    26.         lock.unlock();  
    27.     }  
    28. }  

            put方法与其他方法类似,其中会循环判断队列是否已满,如果已满则阻塞 notFull,如果未满则调用 insert方法添加元素。因为其中运用了循环判断队列是否有位置添加新元素,如果队列已满则产生阻塞等待,直至可以添加元素为止。

            将本文开始时的例子修改一下,去掉消费者,只留下生产者,这样当队列满了之后没有消费者去消费产品,生产者就不会再向队列中插入了:

    Java代码  收藏代码
    1. class Producer implements Runnable {  
    2.     private final ArrayBlockingQueue<Integer> queue;  
    3.     private int i;  
    4.   
    5.     Producer(ArrayBlockingQueue<Integer> q) {  
    6.         queue = q;  
    7.     }  
    8.   
    9.     public void run() {  
    10.         try {  
    11.             while (true) {  
    12.                 int p=produce();  
    13.                 queue.put(p);// 将产品放入缓冲队列  
    14.                 System.out.println("插入成功:"+p);  
    15.             }  
    16.         } catch (InterruptedException e) {  
    17.             e.printStackTrace();  
    18.         }  
    19.     }  
    20.   
    21.     int produce() {  
    22.         return i++;// 生产产品  
    23.     }  
    24. }  
    25.   
    26. public class Runner {  
    27.     public static void main(String[] args) {  
    28.         ArrayBlockingQueue<Integer> q = new ArrayBlockingQueue<Integer>(10);  
    29.         Producer p = new Producer(q);  
    30.         new Thread(p).start();  
    31.     }  
    32. }  
    33. //结果:  
    34. 插入成功:0  
    35. 插入成功:1  
    36. 插入成功:2  
    37. 插入成功:3  
    38. 插入成功:4  
    39. 插入成功:5  
    40. 插入成功:6  
    41. 插入成功:7  
    42. 插入成功:8  
    43. 插入成功:9  

            此时程序并不会退出,而是阻塞在那里等待队列有位置插入。

            4.获取元素

            1)peek方法

            获取但不移除此队列的头;如果此队列为空,则返回 null。以下是peek方法的源代码:

    Java代码  收藏代码
    1. /** 
    2.  * 获取但不移除此队列的头;如果此队列为空,则返回 null 
    3.  */  
    4. public E peek() {  
    5.     final ReentrantLock lock = this.lock;  
    6.     //获取锁  
    7.     lock.lock();  
    8.     try {  
    9.         //判断队列中是否有元素,如果没有则返回null,如果存在则返回该元素  
    10.         return (count == 0) ? null : items[takeIndex];  
    11.     } finally {  
    12.         //释放锁  
    13.         lock.unlock();  
    14.     }  
    15. }  

            peek代码比较简单,首先判断队列是否有元素,即count==0,如果为空则返回null,非空则返回相应元素。

            2)poll方法

            获取并移除此队列的头,如果此队列为空,则返回 null。以下是poll方法的源代码:

    Java代码  收藏代码
    1. /** 
    2.  * 获取并移除此队列的头,如果此队列为空,则返回 null 
    3.  */  
    4. public E poll() {  
    5.     final ReentrantLock lock = this.lock;  
    6.     //获取锁  
    7.     lock.lock();  
    8.     try {  
    9.         //判断是否存在元素  
    10.         if (count == 0)  
    11.             return null;  
    12.         //调用extract方法返回元素  
    13.         E x = extract();  
    14.         return x;  
    15.     } finally {  
    16.         //释放锁  
    17.         lock.unlock();  
    18.     }  
    19. }  

            poll方法调用的是 extract()方法来获取头元素。

            3)extract方法

            以下是extract()方法的源代码:

    Java代码  收藏代码
    1. /** 
    2.  * 从 takeIndex位置获取元素(在获得锁的情况下调用) 
    3.  */  
    4. private E extract() {  
    5.     final E[] items = this.items;  
    6.     //获取元素  
    7.     E x = items[takeIndex];  
    8.     //移除原位置元素  
    9.     items[takeIndex] = null;  
    10.     //计算 takeIndex新值  
    11.     takeIndex = inc(takeIndex);  
    12.     --count;  
    13.     //唤醒添加线程  
    14.     notFull.signal();  
    15.     return x;  
    16. }  

            4)take方法

            获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)。

    Java代码  收藏代码
    1. /** 
    2.  * 获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要) 
    3.  */  
    4. public E take() throws InterruptedException {  
    5.     final ReentrantLock lock = this.lock;  
    6.     // 获取中断锁  
    7.     lock.lockInterruptibly();  
    8.     try {  
    9.         try {  
    10.             // 如果队列未空则阻塞等待,直到有元素为止  
    11.             while (count == 0)  
    12.                 notEmpty.await();  
    13.         } catch (InterruptedException ie) {  
    14.             notEmpty.signal(); // 唤醒获取线程  
    15.             throw ie;  
    16.         }  
    17.         // 调用extract方法返回元素  
    18.         E x = extract();  
    19.         return x;  
    20.     } finally {  
    21.         // 释放锁  
    22.         lock.unlock();  
    23.     }  
    24. }  

            与put方法类似,take方法也是利用循环阻塞的方式来获取元素,如果没有元素则等待,直至获取元素为止。

            与put方法的例子类似,生产者只生产5个产品,消费完这5个产品后,消费者就不得不等待队列有元素可取:

    Java代码  收藏代码
    1. class Producer implements Runnable {  
    2.     private final ArrayBlockingQueue<Integer> queue;  
    3.     private int i;  
    4.   
    5.     Producer(ArrayBlockingQueue<Integer> q) {  
    6.         queue = q;  
    7.     }  
    8.   
    9.     public void run() {  
    10.         try {  
    11.             for (int i = 0; i < 5; i++) {  
    12.                 int p = produce();  
    13.                 queue.put(p);// 将产品放入缓冲队列  
    14.                 System.out.println("插入成功:" + p);  
    15.             }  
    16.         } catch (InterruptedException e) {  
    17.             e.printStackTrace();  
    18.         }  
    19.     }  
    20.   
    21.     int produce() {  
    22.         return i++;// 生产产品  
    23.     }  
    24. }  
    25.   
    26. class Consumer implements Runnable {  
    27.     private final ArrayBlockingQueue<Integer> queue;  
    28.   
    29.     Consumer(ArrayBlockingQueue<Integer> q) {  
    30.         queue = q;  
    31.     }  
    32.   
    33.     public void run() {  
    34.         try {  
    35.             while (true) {  
    36.                 int p = queue.take();  
    37.                 System.out.println("获取成功:" + p);  
    38.             }  
    39.         } catch (InterruptedException e) {  
    40.             e.printStackTrace();  
    41.         }  
    42.     }  
    43.   
    44.     void consume(Object x) {  
    45.         System.out.println("消费:" + x);// 消费产品  
    46.     }  
    47. }  
    48.   
    49. public class Runner {  
    50.     public static void main(String[] args) {  
    51.         ArrayBlockingQueue<Integer> q = new ArrayBlockingQueue<Integer>(10);// 或其他实现  
    52.         Producer p = new Producer(q);  
    53.         Consumer c1 = new Consumer(q);  
    54.         Consumer c2 = new Consumer(q);  
    55.         new Thread(p).start();  
    56.         new Thread(c1).start();  
    57.         new Thread(c2).start();  
    58.     }  
    59. }  
    60. //结果:  
    61. 插入成功:0  
    62. 插入成功:1  
    63. 插入成功:2  
    64. 插入成功:3  
    65. 插入成功:4  
    66. 获取成功:0  
    67. 获取成功:1  
    68. 获取成功:2  
    69. 获取成功:3  
    70. 获取成功:4  

            之后程序也是会阻塞在那里。

            5.移除元素

            1)remove方法

            remove方法从此队列中移除指定元素的单个实例(如果存在)。更确切地讲,如果此队列包含一个或多个满足 o.equals(e) 的元素 e,则移除该元素。如果此队列包含指定的元素(或者此队列由于调用而发生更改),则返回 true。 

    Java代码  收藏代码
    1. /** 
    2.  * 从此队列中移除指定元素的单个实例(如果存在) 
    3.  */  
    4. public boolean remove(Object o) {  
    5.     //判断要移除元素是否为空  
    6.     if (o == null)  
    7.         return false;  
    8.     final E[] items = this.items;  
    9.     final ReentrantLock lock = this.lock;  
    10.     //获取锁  
    11.     lock.lock();  
    12.     try {  
    13.         int i = takeIndex;  
    14.         int k = 0;  
    15.         for (;;) {  
    16.             //判断队列是否含有元素  
    17.             if (k++ >= count)  
    18.                 return false;  
    19.             //比较  
    20.             if (o.equals(items[i])) {  
    21.                 //移除  
    22.                 removeAt(i);  
    23.                 return true;  
    24.             }  
    25.             //返回i新值,以便下次循环使用  
    26.             i = inc(i);  
    27.         }  
    28.   
    29.     } finally {  
    30.         //释放锁  
    31.         lock.unlock();  
    32.     }  
    33. }  

            remove方法其中利用循环来不断判断该元素的位置,如果找到则调用 removeAt方法移除指定位置的数组元素。

            以下是一个移除的小例子:

    Java代码  收藏代码
    1. ArrayBlockingQueue<Integer> q = new ArrayBlockingQueue<Integer>(10);  
    2. // 添加10个元素  
    3. for (int i = 0; i < 10; i++) {  
    4.     q.add(i);  
    5. }  
    6. // 移除值为 1,3,5,7,9 的这五个元素  
    7. q.remove(1);  
    8. q.remove(3);  
    9. q.remove(5);  
    10. q.remove(7);  
    11. q.remove(9);  
    12. //又移除了一次 9  
    13. q.remove(9);  
    14. for (Integer i : q) {  
    15.     System.out.println(i);  
    16. }  
    17. //结果:  
    18. 0  
    19. 2  
    20. 4  
    21. 6  
    22. 8  

            从结果可以看出,从队列中正确的移除了我们指定的元素,在最后即使指定已经不存在的元素值,remove方法也之后返回false。

            2)drainTo方法

            drainTo方法用于移除此队列中所有可用的元素,并将它们添加到给定 collection 中。此操作可能比反复轮询此队列更有效。在试图向 collection c 中添加元素没有成功时,可能导致在抛出相关异常时,元素会同时在两个 collection 中出现,或者在其中一个 collection 中出现,也可能在两个 collection 中都不出现。如果试图将一个队列放入自身队列中,则会导致 IllegalArgumentException 异常。此外,如果正在进行此操作时修改指定的 collection,则此操作行为是不确定的。 

            以下是 drainTo方法的源代码:

    Java代码  收藏代码
    1. public int drainTo(Collection<? super E> c) {  
    2.     //如果指定 collection为 null 抛出异常  
    3.     if (c == null)  
    4.         throw new NullPointerException();  
    5.     //如果指定 collection 是此队列,或者此队列元素的某些属性不允许将其添加到指定 collection 抛出异常  
    6.     if (c == this)  
    7.         throw new IllegalArgumentException();  
    8.     final E[] items = this.items;  
    9.     final ReentrantLock lock = this.lock;  
    10.     //获取锁  
    11.     lock.lock();  
    12.     try {  
    13.         //take操作的位置  
    14.         int i = takeIndex;  
    15.         int n = 0;  
    16.         //元素数量  
    17.         int max = count;  
    18.         //利用循环不断取出元素添加到c中  
    19.         while (n < max) {  
    20.             c.add(items[i]);  
    21.             items[i] = null;  
    22.             i = inc(i);  
    23.             ++n;  
    24.         }  
    25.         //添加完成后初始化必要值,唤醒添加线程  
    26.         if (n > 0) {  
    27.             count = 0;  
    28.             putIndex = 0;  
    29.             takeIndex = 0;  
    30.             notFull.signalAll();  
    31.         }  
    32.         //返回添加元素数量  
    33.         return n;  
    34.     } finally {  
    35.         //释放锁  
    36.         lock.unlock();  
    37.     }  
    38. }  

            代码中并没有添加失败的相关处理,所以结果如上所说并不一定完整。以下是相关实例:

    Java代码  收藏代码
    1. ArrayBlockingQueue<Integer> q = new ArrayBlockingQueue<Integer>(10);  
    2. List<Integer> list = new ArrayList<Integer>();  
    3. // 添加10个元素  
    4. for (int i = 0; i < 10; i++) {  
    5.     q.add(i);  
    6.     list.add(i + 10);  
    7. }  
    8. //将q中的元素添加到 list中  
    9. q.drainTo(list);  
    10. for (Integer i : list) {  
    11.     System.out.println(i);  
    12. }  
    13. //结果:  
    14. 10  
    15. 11  
    16. 12  
    17. 13  
    18. 14  
    19. 15  
    20. 16  
    21. 17  
    22. 18  
    23. 19  
    24. 0  
    25. 1  
    26. 2  
    27. 3  
    28. 4  
    29. 5  
    30. 6  
    31. 7  
    32. 8  
    33. 9  

            需要值得注意的是 drainTo方法是将队列中的元素按顺序添加到指定 Collection中,别弄反了。

            drainTo(Collection<? super E> c, int maxElements)用法类似,只不过指定了移除元素数。

            3)clear方法

            移除此队列中的所有元素。在此调用返回之后,队列将为空。

    Java代码  收藏代码
    1. /** 
    2.  * 移除此队列中的所有元素。在此调用返回之后,队列将为空 
    3.  */  
    4. public void clear() {  
    5.     final E[] items = this.items;  
    6.     final ReentrantLock lock = this.lock;  
    7.     //获取锁  
    8.     lock.lock();  
    9.     try {  
    10.         int i = takeIndex;  
    11.         int k = count;  
    12.         //将数组元素置为null  
    13.         while (k-- > 0) {  
    14.             items[i] = null;  
    15.             i = inc(i);  
    16.         }  
    17.         //初始化其他参数  
    18.         count = 0;  
    19.         putIndex = 0;  
    20.         takeIndex = 0;  
    21.         //唤醒添加线程  
    22.         notFull.signalAll();  
    23.     } finally {  
    24.         //释放锁  
    25.         lock.unlock();  
    26.     }  
    27. }  

            clear方法比较简单,就是利用循环清除数组中的元素,然后将相关参数置为初始值。

            ArrayBlockingQueue还有一些其他方法,这些方法相对简单这里就不细说了。

  • 相关阅读:
    AppCan学习笔记----Request和登录功能简单实现
    漫谈:从APP崩溃率标准,到Monkey介绍拓展Maxim,及Jenkins自动化配置,持续集成获取崩溃monkey日志
    Android:adb shell 命令详解
    Android:adb命令详解
    常用获取Android崩溃日志和IOS崩溃日志的几种方法
    numpy 介绍和基础使用详解
    软件测试之路再谈(三年测试风雨)
    HTTP协议介绍
    Android Jenkins自动打包纪录
    Android专项测试监控资源
  • 原文地址:https://www.cnblogs.com/chencanjian/p/9350383.html
Copyright © 2011-2022 走看看