简介:
在并发场景下,同步容器的使用需要注意复合操作,伪同步,迭代等问题,同时效率也不高。为了解决这些问题,java提供了各种高性能的并发容器。下面我们一起来学习并发容器吧。
实现同步的另外一种思维方式–写时复制:
之前学习保证线程安全的思路是使用synchronized,显示锁和java sun包下Unsafe类提供的CompareAndSet()方法,简称CAS。通过synchronized和显示锁,以及循环CAS对同一资源的访问进行控制。而现在我们提到的写时复制,就是保证线程安全的另外一种思路。写时复制通过复制资源减少冲突。对于绝大部分访问都是读,且有大量并发线程要求读,只有个别线程进行写,而且只是偶尔写的场合,写时复制就是一个很好的解决方案。比如操作系统内部的进程管理和内存管理。在进程管理中,子进程经常共享父进程的资源,只有在写时才复制。在内存管理中,当多个程序同时访问同一个文件时,操作系统在内存中可能只会加载一次,只有程序写时才会复制,分配自己的内存。读写可以同步,写时通过显示锁RenntrantLock来控制只能一个线程写。
写时复制实现的List --CopyOnWriteArrayList:
特点:
(1)它是线程安全的,可以被多线程并发访问。
(2)它的迭代器不支持修改操作,但不会抛ConcurrentModificationException异常。
(3)支持一些复合操作。
支持的复合操作方法:
方法名 | 返回值 | 说明 |
---|---|---|
addIfAbsent(E e) | boolean | 不存在就添加,添加成功返回true,否则返回false |
addAllIAbsent(Collection<? extends E> c) | int | 批量添加元素,不存在才添加,返回实际添加的个数 |
CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
//下面三个方法都是通过synchronized关键字来保证原子性的,写时加锁都是加CopyOnWriteArrayList内部定义的锁,所以可以进行复合操作。
copyOnWriteArrayList.add("1");
copyOnWriteArrayList.addIfAbsent("2");
copyOnWriteArrayList.addAllAbsent(Arrays.asList(new String[]{"3","4"}));
copyOnWriteArrayList.indexOf("1");
CopyOnWriteArrayList实现机制:
/**
* CopyOnWriteArrayList 内部是数组,每次更新都是以原子方式被整体更新。每次修改操作
* 都会新建一个数组,复制原来的数组内容到新数组,在新数组上进行需要的修改,然后以原子的方式
* 设置内部的数组引用,这就是写时复制。所有的读操作都是拿当前的数组引用访问数组,在读的过程中
* 可能内部的数组引用已经被修改,但不会影响读操作,它依旧访问原数组内容。
*/
static class CopyOnWriteList<E> {
//transient 声明变量不被序列化
//使用volatile,保证内存可见性,保证在写操作更新之后读操作可以看到
private volatile transient Object[] array;
//CopyOnWriteArrayList 读不需要锁,可以并行,读和写都可以并行,但多个线程不能同时写,
//每个写操作都需要先获取锁
private final ReentrantLock lock = new ReentrantLock();
public CopyOnWriteList() {
setArray(new Object[0]);
}
public final Object[] getArray(){
return array;
}
public final void setArray(Object[] a){
array = a;
}
/**
*CopyOnWriteArrayList写时复制,加上显示锁ReentrantLock,写时只能一个线程写。
*先把原来数组复制到新数组,在讲新添加的内容加到新数组,然后把内部数组的引用修改成新数组
*
*/
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();
}
}
/**
*查找元素索引
*/
public int indexOf(Object o){
Object[] elements = getArray();
return indexOfRange(o,elements,0,elements.length);
}
public static int indexOfRange(Object o,Object[] es,int from,int to){
int i;
if (o == null) {
for(i = from; i < to; ++i) {
if (es[i] == null) {
return i;
}
}
} else {
for(i = from; i < to; ++i) {
if (o.equals(es[i])) {
return i;
}
}
}
return -1;
}
}
写时复制Set --Copy0nWriteArraySet:
CopyOnWriteArraySet实现了set接口,不包含重复元素。它的内部是通过CopyOnWriteArraySet实现的。
//CopyOnWriteArraySet内部是一个copyOnWriteArrayList。
private final CopyOnWriteArrayList<E> al;
public CopyOnWriteArraySet() {
this.al = new CopyOnWriteArrayList();
}
public CopyOnWriteArraySet(Collection<? extends E> c) {
if (c.getClass() == CopyOnWriteArraySet.class) {
CopyOnWriteArraySet<E> cc = (CopyOnWriteArraySet)c;
this.al = new CopyOnWriteArrayList(cc.al);
} else {
this.al = new CopyOnWriteArrayList();
this.al.addAllAbsent(c);
}
}
//添加方法也都是用CopyOnWriteArrayList的add()方法
public boolean add(E e) {
return this.al.addIfAbsent(e);
}
写时复制总结:
通过上面CopyOnWriteArrayList实现机制,我们可以看出写时复制性能比较低,不适用于元素个数比较多的集合。它们适合于读远多于写,集合不是很大的场合。写时复制是计算机程序中一种重要的思维和技术。
并发容器–ConcurrentHashMap:
ConcurrentHashMap特点:
(1)并发安全;
(2)直接支持一些原子复合操作;
(3)支持高并发,读操作完全并行,写操作支持一定程度的并行;
(4)相比同步容器迭代不用加锁,不会抛出异常。
(5)弱一致性:迭代时,修改的内容是迭代过的部分,就不会反映出来,未迭代的部分修改就会反映出来。
复合操作方法:
方法名 | 返回值 | 说明 |
---|---|---|
putIfAbsent(K key,V value) | V | 条件更新,有key就返回原来key的值,不更新值。没有key就设置key和value.返回null |
remove(Object key,Object value) | boolean | 条件删除,如果Map中有key,且对应的值为value,则删除,返回true,否则false |
replace(K key,V oldValue,V newValue) | boolean | 条件替换,如果Map中有key,且对应的值为oldValue,则替换为newValue,替换了返回true,否则false.。 |
replace(K key,V value) | V | 条件替换,如果map中有key,则替换为value,返回原来的key对应的值,如果没有返回null |
ConcurrentHashMap<String,Object> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put("1",1);
//条件更新,有key就返回原来key的值,不更新值。没有key就设置key和value.返回null
Object str = concurrentHashMap.putIfAbsent("1",2);
Object s = concurrentHashMap.putIfAbsent("key",3);
System.out.println(concurrentHashMap.toString()+": " +s+ ": "+str);
//条件删除,当map中存在key,且值为value,则删除返回true,否则false
boolean flag = concurrentHashMap.remove("2",1);
System.out.println(concurrentHashMap.toString() + ": " + flag);
//条件替换,当map中存在key,且值为oldValue,则替换为newValue,返回true,否则返回false
boolean reFlag = concurrentHashMap.replace("1",1,2);
System.out.println(concurrentHashMap.toString() + ": " + reFlag);
//条件替换,当map中有key就替换为value,返回true。否则返回false
Object o = concurrentHashMap.replace("13",3);
System.out.println(o);
ConcurrentHashMap高并发的基本机制:
(1)分段锁:同步容器使用的是synchronized和显示锁,所有线程都是竞争同一个锁。而ConcurrentHashMap采用分段锁技术,将数据分为多个段,而每个段都有一个独立的锁,每个段相当于一个独立的哈希表,分段的依据也是哈希值,无论是保存键值对还是根据键查找,都先根据键的哈希值映射到段,再在段对应的哈希表上进行操作。采用分段锁,可以大大提高并发度。多个段之间的可以并行读写。在对每个段进行读写的时候,ConcurrentHashMap也不是简单的使用锁进行同步,内部使用了CAS。对一些写采用原子方法的方法。对于写操作,需要获取锁,不能并行。但是读操作可以,多个读可以并行,写的时候也可以读,这使得ConcurrentHashMap的并行度远高于同步容器。ConcurrentHashMap迭代时,另外一个线程对容器作了修改,迭代还是可以进行,不会抛出异常。同时还保持弱一致性,就是修改的内容是迭代过的部分,就不会反映出来,未迭代的部分修改就会反映出来
(2)读不加锁。
(3)迭代时的弱一致性:
final Map<String,Object> concurrentMap = new ConcurrentHashMap<>();
//ConcurrentHashMap 迭代的弱一致性例子:
concurrentMap.put("1",1);
concurrentMap.put("2",2);
Thread thread = new Thread(() -> {
for (Map.Entry<String,Object> entry: concurrentMap.entrySet()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(entry.getKey() + ": " +entry.getValue());
}
});
thread.start();
try {
Thread.sleep(1000);
}catch (InterruptedException i){
i.printStackTrace();
}
concurrentMap.put("4",3);
并发队列:
并发队列分为两种,一种时无锁非阻塞式队列,一种是阻塞队列。无锁非阻塞指的是这些队列不加锁,所有的操作总是能立即执行,主要通过循环CAS实现并发安全。阻塞队列是都采用锁和条件,很多操作需要先获取锁或满足特定条件,获取不到锁或等待条件时,会阻塞,获取到锁或条件满足在返回。并发队列迭代都不会抛出ConcurrentModificationException异常,都是弱一致性。
无锁非阻塞式队列–ConcurrentLinkedQueue与ConcurrentLinkedDeque:
它们适合多个线程访问同一个队里的场合,都是基于链表实现的,都没有大小限制,无界的,与ConcurrentHashMap一样,它们的size不是一个常量运算,不能使用size作为条件。
普通阻塞队列–ArrayBlockingQueue与LinkedBlockingQueue以及LinkedBlockingDeque:
(1)ArrayBlockingQueue实现了BlockingQueue接口,它是基于循环数组实现的,有界,创建时需要指定大小,且在运行过程中不会改变。ArrayBlockingQueue内部有一个数组存储元素,有两个索引表示头和尾,有一个变量表示当前元素个数,有一个锁保护所有访问,有’不满’和’不空’两个条件用于协作。
//实现了BlockingQueue接口,入队和出队时可能等待
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, Serializable {
private static final long serialVersionUID = -817911632652898426L;
final Object[] items;
int takeIndex;
int putIndex;
int count;
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
transient ArrayBlockingQueue<E>.Itrs itrs;
//入队,如果队列满,等待直到队列有空间,传入值为空会抛出空指针异常
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while(this.count == this.items.length) {
this.notFull.await();
}
this.enqueue(e);
} finally {
lock.unlock();
}
}
//出队,如果队列为空,等待直到队列不为空,返回头部元素
public E take() throws InterruptedException {
ReentrantLock lock = this.lock;
lock.lockInterruptibly();
Object var2;
try {
while(this.count == 0) {
this.notEmpty.await();
}
var2 = this.dequeue();
} finally {
lock.unlock();
}
return var2;
}
//入队,如果队列满,最多等待指定的时间,如果超时还是满,返回false,传入值为空会抛出空指针异常
public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {
Objects.requireNonNull(e);
long nanos = unit.toNanos(timeout);
ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
boolean var8;
while(this.count == this.items.length) {
if (nanos <= 0L) {
var8 = false;
return var8;
}
nanos = this.notFull.awaitNanos(nanos);
}
this.enqueue(e);
var8 = true;
return var8;
} finally {
lock.unlock();
}
}
//出队,如果队列为空,最多等待指定的时间,如果超时还是空,返回null
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
Object var7;
while(this.count == 0) {
if (nanos <= 0L) {
var7 = null;
return var7;
}
nanos = this.notEmpty.awaitNanos(nanos);
}
var7 = this.dequeue();
return var7;
} finally {
lock.unlock();
}
}
public E peek() {
ReentrantLock lock = this.lock;
lock.lock();
Object var2;
try {
var2 = this.itemAt(this.takeIndex);
} finally {
lock.unlock();
}
return var2;
}
(2)Link edBlockingQueue也实现了BlockingQueue接口,它是基于单向链表实现的,创建时可以指定最大长度,也可以不指定,默认是无限的,节点时动态创建的。LinkedBlockingQueue实现机制也是使用两个锁和两个条件,一个锁保护头部,一个锁保护尾部,每个锁关联一个条件。
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, Serializable {
private static final long serialVersionUID = -6903933977591709194L;
private final int capacity;
private final AtomicInteger count;
transient LinkedBlockingQueue.Node<E> head;
private transient LinkedBlockingQueue.Node<E> last;
private final ReentrantLock takeLock;
private final Condition notEmpty;
private final ReentrantLock putLock;
private final Condition notFull;
public void put(E e) throws InterruptedException {
if (e == null) {
throw new NullPointerException();
} else {
LinkedBlockingQueue.Node<E> node = new LinkedBlockingQueue.Node(e);
ReentrantLock putLock = this.putLock;
AtomicInteger count = this.count;
putLock.lockInterruptibly();
int c;
try {
while(count.get() == this.capacity) {
this.notFull.await();
}
this.enqueue(node);
c = count.getAndIncrement();
if (c + 1 < this.capacity) {
this.notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0) {
this.signalNotEmpty();
}
}
}
public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {
if (e == null) {
throw new NullPointerException();
} else {
long nanos = unit.toNanos(timeout);
ReentrantLock putLock = this.putLock;
AtomicInteger count = this.count;
putLock.lockInterruptibly();
int c;
try {
while(count.get() == this.capacity) {
if (nanos <= 0L) {
boolean var10 = false;
return var10;
}
nanos = this.notFull.awaitNanos(nanos);
}
this.enqueue(new LinkedBlockingQueue.Node(e));
c = count.getAndIncrement();
if (c + 1 < this.capacity) {
this.notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0) {
this.signalNotEmpty();
}
return true;
}
}
public E take() throws InterruptedException {
AtomicInteger count = this.count;
ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
Object x;
int c;
try {
while(count.get() == 0) {
this.notEmpty.await();
}
x = this.dequeue();
c = count.getAndDecrement();
if (c > 1) {
this.notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == this.capacity) {
this.signalNotFull();
}
return x;
}
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
AtomicInteger count = this.count;
ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
Object x;
int c;
try {
while(true) {
if (count.get() != 0) {
x = this.dequeue();
c = count.getAndDecrement();
if (c > 1) {
this.notEmpty.signal();
}
break;
}
if (nanos <= 0L) {
Object var10 = null;
return var10;
}
nanos = this.notEmpty.awaitNanos(nanos);
}
} finally {
takeLock.unlock();
}
if (c == this.capacity) {
this.signalNotFull();
}
return x;
}
(3)LinkedBlockingDeque也实现了BlockingQueue接口,它是基于双向链表实现的,创建时可以指定最大长度,默认是无限。LinkedBlockingDeque也是使用一个锁和两个条件,使用锁和不空和不满两个条件来控制并发。
优先级阻塞队列–priorityQueue :
PriorityQueue是优先级队列,按优先级出队的。它是无界的,内部的数组大小会动态扩展。要求元素要么实现comparable接口或者创建时提供一个comparator对象。
public class PriorityQueue<E> extends AbstractQueue<E> implements Serializable {
private static final long serialVersionUID = -7720805057305804111L;
private static final int DEFAULT_INITIAL_CAPACITY = 11;
transient Object[] queue;
int size;
private final Comparator<? super E> comparator;
transient int modCount;
延时阻塞队列–DelayQueue:
它是无界的,要求每个元素都是实现Delayed接口.它扩展了comparable接口,delayed每个元素都是可比较的。它的getDelay返回一个给定时间单位的unit整数,表示在延迟 多长时间。如果小于等于0,则表示不在延迟。DelayedQueue可用于定时任务,它按元素的延时时间出队。它的特殊之处在于,只有元素的延时过期之后才能被从队列中拿走,也就是说,take方法总是返回第一个过期的元素。如果没有,则阻塞等待。
public class DelayQueue<E extends Delayed> extends AbstractQueue<E> implements BlockingQueue<E> {
private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue();
private Thread leader;
private final Condition available;
LinkedTransferQueue:
它是基于链表实现的,无界的,它实现了TransferQueue,TransferQueue是BlockingQueue的子接口。但增加了一些额外的功能。生产者在往队列中放元素时,可以等待消费者接受后再返回。适用于一些信息传递类型的应用。
提供了一些方法:
方法名 | 返回值 | 说明 |
---|---|---|
tryTransfer(E e) | boolean | 如果有消费者在等待(执行take或限时的poll),直接转给消费者,返回true,否则false,不入队。 |
Transfer(E e) | boolean | 如果有消费者在等待,直接转给消费者,否则入队,阻塞等待直到被消费者接收,响应中断。 |
tryTransfer(E e,long timeout,TimeUnit unit) | boolean | 如果有消费者等待,直接转给消费者,返回true, |
* 否则入队,阻塞 等待限时时间,如果最后被消费者接收,返回true.响应中断 | ||
hasWaitingConsumer | boolean | 是否有消费者在等待 |
getWaitingConsumer | int | 等待的消费者个数。 |
总结:
本章节列举出了常用的并发容器和并发队列,以及实现并发的另外一种思路,写时复制。