关于ava容器、队列,知识点总结

推荐《java 并发编程实战》这本书,中文的翻译有些差(哈哈,并发确实难,不好翻译),适合有并发经验的人来读。

这篇短文,整理了容器的知识点,对容器的使用场景,容器的原理等有个整体的认知!

 

1. 层次构造

看看下面的Collection层次构造图:

 (这张图为突出某些重点,对层次上的东西进行了取舍,此外类继承的多个接口,也没有表示出来)

FIFO:first-in-first-out

bounded:有界队列

doubly-linked:双向链表

natural ordering:自然顺序

上图中,我们常用的集合基本都罗列了。一些需要注意到的东西也在上面列举了,希望大家好好看看。在类或接口的右上方,有些注释,标明了它的一次特点或是原理。

接下来看看Map的部分:

我们再看一下面这张图,阐述了容器实现的方式:

 

2. 迭代器

 ArrayList、LinkedList等容器的实现都不是同步的。在多线程的情况下,必须进行额外的同步操作。一种方式就是,加上同步器,同步器的锁就是list。另外一种方式,在容器创建的时候,用Collections中的方法对容器进行包装,示例如下:

1 List list = Collections.synchronizedList(new ArrayList());            // 包装之后,就是线程安全的。
2       ...
3   synchronized (list) {                   // 不加锁的话,迭代器会出现ConcurrentModificationException
// 这里呢,还有一种可替代方案:克隆容器,然后在副本上进行迭代
4 Iterator i = list.iterator(); // Must be in synchronized block 5 while (i.hasNext()) 6 foo(i.next()); 7 }

 

1 Map m = Collections.synchronizedMap(new HashMap());
2       ...
3   Set s = m.keySet();  // Needn't be in synchronized block
4       ...
5   synchronized (m) {  // Synchronizing on m, not s!
6       Iterator i = s.iterator(); // Must be in synchronized block
7       while (i.hasNext())
8           foo(i.next());
9   }

Collections中还有其他实用的方法,请查看java api)

 

3. 并发容器

ConcurrentHashMap

使用分段锁的机制,允许任意数量的读取线程并发访问Map,并且一定数量的写入线程可以并发地修改Map。它带来的结果是,在并发访问环境下将实现更好的吞吐量,而在单线程环境中只损失非常小的性能。ConcurrentHashMap和其他并发容器不会抛出ConcurrentModificationException,因此不需要再迭代过程中对容器加锁。ConcurrentHashMap返回的迭代器具有弱一致性,而并非“及时失败”。弱一致性的迭代器可以容忍并发的修改。

(弱一致性该如何理解呢?数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。举例说明,可能你期望往ConcurrentHashMap中加入一个元素后,立马能对get可见,但ConcurrentHashMap并不能如你所愿。换句话说,put操作将一个元素加入后,get可能在某段时间内还看不到这个元素)

对于一些需要在整个Map上进行计算的方法,如size和isEmpty,这些方法的语义被略微减弱了以反映容器的并发特性。

额外的原子Map操作:

一些常见的复合操作,例如“若没有则添加”、“若相等则移除”、“若相等则替换”等,这些操作都是原子操作,不需要加锁。

1 computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)
2 computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)
3 putIfAbsent(K key, V value)
4 remove(Object key, Object value)
5 replace(K key, V value)

 

CopyOnWriteArrayList

用于替代同步List,在某些情况下提供了更好的并发性能,并且在迭代期间不需要对容器进行加锁或是复制。Copy-On-Write的安全性在于每次修改时,都会创建并重新发布一个新的容器的副本,从而实现可变性。但是这需要一定的开销,特别是当容器的规模较大时。仅当迭代操作远多于修改操作时,才应该使用“写入时复制”容器。

 

4. 链表

分为单向链表(singly-linked list)和双向链表(doubly-linked list)。

首先说一下链表(linked list)和数组(Array)的区别:

①Array是静态分配内存,不能动态扩展。在插入和删除方面,开销很大,但随机访问性强,查找速度快;linked list是动态分配内存,nodes数量可以按需求增加或减少,因此,处理未知数量的对象时,应该使用linked list。虽然,它插入删除速度快,但不能随机查找。

②链表结构区别

 singly-linked list:

doubly-linked list:

链表跟内存相比会占用更多内存,我们需花费额外的4bytes(32bitCPU中)内存来存储每个reference。

 ③doubly-linked list在查找和删除时利用了二分法的思想去实现,效率大大提高,但singly-linked list的应用更广泛。原因在于存储效率方面。

每个doubly-linked list的node结构比singly-linked list的多了一个指针,占用更多空间,这时设计者会以时间来换取空间,达到工程总体的平衡。

 

参考资料:https://www.cs.cmu.edu/~adamchik/15-121/lectures/Linked%20Lists/linked%20lists.html

 

5. SynchronousQueue

它有两个操作,put()和take(),这两个操作都是阻塞的。比如:当我们执行put()和,会一直阻塞到其他线程执行take()为止。队列的内部没有任何的存储容量(一个都没有),所以它不能插入数据,也不能移除数据,还不能进行迭代。newCachedThreadPool的实现,队列的部分就是使用了SysnchronousQueue。(newCachedThreadPool的特点,接收到一个新的任务就直接开启一个新的线程来执行)

1 public static ExecutorService newCachedThreadPool() {
2         return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                    // 0代表corePoolSize   Integer.MAX_VALUE代表maximumPoolSize
3                                       60L, TimeUnit.SECONDS,
4                                       new SynchronousQueue<Runnable>());
5     }

我们先一个用公共变量来处理传递:

 1 public class Demo002 {
 2 
 3     public static void main(String args[]) throws InterruptedException {
 4         ExecutorService executor = Executors.newFixedThreadPool(2);
 5         AtomicInteger sharedState = new AtomicInteger();
 6         CountDownLatch countDownLatch = new CountDownLatch(1);
 7         
 8         Runnable producer = () -> {
 9             Integer producedElement = ThreadLocalRandom
10               .current()
11               .nextInt();
12             sharedState.set(producedElement);
13             countDownLatch.countDown();
14         };
15         
16         Runnable consumer = () -> {
17             try {
18                 countDownLatch.await();
19                 Integer consumedElement = sharedState.get();
20             } catch (InterruptedException ex) {
21                 ex.printStackTrace();
22             }
23         };
24         
25         executor.execute(producer);
26         executor.execute(consumer);
27          
28         executor.awaitTermination(500, TimeUnit.MILLISECONDS);
29         executor.shutdown();
30         System.out.println(countDownLatch.getCount());
31         
32     }
33 }

再用SysnchronousQueue处理传递:

 1 public class Demo003 {
 2 
 3     public static void main(String args[]) throws InterruptedException {
 4         ExecutorService executor = Executors.newFixedThreadPool(2);
 5         SynchronousQueue<Integer> queue = new SynchronousQueue<>();
 6         
 7         Runnable producer = () -> {
 8             Integer producedElement = ThreadLocalRandom
 9               .current()
10               .nextInt();
11             try {
12                 queue.put(producedElement);
13             } catch (InterruptedException e) {
14                 // TODO Auto-generated catch block
15                 e.printStackTrace();
16             }
17         };
18         
19         Runnable consumer = () -> {
20             try {
21                 Integer consumedElement = queue.take();
22             } catch (InterruptedException ex) {
23                 ex.printStackTrace();
24             }
25         };
26         
27         executor.execute(producer);
28         executor.execute(consumer);
29          
30         executor.awaitTermination(500, TimeUnit.MILLISECONDS);
31         executor.shutdown();
32         System.out.println(queue.size());
33         
34     }
35 }

至此,我们看到了它的构造,用例子阐述了它的使用方法,SynchronousQueue服务于一个交换点,这个交换点用于生产者和消费者的协作。

 

posted @ 2019-01-31 22:42  Mr.袋鼠  阅读(359)  评论(0编辑  收藏  举报