JavaThread多執行緒同步、鎖、通訊

嗯哼9925發表於2017-11-15

執行緒同步、同步鎖、死鎖

執行緒通訊

執行緒組和未處理異常

Callable和Future

12、執行緒同步
    當多個執行緒訪問同一個資料時,非常容易出現執行緒安全問題。這時候就需要用執行緒同步
    Case:銀行取錢問題,有以下步驟:
    A、使用者輸入賬戶、密碼,系統判斷是否登入成功
    B、使用者輸入取款金額
    C、系統判斷取款金額是否大於現有金額
    D、如果金額大於取款金額,就成功,否則提示小於餘額
 
    現在模擬2個人同時對一個賬戶取款,多執行緒操作就會出現問題。這時候需要同步才行;
    同步程式碼塊:
    synchronized (object) {
        //同步程式碼
    }
    Java多執行緒支援方法同步,方法同步只需用用synchronized來修飾方法即可,那麼這個方法就是同步方法了。
    對於同步方法而言,無需顯示指定同步監視器,同步方法監視器就是本身this
    同步方法:
    public synchronized void editByThread() {
        //doSomething
    }
 
    需要用同步方法的類具有以下特徵:
    A、該類的物件可以被多個執行緒訪問
    B、每個執行緒呼叫物件的任意都可以正常的結束,返回正常結果
    C、每個執行緒呼叫物件的任意方法後,該物件狀態保持合理狀態
    不可變類總是執行緒安全的,因為它的物件狀態是不可改變的,但可變類物件需要額外的方法來保證執行緒安全。
    例如Account就是一個可變類,它的money就是可變的,當2個執行緒同時修改money時,程式就會出現異常或錯誤。
    所以要對Account設定為執行緒安全的,那麼就需要用到同步synchronized關鍵字。
    
    
    下面的方法用synchronized同步關鍵字修飾,那麼這個方法就是一個同步的方法。這樣就只能有一個執行緒可以訪問這個方法,
    在當前執行緒呼叫這個方法時,此方法是被鎖狀態,同步監視器是this。只有當此方法修改完畢後其他執行緒才能呼叫此方法。
    這樣就可以保證執行緒的安全,處理多執行緒併發取錢的的安全問題。
    public synchronized void drawMoney(double money) {
        //取錢操作
    }
    注意:synchronized可以修飾方法、程式碼塊,但不能修飾屬性、構造方法
    
    可變類的執行緒安全是以降低程式的執行效率為代價,為了減少執行緒安全所帶來的負面影響,可以採用以下策略:
    A、不要對執行緒安全類的所有方法都採用同步模式,只對那些會改變競爭資源(共享資源)的方法進行同步。
    B、如果可變類有2中執行環境:單執行緒環境和多執行緒環境,則應該為該可變提供2種版本;執行緒安全的和非執行緒安全的版本。
    在單執行緒下采用非執行緒安全的提高執行效率保證效能,在多執行緒環境下采用執行緒安全的控制安全性問題。
    
    釋放同步監視器的鎖定
    任何執行緒進入同步程式碼塊、同步方法之前,必須先獲得對同步監視器的鎖定,那麼何時會釋放對同步監視器鎖定?
    程式無法顯示的釋放對同步監視器的鎖定,執行緒可以通過以下方式釋放鎖定:
    A、當執行緒的同步方法、同步程式碼庫執行結束,就可以釋放同步監視器
    B、當執行緒在同步程式碼庫、方法中遇到break、return終止程式碼的執行,也可釋放
    C、當執行緒在同步程式碼庫、同步方法中遇到未處理的Error、Exception,導致該程式碼結束也可釋放同步監視器
    D、當執行緒在同步程式碼庫、同步方法中,程式執行了同步監視器物件的wait方法,導致方法暫停,釋放同步監視器
 
    下面情況不會釋放同步監視器:
    A、當執行緒在執行同步程式碼庫、同步方法時,程式呼叫了Thread.sleep()/Thread.yield()方法來暫停當前程式,當前程式不會釋放同步監視器
    B、當執行緒在執行同步程式碼庫、同步方法時,其他執行緒呼叫了該執行緒的suspend方法將該執行緒掛起,該執行緒不會釋放同步監視器。注意儘量避免使用suspend、resume
    
    同步鎖(Lock)
    通常認為:Lock提供了比synchronized方法和synchronized程式碼塊更廣泛的鎖定操作,Lock更靈活的結構,有很大的差別,並且可以支援多個Condition物件
    Lock是控制多個執行緒對共享資源進行訪問的工具。通常,鎖提供了對共享資源的獨佔訪問,每次只能有一個執行緒對Lock物件加鎖,
    執行緒開始訪問共享資源之前應先獲得Lock物件。不過某些鎖支援共享資源的併發訪問,如:ReadWriteLock(讀寫鎖),線上程安全控制中,
    通常使用ReentrantLock(可重入鎖)。使用該Lock物件可以顯示加鎖、釋放鎖。
     
    class C {
        //鎖物件
        private final ReentrantLock lock = new ReentrantLock();
        ......
        //保證執行緒安全方法
        public void method() {
            //上鎖
            lock.lock();
            try {
                //保證執行緒安全操作程式碼
            } catch() {
            
            } finally {
                lock.unlock();//釋放鎖
            }
        }
    }
    使用Lock物件進行同步時,鎖定和釋放鎖時注意把釋放鎖放在finally中保證一定能夠執行。
    
    使用鎖和使用同步很類似,只是使用Lock時顯示的呼叫lock方法來同步。而使用同步方法synchronized時系統會隱式使用當前物件作為同步監視器,
    同樣都是“加鎖->訪問->釋放鎖”的操作模式,都可以保證只能有一個執行緒操作資源。
    同步方法和同步程式碼塊使用與競爭資源相關的、隱式的同步監視器,並且強制要求加鎖和釋放鎖要出現在一個塊結構中,而且獲得多個鎖時,
    它們必須以相反的順序釋放,且必須在與所有鎖被獲取時相同的範圍內釋放所有資源。
    Lock提供了同步方法和同步程式碼庫沒有的其他功能,包括用於非塊結構的tryLock方法,已經試圖獲取可中斷鎖lockInterruptibly()方法,
    還有獲取超時失效鎖的tryLock(long, timeUnit)方法。
    ReentrantLock具有重入性,也就是說執行緒可以對它已經加鎖的ReentrantLock再次加鎖,ReentrantLock物件會維持一個計數器來追蹤lock方法的巢狀呼叫,
    執行緒在每次呼叫lock()加鎖後,必須顯示的呼叫unlock()來釋放鎖,所以一段被保護的程式碼可以呼叫另一個被相同鎖保護的方法。
    
    死鎖
    當2個執行緒相互等待對方是否同步監視器時就會發生死鎖,JVM沒有采取處理死鎖的措施,這需要我們自己處理或避免死鎖。
    一旦死鎖,整個程式既不會出現異常,也不會出現錯誤和提示,只是執行緒將處於阻塞狀態,無法繼續。
    主執行緒保持對Foo的鎖定,等待對Bar物件加鎖,而副執行緒卻對Bar物件保持鎖定,等待對Foo加鎖2條執行緒相互等待對方先釋放鎖,進入死鎖狀態。
    由於Thread類的suspend也很容易導致死鎖,所以Java不推薦使用此方法暫停執行緒。
 
13、執行緒通訊
    (1)、執行緒的協調執行
        場景:用2個執行緒,這2個執行緒分別代表存款和取款。——現在系統要求存款者和取款者不斷重複的存款和取款的動作,
        而且每當存款者將錢存入賬戶後,取款者立即取出這筆錢。不允許2次連續存款、2次連續取款。
        實現上述場景需要用到Object類,提供的wait、notify和notifyAll三個方法,這3個方法並不屬於Thread類。但這3個方法必須由同步監視器呼叫,可分為2種情況:
        A、對於使用synchronized修飾的同步方法,因為該類的預設例項this就是同步監視器,所以可以在同步中直接呼叫這3個方法。
        B、對於使用synchronized修改的同步程式碼塊,同步監視器是synchronized後可括號中的物件,所以必須使用括號中的物件呼叫這3個方法
        方法概述:
        一、wait方法:導致當前執行緒進入等待,直到其他執行緒呼叫該同步監視器的notify方法或notifyAll方法來喚醒該執行緒。
                wait方法有3中形式:無引數的wait方法,會一直等待,直到其他執行緒通知;帶毫秒引數的wait和微妙引數的wait,
                這2種形式都是等待時間到達後甦醒。呼叫wait方法的當前執行緒會釋放對該物件同步監視器的鎖定。
        二、notify:喚醒在此同步監視器上等待的單個執行緒。如果所有執行緒都在此同步監視器上等待,則會隨機選擇喚醒其中一個執行緒。
            只有當前執行緒放棄對該同步監視器的鎖定後(用wait方法),才可以執行被喚醒的執行緒。
        三、notifyAll:喚醒在此同步監視器上等待的所有執行緒。只有當前執行緒放棄對該同步監視器的鎖定後,才能執行喚醒的執行緒。
    (2)、條件變數控制協調
        如果程式不使用synchronized關鍵字來保證同步,而是直接使用Lock物件來保證同步,則系統中不存在隱式的同步監視器物件,
        也不能使用wait、notify、notifyAll方法來協調程式的執行。
        當使用Lock物件同步,Java提供一個Condition類來保持協調,使用Condition可以讓那些已經得到Lock物件卻無法組合使用,
        為每個物件提供了多個等待集(wait-set),這種情況下,Lock替代了同步方法和同步程式碼塊,Condition替代同步監視器的功能。
        Condition例項實質上被繫結在一個Lock物件上,要獲得特定的Lock例項的Condition例項,呼叫Lock物件的newCondition即可。
        Condition類方法介紹:
        一、await:類似於隱式同步監視器上的wait方法,導致當前程式等待,直到其他執行緒呼叫Condition的signal方法和signalAll方法來喚醒該執行緒。
            該await方法有跟多獲取變體:long awaitNanos(long nanosTimeout),void awaitUninterruptibly()、awaitUntil(Date daadline)
        二、signal:喚醒在此Lock物件上等待的單個執行緒,如果所有的執行緒都在該Lock物件上等待,則會選擇隨機喚醒其中一個執行緒。
            只有當前執行緒放棄對該Lock物件的鎖定後,使用await方法,才可以喚醒在執行的執行緒。
        三、signalAll:喚醒在此Lock物件上等待的所有執行緒。只有當前執行緒放棄對該Lock物件的鎖定後,才可以執行被喚醒的執行緒。
     
    (3)、使用管道流
        執行緒通訊使用管道流,管道流有3種形式:
        PipedInputStream、PipedOutputStream、PipedReader和PipedWriter以及Pipe.SinkChannel和Pipe.SourceChannel,
        它們分別是管道流的位元組流、管道字元流和新IO的管道Channel。
        管道流通訊基本步驟:
        A、使用new操作法來建立管道輸入、輸出流
        B、使用管道輸入流、輸出流的connect方法把2個輸入、輸出流連線起來
        C、將管道輸入、輸出流分別傳入2個執行緒
        D、2個執行緒可以分別依賴各自的管道輸入流、管道輸出流進行通訊
    
14、執行緒組和未處理異常
    ThreadGroup表示執行緒組,它可以表示一批執行緒進行分類管理,Java允許程式對
    Java允許直接對執行緒組控制,對執行緒組控制相對於同時控制這批執行緒。使用者建立的所有執行緒都屬於指定的執行緒組。
    如果程式沒有值得執行緒屬於哪個組,那這個執行緒就屬於預設執行緒組。在預設情況下,子執行緒和建立它父執行緒屬於同一組。
    一旦某個執行緒加入了指定執行緒組之後,該執行緒將屬於該執行緒組,直到該執行緒死亡,執行緒執行中途不能改變它所屬的執行緒組。
    Thread類提供一些構造設定執行緒所屬的哪個組,具有以下方法:
    A、Thread(ThreadGroup group, Runnable target):target的run方法作為執行緒執行體建立新執行緒,屬於group執行緒組
    B、Thread(ThreadGroup group, Runnalbe target, String name):target的run方法作為執行緒執行體建立的新執行緒,該執行緒屬於group執行緒組,且執行緒名為name
    C、Thread(ThreadGroup group, String name):建立新執行緒,新執行緒名為name,屬於group組
 
    因為中途不能改變執行緒所屬的組,所以Thread提供ThreadGroup的setter方法,但提供了getThreadGroup方法來返回該執行緒所屬的執行緒組,
    getThreadGroup方法的返回值是ThreadGroup物件的表示,表示一個執行緒組。
    ThreadGroup有2個構造形式:
    A、ThreadGroup(String name):name執行緒組的名稱
    B、ThreadGroup(ThreadGroup parent, String name):指定名稱、指定父執行緒組建立的一個新執行緒組
 
    上面的構造都指定執行緒名稱,也就是執行緒組都必須有自己的一個名稱,可以通過呼叫ThreadGroup的getName方法得到,
    但不允許中途改變名稱。ThreadGroup有以下常用的方法:
    A、activeCount:返回執行緒組活動執行緒數目
    B、interrupt:中斷此執行緒組中的所有執行緒
    C、isDeamon:判斷該執行緒是否在後臺執行
    D、setDeamon:把該執行緒組設定為後臺執行緒組,後臺執行緒具有一個特徵,當後臺執行緒的最後一個執行緒執行結束或最後一個執行緒被銷燬,後臺執行緒組自動銷燬。
    E、setMaxPriority:設定執行緒組最高優先順序
    uncaughtException(Thread t, Throwable e)該方法可以處理該執行緒組內的執行緒所丟擲的未處理的異常,
    Thread.UncaughtExceptionHandler是Thread類的一個內部公共靜態介面,
    該介面內只有一個方法:void uncaughtException(Thread t, Throwable e) 該方法中的t代表出現異常的執行緒,而e代表該執行緒丟擲的異常
     
    Thread類中提供2個方法來設定異常處理器:
    A、staticsetDefaultUnaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):為該執行緒類的所有執行緒例項設定預設的異常處理器
    B、setUncaughtExceptionHandler(Thread.UncaughtExceptionHander eh):為指導執行緒例項設定異常處理器
 
    ThreadGroup實現了Thread.UncaughtExceptionHandler介面,所以每個執行緒所屬的執行緒組將會作為預設的異常處理器。當一個執行緒丟擲未處理異常時,
    JVM會首先查詢該異常對應的異常處理器,(setUncaughtExceptionHandler設定異常處理器),如果找到該異常處理器,將呼叫該異常處理器處理異常。
    否則,JVM將會呼叫該執行緒的所屬執行緒組的uncaughtException處理異常,執行緒組處理異常流程如下:
    A、如果該執行緒有父執行緒組,則呼叫父執行緒組的uncaughtException方法來處理異常
    B、如果該執行緒例項所屬的執行緒類有預設的異常處理器(setDefaultUnaughtExceptionHandler方法設定異常處理器),那就呼叫該異常處理器來處理異常資訊
    C、將異常呼叫棧的資訊列印到System.err錯誤輸出流,並結束該執行緒
 
15、Callable和Future
    Callable介面定義了一個call方法可以作為執行緒的執行體,但call方法比run方法更強大:
    A、call方法可以有返回值
    B、call方法可以申明丟擲異常
 
    Callable介面是JDK5後新增的介面,而且不是Runnable的子介面,所以Callable物件不能直接作為Thread的target。而且call方法還有一個返回值,
    call方法不能直接呼叫,它作為執行緒的執行體被呼叫。那麼如何接收call方法的返回值?
    JDK1.5提供了Future介面來代表Callable介面裡的call方法的返回值,併為Future介面提供了一個FutureTask實現類,該實現類實現Future介面,
    並實現了Runnable介面—可以作為Thread的target。
 
    Future介面裡定義瞭如下幾個公共方法控制他關聯的Callable任務:
    A、boolean cancel(Boolean mayInterruptlfRunning):試圖取消該Future裡關聯的Callable任務
    B、V get():返回Callable任務裡的call方法的返回值,呼叫該方法將導致執行緒阻塞,必須等到子執行緒結束才得到返回值
    C、V get(long timeout, TimeUnit unit):返回Callable任務裡的call方法的返回值,該方法讓程式最多阻塞timeout和unit指定的時間。
        如果經過指定時間後Callable任務依然沒有返回值,將會丟擲TimeoutException。
    D、boolean isCancelled:如果在Callable任務正常完成前被取消,則返回true。
    E、boolean isDone:如果Callable任務已經完成,則返回true
 
    建立、並啟動有返回值的執行緒的步驟如下:
    一、建立Callable介面的實現類,並實現call方法,該call方法的返回值,並作為執行緒的執行體。
    二、建立Callable實現類的例項,使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了該Callable物件的call方法的返回值
    三、使用FutureTask物件作為Thread物件的target建立、並啟動新執行緒
    四、呼叫FutureTask物件的方法來獲得子執行緒執行結束後的返回值
    
本文轉自hoojo部落格園部落格,原文連結:http://www.cnblogs.com/hoojo/archive/2011/05/05/2038101.html,如需轉載請自行聯絡原作者


相關文章