1、實現多線程的兩種方法
實現多線程有兩種方法:繼承Thread和實現Runnable接口。
繼承Thread:
以賣票爲例:
測試使用:
輸出:
thread1-->5
thread2-->5
thread1-->4
thread2-->4
thread1-->3
thread2-->3
thread1-->2
thread2-->2
thread1-->1
thread2-->1
可以看到,這種方式每個線程自己擁有了一份票的數量,沒有實現票的數量共享。下面看實現Runnable的方式:
實現Runnable接口:
測試使用:
輸出:
ticket-->5
ticket-->3
ticket-->2
ticket-->1
ticket-->4
可以看到,實現Runnable的方式可以實現同一資源的共享。
實際工作中,一般使用實現Runnable接口的方式,是因爲:
(1)支持多個線程去處理同一資源,同時,線程代碼和數據有效分離,體現了面向對象的思想;
(2)避免了Java的單繼承性,如果使用繼承Thread的方式,那這個擴展類就不能再去繼承其他類。
拓展:
Thread的start()和run()方法區別:
start()方法用於啓動一個線程,使其處於就緒狀態,得到了CPU就會執行,而直接調用run()方法,就相當於是普通的方法調用,會在主線程中直接運行,此時沒有開啓一個線程。
(可以看第一篇總結中,關於線程啓動)
下列方法中哪個是執行線程的方法? ()
A、run() B、start() C、sleep() D、suspend()
正確答案:A
run()方法用來執行線程體中具體的內容
start()方法用來啓動線程對象,使其進入就緒狀態
sleep()方法用來使線程進入睡眠狀態
suspend()方法用來使線程掛起,要通過resume()方法使其重新啓動
2、訪問控制修飾符(新補充)
關於訪問控制修飾符,在第一篇總結中已有詳細的介紹。但最近在使用String類的一個方法compareTo()的時候,對private修飾符有了新的理解。String類的compareTo方法是用來比較兩個字符串的字典序的,其源碼如下:
上面代碼邏輯很好理解,我在看到它裏面直接使用anotherString.value來獲取String的字符數組的時候很奇怪,因爲value是被定義爲private的,只能在類的內部使用,不能在外部通過類對象.變量名的方式訪問。我們平常都是通過String類的toCharArray()方法來獲取String的字符數組的,看到上面的這種使用方法,我趕緊在別的地方測試了一下,發現的確是不能直接通過xx.value的方法來獲取字符數組。這個問題我開始始終沒有想明白,特意發帖問了一下(http://www.imooc.com/wenda/detail/315660)。經過網友的提示,終於有點明白了。
正如前面所說的,value是被定義爲private的,只能在類的內部使用,不能在外部通過類對象.變量名的方式訪問。因爲compareTo方法就是String類的內部成員方法,compareTo方法的參數傳遞的就是String對象過來,此時使用“類對象.變量名”的方式是在該類的內部使用,因此可以直接訪問到該類的私有成員。自己再模仿String類來測試一下,發現果然如此。。
問題很細節,但是沒有一下想通,說明還是對private的修飾符理解不夠到位,前面自認爲只要是private修飾的,就不能通過“類對象.變量名”的方式訪問,其實還是需要看在哪裏面使用。
3、線程同步的方法
當我們有多個線程要訪問同一個變量或對象時,而這些線程中既有對改變量的讀也有寫操作時,就會導致變量值出現不可預知的情況。如下一個取錢和存錢的場景:
沒有加入同步控制的情形:
測試類:
部分打印結果如下:
餘額不足
賬戶餘額:0
1462265808958存入:200
賬戶餘額:200
1462265809959存入:200
賬戶餘額:200
1462265809959取出:200
賬戶餘額:200
1462265810959取出:200
賬戶餘額:200
1462265810959存入:200
賬戶餘額:200
1462265811959存入:200
賬戶餘額:200
可以看到,此時有兩個線程共同使用操作了bankCount對象中的count變量,使得count變量結果不符合預期。因此需要進行同步控制,同步控制的方法有以下幾種:
(1)使用synchronized關鍵字同步方法
每一個Java對象都有一個內置鎖,使用synchronized關鍵字修飾的方法,會使用Java的內置鎖作爲鎖對象,來保護該方法。每個線程在調用該方法前,都需要獲得內置鎖,如果該鎖已被別的線程持有,當前線程就進入阻塞狀態。
修改BankCount 類中的兩個方法,如下:
運行測試打印如下結果:
餘額不足
賬戶餘額:0
1462266451171存入:200
賬戶餘額:200
1462266452171取出:200
賬戶餘額:0
1462266452171存入:200
賬戶餘額:200
1462266453171存入:200
賬戶餘額:400
1462266453171取出:200
賬戶餘額:200
1462266454171存入:200
賬戶餘額:400
1462266454171取出:200
賬戶餘額:200
1462266455171取出:200
賬戶餘額:0
可以看到,打印結果符合我們的預期。
另外,如果我們使用synchronized關鍵字來修飾static方法,此時調用該方法將會鎖住整個類。(關於類鎖、對象鎖下面有介紹)
(2)使用synchronzied關鍵字同步代碼塊
使用synchronized關鍵字修飾的代碼塊,會使用對象的內置鎖作爲鎖對象,實現代碼塊的同步。
改造BankCount 類的兩個方法:
(注:這裏改造後的兩個方法中因爲synchronized包含了方法體的整個代碼語句,效率上與在方法名前加synchronized的第一種同步方法差不多,因爲裏面涉及到了打印money還是需要同步的字段,所以全部包含起來,僅僅是爲了說明synchronized作用...)
打印結果:
餘額不足
賬戶餘額:0
1462277436178存入:200
賬戶餘額:200
1462277437192存入:200
賬戶餘額:400
1462277437192取出:200
賬戶餘額:200
1462277438207取出:200
賬戶餘額:0
1462277438207存入:200
賬戶餘額:200
1462277439222存入:200
賬戶餘額:400
1462277439222取出:200
賬戶餘額:200
可以看到,執行結果也符合我們的預期。
synchronized同步方法和同步代碼塊的選擇:
同步是一種比較消耗性能的操作,應該儘量減少同步的內容,因此儘量使用同步代碼塊的方式來進行同步操作,同步那些需要同步的語句(這些語句一般都訪問了一些共享變量)。但是像我們上面舉得這個例子,就不得不同步方法的整個代碼塊,因爲方法中的代碼每條語句都涉及了共享變量,因此此時就可以直接使用synchronized同步方法的方式。
(3)使用重入鎖(ReentrantLock)實現線程同步
重入性:是指同一個線程多次試圖獲取它佔有的鎖,請求會成功,當釋放鎖的時候,直到重入次數爲0,鎖才釋放完畢。
ReentrantLock是接口Lock的一個具體實現類,和synchronized關鍵字具有相同的功能,並具有更高級的一些功能。如下使用:
部分打印結果:
1462282419217存入:200
賬戶餘額:200
1462282420217取出:200
賬戶餘額:0
1462282420217存入:200
賬戶餘額:200
1462282421217存入:200
賬戶餘額:400
1462282421217取出:200
賬戶餘額:200
1462282422217存入:200
賬戶餘額:400
1462282422217取出:200
賬戶餘額:200
1462282423217取出:200
賬戶餘額:0
同樣結果符合預期,說明使用ReentrantLock也是可以實現同步效果的。使用ReentrantLock時,lock()和unlock()需要成對出現,否則會出現死鎖,一般unlock都是放在finally中執行。
synchronized和ReentrantLock的區別和使用選擇:
1、使用synchronized獲得的鎖存在一定缺陷:
>不能中斷一個正在試圖獲得鎖的線程
>試圖獲得鎖時不能像ReentrantLock中的trylock那樣設定超時時間 ,當一個線程獲得了對象鎖後,其他線程訪問這個同步方法時,必須等待或阻塞,如果那個線程發生了死循環,對象鎖就永遠不會釋放;
> 每個鎖只有單一的條件,不像condition那樣可以設置多個
2、儘管synchronized存在上述的一些缺陷,在選擇上還是以synchronized優先:
>如果synchronized關鍵字適合程序,儘量使用它,可以減少代碼出錯的機率和代碼數量 ;(減少出錯機率是因爲在執行完synchronized包含完的最後一句語句後,鎖會自動釋放,不需要像ReentrantLock一樣手動寫unlock方法;)
>如果特別需要Lock/Condition結構提供的獨有特性時,才使用他們 ;(比如設定一個線程長時間不能獲取鎖時設定超時時間或自我中斷等功能。)
>許多情況下可以使用java.util.concurrent包中的一種機制,它會爲你處理所有的加鎖情況;(比如當我們在多線程環境下使用HashMap時,可以使用ConcurrentHashMap來處理多線程併發)。
下面兩種同步方式都是直接針對共享變量來設置的:
(4)對共享變量使用volatile實現線程同步
a.volatile關鍵字爲變量的訪問提供了一種免鎖機制
b.使用volatile修飾域相當於告訴虛擬機該域可能會被其他線程更新
c.因此每次使用該變量就要重新計算,直接從內存中獲取,而不是使用寄存器中的值
d.volatile不會提供任何原子操作,它也不能用來修飾final類型的變量。
b.使用volatile修飾域相當於告訴虛擬機該域可能會被其他線程更新
c.因此每次使用該變量就要重新計算,直接從內存中獲取,而不是使用寄存器中的值
d.volatile不會提供任何原子操作,它也不能用來修飾final類型的變量。
修改BankCount類如下:
部分打印結果:
餘額不足
賬戶餘額:200
1462286786371存入:200
賬戶餘額:200
1462286787371存入:200
賬戶餘額:200
1462286787371取出:200
賬戶餘額:200
1462286788371取出:200
1462286788371存入:200
賬戶餘額:200
賬戶餘額:200
1462286789371存入:200
賬戶餘額:200
可以看到,使用volitale修飾變量,並不能保證線程的同步。volitale相當於一種“輕量級的synchronized”,但是它不能代替synchronized,volitale的使用有較強的限制,它要求該變量狀態真正獨立於程序內其他內容時才能使用
volatile。volitle的原理是每次線程要訪問volatile修飾的變量時都是從內存中讀取,而不是從緩存當中讀取,以此來保證同步(這種原理方式正如上面例子看到的一樣,多線程的條件下很多情況下還是會存在很大問題的)。因此,我們儘量不會去使用volitale。
(5)ThreadLocal實現同步局部變量
使用ThreadLocal管理變量,則每一個使用該變量的線程都獲得該變量的副本,副本之間相互獨立,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產生影響。
ThreadLocal的主要方法有:
1、initialValue():返回當前線程賦予當前線程拷貝的局部線程變量的初始值。一般在定義ThreadLocal類的時候會重寫該方法,返回初始值;
2、get():返回當前線程拷貝的局部線程變量的值;
3、set(T value):爲當前線程拷貝的局部線程變量設置一個特定的值;
4、remove():移除當前線程賦予局部線程變量的值
如下使用:
部分打印結果:
餘額不足
1462289139008存入:200
賬戶餘額:0
賬戶餘額:200
餘額不足
賬戶餘額:0
1462289140008存入:200
賬戶餘額:400
餘額不足
賬戶餘額:0
1462289141008存入:200
賬戶餘額:600
餘額不足
賬戶餘額:0
從打印結果可以看到,測試類中的兩個線程分別擁有了一份count拷貝,即取錢線程和存錢線程都有一個count初始值爲0的變量,因此可以一直存錢但是不能取錢。
ThreadLocal使用時機:
由於ThreadLocal管理的局部變量對於每個線程都會產生一份單獨的拷貝,因此ThreadLocal適合用來管理與線程相關的關聯狀態,典型的管理局部變量是private
static類型的,比如用戶ID、事物ID,我們的服務器應用框架對於每一個請求都是用一個單獨的線程中處理,所以事物ID對每一個線程是唯一的,此時用ThreadLocal來管理這個事物ID,就可以從每個線程中獲取事物ID了。
ThreadLocal和前面幾種同步機制的比較:
2、ThreadLocal就從另一個角度來解決多線程的併發訪問,ThreadLocal會爲每一個線程維護一個和該線程綁定的變量的副本,從而隔離了多個線程的數據,每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的整個變量封裝進ThreadLocal,或者把該對象的特定於線程的狀態封裝進ThreadLocal。
3、ThreadLocal並不能替代同步機制,兩者面向的問題領域不同。同步機制是爲了同步多個線程對相同資源的併發訪問,是爲了多個線程之間進行通信的有效方式;而ThreadLocal是隔離多個線程的數據共享,從根本上就不在多個線程之間共享資源(變量),這樣當然不需要對多個線程進行同步了。所以,如果你需要進行多個線程之間進行通信,則使用同步機制;如果需要隔離多個線程之間的共享衝突,可以使用ThreadLocal,這將極大地簡化你的程序,使程序更加易讀、簡潔。
4、鎖的等級:方法鎖、對象鎖、類鎖
Java中每個對象實例都可以作爲一個實現同步的鎖,也即對象鎖(或內置鎖),當使用synchronized修飾普通方法時,也叫方法鎖(對於方法鎖這個概念我覺得只是一種叫法,因爲此時用來鎖住方法的可能是對象鎖也可能是類鎖),當我們用synchronized修飾static方法時,此時的鎖是類鎖。
對象鎖的實現方法:
1、用synchronized修飾普通方法(非static);
2、用synchronized(this){...}的形式包括代碼塊;
上面兩種方式獲得的鎖是同一個鎖對象,即當前的實例對象鎖。(當然,也可以使用其他傳過來的實例對象作爲鎖對象),如下實例:
測試類:
打印結果如下:
取錢線程>取錢:200
取錢線程>取錢:200
取錢線程>取錢:200
取錢線程>取錢:200
取錢線程>取錢:200
存錢線程>存入:200
存錢線程>存入:200
存錢線程>存入:200
存錢線程>存入:200
存錢線程>存入:200
打印結果表明,synchronized修飾的普通方法和代碼塊獲得的是同一把鎖,纔會使得一個線程執行一個線程等待的執行結果。
類鎖的實現方法:
1、使用synchronized修飾static方法
2、使用synchronized(類名.class){...}的形式包含代碼塊
因爲static的方法是屬於類的,因此synchronized修飾的static方法獲取到的肯定是類鎖,一個類可以有很多對象,但是這個類只會有一個.class的二進制文件,因此這兩種方式獲得的也是同一種類鎖。
如下修改一下上面代碼的兩個方法:
打印結果和上面一樣。說明這兩種方式獲得的鎖是同一種類鎖。
類鎖和對象鎖是兩種不同的鎖對象,如果將addMoney方法改爲普通的對象鎖方式,繼續測試,可以看到打印結果是交替進行的。
注:(1)一個線程獲得了對象鎖或者類鎖,其他線程還是可以訪問其他非同步方法,獲得了鎖只是阻止了其他線程訪問使用相同鎖的方法、代碼塊;
(2)一個獲得了對象鎖的線程,可以在該同步方法中繼續去訪問其他相同鎖對象的同步方法,而不需要重新申請鎖。
後面一篇,將總結線程池ThreadPool、生產者消費者問題及實現、sleep和wait方法區別。
【參考文章:
】