高并发编程之并发容器

  之前简单学习了jvm提供的synchronized和JDK提供的ReentrantLock,本文主要学习并发容器。

   首先来看一个简单的业务场景:有1000张火车票,每张火车票都有一个编号,同时有10个窗口对外售票。

  首先来看下面这种写法是否可以实现:

 1 /**
 2  * 有1000张火车票,每张火车票都有一个编号,同时有10个窗口对外售票。
 3  * @author Wuyouxin
 4  *
 5  */
 6 public class TicketSeller1 {
 7 
 8     static List<String> tickets = new ArrayList<String>();
 9     
10     static {
11         for (int i = 0; i < 1000; i++) {
12             tickets.add("座位:" + i);
13         }
14     }
15     
16     public static void main(String[] args) {
17         //10个窗口
18         for (int i = 0; i < 10; i++) {
19             new Thread(new Runnable() {
20                 
21                 @Override
22                 public void run() {
23                     while (tickets.size() > 0){
24                         System.out.println(tickets.remove(0));
25                     }
26                     
27                 }
28             }, "窗口" + i).start();
29         }
30     }
31 }

  上面方式可以实现上面的需求么?其实,不可以,因为上面代码时一个无锁状态,当一个线程判断tickets.size()> 0 之后被其他线程获取抢先进入将tickets中的元素删除,则第一个线程则会出现异常,有可能会出现重复删除,也有可能会出现无元素可以删除。

  那么我们来看下面的代码是否可以删除。

 1 /**
 2  * 有1000张火车票,每张火车票都有一个编号,同时有10个窗口对外售票。
 3  * @author Wuyouxin
 4  *
 5  */
 6 public class TicketSeller2 {
 7 
 8     static Vector<String> tickets = new Vector<String>();
 9     
10     static {
11         for (int i = 0; i < 1000; i++) {
12             tickets.add("座位:" + i);
13         }
14     }
15     
16     public static void main(String[] args) {
17         //10个窗口
18         for (int i = 0; i < 10; i++) {
19             new Thread(new Runnable() {
20                 
21                 @Override
22                 public void run() {
23                     while (tickets.size() > 0){
24                         System.out.println(tickets.remove(0));
25                     }
26                     
27                 }
28             }, "窗口" + i).start();
29         }
30     }
31 }

  我们对代码进行改造,将第一个代码中的list换成了Vector,大家都知道Vector集合呢是一个线程安全的集合,它的所有操作都是原子性的,那么这样做,是否可以实现上述需求呢?其实不然,虽然我们使用了Vector集合,它的方法都是原子性的,但是在tickets.size()方法和tickets.remove(0)方法中间是没有原子性的,其实第一个代码的问题还是会出现。

  继续对代码进行改进:

 1 /**
 2  * 有1000张火车票,每张火车票都有一个编号,同时有10个窗口对外售票。
 3  * @author Wuyouxin
 4  *
 5  */
 6 public class TicketSeller3 {
 7 
 8     static List<String> tickets = new ArrayList<String>();
 9     
10     static {
11         for (int i = 0; i < 1000; i++) {
12             tickets.add("座位:" + i);
13         }
14     }
15     
16     public static void main(String[] args) {
17         //10个窗口
18         for (int i = 0; i < 10; i++) {
19             new Thread(new Runnable() {
20                 
21                 @Override
22                 public void run() {
23                     while (true){
24                         //将存放票的集合上锁
25                         synchronized(tickets){
26                             if (tickets.size() <= 0){
27                                 break;
28                             }
29                             System.out.println(tickets.remove(0));
30                         }
31                     }
32                     
33                 }
34             }, "窗口" + i).start();
35         }
36     }
37 }

  这次,我们在进行判断时将tickets上锁,让其他线程无法获取,这样确实可以实现上面的需求,但是这样效率不高。

  继续将代码进行优化:

 1 /**
 2  * 有1000张火车票,每张火车票都有一个编号,同时有10个窗口对外售票。
 3  * @author Wuyouxin
 4  *
 5  */
 6 public class TicketSeller4 {
 7 
 8     static Queue<String> tickets = new ConcurrentLinkedQueue<String>();
 9     
10     static {
11         for (int i = 0; i < 1000; i++) {
12             tickets.add("座位:" + i);
13         }
14     }
15     
16     public static void main(String[] args) {
17         //10个窗口
18         for (int i = 0; i < 10; i++) {
19             new Thread(new Runnable() {
20                 
21                 @Override
22                 public void run() {
23                     while (true){
24                         String s = tickets.poll();
25                         if (s == null) break;
26                         System.out.println(s);
27                     }
28                     
29                 }
30             }, "窗口" + i).start();
31         }
32     }
33 }

  在这里我也没有使用锁,但是我使用了Queue队列,每次将队首的元素拿出来再去判断,这样就不会出现多拿或者拿多的情况了。

  下面开始本文的核心,并发容器。

一、Map

  在日常的开发过程中,Map集合使用率算是比较高的容器了,像hashMap(hash结构),treeMap(tree结构),linkedHashMap(hash结构,加双向链表)等等,但是上述两种容器都不是并发容器,其中的方法都不能保证原子性,而在高并发编程中,我们使用更多的是hashTable(hash结构),Collection.synchronizedMap(将非线程安全的容器变为线程安全的容器),concurrentHashMap(hash结构),concurrentSkiplistMap(跳表结构)。

  当然在选用时也要根据业务场景去选择:

    非并发业务不需要排序:hashMap

    非并发业务需要排序:treeMap,linkedHashMap

    并发业务量少不需要排序:hashTable,Collection.synchronizedMap

    并发业务量大不需要排序:concurrentHashMap

    并发业务量大需要排序:concurrentSkiplistMap(https://blog.csdn.net/sunxianghuang/article/details/52221913

  我也简单测试了一下性能,发现concurrentHashMap的性能确实要比hashTable性能要高这时因为在hashTable在操作时会将整个容器上锁,而concurrentHashMap操作时只会将hash表中下标位置上锁。

 

 1 /**
 2  * 效率测试
 3  * @author Wuyouxin
 4  *
 5  */
 6 public class ConcurrentMaP {
 7     public static void main(String[] args) {
 8         //final Map<String, String> map = new Hashtable<String, String>();
 9         //final Map<String, String> map = new ConcurrentHashMap<String, String>();
10         //final Map<String, String> map = new ConcurrentSkipListMap<String, String>();
11         final Map<String, String> map = new HashMap<String, String>();
12         final Random r = new Random();
13         Thread [] t = new Thread[100];
14         final CountDownLatch c = new CountDownLatch(t.length);
15         long start = System.currentTimeMillis();
16         for (int i = 0; i < t.length; i++) {
17             new Thread(new Runnable() {
18                 
19                 @Override
20                 public void run() {
21                     for (int j = 0; j < 10000; j++) {
22                         map.put(Thread.currentThread().getName() + j, "a" + r.nextInt(10000));
23                     }
24                     c.countDown();
25                 }
26             }, "线程" + i).start();
27         }
28         try {
29             c.await();
30         } catch (InterruptedException e) {
31             e.printStackTrace();
32         }
33         long end = System.currentTimeMillis();
34         System.out.println(end - start);
35     }
36 }

 

 

 

二、List

  在开发过程中,list也时比较常用的容器了,我们使用比较多的时arrayList,linkedList但是它们都是线程不安全的,在高并发编程中我们使用更多的是Vector,Collection.synchronizedList,和CopyOnWritList(写时复制容器,插入时复制新的List装新的集合,所以不会有线程问题),但是也是要根据业务场景去使用。

  无并发问题,读多:arrayList

  无并发问题,写多:linkedList

  有并发问题,读多写少:CopyOnWritList

  有并发问题,写多:Vector,Collection.synchronizedList

 

  我也简单测试了一下性能:

 

 1 public class Top2_ConcurrentList {
 2     public static void main(String[] args) {
 3         //final Vector<String> list = new Vector<String>();
 4         final List<String> list = new CopyOnWriteArrayList<String>();
 5         Thread [] t = new Thread[100];
 6         final CountDownLatch c = new CountDownLatch(t.length);
 7         long start = System.currentTimeMillis();
 8         for (int i = 0; i < t.length; i++) {
 9             new Thread(new Runnable() {
10                 
11                 @Override
12                 public void run() {
13                     for (int j = 0; j < 10000; j++) {
14                         list.add(Thread.currentThread().getName() + j);
15                     }
16                     c.countDown();
17                 }
18             }, "线程" + i).start();
19         }
20         try {
21             c.await();
22         } catch (InterruptedException e) {
23             e.printStackTrace();
24         }
25         long end = System.currentTimeMillis();
26         System.out.println(end - start);
27     }
28 
29 }

 

三、ConcurrentLinkedQueue队列(内部加锁)

  队列,在并发编程中使用的频率时非常高的,ConcurrentLinkedQueue为单向链表结构,这个队列也是一个无界队列。下面,简单介绍一下ConcurrentLinkedQueue的一些方法。

 1 public class Top3_ConcurrentQueue {
 2     public static void main(String[] args) {
 3         Queue<String> queue = new ConcurrentLinkedQueue<String>();
 4         for (int i = 0; i < 10; i++) {
 5             //等价与add,但是offer有返回值,表示是否新增成功
 6             queue.offer(""+ i );
 7         }
 8         System.out.println(queue);
 9         System.out.println(queue.size());
10         //删除头
11         System.out.println(queue.poll());
12         System.out.println(queue.size());
13         //拿出头,但是不删除
14         System.out.println(queue.peek());
15         System.out.println(queue.size());
16     }
17 }

  当然,还有一种队列,叫做ConcurrentLinkedDeque,这个队列为双向队列,其结构为双向链表。其中方法跟上面单项链表大致相同,只不过有从头加数据或者从尾加数据,删除也一样。

 

四、BlockingQueue队列(阻塞式队列)

  BlockingQueue为阻塞式队列,当队列中没值时,消费者自动等待,当队列满时,生产者自动等待。

  下面看一个生产者消费者模式的简单的例子:

 1 public class Top4_LinkedBlockingQueue {
 2 
 3     static BlockingQueue<String> queue = new LinkedBlockingQueue<String>();
 4     
 5     public static void main(String[] args) {
 6         new Thread(new Runnable() {
 7             
 8             @Override
 9             public void run() {
10                 for (int i = 0; i < 100; i++) {
11                     try {
12                         //向队列中加元素,如果队列满了则自动等待
13                         queue.put("a" + i);
14                     } catch (InterruptedException e) {
15                         e.printStackTrace();
16                     }
17                 }
18             }
19         }, "p1").start();
20         
21         for (int i = 0; i < 5; i++) {
22             new Thread(new Runnable() {
23                 
24                 @Override
25                 public void run() {
26                     while(true){
27                         try {
28                             //如果队列空了,则自动等待
29                             System.out.println(Thread.currentThread().getName() + 
30                                     "take-" + queue.take());
31                         } catch (InterruptedException e) {
32                             e.printStackTrace();
33                         }
34                     }
35                 }
36             }, "c" + i).start();
37         }
38     }
39 }

  上面的LinkedBlockingQueue队列为无界队列,还有一种叫做ArrayBlockingQueue为有界队列,如果队列满了,这时使用add会抛出异常,如果队列满了,使用的时offer方法的话会有一个boolean返回值,这时会返回false,offer还有另一种用法,如果1秒内无法加入的话就不添加,还有一个put方法,当队列满了之后,会阻塞线程。

 

 1 public class Top5_ArrayBlockingQueue {
 2 
 3     static BlockingQueue<String> queue = new ArrayBlockingQueue<String>(10);
 4     public static void main(String[] args) {
 5         for (int i = 0; i < 10; i++) {
 6             queue.offer("a" + i);
 7         }
 8         
 9         //如果队列满了,这时使用add会抛出异常
10         //queue.add("aaa");
11         
12         //如果队列满了,使用的时offer方法的话会有一个boolean返回值,这时会返回false
13         queue.offer("aaa");
14         
15         try {
16             //put方法当队列满了之后会阻塞线程
17             queue.put("aaa");
18         } catch (InterruptedException e1) {
19             // TODO Auto-generated catch block
20             e1.printStackTrace();
21         }
22         try {
23             //这个是offer的另一种用法,如果1秒内无法加入的话就不添加
24             queue.offer("aaa", 1, TimeUnit.SECONDS);
25         } catch (InterruptedException e) {
26             // TODO Auto-generated catch block
27             e.printStackTrace();
28         }
29     }
30 }

 

 

五、DelayQueue队列

  这个队列比较特殊,加入它的元素都必须实现Delayed接口,而且在往里面put数据时需要加入时间,表示多久后可以被拿出,而且在其中默认会自动按照时间排好顺序,这个队列可以用于定时任务执行。

 1 public class Top6_DelayQueue {
 2 
 3     static BlockingQueue<MyTask> tasks = new DelayQueue<MyTask>();
 4     
 5     static class MyTask implements Delayed {
 6         
 7         long runningTime;
 8         
 9         MyTask(long rt) {
10             this.runningTime = rt;
11         }
12 
13         @Override
14         public int compareTo(Delayed o) {
15             if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS)) {
16                 return -1;
17             } else if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) {
18                 return 1;
19             } else {
20                 return 0;
21             }
22         }
23 
24         //加入元素时到现在过了多久
25         @Override
26         public long getDelay(TimeUnit unit) {
27             return unit.convert(runningTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
28         }
29         
30     }
31     
32     public static void main(String[] args) {
33         long now = System.currentTimeMillis();
34         MyTask t1 = new MyTask(now + 1000);
35         MyTask t2 = new MyTask(now + 2000);
36         MyTask t3 = new MyTask(now + 1500);
37         MyTask t4 = new MyTask(now + 2500);
38         MyTask t5 = new MyTask(now + 500);
39         
40         try {
41             tasks.put(t1);
42             tasks.put(t2);
43             tasks.put(t3);
44             tasks.put(t4);
45             tasks.put(t5);
46         } catch (InterruptedException e) {
47             e.printStackTrace();
48         }
49         
50         System.out.println(tasks);
51         for (int i = 0; i < 5; i++) {
52             try {
53                 System.out.println(tasks.take());
54             } catch (InterruptedException e) {
55                 e.printStackTrace();
56             }
57         }
58     }
59     
60 }

 

六、TransferQueue队列

  TransferQueue队列也比较特殊,他有自己的transfer方法往队列中加数据,如果发现有消费者处于空闲状态,则直接给消费者,如果消费者都处于忙碌状态,则加入队列。但是如果此时没有消费者,则会阻塞线程。

 1 public class Top7_TransferQueue {
 2 
 3     public static void main(String[] args) {
 4         LinkedTransferQueue<String> queue = new LinkedTransferQueue<String>();
 5         
 6         new Thread(new Runnable() {
 7             
 8             @Override
 9             public void run() {
10                 try {
11                     System.out.println(queue.take());
12                 } catch (Exception e) {
13                     e.printStackTrace();
14                 }
15                 
16             }
17         }).start();
18         //首先查看是否有空闲的消费者,如果有则直接交给消费者
19         queue.transfer("aaa");
20     }
21 }

 

六、SynchronizedQueue队列

  SynchronizedQueue队列时一种特殊的TransferQueue队列,他的队列容量为0,直接等待消费者消费。

  

  本文简单介绍了并发编程中用到的一些容器,但是具体使用要根据业务情况进行选择使用,不可盲目使用。

 

posted on 2018-04-22 21:23  Herrt灬凌夜  阅读(595)  评论(0编辑  收藏  举报

导航