作者:Kerwin
说到做到,就是我的忍道!
ConcurrentHashMap 是线程安全的,用法和HashMap基本一致,原理部分可参考以下文章:
源码分析
ConcurrentSkipListSet是对ConcurrentHashMap 的一个补充,有点像TreeMap,LinkedHashMap对HashMap的补充一样,注意的点如下:
ConcurrentSkipListSet俗称 “跳表”,数据结构如下:
每个节点有随机算法概率性的为链表加层(也可以理解为索引),有参数约束了最大层的数量,因此在查询的时候就像是在跳跃一样,跳表的名称由此而来,这种数据结构实现远比红黑树简单,且查询修改删除的效率都较快
常用方法:
put,get,containsKey,containsValue,keySet等常用方法
还有如下:
firstKey()
lastKey()
firstEntry()
lastEntry()
subMap(fromKey, toKey)...
大多数业务场景都是一种“读多写少”的情形,CopyOnWriteArrayList就是为适应这种场景而诞生的。
CopyOnWriteArrayList,运用了一种“写时复制”的思想。通俗的理解就是当我们需要修改(增/删/改)列表中的元素时,不直接进行修改,而是先将列表Copy,然后在新的副本上进行修改,修改完成之后,再将引用从原列表指向新列表。
这样做的好处是读/写是不会冲突的,可以并发进行,读操作还是在原列表,写操作在新列表。仅仅当有多个线程同时进行写操作时,才会进行同步
CopyOnWriteArrayList提供了三种不同的构造器 :
最终都是创建了一个CopyOnWriteArrayList集合
核心方法:
// get方法
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
// add方法
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); // 内部array引用指向新数组
return true;
} finally {
lock.unlock();
}
}
// remove方法
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index); // 获取旧数组中的元素, 用于返回
int numMoved = len - index - 1; // 需要移动多少个元素
if (numMoved == 0) // index位置刚好是最后一个元素
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index, numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
1. 内存的使用
由于CopyOnWriteArrayList使用了“写时复制”,所以在进行写操作的时候,内存里会同时存在两个array数组,如果数组内存占用的太大,那么可能会造成频繁GC,所以CopyOnWriteArrayList并不适合大数据量的场景。2. 数据一致性
CopyOnWriteArrayList只能保证数据的最终一致性,不能保证数据的实时一致性——读操作读到的数据只是一份快照。所以如果希望写入的数据可以立刻被读到,那CopyOnWriteArrayList并不适合。
ConcurrentLinkedQueue和ConcurrentLinkedDeque都是基于CAS操作的无锁,非阻塞队列,本文重点讲一下ConcurrentLinkedQueue的应用
当许多线程共享访问一个公共集合时,ConcurrentLinkedQueue 是一个恰当的选择
注意点如下:
Demo如下:
// 我们假设有很多接口请求,有的正常,有的失败了,失败了一定要打日志,但是打日志又涉及到流,较为耗时,所以我们可以把code码标识为错误的信息拿出来,塞到队列里,然后另起一定的线程去专门打印日志,场景可能不对,但就是这个意思,或者处理订单等等,一个道理
public class TestConcurrentLinkedQueue {
private static Integer totalSize = 500;
private static ConcurrentLinkedQueue<String> lQueue = new ConcurrentLinkedQueue<String>();
private static CountDownLatch latch = new CountDownLatch(totalSize);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < totalSize; i++) {
final int k = i;
new Thread(new Runnable() {
@Override
public void run() {
latch.countDown();
int num = (int) (Math.random() * 10);
if (num < 5) {
lQueue.offer("mytest-" + num);
}
}
}).start();
}
latch.await();
while (true) {
System.out.println(lQueue.poll());
Thread.sleep(50);
}
}
}
// 核心方法:
offer 入队 --- 如果队列满了,返回false
poll 出队 --- 如果队列为null,返回null
注意点:
BlockingQueue是一个阻塞队列接口,下面是具体的实现
BlockingQueue是阻塞队列,适用于任务队列,由一个线程去处理数据等等,比如发快递,有很多人发快递,但是快递员只有一个,如果没活干,快递员就等着,如果有活,就开干
核心方法:
public interface BlockingQueue<E> extends Queue<E> {
//将给定元素设置到队列中,如果设置成功返回true, 否则返回false。如果是往限定了长度的队列中设置值,推荐使用offer()方法。
boolean add(E e);
//将给定的元素设置到队列中,如果设置成功返回true, 否则返回false. e的值不能为空,否则抛出空指针异常。
boolean offer(E e);
//将元素设置到队列中,如果队列中没有多余的空间,该方法会一直阻塞,直到队列中有多余的空间。
void put(E e) throws InterruptedException;
//将给定元素在给定的时间内设置到队列中,如果设置成功返回true, 否则返回false.
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
//从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。
E take() throws InterruptedException;
//在给定的时间里,从队列中获取值,时间到了直接调用普通的poll方法,为null则直接返回null。
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
//获取队列中剩余的空间。
int remainingCapacity();
//从队列中移除指定的值。
boolean remove(Object o);
//判断队列中是否拥有该值。
public boolean contains(Object o);
//将队列中值,全部移除,并发设置到给定的集合中。
int drainTo(Collection<? super E> c);
//指定最多数量限制将队列中值,全部移除,并发设置到给定的集合中。
int drainTo(Collection<? super E> c, int maxElements);
}
注意:
BlockingQueue在使用的时候不要去进行各种是否为空,或者满了等等的判断,另外这是阻塞队列,凡是涉及到前台页面交互的,需要快点得到结果的都不应该直接使用阻塞队列,否则会导致假死的情况
ArrayBlockingQueue利用了ReentrantLock来保证线程的安全性,针对队列的修改都需要加全局锁。在一般的应用场景下已经足够。对于超高并发的环境,由于生产者-消息者共用一把锁,可能出现性能瓶颈
ArrayBlockingQueue维护了一把全局锁,无论是出队还是入队,都共用这把锁,这就导致任一时间点只有一个线程能够执行。那么对于“生产者-消费者”模式来说,意味着生产者和消费者不能并发执行
构造方法:
构造方法 | |
---|---|
public ArrayBlockingQueue(int capacity) | 构造指定大小的有界队列 |
public ArrayBlockingQueue(int capacity, boolean fair) | 构造指定大小的有界队列,指定为公平或非公平锁 |
public ArrayBlockingQueue(int capacity, boolean fair, Collection extends E> c) | 构造指定大小的有界队列,指定为公平或非公平锁,指定在初始化时加入一个集合 |
LinkedBlockingQueue是一种近似有界阻塞队列,为什么说近似?因为LinkedBlockingQueue既可以在初始构造时就指定队列的容量,也可以不指定,如果不指定,那么它的容量大小默认为
Integer.MAX_VALUE
。LinkedBlockingQueue除了底层数据结构(单链表)与ArrayBlockingQueue不同外,另外一个特点就是:
它维护了两把锁——takeLock
和putLock
。takeLock用于控制出队的并发,putLock用于入队的并发。这也就意味着,同一时刻,只能只有一个线程能执行入队/出队操作,其余入队/出队线程会被阻塞;但是,入队和出队之间可以并发执行,即同一时刻,可以同时有一个线程进行入队,另一个线程进行出队,这样就可以提升吞吐量
基本方法和上文一致,只不过内部实现不一样而已
PriorityBlockingQueue是一种无界阻塞队列,在构造的时候可以指定队列的初始容量。具有如下特点:
Integer.MAX_VALUE
;Comparable
接口;核心方法基本一致,重点在于其使用场景,即有优先级的阻塞队列,可以用作,会员特权场景? 嘻嘻
Java 6的并发编程包中的SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。
不像ArrayBlockingQueue或LinkedListBlockingQueue,SynchronousQueue内部并没有数据缓存空间,你不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行的,当然遍历这个队列的操作也是不允许的。队列头元素是第一个排队要插入数据的线程,而不是要交换的数据。数据是在配对的生产者和消费者线程之间直接传递的,并不会将数据缓冲数据到队列中。可以这样来理解:生产者和消费者互相等待对方,握手,然后一起离开。
特点:
SynchronousQueue 内部没有容量,但是由于一个插入操作总是对应一个移除操作,反过来同样需要满足。那么一个元素就不会再SynchronousQueue 里面长时间停留,一旦有了插入线程和移除线程,元素很快就从插入线程移交给移除线程。也就是说这更像是一种信道(管道),资源从一个方向快速传递到另一方 向。显然这是一种快速传递元素的方式,也就是说在这种情况下元素总是以最快的方式从插入着(生产者)传递给移除着(消费者),这在多任务队列中是最快处理任务的方式。在线程池里的一个典型应用是Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收
所以总结就是,一般情况用不到,必要的时候再去研究就好