非阻塞队列的特色就是队列里面没有数据时,操作队列出现异常或返回null,不具有等待/阻塞的特色。
免锁容器背后的通用策略是:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可。修改是在容器数据结构的某个部分的一个单独副本(有时是整个数据结构的副本)上执行的,并且这个副本在修改过程中是不可视的。只有当修改完成时,被修改的结构才会自动地与主数据结构进行交换,之后读取这就可以看到这个修改了。
ConcurrentHashMap类是支持并发操作的Map对象。
与HashTable的区别:HashTable不支持在循环中remove()元素,HashTable在获得Iterator对象后,不允许更改其结构,否则出现java.util.ConcurrentModificationException异常。
ConcurrentHashMap是Java5中新增加的一个线程安全的Map集合,可以用来替代HashTable。通过分析Hashtable就知道,synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,安全的背后是巨大的浪费。那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap的结构
ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。ConcurrentHashMap的类图如下所示:
这是java8之前ConcurrentHashMap的实现原理,每个Segement其实就相当于一个小的Hashtable,在java8中,对ConcurrentHashMap的结构进行了更新,锁的粒度更加小
改进一:取消segments字段,直接采用transient volatile HashEntry
改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。
类ConcurrentSkipListMap支持根据对象的compareTo()方法进行排序
类ConcurrentSkipListSet支持排序而且不允许重复的元素
类ConcurrentLinkedQueue提供了并发环境的队列操作。
ConcurrentLinkedQueue仅支持对列头进行操作,而ConcurrentLinkedDeque支持对列头列尾双向操作。
线程安全的ArrayList,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得赋值的数组在被修改时,读取操作可以安全的执行,当修改完成时,一个原子性的操作将把新的数组换入,使得新的读取操作可以看到这个新的修改。
线程安全的HashSet
类ArrayBlockingQueue提供一种有界阻塞队列的功能。底层使用数组实现。
类LinkedBlockingQueue和ArrayBlockingQueue在功能上大体一样,只不过ArrayBlockingQueue是有界的,而LinkedBlockingQueue是无界的,但也可以定义为有界的。底层使用链表实现。
从LinkedBlockingQueue的几个常用方法讲解它是如何实现阻塞的
//采用AtomicInteger保存队列中的元素个数
private final AtomicInteger count = new AtomicInteger();
//插入操作的重入锁
private final ReentrantLock putLock = new ReentrantLock();
//不满
private final Condition notFull = putLock.newCondition();
//向队列中插入一个元素
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
return false;
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(node);
//通过CAS增加当前队列中的元素个数
c = count.getAndIncrement();
if (c + 1 < capacity)
//唤醒因为队列不满而阻塞的线程
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)
//唤醒因为队列为空阻塞的线程
signalNotEmpty();
return c >= 0;
}
根据源码得知,阻塞队列的安全主要是采用JUC包中的原子类和锁来完成的,会在后面的章节详细介绍
//取出操作的重入锁
private final ReentrantLock takeLock = new ReentrantLock();
//非空
private final Condition notEmpty = takeLock.newCondition();
//从队列中取出一个元素
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
if (count.get() > 0) {
x = dequeue();
//通过CAS减少当前队列中的元素个数
c = count.getAndDecrement();
if (c > 1)
//唤醒因为队列为空而阻塞的线程
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)
//唤醒因为队列不满而阻塞的线程
signalNotFull();
return x;
}
poll()方法主要是从
//从队列中取出一个元素
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
//当队列为空时会一直阻塞
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
从源码中可以看出,poll()和take()的区别是当队列为空时是否会进行阻塞。
类LinkedBlockingQueue只支持对列头的操作,而LinkedBlockingDeque支持对双端结点的操作。
类PriorityBlockingQueue支持在并发情况下的优先级队列。
类SynchronousQueue为异步队列,是一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作,反之亦然。
类LinkedTransferQueue提供的功能与SynchronousQueue有些类似,但其具有嗅探功能,也就是可以尝试性地添加一些数据。
类DelayQueue提供一种延时执行任务的队列。