JAVA中線程同步的方法(7種)匯總

https://www.cnblogs.com/duanxz/p/3709608.html?utm_source=tuicool&utm_medium=referral

同步的方法:

一、同步方法

  即有synchronized關(guān)鍵字修飾的方法。?由于java的每個對象都有一個內(nèi)置鎖,當用此關(guān)鍵字修飾方法時,?內(nèi)置鎖會保護整個方法。在調(diào)用該方法前,需要獲得內(nèi)置鎖,否則就處于阻塞狀態(tài)。

注: synchronized關(guān)鍵字也可以修飾靜態(tài)方法,此時如果調(diào)用該靜態(tài)方法,將會鎖住整個類。


二、同步代碼塊

  即有synchronized關(guān)鍵字修飾的語句塊。?被該關(guān)鍵字修飾的語句塊會自動被加上內(nèi)置鎖,從而實現(xiàn)同步

? ? 代碼如:?

synchronized(object){

}

注:同步是一種高開銷的操作,因此應(yīng)該盡量減少同步的內(nèi)容。通常沒有必要同步整個方法,使用synchronized代碼塊同步關(guān)鍵代碼即可。

package com.xhj.thread;

? ? /**? ? * 線程同步的運用

? ? *

? ? * @author XIEHEJUN

? ? *

? ? */publicclass SynchronizedThread {

? ? ? ? class Bank {

? ? ? ? ? ? privateintaccount = 100;

? ? ? ? ? ? publicint getAccount() {

? ? ? ? ? ? ? ? return account;

? ? ? ? ? ? }

? ? ? ? ? ? /**? ? ? ? ? ? * 用同步方法實現(xiàn)

? ? ? ? ? ? *

? ? ? ? ? ? * @param money

? ? ? ? ? ? */publicsynchronizedvoidsave(int money) {

? ? ? ? ? ? ? ? account += money;

? ? ? ? ? ? }

? ? ? ? ? ? /**? ? ? ? ? ? * 用同步代碼塊實現(xiàn)

? ? ? ? ? ? *

? ? ? ? ? ? * @param money

? ? ? ? ? ? */publicvoidsave1(int money) {

? ? ? ? ? ? ? ? synchronized(this) {

? ? ? ? ? ? ? ? ? ? account += money;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

classNewThreadimplements Runnable {

? ? ? ? ? ? private Bank bank;

? ? ? ? ? ? public NewThread(Bank bank) {

? ? ? ? ? ? ? ? this.bank = bank;

? ? ? ? ? ? }

? ? ? ? ? ? @Override

? ? ? ? ? ? publicvoid run() {

? ? ? ? ? ? ? ? for(inti = 0; i < 10; i++) {

? ? ? ? ? ? ? ? ? ? // bank.save1(10);bank.save(10);

? ? ? ? ? ? ? ? ? ? System.out.println(i + "賬戶余額為:" + bank.getAccount());

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? /**? ? ? ? * 建立線程,調(diào)用內(nèi)部類

? ? ? ? */publicvoid useThread() {

? ? ? ? ? ? Bank bank =new Bank();

? ? ? ? ? ? NewThread new_thread =new NewThread(bank);

? ? ? ? ? ? System.out.println("線程1");

? ? ? ? ? ? Thread thread1 =new Thread(new_thread);

? ? ? ? ? ? thread1.start();

? ? ? ? ? ? System.out.println("線程2");

? ? ? ? ? ? Thread thread2 =new Thread(new_thread);

? ? ? ? ? ? thread2.start();

? ? ? ? }

? ? ? ? publicstaticvoid main(String[] args) {

? ? ? ? ? ? SynchronizedThread st =new SynchronizedThread();

? ? ? ? ? ? st.useThread();

? ? ? ? }

? ? }




=====================================

示例加講解

同步是多線程中的重要概念。同步的使用可以保證在多線程運行的環(huán)境中,程序不會產(chǎn)生設(shè)計之外的錯誤結(jié)果。同步的實現(xiàn)方式有兩種,同步方法和同步塊,這兩種方式都要用到synchronized關(guān)鍵字。同步相關(guān)的參見《Synchronized之一:基本使用

同步方法:給一個方法增加synchronized修飾符之后就可以使它成為同步方法,這個方法可以是靜態(tài)方法和非靜態(tài)方法,但是不能是抽象類的抽象方法,也不能是接口中的接口方法。下面代碼是一個同步方法的示例:

publicsynchronizedvoid aMethod() {

? ? // do something } publicstaticsynchronizedvoid anotherMethod() {

? ? // do something }

線程在執(zhí)行同步方法時是具有排它性的。當任意一個線程進入到一個對象的任意一個同步方法時,這個對象的所有同步方法都被鎖定了,在此期間,其他任何線程都不能訪問這個對象的任意一個同步方法,直到這個線程執(zhí)行完它所調(diào)用的同步方法并從中退出,從而導致它釋放了該對象的同步鎖之后。在一個對象被某個線程鎖定之后,其他線程是可以訪問這個對象的所有非同步方法的。

同步塊:同步塊是通過鎖定一個指定的對象,來對同步塊中包含的代碼進行同步;而同步方法是對這個方法塊里的代碼進行同步,而這種情況下鎖定的對象就是同步方法所屬的主體對象自身。如果這個方法是靜態(tài)同步方法呢?那么線程鎖定的就不是這個類的對象了,也不是這個類自身,而是這個類對應(yīng)的java.lang.Class類型的對象。同步方法和同步塊之間的相互制約只限于同一個對象之間,所以靜態(tài)同步方法只受它所屬類的其它靜態(tài)同步方法的制約,而跟這個類的實例(對象)沒有關(guān)系。

如果一個對象既有同步方法,又有同步塊,那么當其中任意一個同步方法或者同步塊被某個線程執(zhí)行時,這個對象就被鎖定了,其他線程無法在此時訪問這個對象的同步方法,也不能執(zhí)行同步塊。

synchronized?關(guān)鍵字用于保護共享數(shù)據(jù)。請大家注意“共享數(shù)據(jù)”,你一定要分清哪些數(shù)據(jù)是共享數(shù)據(jù),請看下面的例子:

publicclassThreadTestimplements Runnable{publicsynchronizedvoid run(){

  for(inti=0;i<10;i++) {

    System.out.print(" " + i);

  }

}publicstaticvoid main(String[] args) {

  Runnable r1 =newThreadTest();//也可寫成ThreadTest r1 = new ThreadTest();  Runnable r2 =new ThreadTest();

  Thread t1 =new Thread(r1);

  Thread t2 =new Thread(r2);

  t1.start();

  t2.start();

}}


在這個程序中,run()雖然被加上了synchronized?關(guān)鍵字,但保護的不是共享數(shù)據(jù)。因為這個程序中的t1,t2?是兩個對象(r1,r2)的線程。而不同的對象的數(shù)據(jù)是不同的,r1,r2?有各自的run()方法,所以輸出結(jié)果無法預(yù)知。

synchronized的目的是使同一個對象的多個線程,在某個時刻只有其中的一個線程可以訪問這個對象的synchronized?數(shù)據(jù)。每個對象都有一個“鎖標志”,當這個對象的一個線程訪問這個對象的某個synchronized?數(shù)據(jù)時,這個對象的所有被synchronized?修飾的數(shù)據(jù)將被上鎖(因為“鎖標志”被當前線程拿走了),只有當前線程訪問完它要訪問的synchronized?數(shù)據(jù)時,當前線程才會釋放“鎖標志”,這樣同一個對象的其它線程才有機會訪問synchronized?數(shù)據(jù)。

示例3:

publicclassThreadTestimplements Runnable{publicsynchronizedvoid run(){

  for(inti=0;i<10;i++){

    System.out.print(" " + i);

  }

}publicstaticvoid main(String[] args){

  Runnable r =new ThreadTest();

  Thread t1 =new Thread(r);

  Thread t2 =new Thread(r);

  t1.start();

  t2.start();

}}


如果你運行1000?次這個程序,它的輸出結(jié)果也一定每次都是:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9。因為這里的synchronized?保護的是共享數(shù)據(jù)。t1,t2?是同一個對象(r)的兩個線程,當其中的一個線程(例如:t1)開始執(zhí)行run()方法時,由于run()受synchronized保護,所以同一個對象的其他線程(t2)無法訪問synchronized?方法(run?方法)。只有當t1執(zhí)行完后t2?才有機會執(zhí)行。

示例4:

publicclassThreadTestimplements Runnable{publicvoid run(){

? ? synchronized(this){

? ? for(inti=0;i<10;i++){

? ? ? ? System.out.print(" " + i);

? ? }

}

}publicstaticvoid main(String[] args){

? ? Runnable r =new ThreadTest();

? ? Thread t1 =new Thread(r);

? ? Thread t2 =new Thread(r);

? ? t1.start();

? ? t2.start();

}

}? ?


這個程序與示例3?的運行結(jié)果一樣。在可能的情況下,應(yīng)該把保護范圍縮到最小,可以用示例4?的形式,this?代表“這個對象”。沒有必要把整個run()保護起來,run()中的代碼只有一個for循環(huán),所以只要保護for?循環(huán)就可以了。

示例5:

publicclassThreadTestimplements Runnable{publicvoid run(){

  for(intk=0;k<5;k++){

    System.out.println(Thread.currentThread().getName()+ " : for loop : " + k);

  }synchronized(this){

  for(intk=0;k<5;k++) {

    System.out.println(Thread.currentThread().getName()+ " : synchronized for loop : " + k);

  }} }publicstaticvoid main(String[] args){

  Runnable r =new ThreadTest();

  Thread t1 =newThread(r,"t1_name");

  Thread t2 =newThread(r,"t2_name");

  t1.start();

  t2.start();

} }


運行結(jié)果:

t1_name : for loop : 0

t1_name : for loop : 1

t1_name : for loop : 2

t2_name : for loop : 0

t1_name : for loop : 3

t2_name : for loop : 1

t1_name : for loop : 4

t2_name : for loop : 2

t1_name : synchronized for loop : 0

t2_name : for loop : 3

t1_name : synchronized for loop : 1

t2_name : for loop : 4

t1_name : synchronized for loop : 2

t1_name : synchronized for loop : 3

t1_name : synchronized for loop : 4

t2_name : synchronized for loop : 0

t2_name : synchronized for loop : 1

t2_name : synchronized for loop : 2

t2_name : synchronized for loop : 3

t2_name : synchronized for loop : 4

第一個for?循環(huán)沒有受synchronized?保護。對于第一個for?循環(huán),t1,t2?可以同時訪問。運行結(jié)果表明t1?執(zhí)行到了k=2?時,t2?開始執(zhí)行了。t1?首先執(zhí)行完了第一個for?循環(huán),此時t2還沒有執(zhí)行完第一個for?循環(huán)(t2?剛執(zhí)行到k=2)。t1?開始執(zhí)行第二個for?循環(huán),當t1的第二個for?循環(huán)執(zhí)行到k=1?時,t2?的第一個for?循環(huán)執(zhí)行完了。t2?想開始執(zhí)行第二個for?循環(huán),但由于t1?首先執(zhí)行了第二個for?循環(huán),這個對象的鎖標志自然在t1?手中(synchronized?方法的執(zhí)行權(quán)也就落到了t1?手中),在t1?沒執(zhí)行完第二個for?循環(huán)的時候,它是不會釋放鎖標志的。所以t2?必須等到t1?執(zhí)行完第二個for?循環(huán)后,它才可以執(zhí)行第二個for?循環(huán)。

=====================================

三、wait與notify

wait():使一個線程處于等待狀態(tài),并且釋放所持有的對象的lock。

sleep():使一個正在運行的線程處于睡眠狀態(tài),是一個靜態(tài)方法,調(diào)用此方法要捕捉InterruptedException異常。

notify():喚醒一個處于等待狀態(tài)的線程,注意的是在調(diào)用此方法的時候,并不能確切的喚醒某一個等待狀態(tài)的線程,而是由JVM確定喚醒哪個線程,而且不是按優(yōu)先級。

notifyAll():喚醒所有處入等待狀態(tài)的線程,注意并不是給所有喚醒線程一個對象的鎖,而是讓它們競爭。

詳細見:wait、notify、notifyAll的使用方法

四、使用特殊域變量(volatile)實現(xiàn)線程同步

見《Java并發(fā)編程之三:volatile關(guān)鍵字解析 轉(zhuǎn)載

下面使用volatile示例場景是錯誤的:

package com.dxz.volatiledemo;//只給出要修改的代碼,其余代碼與上同class Bank {

? ? // 需要同步的變量加上volatileprivatevolatileintaccount = 100;

? ? publicint getAccount() {

? ? ? ? return account;

? ? }

? ? // 這里不再需要synchronizedpublicvoidsave(int money) {

? ? ? ? account += money;

? ? }

}package com.dxz.volatiledemo;import java.util.concurrent.CountDownLatch;publicclassMyThreadimplements Runnable {

? ? Bank bank;

? ? CountDownLatch cdl;

? ? MyThread(Bank bank, CountDownLatch cdl) {

? ? ? ? this.bank = bank;

? ? ? ? this.cdl = cdl;

? ? }

? ? @Override

? ? publicvoid run() {

? ? ? ? for(inti = 1; i < 101; i++) {

? ? ? ? ? ? bank.save(i);

? ? ? ? }

? ? ? ? cdl.countDown();

? ? }

}package com.dxz.volatiledemo;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.TimeUnit;publicclass Client {

? ? publicstaticvoidmain(String[] args)throws InterruptedException {

? ? ? ? intnum = 70;

? ? ? ? ExecutorService es = Executors.newFixedThreadPool(num);

? ? ? ? Bank bank =new Bank();

? ? ? ? CountDownLatch cdl =new CountDownLatch(num);

? ? ? ? for(inti = 0; i

? ? ? ? ? ? MyThread mt =new MyThread(bank, cdl);

? ? ? ? ? ? es.submit(mt);

? ? ? ? ? ? //TimeUnit.SECONDS.sleep(1);? ? ? ? }

? ? ? ? cdl.await();

? ? ? ? es.shutdown();

? ? ? ? System.out.println(bank.getAccount());


? ? }

}

每個線程間隔一秒時,計算結(jié)果是:353600,如果去掉線程間隔讓線程競爭起來,結(jié)果很多時候不一致。驗證了volatile變量的以下特性:

可見性。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。

原子性:對任意單個volatile變量的讀/寫具有原子性,但類似于volatile++這種復合操作不具有原子性。

五、使用重入鎖實現(xiàn)線程同步

JavaSE5.0中新增了一個java.util.concurrent包來支持同步。

? ? ReentrantLock類是可重入、互斥、實現(xiàn)了Lock接口的鎖,它與使用synchronized方法和快具有相同的基本行為和語義,并且擴展了其能力。

?ReenreantLock類的常用方法有:

ReentrantLock() : 創(chuàng)建一個ReentrantLock實例

lock() : 獲得鎖

unlock() : 釋放鎖

注:ReentrantLock()還有一個可以創(chuàng)建公平鎖的構(gòu)造方法,但由于能大幅度降低程序運行效率,不推薦使用?


? ? 例如:?

? ? ? ? 在上面例子的基礎(chǔ)上,改寫后的代碼為:?

//只給出要修改的代碼,其余代碼與上同class Bank {


? ? ? ? ? ? privateintaccount = 100;

? ? ? ? ? ? //需要聲明這個鎖privateLock lock =new ReentrantLock();

? ? ? ? ? ? publicint getAccount() {

? ? ? ? ? ? ? ? return account;

? ? ? ? ? ? }

? ? ? ? ? ? //這里不再需要synchronized publicvoidsave(int money) {

? ? ? ? ? ? ? ? lock.lock();

? ? ? ? ? ? ? ? try{

? ? ? ? ? ? ? ? ? ? account += money;

? ? ? ? ? ? ? ? }finally{

? ? ? ? ? ? ? ? ? ? lock.unlock();

? ? ? ? ? ? ? ? }


? ? ? ? ? ? }

? ? ? ? }

?注:關(guān)于Lock對象和synchronized關(guān)鍵字的選擇:

? ? ? ? a.最好兩個都不用,使用一種java.util.concurrent包提供的機制,能夠幫助用戶處理所有與鎖相關(guān)的代碼。?

? ? ? ? b.如果synchronized關(guān)鍵字能滿足用戶的需求,就用synchronized,因為它能簡化代碼?

? ? ? ? c.如果需要更高級的功能,就用ReentrantLock類,此時要注意及時釋放鎖,否則會出現(xiàn)死鎖,通常在finally代碼釋放鎖?


六、使用局部變量實現(xiàn)線程同步

? ? 如果使用ThreadLocal管理變量,則每一個使用該變量的線程都獲得該變量的副本,副本之間相互獨立,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產(chǎn)生影響。

?? ? ThreadLocal 類的常用方法

ThreadLocal() : 創(chuàng)建一個線程本地變量

get() : 返回此線程局部變量的當前線程副本中的值

initialValue() : 返回此線程局部變量的當前線程的"初始值"

set(T value) : 將此線程局部變量的當前線程副本中的值設(shè)置為value

? ? 例如:?

? ? ? ? 在上面例子基礎(chǔ)上,修改后的代碼為:?

//只改Bank類,其余代碼與上同publicclass Bank{

? ? ? ? ? ? //使用ThreadLocal類管理共享變量accountprivatestaticThreadLocal account =newThreadLocal(){

? ? ? ? ? ? ? ? @Override

? ? ? ? ? ? ? ? protected Integer initialValue(){

? ? ? ? ? ? ? ? ? ? return100;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? };

? ? ? ? ? ? publicvoidsave(int money){

? ? ? ? ? ? ? ? account.set(account.get()+money);

? ? ? ? ? ? }

? ? ? ? ? ? publicint getAccount(){

? ? ? ? ? ? ? ? return account.get();

? ? ? ? ? ? }

? ? ? ? }

? ? 注:ThreadLocal與同步機制?

? ? ? ? a.ThreadLocal與同步機制都是為了解決多線程中相同變量的訪問沖突問題。?

? ? ? ? b.前者采用以"空間換時間"的方法,后者采用以"時間換空間"的方式


七、使用阻塞隊列實現(xiàn)線程同步

前面5種同步方式都是在底層實現(xiàn)的線程同步,但是我們在實際開發(fā)當中,應(yīng)當盡量遠離底層結(jié)構(gòu)。 使用javaSE5.0版本中新增的java.util.concurrent包將有助于簡化開發(fā)。 本小節(jié)主要是使用LinkedBlockingQueue<E>來實現(xiàn)線程的同步 LinkedBlockingQueue<E>是一個基于已連接節(jié)點的,范圍任意的blocking queue。 隊列是先進先出的順序(FIFO),關(guān)于隊列以后會詳細講解~LinkedBlockingQueue 類常用方法?LinkedBlockingQueue() : 創(chuàng)建一個容量為Integer.MAX_VALUE的LinkedBlockingQueue put(E e) : 在隊尾添加一個元素,如果隊列滿則阻塞 size() : 返回隊列中的元素個數(shù) take() : 移除并返回隊頭元素,如果隊列空則阻塞代碼實例:?實現(xiàn)商家生產(chǎn)商品和買賣商品的同步


注:BlockingQueue<E>定義了阻塞隊列的常用方法,尤其是四種操作元素的方法,我們要多加注意,當隊列滿或空時:

  add()方法會拋出異常

  offer()方法返回false

  take()方法會阻塞

  put()方法會阻塞



7.使用原子變量實現(xiàn)線程同步


需要使用線程同步的根本原因在于對普通變量的操作不是原子的。

那么什么是原子操作呢?原子操作就是指將讀取變量值、修改變量值、保存變量值看成一個整體來操作即-這幾種行為要么同時完成,要么都不完成。在java的util.concurrent.atomic包中提供了創(chuàng)建了原子類型變量的工具類,使用該類可以簡化線程同步。其中AtomicInteger表可以用原子方式更新int的值,可用在應(yīng)用程序中(如以原子方式增加的計數(shù)器),但不能用于替換Integer;可擴展Number,允許那些處理機遇數(shù)字類的工具和實用工具進行統(tǒng)一訪問。

AtomicInteger類常用方法:

AtomicInteger(int initialValue) : 創(chuàng)建具有給定初始值的新的

AtomicIntegeraddAddGet(int dalta) : 以原子方式將給定值與當前值相加

get() : 獲取當前值

代碼實例:

只改Bank類,其余代碼與上面第一個例子同

class Bank {

? ? privateAtomicInteger account =newAtomicInteger(100);

? ? public AtomicInteger getAccount() {

? ? ? ? return account;

? ? }

? ? publicvoidsave(int money) {

? ? ? ? account.addAndGet(money);

? ? }

}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容