Java Thread 多線程同步、鎖、通信

線程同步
    當多個線程訪問同一個數據時,非常容易出現線程安全問題。這時候就需要用線程同步
    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不推薦使用此方法暫停線程。
 
線程通信
    (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個線程可以分別依賴各自的管道輸入流、管道輸出流進行通信
    
線程組和未處理異常
    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錯誤輸出流,並結束該線程
 
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對象的方法來獲得子線程執行結束後的返回值
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章