从并发模型Master-Worker说起
Java多线程编程中,常用的有Future模式、Master-Worker模式、不变模式、生产者消费者模式、Guarded Suspeionsion模式。
简单来说Master-Worker模式就是Master负责接收和分配任务,Worker负责处理子任务。当各个Work进程处理完任务之后,将结果返回给Master进程,Master进程负责汇总,获得最终的结果。
Master-Worker源码
package com.chen.master;
import java.util.Map;
import java.util.Set;
/**
* Created by CHEN on 2016/9/28.
*/
public class Application {
public static void main(String[] args) {
Master master=new Master(new PlusWorker(),4);
for(int i=1;i<=100;i++) {
//添加1,因为工作队列等会会从这里去加工对象,
//假如加工对象是一头猪,那就传入一头猪
master.submit(i);
}
//开始加工
master.execute();
Map<String,Object> resultMap=master.getResultMap();
int re=0;
while(true) {
Set<String> keys =resultMap.keySet();//获得工作队列中的所有key组
String key=null;
//取出第一个key 这里的写法 可以防止出现null的情况
for(String k:keys) {
key=k;
break;
}
Integer i = null;
if(key != null)
i = (Integer)resultMap.get(key);//根据key取i
if(i != null)
re += i; //最终结果
if(key != null)
resultMap.remove(key); //移除已被计算过的项目
//isComplete 函数 每次都去遍历工作队列把 完成的去掉
if(master.isComplete() && resultMap.size()==0)
break;
}
System.out.println(re);
}
}
package com.chen.master;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* Created by CHEN on 2016/9/28.
*/
public class Master {
//任务队列
protected Queue<Object> workQueue =
new ConcurrentLinkedQueue<Object>();
//Worker进程队列
protected Map<String, Thread> threadMap =
new HashMap<String, Thread>();
//子任务处理结果集
protected Map<String, Object> resultMap =
new ConcurrentHashMap<String, Object>();
public Master(Worker worker, int countWorker) {
worker.setWorkQueue(workQueue);
worker.setResultMap(resultMap);
for(int i=0; i<countWorker; i++) {
threadMap.put(Integer.toString(i),
new Thread(worker, Integer.toString(i)));
}
}
//是否所有的子任务都加工完成了
public boolean isComplete() {
for(Map.Entry<String, Thread> entry : threadMap.entrySet()) {
if(entry.getValue().getState() != Thread.State.TERMINATED)
//存在未完成的线程
return false;
}
return true;
}
//提交一个子任务
public void submit(Object job) {
workQueue.add(job);
}
//返回子任务结果集
public Map<String, Object> getResultMap() {
return resultMap;
}
//执行所有Worker进程,进行处理
public void execute() {
for(Map.Entry<String, Thread> entry : threadMap.entrySet()) {
entry.getValue().start();
}
}
}
package com.chen.master;
import java.util.Map;
import java.util.Queue;
/**
* Created by CHEN on 2016/9/28.
*/
public class Worker implements Runnable{
protected Queue<Object> workQueue;//从master那里获得的工作加工队列
protected Map<String,Object> resultMap;
public void setWorkQueue(Queue<Object> workQueue) {
this.workQueue=workQueue;
}
public void setResultMap(Map<String,Object> resultMap){
this.resultMap=resultMap;
}
/**
* 留给子类实现的方法
* @param input
* @return
*/
public Object handle(Object input) {
return input;
}
public void run() {
while(true) {
Object input=workQueue.poll();
if(input==null) break;
Object re=handle(input);
resultMap.put(Integer.toString(input.hashCode()),re);
}
}
}
package com.chen.master;
/**
* Created by CHEN on 2016/9/28.
*/
public class PlusWorker extends Worker{
//三次方
public Object handle(Object input) {
int i=(Integer) input;
return i*i*i;
}
}
注意
从程序中,我们可以看出,Application生产了100个任务,然后,Master调用4个Worker工作进程进行加工。然后没有等所有的加工完成就开始汇总数据了。最后根据加工任务是否完成,并且汇总任务是否完成,结束所有的任务。
那么问题就来了,在加工任务的时候,Worker 去取任务,难道就不会出现出现,两个线程去取同一个进行加工?如果是一般的任务队列,还真会。但是这里使用的是ConcurrentLinkedQueue这是JDK提供的包 java.util.concurrent 下的类,包括很多的线程安全、测试良好、高性能的并发构件块。
ConcurrentLinkedQueue
想想,如果要实现,任务队列的顺序读取,要怎么做。比如说,使用synchronized。如果你使用了synchronized,那就大错了,这样的话,并发效果就差了。
ConcurrentLinkedQueue使用了CAS算法,一种非阻塞算法。之后讨论,现在继续说ConcurrentLinkedQueue。
ConcurrentLinkedQueue首先是一个队列。他是一个基于链表节点的无界线程安全队列,采用先进先出对节点进行排序,当我们添加一个元素的时候,就会添加到队尾,当我们去一个元素的时候,就返回队头元素。那么,多线程下,谁知道谁是队尾阿。比如说我是一个想排队的人,看了一下队伍,阿程是队尾,这时候,我就被中断了(时间片用完),没想到这时候,阿杰就来了,然后他就插到了队尾。这时候,又到了我的时间片了。要不是阿杰是我老友,我就打他了。可是真实的数据可不是这样,直接就覆盖了。
我们来看看ConcurrentLinkedQueue源码的节点定义。
private transient volatile Node<E> head;
private transient volatile Node<E> tail;
原来,和volatile有关。如果先想看可以跳到下文。
通过volatile使得队尾元素在各个线程中是可见的。如果发现已经变化了(其他线程插队),那么当前线程就会暂停入队操作,重试获得尾节点。
分析一下源码吧。下面是JDK8的ConcurrentLinkedQueue。
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;
}
}
//判断
boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
从源码上看,主要做了两件事,第一,定位出尾节点,第二使用CAS算法能将入队节点设置成尾节点的next节点,如果不成功就重试。
第一步定位尾节点。tail节点并不总是尾节点,所以每次入队都必须先通过tail节点来找到尾节点,尾节点可能就是tail节点,也可能是tail节点的next节点。代码中循环体中的第一个if就是判断tail是否有next节点,有则表示next节点可能是尾节点。获取tail节点的next节点需要注意的是p节点等于p的next节点的情况,只有一种可能就是p节点和p的next节点都等于空,表示这个队列刚初始化,正准备添加第一次节点,所以需要返回head节点。
第二步设置入队节点为尾节点。p.casNext(null, n)方法用于将入队节点设置为当前队列尾节点的next节点,p如果是null表示p是当前队列的尾节点,如果不为null表示有其他线程更新了尾节点,则需要重新获取当前队列的尾节点。
参考:http://www.infoq.com/cn/articles/ConcurrentLinkedQueue
CAS算法
锁机制存在以下问题:
1. 在多线程竞争下,加锁释放锁会导致比较多的上下文切换(上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。)和调度延时(线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。)。
2. 一个线程持有锁,导致其他所需此锁的线程挂起。
3. 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
CAS:Compare and Swap。比较交换算法,是一个非阻塞算法,java.util.concurrent包的主力。它包括三个操作数内存值V,旧的预期值A,要修改的新值B,当且仅当预期值A与内存值V相同,则将内存中的V修改成B,否则什么都不做。
volatile
一个线程在JVM中代表一个工作区,每个工作区都是对主工作区的拷贝。因此,不得不提防缓存不一致的问题。比如说两个线程同时读了一个数值,然后都加一,然后先后写回主工作区。这时候,这个数值仅仅是加了一次一。
在并发编程中,有三个概念。原子性、可见性、有序性。而volatile仅符合可见性,可就是线程之间的变量可以互相看到,拿到的都是最新的。
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
但是这不代表多线程下volatile修饰的变量就不会出现缓存不一致现象,因为volatile操作可不是原子性的。
参考:http://www.cnblogs.com/dolphin0520/p/3920373.html
Unsafe
当使用框架反序列化或者构建对象时,会假设从已存在的对象中重建,你期望使用反射来调用类的设置函数,或者更准确一点是能直接设置内部字段甚至是final字段的函数。问题是你想创建一个对象的实例,但你实际上又不需要构造函数,因为它可能会使问题更加困难而且会有副作用。
public class A implements Serializable {
private final int num;
public A(int num) {
System.out.println("Hello Mum");
this.num = num;
}
public int getNum() {
return num;
}
}
在这个类中,应该能够重建和设置final字段,但如果你不得不调用构造函数时,它就可能做一些和反序列化无关的事情。有了这些原因,很多库使用Unsafe创建实例而不是调用构造函数。
Unsafe unsafe = getUnsafe();
Class aClass = A.class;
A a = (A) unsafe.allocateInstance(aClass);
用Unsafe实现线程安全对象:
http://blog.csdn.net/cjm812752853/article/details/52692747