從併發模型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