线程基础知识系列(三)线程的同步
本文是系列的第三篇,前面2篇,主要是針對單個線程如何管理,啟動等,沒有過多涉及多個線程是如何協同工作的。
線程基礎知識系列(二)線程的管理 :線程的狀態,控制,休眠,Interrupt,yield等
線程基礎知識系列(一)線程的創建和啟動? :線程的創建和啟動,join(),daemon線程,Callable任務。
本文的主要內容
何謂線程安全?
何謂共享可變變量?
認識synchronized關鍵字
認識Lock
synchronized vs Lock
1.何謂線程安全
多線程是把雙刃劍,帶來高效的同時,也帶來了安全隱患。什么是線程安全?眾說一次,很多版本的說辭。引用《Java并發編程實戰》書中的定義,如下:當多線程訪問時,永遠都能表現正確的行為。延伸解讀下“何謂正確性”。正確性就是不管是多線程訪問,還是單線程訪問,影響的結果是一致的。可以將單線程的正確性形容為“所見即所知”。借助下面的例子解釋下。
SysnExampleV1.java
package?com.threadexample.sysn; import?java.util.Random; import?java.util.concurrent.TimeUnit; public?class?SysnExampleV1?{static?class?Task?implements??Runnable{private?Integer?count=0;private?int?cycleSize;public?Task(int?cycleSize)?{this.cycleSize=cycleSize;}@Overridepublic?void?run()?{for(int?i=0;i<cycleSize;i++){this.count++;}}private?void?doSomething(){final?Random?random?=new?Random();try?{TimeUnit.MILLISECONDS.sleep(random.nextInt(10));}?catch?(InterruptedException?e)?{e.printStackTrace();}}public?int?getCount(){return?this.count;}}public?static?void?main(String[]?args)?throws?InterruptedException?{Task?task?=?new?Task(1000);Thread?t1=new?Thread(task);Thread?t2=new?Thread(task);t1.start();t2.start();t1.join();t2.join();System.out.println("計數(線程數*循環數)="+task.getCount());} }Task 類維護一個實例變量count,作為計數器。每循環一次計數加1.一共啟用2個線程,每個線程循環1000次。為了保證線程完整執行調用線程的join(),最后的預期效果:2*1000=2000.
測試結果如下(而且結果經常變化)
計數(線程數*循環數)=1958根據“所見即所知”,2個線程,每個循環1000次,當然是2000了。可結果不是2000.說明Task類不是線程安全的。
簡單剖析下原因: 問題出在this.count++,這個操作是符合操作。
使用自帶的javap -v SysnExampleV1$Task.class 命令查看字節碼文件 ,可以很容易找到原因。
簡單的一個自增操作,被分解為
獲取當前c的值;
對獲取到的值加1;
把遞增后的值寫回到c;
既然是復合操作,一個線程更新了數據,還沒有保存到共享緩存,另外一個線程這時候讀取數據,就會存在拿到過期數據的情況。簡單演示下,假設count c初始化為0,
線程A:獲取c;
線程B:獲取c;
線程A:對獲取的值加1,結果為1;
線程B:對獲取的值加1,結果為1;
線程A:結果寫回到c,c現在是1;
線程B:結果寫回到c,c現在是1;
按正常理解,B應該寫回2才正確。
接下來如何解決這個問題呢。其實很簡單。就是用synchronized處理。在介紹synchronized之前,先簡單說一下共享變量。
2.何謂共享可變變量
要編寫線程安全的代碼,其核心在于對狀態訪問操作的管理上,特別是對共享的和可變的狀態的訪問。“共享”意味著可以由多個線程同時訪問,而”可變“意味著變量的值在其生命周期內可以發生變化。根據不同分類,簡單介紹幾種變量
局部變量
局部變量存儲在線程自己的棧中。也就是說,局部變量永遠也不會被多個線程共享。所以,基礎類型的局部變量是線程安全的。下面是基礎類型的局部變量的一個例子:
public?void?someMethod(){long?threadSafeInt?=?0;threadSafeInt++; }局部的對象引用
對象的局部引用和基礎類型的局部變量不太一樣。盡管引用本身沒有被共享,但引用所指的對象并沒有存儲在線程的棧內。所有的對象都存在共享堆中。所以存在變量逸出現象。關于逸出的相關知識,可以參考《JAVA并發編程實戰》3.2節“發布與逸出”。
public?void?someMethod(){LocalObject?localObject?=?new?LocalObject();localObject.callMethod();method2(localObject); }public?void?method2(LocalObject?localObject){localObject.setValue("value"); }樣例中LocalObject對象沒有被方法返回,也沒有被傳遞給someMethod()方法外的對象。每個執行someMethod()的線程都會創 建自己的LocalObject對象,并賦值給localObject引用。因此,這里的LocalObject是線程安全的。事實上,整個 someMethod()都是線程安全的。即使將LocalObject作為參數傳給同一個類的其它方法或其它類的方法時,它仍然是線程安全的。當然,如 果LocalObject通過某些方法被傳給了別的線程,那它就不再是線程安全的了。
對象成員
對象成員存儲在堆上。如果兩個線程同時更新同一個對象的同一個成員,那這個代碼就不是線程安全的。下面是一個樣例:
public?class?NotThreadSafe{StringBuilder?builder?=?new?StringBuilder();public?add(String?text){this.builder.append(text);} }如果兩個線程同時調用同一個NotThreadSafe實例上的add()方法,就會有競態條件問題.這時候如果多線程訪問對象成語變量,線程就不是線程安全的,所以需要使用java提供的加鎖機制進行保護了。目前在Java中存在兩種鎖機制:synchronized和Lock,Lock接口及其實現類是JDK5增加的內容,其作者是大名鼎鼎的并發專家Doug Lea。本文并不比較synchronized與Lock孰優孰劣。
3.認識synchronized關鍵字
synchronized是java的一個關鍵字。java提供了2個同步機制,同步方法和同步塊。同步塊要關聯一個保護的對象,同步方法關聯的是this對象。受同步保護的代碼塊,一次只允許一個線程進入,至于JVM底層又是如何實現synchronized的。
同步機制的建立是基于其內部一個叫內部鎖或者監視鎖的實體。(在Java API規范中通常被稱為監視器。)內部鎖在同步機制中起到兩方面的作用:對一個對象的排他性訪問;建立一種happens-before關系,而這種關系正是可見性問題的關鍵所在。
每個對象都有一個與之關聯的內部鎖。通常當一個線程需要排他性的訪問一個對象的域時,首先需要請求該對象的內部鎖,當訪問結束時釋放內部鎖。在線程 獲得內部鎖到釋放內部鎖的這段時間里,我們說線程擁有這個內部鎖。那么當一個線程擁有一個內部鎖時,其他線程將無法獲得該內部鎖。其他線程如果去嘗試獲得 該內部鎖,則會被阻塞。
當線程釋放一個內部鎖時,該操作和對該鎖的后續請求間將建立happens-before關系。
更多的原理解釋,可以參考深入JVM鎖機制之一:synchronized和相關Java memory model知識。
使用synchronized塊保護方法
代碼塊1.1
或者
代碼塊1.2
兩個方法的區別是synchronized塊的保護范圍區別,結果是一樣的,前者保護的范圍大一些,但上下文切換少一些;后者與之相反。具體哪個形式更好,具體要看保護的代碼塊邏輯了。
?? 2.將方法用synchronized聲明
代碼塊2.1
這種方式,保護效果與代碼塊1.1達到的效果是一樣的。
3.在類級別synchronized 進行保護
代碼塊3.1
這種方式是4種種最差的。因為它的保護范圍最高,并發性最差。此種情景,不適合此種方式。類級別的加鎖,一般使用在單例模式(雙重校驗鎖)。一句話,要判斷同步代碼塊的合理大小,需要在各種設計需求之間進行權衡,包括安全性,簡單性和性能。
synchronized不能修飾構造函數。
4.認識Lock
?與synchronized不同,要手動創建鎖,釋放鎖,獲取鎖。如下
Lock?lock?=?new?ReentrantLock();? lock.lock();?//critical?section? lock.unlock();SysnExampleV3.java, 展示了Lock的用法。
package?com.threadexample.sysn; import?java.util.Random; import?java.util.concurrent.TimeUnit; import?java.util.concurrent.locks.Lock; import?java.util.concurrent.locks.ReentrantLock;public?class?SysnExampleV3?{static?class?Task?implements??Runnable{private?final?Lock?lock?=?new?ReentrantLock();private?Integer?count=0;private?int?cycleSize;public???Task(int?cycleSize)?{this.cycleSize=cycleSize;}@Overridepublic?void?run()?{for(int?i=0;i<cycleSize;i++){try?{if(lock.tryLock(10,?TimeUnit.SECONDS)){this.count++;}}?catch?(InterruptedException?e)?{e.printStackTrace();}finally?{lock.unlock();}}}//一般這樣實現/*public?void?run()?{for(int?i=0;i<cycleSize;i++){lock.lock();??//?block?until?condition?holdstry?{this.count++;}?finally?{lock.unlock();}}}*/public?int?getCount(){return?this.count;}}public?static?void?main(String[]?args)?throws?InterruptedException?{Task?task?=?new?Task(1000);Task?task2?=?new?Task(1000);Thread?t1=new?Thread(task);Thread?t2=new?Thread(task);t1.start();t2.start();t1.join();t2.join();System.out.println("計數(線程數*循環數)="+task.getCount());} }鎖像synchronized同步塊一樣,是一種線程同步機制,但比Java中的synchronized同步塊更復雜。java提供了以下的鎖。
這幾種鎖的區別與原理,本文不做深入探討。
5.synchronized vs Lock
synchronized同步塊 不提供超時功能,Lock提供了超時功能,使用Lock.tryLock(long timeout, TimeUnit timeUnit)
synchronized同步塊,使用簡單快捷,這一點也造成了它的濫用。可以配合使用wait(),notify()。lock屬于JUC的一部分。
synchronized造成的線程阻塞,可以被dump,而lock造成的線程阻塞不能dump。
synchronized是托管給JVM執行的,而lock是java寫的控制鎖的代碼。在Java1.5中,synchronize是性能低效的。因為這是一個重量級操作,需要調用操作接口,導致有可能加鎖消耗的系統時間比加鎖以外的操作還多。相比之下使用Java提供的Lock對象,性能更高一些。但是到了Java1.6,發生了變化。synchronize在語義上很清晰,可以進行很多優化,有適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他們也更支持synchronize,在未來的版本中還有優化余地。
lock有公平鎖,非公平鎖之分。synchronized只有非公平
synchronized原始采用的是CPU悲觀鎖機制,即線程獲得的是獨占鎖;Lock用的是樂觀鎖方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。樂觀鎖實現的機制就是CAS操作(Compare and Swap)。
lock額外提供了Conditon
資源
http://ifeve.com/synchronization/
http://ifeve.com/locks/
http://blog.csdn.net/natian306/article/details/18504111
?
轉載于:https://blog.51cto.com/dba10g/1793815
總結
以上是生活随笔為你收集整理的线程基础知识系列(三)线程的同步的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 11. Container With M
- 下一篇: 1.1 sikuli 安装