JDK并发容器初步认识
来源:互联网 发布:爱动体感运动机 知乎 编辑:程序博客网 时间:2024/05/22 13:08
JDK为我们提供了很好的线程安全的容器,包括以下几种:
ConcurrentHashMap:是一个高效并发的HashMap。可以理解为一个线程安全的HashMap.
CopyOnWriteArrayList:是一个并发的list,在读多写少的场合下,性能远远好于Vector.
ConcurrentLinkedQueue:高效的并发队列,使用链表实现,可以看做一个线程安全的LinkedList。
BlockingQueue:这是一个接口,JDK内部通过链表,数组等方式实现了这个接口,表示阻塞队列,适合用于作为数据的共享通道。
ConCurrentSkipListMap:跳表实现。是一个Map,利用跳表的数据结构进行快速查找。
1.实现的方式
1.1 ConcurrentHashMap
ConcurrentHashMap是减小锁粒度的一种典型实现,对于ConcurrentHashMap,它内部进一步细分了若干个小的hashMap,称之为段。缺省情况下它被进一步分为16个段。
如果需要添加一个表项。并不是将整个Map加锁,而是根据hashcode得到该表项应该被存放到哪个段中,然后对该段加锁。如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程之间便可以做到并行操作。下面来看它的put和get方法:
public V put(K key, V value) { Segment<K,V> s; if (value == null) throw new NullPointerException(); //判断是否为空 int hash = hash(key); int j = (hash >>> segmentShift) & segmentMask; if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment s = ensureSegment(j); return s.put(key, hash, value, false); }
在第5~6行,根据key,获得对应的段的序号。接着在第9行,得到段,然后插入数据。在第3~4行可知,ConcurrentHashMap和HashTable一样,都是不可以用null值作为key或者value的,而HashMap可以。get方法:
public V get(Object key) { Segment<K,V> s; // manually integrate access methods to reduce overhead HashEntry<K,V>[] tab; int h = hash(key); long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); e != null; e = e.next) { K k; if ((k = e.key) == key || (e.hash == h && key.equals(k))) return e.value; } } return null; }
get方法和put的方法一样,先根据key获得对应的段号,然后再到对应的段取值。
1.2 CopyOnWriteArrayList
在很多的应用场景,读操作可能会远远大于写操作,这时我们可以用ReadWriteLock(读写锁),读写分离锁可以有效地帮助减少锁竞争,以提升系统性能。读写锁在读读之间不阻塞,但是读写和写写之间会阻塞。但是在CopyOnWriteArrayList类中,写入也不会阻塞读取操作,只有写写之间需要进行同步等待。
实现原理:在写入操作时,list进行一次自我的复制,换句话说,当这个List需要修改时,我并不修改原有的内容,而是对原有的数据进行一次复制,将修改的内容写入副本中。写完之后再将修改完的副本替换原来的数据。这样就可以保证写操作不会影响读了。写操作代码:
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); //将原来的数据复制一遍 newElements[len] = e; //新建数组,赋入新值 setArray(newElements); //将新数组赋入 return true; } finally { lock.unlock(); } }
通过代码可知,在多线程的情况下,写线程并不会影响读线程,因为array变量是volatile类型的。
1.3 ConcurrentLinkedQueue
ConcurrentLinkedQueue是在高并发环境中性能最好的队列,之所它的性能高,是因为它内部都是无锁操作,使用了CAS(比较交换)。这样就可以减少对锁的申请等操作,释放了系统的资源。它的节点定义如下:
volatile E item;volatile Node<E> next;
item表示目标元素,next表示当前node的下一个元素。在对node操作时,使用了CAS操作
boolean casItem(E cmp, E val) { return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val); }void lazySetNext(Node<E> val) { UNSAFE.putOrderedObject(this, nextOffset, val); }boolean casNext(Node<E> cmp, Node<E> val) { return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); }
方法casItem()表示设置当前Node的item值,它需要两个参数,第一个参数为期望值,第二个参数设置目标值。当当前值等于cmp期望值时,就会将目标设置为val。同样casNext()也是类似,用来设置next字段。
ConcurrentLinkedQueue有两个重要的字段:head和tail。分别表示链表的头部和尾部。对于head来说永远不会为null。一般来说我们期望tail总是为链表的末尾,但实际上tail的更新并不是及时的,会产生拖延现象。tail在插入后会滞后,并且每次更新会跳跃两个元素。原因是什么呢?插入代码如下:
public boolean offer(E e) { checkNotNull(e); final Node<E> newNode = new Node<E>(e); for (Node<E> t = tail, p = t;;) { Node<E> q = p.next; if (q == null) { //CAS操作 if (p.casNext(null, newNode)) { if (p != t) //CAS操作 casTail(t, newNode); return true; } } else if (p == q) //处理哨兵(自己指向自己的节点)节点,自己指向自己的节点 p = (t != (t = tail)) ? t : head; else //获取p.next的值然后赋p p = (p != t && t != (t = tail)) ? t : q; } }
通过上述代码可知不使用锁而单纯的使用CAS会要求在应用层面保证线程安全,到那时程序设计的难度会大大增加。
1.4 BlockingQueue
BlockingQueue是一个接口,是数据共享的通道。它的实现类有如下:ArrayBlockingQueue,DealyedWorkQueue,DelayQueue,LinkedBlockingQueue,PriorityBlockingQueue,SynchronousQueue,BlockingDeque。
那么BlockingQueue是怎么实现数据共享的呢,我们以ArrayBlockingQueue来一探究竟。向队列压入元素可以使用offer()方法和put方法。对于offer()方法,如果当前队列已经满了,它就会立即返回false,put方法也是将元素压入队列,但如果队列满了,他会一直等待,直到队列中有空闲的位置。从队列中弹出元素可以使用poll()方法和take()方法。不同之处在于:如果队列为空poll()方法会返回null,而take()方法会等待,直到队列中有可用的元素。
为了做好等待和通知两件事,定义了以下字段:
final ReentrantLock lock; private final Condition notEmpty; private final Condition notFull;
当执行take()操作时,如果队列为空则让当前线程等待在notEmpty上。新元素入队时,则进行异常notEmpty上的通知。take()过程:
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) notEmpty.await(); return extract(); } finally { lock.unlock(); } }
如果队列为空,在第6行时会让当前线程等待。当有元素插入时线程会得到一个通知,insert代码:
private void insert(E x) { items[putIndex] = x; putIndex = inc(putIndex); ++count; notEmpty.signal(); }
在第5行操作会通知等待在notEmpty上的线程。
同理,对于put操作也是一样,当队列满时,需要压入线程等待,put代码:
public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) notFull.await(); insert(e); } finally { lock.unlock(); } }
如果队列已满,在第7行会让当前线程等待。在一个元素被移动时会通知等待插入的线程。extract代码:
private E extract() { final Object[] items = this.items; E x = this.<E>cast(items[takeIndex]); items[takeIndex] = null; takeIndex = inc(takeIndex); --count; notFull.signal(); return x; }
在第7行通知等待在notFull上的线程。
- JDK并发容器初步认识
- JDK并发容器
- JDK容器与并发—并发
- java并发包:jdk并发容器
- JDK容器与并发—JDK容器框架
- JDK容器与并发—数据结构
- JDK容器与并发—List
- JDK容器与并发—Map
- JDK容器与并发—Queue
- JDK容器并发IO经典blog
- java并发包学习系列:jdk并发容器
- 《Java高并发程序设计》学习 --3.3 JDK的并发容器
- java并发程序设计总结七:jdk的并发容器
- 初步认识迭代服务器和并发服务器
- 处理大并发之三 对libevent的初步认识
- 处理大并发之三 对libevent的初步认识
- 处理大并发之三 对libevent的初步认识
- 初步认识迭代服务器和并发服务器
- Jsp 综述(上)
- 神坑之java、安卓从Uri获取文件路径错误
- codeforces round 377 div2 F Tourist Reform tarjan求边双连通分量
- Hadoop作业提交多种方案具体流程详解
- 清华EMBA课程系列思考之十七(1) -- 新企业的孵化与创业投资
- JDK并发容器初步认识
- [SDOI2008]沙拉公主的困惑 线性筛 素数+欧拉
- PopupWindow+ListView并实现点击事件
- Linux内核移植 part3:串口驱动
- MyBatis的集合查询
- 冒泡排序
- c#数据结构之最大子数组问题(分治法)
- eclipse设置背景色为豆沙绿
- 学习Redis从这里开始