并发集合
数据结构是编程中的基本元素,几乎每个程序都使用一种或多种数据结构来存储和管理数据。java api提供了包含接口、类和算法的java集合框架,它实现了可用在程序中的大量数据结构。
当需要在并发程序中使用数据集合时,必须要谨慎地选择相应的实现方式。大多数集合类不能直接用于并发应用,因为它们没有对本身数据的并发访问进行控制。
如果一些并发任务共享了一个不适用于并发任务的数据结构,将会遇到数据不一致的错误,并将影响程序的准确运行。这类数据结构的一个例子是ArrayList类。
java提供了一些可以用于并发程序中的数据集合,它们不会引起任何问题。一般来说,java提供了两类适用于并发应用的集合:
1.阻塞式集合(Blocking Collection):这类集合包括添加和移除数据的方法。当集合已满或者为空时,被调用的添加或移除方法就不能立即被执行,那么调用这个方法的线程将被阻塞,一直到该方法可以被成功执行。
2.非阻塞式集合(Non-Blocking Collection):这类集合也包括添加和移除数据的方法。如果方法不能立即被执行,则返回null或者抛出异常,但是调用这个方法的线程不会被阻塞。
并发应用中常用的java集合类:
1.非阻塞式列表对应的实现类:ConcurrentLinkedDeque类
2.阻塞式列表对应的实现类:LinkedBlockingDeque类
3.用于数据生成或消费的阻塞式列表对应的实现类:LinkedTransferQueue类
4.按优先级排序列表元素的阻塞式列表对应的实现类:PriorityBlockingQueue类
5.带有延迟列表元素的阻塞式列表对应的实现类:DelayQueue类
6.非阻塞式可遍历映射对应的实现类:ConcurrentSkipListMap类
7.随机数字对应的实现类:ThreadLocalRandom类
8.原子变量对应的实现类:AtomicLong和AtomicIntegerArray类
一、ConcurrentLinkedDeque类提供的常用方法:
1.getFirst()和getLast():分别返回列表中的第一个和最后一个元素,返回的元素不会从列表中移除。如果列表为空,这两个方法抛出NoSuchElementExcpetion异常。
2.peek()、peekFirst()和peekLast():分别返回列表中第一个和最后一个元素,返回的元素不会从列表中移除。如果列表为空,这些方法返回null。
3.remove(), removeFirst(), removeLast():这些方法返回列表的第一个和最后一个元素。他们从列表中移除返回的元素。如果列表是空的,这些方法抛出一个 NoSuchElementException例外。
4.pollFirst()和pollLast():pollFirst()方法返回和删除列表的第一个元素和pollLast()方法返回和删除最后一个元素的列表。如果列表为空,这些方法返回一个null值。
5.size():该方法返回的值可能不是真实的,尤其当有线程在添数据或者移除数据时。这个方法需要遍历整个列表来计算元素数量,而遍历过的数据可能已经改变。仅当没有任何线程修改列表时,才能保证返回的结果是准确的。
二、LinkedBlockingDeque类提供的常用方法:
1.takeFirst和takeLast():分别返回列表中第一个和最后一个元素,返回的元素会从列表中移除。如果列表为空,调用方法的线程将被阻塞直到列表中有可用的元素出现。
2.getFirst()和getLast():分别返回列表中第一个和最后一个元素,返回的元素不会从列表中移除。如果列表为空,则抛出NoSuchElementException异常。
3.peek()、peekFirst()和peekLast():分别返回列表中第一个和最后一个元素,返回的元素不会从列表中移除。如果列表为空,返回null。
4.poll()、pollFirst()和pollLast():分别返回列表中第一个和最后一个元素,返回的元素将会从列表中移除。如果列表为空,返回null。
5.add()、addFirst()和addLast():分别将元素 添加到列表中第一位和最后一位。如果列表已经满了,这些方法将抛出ILLegalStateException异常。
三、PriorityBlockingQueue类
数据结构中的一个经典需求是实现一个有序列表。java引用了PriorityBlockingQueue类来满足这类需求。
所有添加进PriorityBlockingQueue的元素必须实现Comparable接口。这个接口提供了compareTo()方法,它的传入参数是一个同类型的对象。这样就有了两个类型的对象并且相互比较:其中一个是执行这个方法的对象,另一个是参数传入的对象。这个方法必须返回一个数字值,如果当前对象小于参数传入的对象,那么返回一个小于0的值;如果当前对象大于参数传入的对象,那么返回一个大于0的值;如果两个对象相等就是返回0.
当插入元素时,PriorityBlockingQueue使用compareTo()方法来决定插入元素的位置。元素越大越靠后。
PriorityBlockingQueue的另一个重要的特性是:它是阻塞式数据结构。当它的方法被调用并且不能立即执行时,调用这个方法的线程将被阻塞直到方法执行成功。
PriorityBlockingQueue类提供的常用方法:
1.clear():移除队列中的所有元素
2.take():返回队列中的第一个元素并将其移除。如果队列为空,线程阻塞直到队列中有可用的元素。
3.put(E e):E是PriorityBlockingQueue的泛型参数,表示传入参数的类型。这个方法把参数对应的元素插入到队列中。
4.peek():返回队列中的第一个元素,但不将其移除。
四、DelayQueue类
java api提供了一种用于并发应用的有趣的数据结构,即DelayQueue类。这个类可以存放带有激活日期的元素。当调用方法从队列中返回或提取元素时,未来的元素日期将被忽略。这些元素对于这些方法是不可见的。
为了具有条用行为,存放到DelayQueue类中的元素必须继承Delayed接口。delayed接口使对象成为延迟对象,它使存放在DelayQueue类中的对象具有了激活日期,即到激活日期的时间。该接口强制执行下列两个方法:
1.compareTo(Delayed o):Delayed接口继承了Comparable接口,因此有了这个方法。如果当前对象的延迟值小于参数对象的值,将返回一个小于0的值;如果当前对象的延迟值大于参数对象的值,将返回一个大于0的值;如果两者的延迟值相等则返回0.
2.getDelay(TimeUnit unit):这个方法返回到激活日期的剩余时间,单位由单位参数指定。
DelayQueue类提供的常用方法:
1.clear():移除队列中的所有元素。
2.offer(E e):E是DelayQueue的泛型参数,表示传入参数的类型。这个方法把参数对应的元素插入到队列中。
3.peek():返回队列中的第一个元素,但不将其移除。
4.take():返回队列中的第一个元素,并将其移除。如果队列为空,线程将被阻塞直到队列中有可用的元素。
五、ConcurrentSkipListMap类
java api提供了一种用于并发应用程序中的有趣数据结构,即ConcurrentNavigableMap接口及其实现类。实现这个接口的类以如下两部分存放元素:
1.一个键值(Key),它是元素的标识并且是唯一的。
2.元素其他部分数据。
每一个组成部分必须在不同的类中实现。
java api也提供了一个实现ConcurrentSkipListMap接口的类,ConcurrentSkipListMap接口实现了与ConcurrentNavigableMap接口有相同行为的一个非阻塞式列表。从内部实现机制来讲,它使用了一个Skip List来存放数据。Skip List是基于并发列表的数据结构,效率与二叉树相近。
当插入元素到映射中时,ConcurrentSkipListMap接口类使用键值来排序所有元素。除了提供返回一个具体元素的方法外,这个类也提供获取子映射的方法。
ConcurrentSkipListMap类提供的常用方法:
1.headMap(K toKey):K是在ConcurrentSkipListMap对象的 泛型参数里用到的键。这个方法返回映射中所有键值小于参数值toKey的子映射。
2.tailMap(K fromKey):K是在ConcurrentSkipListMap对象的 泛型参数里用到的键。这个方法返回映射中所有键值大于参数值fromKey的子映射。
3.putIfAbsent(K key,V value):如果映射中不存在键key,那么就将key和value保存到映射中。
4.pollLastEntry():返回并移除映射中的最后一个Map.Entry对象。
5.replace(K key,V value):如果映射中已经存在键key,则用参数中的value替换现有的值。
六、ThreadLocalRandom类
java api提供了一个特殊类用以在并发程序中生成伪随机数,即ThreadLocalRandom类。它是线程本地变量。每个生成随机数的线程都有一个不同的生成器,但是都在同一个类中被管理,对程序员来讲是透明的。
相比于使用共享的Random对象为所有线程生成随机数,这种机制具有更好的性能。
七、AtomicLong类
原子变量(Atomic Variable)是从java5开始引入的,它提供了单个变量上的原子操作。在编译程序时,java代码中的每个变量、每个操作都将被转换成机器可以理解的指令。
例如,当给一个变量赋值时,在java代码中只使用一个指令,但是编译这个程序时,指令被转换成JVM语言中的不同指令。当多个线程共享同一个变量时,就会发生数据不一致的错误。
为了避免这类错误,java引入了原子变量。当一个线程在对原子变量操作时,如果其他线程也试图对同一原子变量执行操作,原子变量的实现类提供了一套机制来检查操作是否在一步内完成。一般来说,这个操作先获取变量值,然后在本地改变变量的值,然后试图用这个改变的值去替换之前的值。如果之前的值没有被其他线程改变,就可以执行这个替换操作。否则,方法将再次执行这个操作。这种操作称为CAS原子操作。
原子变量不使用锁或其他同步机制来保护对其值的并发访问。所有操作都是基于CAS原子操作的。它保证了多线程在同一时间操作一个原子变量而不会产生数据不一致的错误,并且它的性能优于使用同步机制保护的普通变量。
八、AtomicIntegerArray类
当实现一个并发应用时,将不可避免地会有多线程共享一个或多个对象的现象,为了避免数据不一致错误,需要使用同步机制来保护对这些共享属性的访问。但是这些同步机制存在下列问题。
1.死锁:一个线程被阻塞,并且试图获得的锁正被其他线程使用,但其他线程永远不会释放这个锁。这种情况使得应用不会继续执行,并且永远不会结束。
2.即使只有一个线程访问共享对象,它仍然需要执行必须的代码来获取和释放锁。
针对这种情况,为了提供更优的性能,java于是引入了比较和交换操作。这个操作使用一下三步修改变量的值:
1.取得变量值,即变量的旧值。
2.在一个临时变量中修改变量值,即变量的新值。
3.如果上面获得的变量旧值与当前变量值相等,就用新值替换旧值。如果已有其他线程修改了这个变量的值,上面获得的变量的旧值就可能与当前变量值不同。
采用比较和交换机制不需要使用同步机制,不仅可以避免死锁并且性能更好。
java在原子变量中实现了这种机制。这些变量提供了实现比较和交换操作的compareAndSet()方法,其他方法也基于它展开。
java也引入了原子数组(Atomic Array)提供对integer或long数字数组的原子操作。