日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

线程基础知识系列(三)线程的同步

發布時間:2024/4/13 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 线程基础知识系列(三)线程的同步 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本文是系列的第三篇,前面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 命令查看字節碼文件 ,可以很容易找到原因。

    ?????...42:?getfield??????#3??????????????????//?Field?count:Ljava/lang/Integer;45:?astore????????447:?aload_348:?aload_349:?getfield??????#3??????????????????//?Field?count:Ljava/lang/Integer;52:?invokevirtual?#12?????????????????//?Method?java/lang/Integer.intValue:()I55:?iconst_156:?iadd57:?invokestatic??#2??????????????????//?Method?java/lang/Integer.valueOf:(I)Ljava/lang/Integer;60:?dup_x161:?putfield??????#3??????????????????//?Field?count:Ljava/lang/Integer;...

    簡單的一個自增操作,被分解為

  • 獲取當前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

  • public?void?run()?{synchronized?(this){for(int?i=0;i<cycleSize;i++){this.count++;}} }

    或者

    代碼塊1.2

    public?void?run()?{for(int?i=0;i<cycleSize;i++){synchronized?(this){this.count++;}}}

    兩個方法的區別是synchronized塊的保護范圍區別,結果是一樣的,前者保護的范圍大一些,但上下文切換少一些;后者與之相反。具體哪個形式更好,具體要看保護的代碼塊邏輯了。

    ?? 2.將方法用synchronized聲明

    代碼塊2.1

    public?synchronized?void?run()?{for(int?i=0;i<cycleSize;i++){this.count++;}}

    這種方式,保護效果與代碼塊1.1達到的效果是一樣的。

    3.在類級別synchronized 進行保護

    代碼塊3.1

    public??void?run()?{synchronized?(SysnExampleV2.class)?{for?(int?i?=?0;?i?<?cycleSize;?i++)?{this.count++;}} }

    這種方式是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

    總結

    以上是生活随笔為你收集整理的线程基础知识系列(三)线程的同步的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。