Java多線程技術研究(二)-線程同步,通信及ThreadLocal

本篇博客主要介紹Java多線程之間的同步與通信,以及ThreadLocal。

一、線程同步
在多線程環境中,可能會有兩個甚至更多的線程試圖同時訪問一個有限的資源(代碼,數據庫等)。我們把多線程訪問同一代碼,產生不確定的結果,稱爲是線程不安全的,否則稱之爲線程安全的。對於String類就是線程安全的,而對於HashMap類是線程不安全的。
下面看一段代碼:

package com.wygu.multiThread.synchro;

public class Bank {
    int money = 100;
    public Bank(int money){
        this.money = money;
    }

    public int getLeftMoney(int withDraw){
        if(withDraw<0){
            return -1;
        }else if(this.money<0){
            return -2;
        }else if(this.money<withDraw){
            return -3;
        }else{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.money = this.money-withDraw;
            System.out.println(Thread.currentThread().getName()+"取走金額:"+withDraw+",銀行剩餘金額爲:"+this.money);
            return this.money;
        }
    }
}
package com.wygu.multiThread.synchro;

public class ThreadSynchronise implements Runnable{
    Bank bank = null;
    public ThreadSynchronise(Bank bank){
        this.bank = bank;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"start!");
        for(int i=0;i<10;i++){
            if(bank.getLeftMoney((i+1)*10)<0){
                break;
            }
        }
        System.out.println(Thread.currentThread().getName()+"start!");  
    }

}
package com.wygu.multiThread.synchro;

public class Main {

    public static void main(String[] args) {
        Bank bank = new Bank(80);
        ThreadSynchronise runnableA = new ThreadSynchronise(bank);
        ThreadSynchronise runnableB = new ThreadSynchronise(bank);
        Thread threadA = new Thread(runnableA,"User A-->");
        Thread threadB = new Thread(runnableB,"User B-->");
        threadA.start();
        threadB.start();
    }

}

程序運行結果爲:
User B–>start!
User A–>start!
User A–>取走金額:10,銀行剩餘金額爲:70
User B–>取走金額:10,銀行剩餘金額爲:70
User B–>取走金額:20,銀行剩餘金額爲:30
User A–>取走金額:20,銀行剩餘金額爲:30
User B–>取走金額:30,銀行剩餘金額爲:-30
User B–>start!
User A–>取走金額:30,銀行剩餘金額爲:-30
User A–>start!

程序中定義了Bank類,線程UserA和線程UserB共享一個Bank的實例bank,兩個線程各自去銀行取錢,我們發現出現取了錢後,剩餘的金額竟然相同。是什麼原因導致出現上述不正常的運行結果呢,這是因爲,多線程可以同時調用方法getLeftMoney(),每個線程跳過判斷以後都會休息一會,無法保證先調用方法的線程先執行完,導致可能會出現髒數據的情形。換句話說,對於上述對象bank不是線程安全的。

如何編寫線程安全的代碼或者把Java提供的類變成線程安全的,我們可以通過加入鎖的機制實現線程同步的安全。Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性,volatile 是因爲其本身包含“禁止指令重排序”的語義,synchronized 是由“一個變量在同一個時刻只允許一條線程對其進行 lock 操作”這條規則獲得的,此規則決定了持有同一個對象鎖的兩個同步塊只能順序執行。

1、synchronized

synchronized修飾的對象有以下幾種:
(1) 修飾一個代碼塊,被修飾的代碼塊稱爲同步語句塊,其作用的範圍是{}括起來的代碼,作用的對象是調用這個代碼塊的對象;
(2) 修飾一個方法,被修飾的方法稱爲同步方法,其作用的範圍是整個方法,作用的對象是調用這個方法的對象;
(3) 修飾一個靜態的方法,其作用的範圍是整個靜態方法,作用的對象是調用這個類的所有對象;
(4) 修飾一個類,其作用的範圍是synchronized後面()括起來的部分,作用的對象是這個類的所有對象。

針對上述的代碼出現線程不安全的情況,可以通過下述兩種方法實現線程安全的,Bank類中的方法getLeftMoney()修改爲:
方法一:

    //方法之前加入關鍵字 synchronized 修飾getLeftMoney()方法
    public synchronized int getLeftMoney(int withDraw){
        if(withDraw<0){
            return -1;
        }else if(this.money<0){
            return -2;
        }else if(this.money<withDraw){
            return -3;
        }else{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.money = this.money-withDraw;
            System.out.println(Thread.currentThread().getName()+"取走金額:"+withDraw+",銀行剩餘金額爲:"+this.money);
            return this.money;
        }
    }

方法二:

//利用synchronized 修飾整個代碼塊或者說修飾一個類
public  int getLeftMoney(int withDraw){
        synchronized (this) {
            if(withDraw<0){
                return -1;
            }else if(this.money<0){
                return -2;
            }else if(this.money<withDraw){
                return -3;
            }else{
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.money = this.money-withDraw;
                System.out.println(Thread.currentThread().getName()+"取走金額:"+withDraw+",銀行剩餘金額爲:"+this.money);
                return this.money;
            }
        }   
    }

執行結果爲:
User A–>start!
User B–>start!
User A–>取走金額:10,銀行剩餘金額爲:70
User B–>取走金額:10,銀行剩餘金額爲:60
User A–>取走金額:20,銀行剩餘金額爲:40
User B–>取走金額:20,銀行剩餘金額爲:20
User A–>start!
User B–>start!
程序運行正常

2、volatile
Java語言提供了一種相對於synchronized 稍弱的同步機制volatile,主要用於修飾變量,確保此變量對所有的線程均是可見的。volatile變量不會被緩存在線程的工作內存中,而是直接儲存子主存中,從而保證了讀取到的volatile類型的變量時總會是最新寫入的。
此外,在訪問voliatile變量時不會執行加鎖操作,因而不會是線程發生阻塞,進而相對於synchronized 不會出現死鎖的情形(使用synchronized 修飾多個方法,多線程方法可能會出現死鎖的情形)。
多線程訪問單例:

package com.wygu.thread.study;

public class SingleInstanceBasic {
    private static SingleInstanceBasic instance = null;
    //構造方法私有化,保證外部無法創建該類的實例
    private SingleInstanceBasic(){}

    public synchronized static SingleInstanceBasic getInstance(){
        if(null == instance){
            instance = new SingleInstanceBasic();
        }
        return instance;
    }
}

上述方式中存在性能上的問題,因爲只有第一次執行getInstance()時,才真正需要同步。換句話,第一次之後的每次調用該方法,同步都是一種累贅。我們可以利用volatile和synchronized 實現雙重鎖機制。

package com.wygu.thread.study;

public class SingleInstanceImprove {
    private volatile static SingleInstanceImprove instance= null;
    //構造方法私有化,保證外部無法創建該類的實例
    private SingleInstanceImprove(){}

    public static SingleInstanceImprove getInstance(){
        if(null == instance){//檢查實例,如果不存在,就進入同步區塊
            synchronized (SingleInstanceImprove.class) {//只有第一次執行時纔會執行到此處
                if(null == instance){//再檢查一次
                    instance = new SingleInstanceImprove();
                }
            }
        }
        return instance;
    }
}

可以看到利用volatile修飾變量確保其對所有的線程都是可見的。那麼有這樣一個問題,static關鍵字修飾的變量,會單獨存放在靜態存儲區中,而且保證只有一個副本。

static和volatile有什麼區別呢?
volatile修飾的變量保證了主存中保存的變量總會是最新寫入的,但是static變量可能在線程工作內存中存在本地緩存的值,導致主存中的值不一定是最新的。因而可以使用volatile和static共同修飾變量,從而強制線程每次需要從主存中讀取全局值,每次寫入值時直接寫入到主存中。

注意:使用volatile 修飾的變量不是線程安全的,只是保證了變量的可見性,無法保證多種的操作的原子性,比如一個線程從主存中讀入該變量後,另一個線程發生了寫的操作,從而導致出現了髒讀。

二、線程通信

1、共享內存機制
Java中共享內存是通過多線程同步機制實現的,比如共享Runnable的實例,具體事例如下:

package wygu.multiThread.study;

public class MultiThreadShare implements Runnable{

    private volatile int breakFast=10;

    @Override
    public void run() {
        for(int i=0;breakFast>0;i++){
            System.out.println(Thread.currentThread().getName()+"---->"+breakFast--);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

    }

    public static void main(String[] args) {
        MultiThreadShare mThreadShare = new MultiThreadShare();
        Thread threadG = new Thread(mThreadShare, "KFC窗口1");
        Thread threadH = new Thread(mThreadShare, "KFC窗口2");
        Thread threadI = new Thread(mThreadShare, "KFC窗口3");
        threadG.start();
        threadH.start();
        threadI.start();
    }

}

程序運行結果爲:
KFC窗口1—->10
KFC窗口3—->9
KFC窗口1—->8
KFC窗口3—->7
KFC窗口1—->6
KFC窗口3—->5
KFC窗口2—->4
KFC窗口1—->3
KFC窗口2—->2
KFC窗口3—->1

2、wait()/notify()/notifyAll()機制
下面利用生產者/消費者模式示例說明如何通過wait()/notify()/notifyAll()機制。
生產者

package wygu.multiThread.study;

public class Producer extends Thread{
    private ShareResource shareResource = null;
    public Producer(ShareResource shareResource) {
        this.shareResource = shareResource;
    }

    @Override
    public void run(){
        for(int i=0;i<5;i++){
            shareResource.set(String.valueOf(i));
        }
    }
}

消費者

package wygu.multiThread.study;

public class Consumer extends Thread{

    private ShareResource shareResource = null;
    public Consumer(ShareResource shareResource) {
        this.shareResource = shareResource;
    }   
    @Override
    public void run(){
        for(int i=0;i<5;i++){
            shareResource.get();
        }
    }
}

共享資源池

package wygu.multiThread.study;

import java.util.LinkedList;

public class ShareResource {
    private int maxSize = 3;
    private LinkedList<String> resCatchList = new LinkedList<String>();

    public void get(){
        synchronized (this) {
            while(resCatchList.isEmpty()){
                try {
                    System.out.println(Thread.currentThread().getName()+":釋放對象鎖,CPU");
                    wait();
                    System.out.println(Thread.currentThread().getName()+":重新獲得對象鎖,CPU");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"獲得資源---->"+resCatchList.poll());
            notify();
        }

    }

    public  void set(String resource){
        synchronized (this) {
            while(maxSize==resCatchList.size()){
                try {
                    System.out.println(Thread.currentThread().getName()+":釋放對象鎖,釋放CPU");
                    wait();
                    System.out.println(Thread.currentThread().getName()+":重新獲得對象鎖,CPU");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"放入新資源---->"+resource);
            resCatchList.push(resource);
            notify();
        }
    }
}

測試程序及運行結果

package wygu.multiThread.study;

public class Main {
    public static void main(String [] argv){
        ShareResource shareResource = new ShareResource();
        Producer producer = new Producer(shareResource);
        producer.setName("Producer");
        Consumer consumer = new Consumer(shareResource);
        consumer.setName("Consumer");
        producer.start();
        consumer.start();
    }

}

Consumer:釋放對象鎖,CPU
Producer放入新資源—->0
Consumer:重新獲得對象鎖,CPU
Consumer獲得資源—->0
Consumer:釋放對象鎖,CPU
Producer放入新資源—->1
Consumer:重新獲得對象鎖,CPU
Consumer獲得資源—->1
Consumer:釋放對象鎖,CPU
Producer放入新資源—->2
Consumer:重新獲得對象鎖,CPU
Consumer獲得資源—->2
Consumer:釋放對象鎖,CPU
Producer放入新資源—->3
Consumer:重新獲得對象鎖,CPU
Consumer獲得資源—->3
Consumer:釋放對象鎖,CPU
Producer放入新資源—->4
Consumer:重新獲得對象鎖,CPU
Consumer獲得資源—->4

3、管道流通信機制

管道流過程:生產數據者(生產者)向管道中輸出數據,讀數據者(消費者)從管道中讀取數據。此外,輸入管道流和輸出管道流之間通過方法connection()建立連接。具體事例如下:

package wygu.multiThread.Pipe;

import java.io.IOException;
import java.io.PipedInputStream;
//消費者
public class ReadThread extends Thread{
    private PipedInputStream pipedInput;
    public ReadThread(PipedInputStream pipedInput) {
        this.pipedInput = pipedInput;
    }
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName()+"-->當前管道沒有數據,阻塞中...");  
        byte[] buf = new byte[1024];  
        int len;
        try {
            len = pipedInput.read(buf);
             System.out.println(Thread.currentThread().getName()+"-->讀取管道中的數據:"+new String(buf,0,len));    
             pipedInput.close(); 
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }          
    }
}

//生產者
package wygu.multiThread.Pipe;

import java.io.IOException;
import java.io.PipedOutputStream;
public class WriteThread extends Thread{
    private PipedOutputStream pipedOutput;
    public WriteThread(PipedOutputStream pipedOutput) {
        this.pipedOutput = pipedOutput;
    }
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName()+"-->開始將數據寫入管道中...");   
        try {
            Thread.sleep(5000);
            pipedOutput.write(new String("Hello World !!").getBytes());
            pipedOutput.close(); 
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

//主程序
package wygu.multiThread.Pipe;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

public class Main {

    public static void main(String[] args) {
        PipedInputStream pInputStream = new PipedInputStream();
        PipedOutputStream pOutputStream = new PipedOutputStream();
        try {
            pInputStream.connect(pOutputStream); //連接管道的輸入流和輸出流
        } catch (IOException e) {
            e.printStackTrace();
        }
        ReadThread readThread = new ReadThread(pInputStream);
        WriteThread writeThread = new WriteThread(pOutputStream);
        readThread.setName("ReadThread");
        writeThread.setName("writeThread");
        readThread.start();
        writeThread.start();
    }
}

程序運行結果爲:
ReadThread–>當前管道沒有數據,阻塞中…
writeThread–>開始將數據寫入管道中…
ReadThread–>讀取管道中的數據:Hello World !!

三、ThreadLocal(線程本地變量)

在Web開發過程中,每個外部請求到服務器後,服務器會創建一個Thread去處理該請求。在處理請求的過程中,可能會出現許多報錯信息,希望在線程執行快結束時再打印出來。一種簡單的方式就是定義一個public static變量或者一個單例,然而這種方式無法解決多線程併發問題。那麼是否存在一種本地變量,它的生命週期和線程的生命週期是一樣的呢。早在JDK 1.2的版本中就提供Java.lang.ThreadLocal,ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路,並且在JDK 1.5後提供了對泛型的支持。

ThreadLocal很容易讓人望文生義,認爲是一種Thread(本地線程)。實際上ThreadLocal是Thread的的一個變量,稱它爲ThreadLocalVariable可能更合適一點。
1、 每個線程都有自己的局部變量
每個線程都有一個獨立於其他線程的上下文來保存這個變量,一個線程的本地變量對其他線程是不可見的。當線程結束後,對應該線程的局部變量將自動被垃圾回收。因而顯示的調用remove()方法不是必須的操作,當然顯示調用可以加快內存的清理速度。
2、獨立於變量的初始化副本
ThreadLocal可以給一個初始值,而每個線程都會獲得這個初始化值的一個副本,這樣才能保證不同的線程都有一份拷貝。
3、狀態與某一個線程相關聯
ThreadLocal不是用於解決共享變量的問題的,不是爲了協調線程同步而存在,而是爲了方便每個線程處理自己的狀態而引入的一個機制。
下面是處理多線程打印報錯信息的部分代碼:

package com.wygu.threadlocal;

import java.util.ArrayList;

public class ErrorInfoUtil {

    public static class ThreadShareList {
        private static final ThreadLocal<ArrayList<String>> threadLocal = new ThreadLocal<ArrayList<String>>();  
        public static ArrayList<String> getCurrList() {  
            // 獲取當前線程內共享的arrayList  
            ArrayList<String> arrayList = threadLocal.get();  
            if(null==arrayList) {  
                arrayList = new ArrayList<String>();
                threadLocal.set(arrayList);  
            }       
            return arrayList;  
        }  
    }

    //將詳細的報錯信息填充線程本地變量中
    public static void fillErrorInfo(String errorReason){
        String respStringInfo = null;
        StackTraceElement[] stack = (new Throwable()).getStackTrace();  
        if(null!=stack && 1<stack.length){
            respStringInfo = errorReason+" | "+stack[1].getClassName()+" | "+stack[1].getMethodName()+
                    " | "+stack[1].getLineNumber();
        }       
        ThreadShareList.getCurrList().add(respStringInfo);
    }

    //線程即將執行結束之前,打印所有的錯誤信息
    public static void dumpTraceError(){
        System.out.println("Error Information Begin...");
        ArrayList<String> arrayList = ThreadShareList.getCurrList();
        int errInfoIndex=1;
        for(String string : arrayList){
            System.out.println("["+(errInfoIndex++)+"]: "+string);
        }
        System.out.println("Error Information End...");
        ThreadShareList.getCurrList().clear();
    }   
}

貴在精

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章