看《java并发编程的艺术》,发现这一章中很多内容是基于java7甚至更早之前的jdk版本,导致一些内容已经过时了,所以这里记录一下新的java并发容器的内容,内容主要来自《java并发编程的艺术》,《实战java高并发程序设计》。
jdk提供的线程安全的并发容器大部分在java.util.concurrent包中,下面是一些常用的类,java.util中的Vector也是线程安全的,但是效率较低。LinkedList并不是线程安全的,不过可以使用Collections.synchronizedList()方法来包装。
List<String> list = Collections.synchronizedList(new ArrayList<>());
一、ConcurrentHashMap的实现原理与使用
HashMap多线程下可能会使内部的链表成环,造成死循环,使得CPU的占用率很高,达到100%,甚至可能导致死机。一个方案是使用Collections.synchronizedMap()方法进行包装。
Map<String, String> m = Collections.synchronizedMap(new HashMap<>());
但这样做无论是数据的写入还是读取,都需要获得mutex锁,这样在多线程情况下并发度不高。
还有一个方案是使用ConcurrentHashMap代替HashMap。ConcurrentHashMap使用上难度不大,但要注意ConcurrentHashMap不允许key或value为null,这个博客总结得不错https://blog.youkuaiyun.com/u010723709/article/details/48007881#commentBox
https://blog.youkuaiyun.com/weixin_44460333/article/details/86770169 https://blog.youkuaiyun.com/bill_xiang_/article/details/81122044
ConcurrentHashMap 1.7:是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。ConcurrentHashMap 1.8:1.7版的问题是查询遍历链表效率太低。其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized
来保证并发安全性。也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。其中的 val next
都用了 volatile 修饰,保证了可见性。
面试5连:
-
谈谈你理解的 HashMap,讲讲其中的 get put 过程。
-
1.8 做了什么优化?
-
是线程安全的嘛?
-
不安全会导致哪些问题?
-
如何解决?有没有线程安全的并发容器?
-
ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做?
二、ConcurrentLinkQueue
1. CLQ简介
https://blog.youkuaiyun.com/qq_38293564/article/details/80798310 前面几段
2. offer过程简介(结合着上链接文和下文),这么复杂的原因
3. put过程简介
4. 其他方法
ConcurrentLinkedQueue类应该算是在高并发环境中性能最好的队列了,它之所以有很好的性能,是因为它内部复杂的实现。
其内部有一个静态内部类Node,其部分源码如下所示:
private static class Node<E> {
volatile E item;
volatile Node<E> next;
item用来存放其目标元素,比如泛型定义该队列为String时,item就是String类型。
对Node进行操作,使用了CAS
关于head和tail节点的说明
进队列方法源码如下所示:
p.casNext(null,newNode),判断p的next是否为null,为null则将newNode设置为next,否则设置失败
casTail(t,newNode),判断t是否指向tail,为true则将更新tail节点,将tail指向新节点
第十七行是判断是否是哨兵节点,哨兵节点是next指向自己的节点
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) {
// p is last node
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t && t != (t = tail)) ? t : q;
}
}
<<实战java高并发程序设计》中有解释,搬运一下:
哨兵如何产生以及poll方法:
定义一个ConcurrentLinkedQUeue对象
poll()函数源码:
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
《实战java高并发程序设计中的解释 》中哨兵的解释:
ConcurrentLinkedQueue的其他方法:
peek():获取表头元素但不移除队列的头,如果队列为空则返回null。
remove(Object obj):移除队列已存在的元素,返回true,如果元素不存在,返回false。
add(E e):将指定元素插入队列末尾,成功返回true,失败返回false(此方法非线程安全的方法,不推荐使用)。
注意:
虽然ConcurrentLinkedQueue的性能很好,但是在调用size()方法的时候,会遍历一遍集合,对性能损害较大,执行很慢,因此应该尽量的减少使用这个方法,如果判断是否为空,最好用isEmpty()方法。
ConcurrentLinkedQueue不允许插入null元素,会抛出空指针异常。
ConcurrentLinkedQueue是无界的,所以使用时,一定要注意内存溢出的问题。即对并发不是很大中等的情况下使用,不然占用内存过多或者溢出,对程序的性能影响很大,甚至是致命的。
通过以上这些说明,可以明显的感觉到,不使用锁而单纯的使用CAS操作要求在应用层面上保证线程安全,并处理一些可能存在的不一致问题,大大增加了程序的设计和实现的难度。但是它带来的好处就是可以得到性能的飞速提升。因此,有些场合也是值得的。
我有一个问题,一直没想明白(现在想明白了):
ConcurrentLinkedQueue中offer(E e)方法中执行到最后一个分支时p = (p != t && t != (t = tail)) ? t : q;p一定是等于t的吧,那么p!=t为false,&&之后的部分不就被逻辑短路了吗,那之后的代码有什么意义呢难道如果其他线程改变了tail,t就不等于p了?
这个问题是我傻了,for循环中第一个第一个初始化条件只在第一次执行时有效,后面就不执行了,&&后面的部分执行的情况是其他线程修改了tail,q不是null,也不等于q(非哨兵),进入最后一个分支,p已经改变(在前一次执行这个语句时),这时&&后面的代码可以很快的找到最后一个节点。
另一个问题:
这么复杂的操作,是为了什么:为了少执行casTail()操作,可以提升效率。
三、BlockingQueue
(网上的原理解释不多,估计考的不多)BlockingQueue是一种特殊的队列,支持阻塞的插入和阻塞的移出。阻塞的插入是指当队列满时,队列会阻塞插入元素的线程,直到队列不满。阻塞的移出是指当队列为空时,队列会阻塞移出元素的线程,直到队列不空。put,take,offer,poll,add,remove都可以对阻塞队列进行操作,下面是阻塞队列为空或为满时各个方法的处理方式:
BlockingQueue是一个接口,它的主要实现有下面一些:
(1) ArrayBlockingQueue时一个用数组实现的有界阻塞队列。次队列按照先进先出的原则对元素进行排序。默认情况下,不保证线程访问的公平性。但也可以在定义阻塞队列的实例时传入参数,将其定义为公平的阻塞队列。
(2)LinkedBlockingQueue时一个用链表实现的无界阻塞队列。此队列的默认最大长度为Integer.MAX_VALUE(约21亿多),此队列按照先进先出的原则对元素进行排序。
(3)PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序排列,当然也可以自定义排序规则。
(4)DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
队列中的元素必须实现Delayed接口,实现Delayed接口要覆写getDelay()方法和compareTo()方法,getDelay()方法返回当前元素还需要多长时间才能从队列中返回。CompareTo()方法用于延迟队列内部排序。
延时阻塞队列的实现比较简单,当消费者从队列里获取元素时,如果元素没有达到延迟时间,就阻塞当前线程。
一个DelayQueue的例子:https://blog.youkuaiyun.com/toocruel/article/details/82769595
(5)SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作,必须等待一个take操作,否则不能继续添加元素。而且,take和put不能是同一个线程,put方法执行后,put线程会休眠,等待另一个线程来take。
一个好玩的小例子:下面代码无法停止,因为主线程再执行了put("a")后,会休眠,下面的线程无法开始执行。
import java.util.concurrent.SynchronousQueue;
public class JustTry {
public static void main(String[] args) throws InterruptedException {
SynchronousQueue<String> queue = new SynchronousQueue<>();
System.out.println("put");
queue.put("a");
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("take");
String str = queue.take();
System.out.println(str);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"thread-1").start();
}
}
而下面的代码可以成果执行,因为take线程在尝试获得元素,一旦主线程put(),take线程会执行take操作,然后主线程也不再休眠,程序结束。 当然,最好是用两个线程,一个用来put,一个用来get。
import java.util.concurrent.SynchronousQueue;
public class JustTry {
public static void main(String[] args) throws InterruptedException {
SynchronousQueue<String> queue = new SynchronousQueue<>();
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("take");
String str = queue.take();
System.out.println(str);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"thread-1").start();
System.out.println("put");
queue.put("a");
}
}
Synchronous默认是非公平的,但也可以在初始化时设置为公平的。
(6)LinkedTransferQueue
LinkedTransferQueue是一个由链表结构组成的无界阻塞队列。相对于其他阻塞队列,它多了tryTransfer和transfer方法。
transfer(E e)方法:
tryTransfer(E e)方法:
tryTransfer(E e,long timeout,TimeUnit unit)方法
(7)LinkedBlockingDeque
ArrayBlockingQueue的原理:
四、Fork/Join框架
1.什么是Fork/Join框架:
Fork/join框架是一个用于并行执行任务的框架,是一个把大人物分割成若干个小任务,最总汇总每个小人物结果后得到大任务结果的框架。Fork就是把一个大人物划分成若干个小任务,join等待所有子任务完成,合并这些子任务的结果。
2. Fork/Join框架设计
要实现Fork/Join框架,必须实现分割任务和执行任务并合并结果这两件事情。Fork/Join框架使用两个类来完成这件事情,
ForkJoinTask类,它有两个子类,RecursiveAction:用于没有返回结果的任务,RecursiveTask:用于有返回结果的任务,我们实现自己的Fork/Join任务,如果任务有返回结果,继承RecursiveTask,否则继承RecursiveAction。
ForkJoinPool类:在实际使用中,如果毫无顾忌地使用fork()方法开启线程进行处理,很有可能导致系统开启过多的线程而影响性能。所以JDK给出了一个ForkJoinPoll线程池,对于fork()方法不着急开启线程,而是交给ForkJoinPoll线程池进行处理,以节省系统资源。
Fork/Join框架还是用了工作窃取算法:
3. 使用forkJoin框架,重点是覆写compute()方法,ForkJoinTask提供了isCompletedAbnormally()方法来检查任务是否已经抛出异常或者已经被取消,getEception方法返回Throwable对象,如果任务被取消了则返回CancellationException。如果任务没有完成或者没抛出异常则返回null。下面是一个例子:
import java.util.concurrent.*;
/**
* ����������
*
* @author tengfei.fangtf
* @version $Id: CountTask.java, v 0.1 2015-8-1 ����12:00:29 tengfei.fangtf Exp $
*/
public class CountTask extends RecursiveTask<Integer> {
private static final int THRESHOLD = 2; // ��ֵ
private int start;
private int end;
public CountTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
// ��������㹻С�ͼ�������
boolean canCompute = (end - start) <= THRESHOLD;
if (canCompute) {
for (int i = start; i <= end; i++) {
sum += i;
}
} else {
// ������������ֵ���ͷ��ѳ��������������
int middle = (start + end) / 2;
CountTask leftTask = new CountTask(start, middle);
CountTask rightTask = new CountTask(middle + 1, end);
//ִ��������
leftTask.fork();
rightTask.fork();
//�ȴ�������ִ���꣬���õ�����
int leftResult = leftTask.join();
int rightResult = rightTask.join();
//�ϲ�������
sum = leftResult + rightResult;
}
return sum;
}
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
// ����һ���������?������1+2+3+4
CountTask task = new CountTask(1, 4);
// ִ��һ������
ForkJoinTask<Integer> result = forkJoinPool.submit(task);
//检查中断
if(result.isCompletedAbnormally()){
System.out.println(task.getException());
}
try {
System.out.println(result.get());
} catch (InterruptedException e) {
} catch (ExecutionException e) {
}
}
}
五、CAS及ABA问题
六、跳表