ConcurrentHashMap
在《java并发编程的艺术》书中,ConcurrentHashMap是根据JDK1.7来介绍的,但是现在大都在用1.8了。所以对两个版本都稍作笔记。
JDK1.7:
ConcurrentHashMap是由Segment数组和HashEntry数组组成的。Segment是ReentranLock的子类,扮演锁的角色;HashEntry用于存储键值对数据。一个ConcurrentHashMap包含一个Segment数组,一个Segment数组包含一个HashEntry数组,每个HashEntry是一个链表结构的元素。也就是说,每个Segment可以看作是类似于Hashmap那样,一种数组和链表的数据结构,且具备可重入锁特性。
通常new一个ConcurrentHashMap都是采用默认构造器。ConcurrentHashMap中有两个用于散列算法的全局变量,segmentShift和segmentMask,在默认构造器中,这两个变量的值分别为28和15。
既然ConcurrentHashMap使用分段所Segment来保护不同段的数据,在操作数据的时候就必须要通过散列算法来获知Segment的定位。而ConcurrentHashMap首先会对hashcode进行一次再散列,以此减少散列冲突,使元素能均匀的分散到不同Segment上。
JDK1.8:
随着JDK的提高,Synchronized关键字也在不断进行优化。ConcurrentHashMap在1.8后弃用了分段锁,而是采用Synchronized关键字和cas操作来维护并发。原因如下:
1)采用分段锁的话,每个分段锁都是一个对象,且是ReentranLock的子类,会浪费内存空间,而Synchronize关键字是通过JVM隐式命令来实现的,且ReentranLock的底层实际上也是unsafe类的cas操作来实现的;
2)放入同一个map中竞争同一个锁的概率很小,但由于分段锁可能会管理多个链表,强行聚合在一起反而会造成更新等操作的长时间等待,增大了锁的粒度,1.8之后只会锁住单个链表或一棵树,并发效率得到提升;
3)为了提高GC的效率。
更新之后的ConcurrentHashMap与1.8的HashMap是极其相似的,也就采用了数组+链表+红黑树的方式。不做赘述。
Fork/Join框架
该框架是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
工作窃取算法:为了减少竞争,把分割的子任务放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务。但有的线程会先把自己队列里的任务干完,而其它线程对应的队列里还有任务等待处理,那么它就会去其它线程的队列里窃取一个任务执行。为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务,窃取任务线程永远从双端队列的尾部拿任务。其优点在于充分利用线程进行并行计算,减少了线程间的竞争;其缺点在于,该算法会消耗更多的系统资源,且在某些情况下还是存在竞争。
使用Fork/Join框架需要创建一个ForkJoin任务。该类提供了在任务中执行fork和join的机制。通常情况下我们只需要继承Fork/Join框架提供的两个子类:
1)RecursiveAction:用于没有返回结果的任务。
2)RecursiveTask<V>:用于有返回结果的任务。
可以在下例中看到,我并未按照常用的模式,先fork执行子任务,再join等待子任务执行完,而是直接用invokeAll方法来管理子任务的执行。其实在invokeAll中仍是执行的fork/join方法,只不过存在一个资源分配的问题。假如pool里面线程数量是固定的,那么调用子任务的fork方法相当于A先分工给B,然后A当监工不干活,B去完成A交代的任务。所以上面的模式相当于浪费了一个线程。那么如果使用invokeAll相当于A分工给B后,A和B都去完成工作。这样就缩短了执行的时间。所以,如果在子任务执行期间没有额外操作的话,可以直接使用invokeAll方法取代fork/join。
ForkJoinTask需要通过ForkJoinPool来执行。Pool有多种提交任务的方法,execute(), submit(), invoke()。execute和submit的相同点在于,提交的任务都是异步执行;区别在于,submit是有返回值的,返回Future类。而invoke方法提交的任务是同步执行。
package com.example.demo.test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) throws Exception {
Node root = new Node(0);
Node node1 = new Node(1);
Node node2 = new Node(2);
Node node3 = new Node(3);
Node node4 = new Node(4);
Node node5 = new Node(5);
Node[] nodes1 = {node1, node2, node3};
Node[] nodes2 = {node4, node5};
root.next = nodes1;
node1.next = nodes2;
node2.next = nodes2;
// 构建了如下一棵树,每个节点是一个任务,任务的执行顺序只遵循父子关系的父优先
// 0
// / | \
// 1 2 3
// / \
// 4 5
Task task = new Task(root);
//ForkJoinTask需要通过ForkJoinPool来执行
ForkJoinPool pool = new ForkJoinPool();
pool.invoke(task);
}
//使用Fork/Join框架需要创建一个ForkJoin任务。该类提供了在任务中执行fork和join的机制。通常情况下我们只需要继承Fork/Join框架提供的两个子类:
//a.RecursiveAction:用于没有返回结果的任务
//b.RecursiveTask<V>:用于有返回结果的任务
static class Task extends RecursiveAction {
private Node node;
Task(Node node) {
this.node = node;
}
@Override
protected void compute() {
boolean work = node.work();
if (!work) {
return;
}
if (node.next == null) {
return;
}
boolean flag2 = node.next.length == 1;
if (flag2) {
node.next[0].work();
} else {
List<Task> list = new ArrayList<>();
for (Node next: node.next) {
Task task = new Task(next);
list.add(task);
// task.fork();
}
invokeAll(list);
// for (Task task: list) {
// task.join();
// }
}
}
}
static class Node {
Node[] next;
private int i;
Node(int i) {
this.i = i;
}
boolean work() {
System.out.println("done=======" + i);
return i != 2;
}
}
}