java高併發-並行模式(下).md

高性能的生產者-消費者:無鎖的實現

在上文中,BlockingQueue用於實現生產者-消費者,其作爲生產者和消費者的內存緩衝區,目的是爲了方便共享數據,但BlockigQueue並不是一個高性能的實現,它完全使用鎖和阻塞來實現線程間的同步,在高併發的場合下,它的性能不是很優越,ConcurrentLinkedQueue是一個高性能的隊列,其祕訣在於大量使用了無鎖的CAS操作; 
阻塞方式是指:當試圖對文件描述符進行讀寫時,如果當時沒有東西可讀,或者暫時不可寫,程序進入等待狀態,直到所有東西可讀可寫爲止; 
非阻塞方式是指:如果沒有東西可讀,或者不可寫,讀寫函數馬上返回,而不會等待;

剖析ConcurrentLinkedQueue

ConcurrentLinkedQueue類用於實現高併發的隊列,這個隊列使用鏈表作爲其數據結構,作爲一個鏈表,需要定義有關鏈表的節點,在ConcurrentLinkedQueue中,定義的節點Node核心如下:

private static class Node<E>{
volatile E item;
volatile Node<E> next;
}

其中item是用來表示目標元素,字段next表示當前Node的下一個元素,這樣的每個Node就能環環相扣了,下圖是ConcurrentLinkedQueue的基本結構。 


 
其中的offer()起了添加元素的作用,poll()的起了獲取但不移除隊列的頭,如果隊列爲空,則返回null。

  • CAS(compare and swap)操作的基本原理:有3個操作數,內存值爲V,舊的預期值爲A,要修改的新值爲B,當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做。但是利用CAS操作來實現並行模式,難度較大,此時Disruptor框架,將會代替CAS操作來實現高性能的生產者-消費者。

無鎖的緩存框架:Disruptor

Disruptor框架是一款高效的無鎖內存隊列,它使用無鎖的方式實現了一個環形隊列,在Disruptor中使用了環形隊列(RingBuffer)來代替普通的線性隊列,其內部實現爲一個普通的數組。對於一般的隊列,必須要提供隊列同步head和尾部tail兩個指針用於線程的出隊和入隊,這樣明顯增加了線程協作的複雜度,但是Disruptor框架實現了一個環形隊列,這樣就省去了一些不必要的麻煩,只需要對外提供一個當前位置position,利用這個指針既可以進入隊列,也可以進行出隊操作,由於隊列是環形的,隊列的總大小必須事先指定,不能動態擴展,爲了快速從序列對應到數組的實際位置(每次有元素入隊,序列加1)。 
注意:Disruptor要求必須將數組的大小設置爲2的整數次方,這樣就能立即定位到實際的元素位置index; 
Disruptor框架之所以應用廣泛,究其原因在於其利用無鎖的方式實現了一個環形隊列,其好處表現在以下兩個方面:

  • 巧妙利用環形隊列(RingBuffer)來代替普通線性隊列,當生產者向緩衝區寫入數據時,消費者則從中讀取數據,生產者寫入數據時,使用CAS操作,消費者讀取數據時,爲了防止多個消費者處理同一個數據,也使用CAS操作進行數據保護。
  • RingBuffer的大小是固定的,可以做到完全的內存複用,在運行過程中,不會有新的空間需要分配或者老的空間需要回收,因此大大減少了系統分配空間以及回收空間的額外開銷。

用Disruptor實現生產者-消費者案例

import java.nio.ByteBuffer;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
//代表數據的PCData1
class PCData1{
private long value;
public void set(long value){
this.value = value;
}
public long get(){
return value;
}
}
//消費者實現爲WorkHandler接口,它來自Disruptor框架:
class Consumer1 implements WorkHandler<PCData1>{
public void onEvent(PCData1 event) throws Exception{
System.out.println(Thread.currentThread().getId()+":Event:--"+event.get()*event.get()+"--");
}
}
//產生PCData1的工廠類,在Disruptor系統初始化時,構造所有的緩衝區中的對象實例
class PCData1Factory implements EventFactory<PCData1>{
public PCData1 newInstance(){
return new PCData1();
}
}
//生產者
class Producer1{
private final static RingBuffer<PCData1> ringBuffer;//創建一個RingBuffer的引用,即環形緩衝區;
public Producer1(RingBuffer<PCData1> ringBuffer){//創建一個生產者的構造函數,將RingBuffer的引用作爲參數傳入到函數當中;
this.ringBuffer = ringBuffer;
}
public static void pushData(ByteBuffer bb){//pushData作爲RingBuffer的重要方法將數據推入緩衝區,接收一個ByteBuffer對象;
long sequence = ringBuffer.next(); //得到一個可用的序列號
/*java中的異常處理
try{
存放可能出現的異常
}
catch{
處理異常
}
finally{
清理資源,釋放資源;
}
三種方法也可以組合在一起使用,其使用方法如下所示:
1.try+catch:運行到try塊中,如果有異常拋出,則轉到catch塊去處理。然後執行catch塊後面的語句
2.try+catch+finally:運行到try塊中,如果有異常拋出,則轉到catch塊,catch塊執行完畢後,執行finally塊的代碼,再執行finally塊後面的代碼。注意:如果沒有異常拋出,執行完try塊,也要去執行finally塊的代碼,然後執行finally塊後面的語句;
3.try+finally:運行到try塊中,如果有異常拋出的話,程序轉向執行finally塊的代碼。由此引發一個問題?就是程序是否會執行finall塊後面的代碼呢,結果是不會,因爲沒有處理異常,所以遇到異常後,執行完finally後,方法就已拋出異常的方式退出了。這種方法需要注意的是:由於你沒有捕獲異常,所以要在方法後面聲明拋出異常;
*/
try{
PCData event = ringBuffer.get(sequence);
event.set(bb.getLong(0));
}
finally{
ringBuffer.publish(sequence);
}
}
}
 
//生產者和消費者和數據已準備就緒,只差主函數將所有的內容整合起來;
public static void main(String[] args) throws Exception{
Executor executor = Executors.newCachedThreadPool();//調用線程池工廠Executors中newCachedThreadPool()的方法,該方法返回一個可以根據實際情況調整線程數量的線程池,線程池的線程數量不確定,但若有空閒線程可以複用,則會優先使用可複用的線程。
PCData1Factory factory = new PCData1Factory();
int bufferSize = 1024;//設置緩衝區的大小;
Disruptor<PCData1> disruptor = new Disruptor<PCData1>(factory,
bufferSize,
executor,
ProducerType.MULTI,
new BlockingWaitStrategy()
); //創建disruptor對象,它封裝了整個disruptor庫的使用,提供了一些API,方便其他方法進行調用
disruptor.handlerEventWithWorkerPool(//設置用於數據處理的消費者,設置了4個消費者實例;
new Consumer1(),
new Consumer1(),
new Consumer1(),
new Consumer1());
disruptor.start();//啓動並初始化disruptor系統
RingBuffer<PCData1> ringBuffer = disruptor.getRingBuffer();
Producer1 producer1 = new Producer1(ringBuffer);//創建了生產者類Producer1,將ringBuffer作爲實例傳入到生產者類中;
ByteBuffer bb = ByteBuffer.allocate(8);//allocate()方法是用來說明ByteBuffer對象的容量大小,在ByteBuffer對象中可以用來包裝任何數據類型,這裏用來存儲long型整數;
for(long i = 0;true;i++){
bb.putLong(0,i);
Producer1.pushData(bb);
Thread.sleep(100);
System.out.println("add data"+i);//這部分的代碼實際上是爲了讓生產者不斷的向緩衝區中存入數據;
}
}
}
  • poll()和push()方法 
    poll方式,也稱爲輪循,是一種數據同步方式,客戶端定期去ping查詢服務器,確定是否有需要的數據,當服務器沒有數據的時候,poll方式會浪費大量的帶寬。爲了降低帶寬,通常採用減低poll的頻率來實現的,這就導致了消息的長延遲,實時性不高。 
    push方式在大多數的情況下通信信道往往是單向的,爲了解決poll的問題將其設計雙向的,這樣服務器就可以採用push方式主動向客戶端進行數據同步,當前實現push的方法有兩種:
    1. 客戶端首先連接到服務器,並維持長連接;
    2. 服務器能夠直接訪問到客戶端,不需要長連接;

CPU Cache 的優化:解決僞共享問題

在上面提到Disruptor 使用CAS和提供不同的等待策略來提高系統的吞吐量,還嘗試着解決CPU緩存的僞共享問題。

  • 什麼是僞共享模式? 
    爲了提高CPU的速度,CPU有一個高速緩存Cache,在高速緩存中,讀寫數據最小的是緩存行(Cache Line),它是從主存(memory)複製到緩存(Cache)的最小單位,一般爲32個字節到128字節。                                                                  X和Y在同一個緩存行中 
  • 從上圖可以看出,如果將兩個變量放在同一個緩存行中,在多線程訪問過程中,可能會互相影響彼此的性能。假設X和Y同在一個緩存行中,運行在CPU1上的線程更新了X,那麼CPU2上的緩存行就會失效,同一行的Y即使沒有被修改也會變成無效的,導致Cache無法命中,接着,如果在CPU2上的線程更新了Y,則導致CPU1上的緩存行失效,此時,同一行的X又變得無法訪問,這樣反覆發生,使得CPU經常不能命中緩存,那麼系統的吞吐量(指的是網絡設備,端口,虛電路或其他設施,單位時間內成功地傳遞數據的數量,以比特,字節,分組等測量)會急劇下降。
  •  
  • 變量X和Y各佔據一個緩衝行
public final class FalseSharing implements Runnable{
public final static int NUM_THREADS = 1;//線程的數量是可變的;
public final static long ITERATIONS = 500L*1000L*1000L;//設置迭代次數;
private final int arrayIndex;
 
private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];//數組的元素個數和線程數量一致,每個線程都會訪問自己對應的longs中的元素;
static{
for(int i= 0;i<longs.length;i++){//遍歷數組;
longs[i] = new VolatileLong();
}
}
 
public FalseSharing(final int arrayIndex){
this.arrayIndex = arrayIndex;
}
 
public static void main(final String[] args) throws Exception{
final long start = System.currentTimeMillis();//獲取當前的時間戳;
runTest();
System.out.println("duration = "+(System.currentTimeMillis() - start));
}
 
private static void runTest() throws InterruptedException {
Thread[] threads = new Thread[NUM_THREADS];//限定線程的長度
 
for(int i=0;i<threads.length;i++){
threads[i] = new Thread(new FalseSharing(i));
}
 
for(Thread t : threads){//遍歷線程數組,變量定義爲t,這是java for循環的一種,將其翻譯過來的是for(int i=0;i<threads.length;i++)
t.join();
/*
join的字面意思是加入,即一個線程要加入另一個線程,那麼最好的方法就是等着它一起走,
1.public final void join() throws InterruptedException,第一個join()方法表示無限等待,它會一直阻塞當前線程,直到目標線程執行完畢;
2.public final synchronized void join(long millis) throws InterruptedException ,第二個方法給出了一個最大的等待時間,如果超過給定時間目標線程還在執行,當前線程也會因爲“等不及了 ”,而繼續往下執行;
 
*/
}
}
 
public void run(){
long i = ITERATIONS + 1; //定義i的變量值大小爲迭代次數加1;
while(0 != --i){
longs[arrayIndex].value = i;
}
}
 
public final static class VolatileLong{//本程序中最關鍵的點是VolatileLong,定義了7個long型變量用來填充緩存,使得變量不在一個緩存行中
public volatile long value = 0L;
// public long p1,p2,p3,p4,p5,p6,p7;
}
}

Disruptor框架充分了考慮了由於各個JDK版本內部實現不一致的問題,它的核心組件Sequence會被非常頻繁的訪問(每次入隊,它都會被加1)其結構如下:

class LhsPadding{
protected long p1,p2,p3,p4,p5,p6,p7;
}
class Value extends LhsPadding{
protected volatile long value;
}
class RhsPadding extends Value{
protected long p9,p10,p11,p12,p13,p14,p15;
}
public class Sequence extends RhsPadding{
}

在Sequence中,主要使用的是value,但是,通過LhsPadddin和RhsPaddding,這個value的前後安置了一些佔位空間,使得value可以無衝突的存在於緩存中。

Future模式      

        Future模式是多線程開發中非常常見的一種設計模式,核心思想是異步調用。
        Future模式有點類似於在網上買東西,如果我們在網上買了一個手機,當我們支付完成後,手機並沒有立即送到家裏,但是電腦上會產生一個訂單,這個訂單就是將來發貨或者領取手機的重要憑證,換句話說,就是Future模式的一種契約,這樣,大家不用再等待,而是各自忙各自的事情,這張訂單會幫你處理這些事情。
       對於Future模式來說,雖然它無法立即給出你需要的數據。但是,它會返回給你一個契約,將來,你可以憑藉這個契約去重新獲得你需要的信息;


傳統的串行程序調用流程

從上圖中可以看出,客戶端發出call請求,這個請求需要相當一段時間才能返回,客戶端一直在等待,直到數據返回,隨後,再進行其他任務的處理。這樣的話就存在一種弊端,有相當長的一段空閒時間,一直在等待。

 

Future模式流程圖 
從上述的流程圖可以看出,Data_Future對象可以看出,雖然call本身需要很長一段時間處理程序,但是,服務程序不等數據處理完成便立即返回客戶端一個僞造的數據(相當於商品的訂單,而不是商品本身),實現了Future模式的客戶端在拿到這個返回結果後,並不急於對其進行處理,而去調用了其他業務邏輯,充分利用了等待時間,這是Future模式的核心所在,在完成了其他業務邏輯的處理後,最後再使用返回比較慢的Future數據,這樣在整個調用過程中,就不存在無謂的等待,充分利用了所有的時間片段,從而提高系統的響應速度。

Future模式的主要角色





Future模式結構圖

//Data數據
interface Data{
public String getResult();
}
 
//FutureData是Future模式的關鍵。它實際上是真實數據RealData的代理,封裝了獲取RealData的等待過程import javax.xml.crypto.Data;
class FutureData implements Data{
protected RealData realdata = null;
protected boolean isReady = false;
public synchronized void setRealData(RealData realdata){
if(isReady){
return;
}
this.realdata = realdata;
isReady = true;
notifyAll();
}
public synchronized String getResult(){
while(!isReady){
try{
wait();
}catch(InterruptedException e){
}
}
return realdata.result;
}
}
 
class RealData implements Data{
protected final String result;
public RealData(String para){
//RealData的構造可能很慢,需要用戶等待很久,這裏使用sleep模擬;
StringBuffer sb = new StringBuffer();
for(int i =0;i<10;i++){
sb.append(para);
try{
//這裏使用sleep,代替一個很慢的操作過程;
Thread.sleep(100);
}catch(InterruptedException e){
}
}
result = sb.toString();
}
public String getResult(){
return result();
}
private String result() {
// TODO Auto-generated method stub
return null;
}
}
//接下來是客戶端程序,Client主要實現了獲取FutureData,並開啓構造RealData的線程,並在接受請求之後,很快的返回FutureData.
//注意:它不會等待數據真的構造完畢再返回,而是立即返回FutureData,即使這個時候FutureData內並沒有真實的數據
class Client{
public Data request(final String queryStr){
final FutureData future = new FutureData();
new Thread(){
public void run(){
RealData realdata = new RealData(queryStr);
future.setRealData(realdata);
}
}.start();
return future;
}
 
 
//主函數Main,主要負責調用Client發起請求,並消費要返回的數據;
public static void main(String[] args){
Client client = new Client();
//這裏會立即返回,因爲得到的是FutureData而不是RealData
Data data = client.request("name");
System.out.println("請求完畢");
try{
//這裏可以用一個sleep代替了對其他業務邏輯的處理;
//在處理這些業務邏輯的過程中,RealData被創建;
Thread.sleep(2000);
}catch(InterruptedException e){
}
//使用真實數據;
System.out.println("數據= "+ ((RealData) data).getResult());
}
}

JDK中的Future模式

JDK中的Future模式相比上文所講的Future模式更加複雜,在這裏我們先來向大家介紹一下它的使用方式。 



 
JDK內置的Future模式 
從上圖中可知,Future接口類似於前文中描述的訂單或者契約。通過它,你可以得到真實的數據。RunnableFuture繼承了Future和Runnable兩個接口,其中run()方法用於構造真實的數據。它有一個具體的實現FutureTask類,FutureTask有一個內部類Sync,將一些實質性的工作交給內部類來實現,Sync類最終會調用Callable接口,完成實際數據的組裝。 
Callable接口只有一個方法call(),它會返回需要構造的實際數據,這個Callable接口也是Future框架和應用程序之間的重要接口,如果我們要實現自己的業務系統,通常需要實現自己的Callable對象。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
//上述代碼實現了Callable接口,它的call()方法會構造我們需要的真實數據並返回,當然這個構造過程很緩慢,這裏使用Thread.sleep()模擬它;
class RealData1 implements Callable<String>{
private String para;
public RealData1(String para){
this.para = para;
}
public String call() throws Exception{
StringBuffer sb = new StringBuffer();
for(int i = 0;i<10;i++){
sb.append(para);
try{
Thread.sleep(100);
}catch(InterruptedException e){
}
}
return sb.toString();
}
}
//以下代碼是使用Future模式的典型,
public class FutureMain {
public static void main(String[] args) throws InterruptedException,ExecutionException{
//構造FutureTask
FutureTask<String> future = new FutureTask<String>(new RealData1("a"));//構造Future對象實例,表示這個任務有返回值。在構造FutureTask時,使用Callable接口,告訴FutureTask我們需要的數據是如何產生的。
ExecutorService executor = Executors.newFixedThreadPool(1);
//執行FutureTask,相當於上例中的client.request("a")發送請求
//在這裏開啓線程進行RealData的call()執行
executor.submit(future);//將FutureTask提交給線程池,顯然,作爲一個簡單的任務提交,在此會立即返回的,因此程序不會阻塞
 
System.out.println("請求完畢");
try{
//這裏依然可以做額外的數據操作,這裏使用sleep代替其他業務邏輯的處理
Thread.sleep(2000);
}catch(InterruptedException e){
}
//相當於在data.getResult(),取得call()方法的返回值
//如果此時call()方法沒有執行完成,則會依然等待
System.out.println("數據 = "+future.get());//在需要數據時,利用future.get()得到實際的數據;
}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章