JAVA中线程同步的方法(7种)汇总
JAVA中線程同步的方法(7種)匯總
同步的方法:
一、同步方法
即有synchronized關鍵字修飾的方法。?由于java的每個對象都有一個內置鎖,當用此關鍵字修飾方法時,?內置鎖會保護整個方法。在調用該方法前,需要獲得內置鎖,否則就處于阻塞狀態。 注: synchronized關鍵字也可以修飾靜態方法,此時如果調用該靜態方法,將會鎖住整個類。?
二、同步代碼塊
即有synchronized關鍵字修飾的語句塊。?被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步 ? ? 代碼如:? synchronized(object){ } ???注:同步是一種高開銷的操作,因此應該盡量減少同步的內容。通常沒有必要同步整個方法,使用synchronized代碼塊同步關鍵代碼即可。? package com.xhj.thread;/*** 線程同步的運用* * @author XIEHEJUN* */public class SynchronizedThread {class Bank {private int account = 100;public int getAccount() {return account;}/*** 用同步方法實現* * @param money*/public synchronized void save(int money) {account += money;}/*** 用同步代碼塊實現* * @param money*/public void save1(int money) {synchronized (this) {account += money;}}} class NewThread implements Runnable {private Bank bank;public NewThread(Bank bank) {this.bank = bank;}@Overridepublic void run() {for (int i = 0; i < 10; i++) {// bank.save1(10);bank.save(10);System.out.println(i + "賬戶余額為:" + bank.getAccount());}}}/*** 建立線程,調用內部類*/public void 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();}public static void main(String[] args) {SynchronizedThread st = new SynchronizedThread();st.useThread();}}?
? ? ? ? ?? =====================================示例加講解
同步是多線程中的重要概念。同步的使用可以保證在多線程運行的環境中,程序不會產生設計之外的錯誤結果。同步的實現方式有兩種,同步方法和同步塊,這兩種方式都要用到synchronized關鍵字。
同步方法:給一個方法增加synchronized修飾符之后就可以使它成為同步方法,這個方法可以是靜態方法和非靜態方法,但是不能是抽象類的抽象方法,也不能是接口中的接口方法。下面代碼是一個同步方法的示例:
public synchronized void aMethod() { // do something } public static synchronized void anotherMethod() { // do something }線程在執行同步方法時是具有排它性的。當任意一個線程進入到一個對象的任意一個同步方法時,這個對象的所有同步方法都被鎖定了,在此期間,其他任何線程都不能訪問這個對象的任意一個同步方法,直到這個線程執行完它所調用的同步方法并從中退出,從而導致它釋放了該對象的同步鎖之后。在一個對象被某個線程鎖定之后,其他線程是可以訪問這個對象的所有非同步方法的。
同步塊:同步塊是通過鎖定一個指定的對象,來對同步塊中包含的代碼進行同步;而同步方法是對這個方法塊里的代碼進行同步,而這種情況下鎖定的對象就是同步方法所屬的主體對象自身。如果這個方法是靜態同步方法呢?那么線程鎖定的就不是這個類的對象了,也不是這個類自身,而是這個類對應的java.lang.Class類型的對象。同步方法和同步塊之間的相互制約只限于同一個對象之間,所以靜態同步方法只受它所屬類的其它靜態同步方法的制約,而跟這個類的實例(對象)沒有關系。
如果一個對象既有同步方法,又有同步塊,那么當其中任意一個同步方法或者同步塊被某個線程執行時,這個對象就被鎖定了,其他線程無法在此時訪問這個對象的同步方法,也不能執行同步塊。
synchronized?關鍵字用于保護共享數據。請大家注意“共享數據”,你一定要分清哪些數據是共享數據,請看下面的例子:
public class ThreadTest implements Runnable{public synchronized void run(){for(int i=0;i<10;i++) {System.out.print(" " + i);} }public static void main(String[] args) {Runnable r1 = new ThreadTest(); //也可寫成ThreadTest r1 = new ThreadTest();Runnable r2 = new ThreadTest();Thread t1 = new Thread(r1);Thread t2 = new Thread(r2);t1.start();t2.start(); }}?
在這個程序中,run()雖然被加上了synchronized?關鍵字,但保護的不是共享數據。因為這個程序中的t1,t2?是兩個對象(r1,r2)的線程。而不同的對象的數據是不同的,r1,r2?有各自的run()方法,所以輸出結果無法預知。
synchronized的目的是使同一個對象的多個線程,在某個時刻只有其中的一個線程可以訪問這個對象的synchronized?數據。每個對象都有一個“鎖標志”,當這個對象的一個線程訪問這個對象的某個synchronized?數據時,這個對象的所有被synchronized?修飾的數據將被上鎖(因為“鎖標志”被當前線程拿走了),只有當前線程訪問完它要訪問的synchronized?數據時,當前線程才會釋放“鎖標志”,這樣同一個對象的其它線程才有機會訪問synchronized?數據。
示例3:
public class ThreadTest implements Runnable{public synchronized void run(){for(int i=0;i<10;i++){System.out.print(" " + i);} }public static void main(String[] args){Runnable r = new ThreadTest();Thread t1 = new Thread(r);Thread t2 = new Thread(r);t1.start();t2.start(); }}?
如果你運行1000?次這個程序,它的輸出結果也一定每次都是:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9。因為這里的synchronized?保護的是共享數據。t1,t2?是同一個對象(r)的兩個線程,當其中的一個線程(例如:t1)開始執行run()方法時,由于run()受synchronized保護,所以同一個對象的其他線程(t2)無法訪問synchronized?方法(run?方法)。只有當t1執行完后t2?才有機會執行。
示例4:
public class ThreadTest implements Runnable{public void run(){synchronized(this){for(int i=0;i<10;i++){System.out.print(" " + i);} } }public static void main(String[] args){Runnable r = new ThreadTest();Thread t1 = new Thread(r);Thread t2 = new Thread(r);t1.start();t2.start(); } }?
這個程序與示例3?的運行結果一樣。在可能的情況下,應該把保護范圍縮到最小,可以用示例4?的形式,this?代表“這個對象”。沒有必要把整個run()保護起來,run()中的代碼只有一個for循環,所以只要保護for?循環就可以了。
示例5:
public class ThreadTest implements Runnable{public void run(){for(int k=0;k<5;k++){System.out.println(Thread.currentThread().getName()+ " : for loop : " + k);}synchronized(this){for(int k=0;k<5;k++) {System.out.println(Thread.currentThread().getName()+ " : synchronized for loop : " + k);}} }public static void main(String[] args){Runnable r = new ThreadTest();Thread t1 = new Thread(r,"t1_name");Thread t2 = new Thread(r,"t2_name");t1.start();t2.start(); } }?
運行結果:
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?循環沒有受synchronized?保護。對于第一個for?循環,t1,t2?可以同時訪問。運行結果表明t1?執行到了k=2?時,t2?開始執行了。t1?首先執行完了第一個for?循環,此時t2還沒有執行完第一個for?循環(t2?剛執行到k=2)。t1?開始執行第二個for?循環,當t1的第二個for?循環執行到k=1?時,t2?的第一個for?循環執行完了。t2?想開始執行第二個for?循環,但由于t1?首先執行了第二個for?循環,這個對象的鎖標志自然在t1?手中(synchronized?方法的執行權也就落到了t1?手中),在t1?沒執行完第二個for?循環的時候,它是不會釋放鎖標志的。所以t2?必須等到t1?執行完第二個for?循環后,它才可以執行第二個for?循環。
=====================================
三、wait與notify
wait():使一個線程處于等待狀態,并且釋放所持有的對象的lock。
sleep():使一個正在運行的線程處于睡眠狀態,是一個靜態方法,調用此方法要捕捉InterruptedException異常。
notify():喚醒一個處于等待狀態的線程,注意的是在調用此方法的時候,并不能確切的喚醒某一個等待狀態的線程,而是由JVM確定喚醒哪個線程,而且不是按優先級。
Allnotity():喚醒所有處入等待狀態的線程,注意并不是給所有喚醒線程一個對象的鎖,而是讓它們競爭。
詳細見:wait、notify、notifyAll的使用方法
四、使用特殊域變量(volatile)實現線程同步
? ? a.volatile關鍵字為域變量的訪問提供了一種免鎖機制 ? ? b.使用volatile修飾域相當于告訴虛擬機該域可能會被其他線程更新 ? ? c.因此每次使用該域就要重新計算,而不是使用寄存器中的值? ? ? d.volatile不會提供任何原子操作,它也不能用來修飾final類型的變量? ? ?? ? ? 例如:? ? ? ? ? 在上面的例子當中,只需在account前面加上volatile修飾,即可實現線程同步。? ? ?? ? ? 代碼實例:? ? //只給出要修改的代碼,其余代碼與上同class Bank {//需要同步的變量加上volatileprivate volatile int account = 100;public int getAccount() {return account;}//這里不再需要synchronized public void save(int money) {account += money;}} ? ? 注:多線程中的非同步問題主要出現在對域的讀寫上,如果讓域自身避免這個問題,則就不需要修改操作該域的方法。? ? ? 用final域,有鎖保護的域和volatile域可以避免非同步的問題。?五、使用重入鎖實現線程同步
? ? 在JavaSE5.0中新增了一個java.util.concurrent包來支持同步。? ? ? ReentrantLock類是可重入、互斥、實現了Lock接口的鎖,它與使用synchronized方法和快具有相同的基本行為和語義,并且擴展了其能力。 ?ReenreantLock類的常用方法有: ReentrantLock() : 創建一個ReentrantLock實例 lock() : 獲得鎖 unlock() : 釋放鎖 注:ReentrantLock()還有一個可以創建公平鎖的構造方法,但由于能大幅度降低程序運行效率,不推薦使用? ? ? ? ?? ? ? 例如:? ? ? ? ? 在上面例子的基礎上,改寫后的代碼為:? //只給出要修改的代碼,其余代碼與上同class Bank {private int account = 100;//需要聲明這個鎖private Lock lock = new ReentrantLock();public int getAccount() {return account;}//這里不再需要synchronized public void save(int money) {lock.lock();try{account += money;}finally{lock.unlock();}}} ? ??注:關于Lock對象和synchronized關鍵字的選擇:? ? ? ? ? a.最好兩個都不用,使用一種java.util.concurrent包提供的機制,能夠幫助用戶處理所有與鎖相關的代碼。? ? ? ? ? b.如果synchronized關鍵字能滿足用戶的需求,就用synchronized,因為它能簡化代碼? ? ? ? ? c.如果需要更高級的功能,就用ReentrantLock類,此時要注意及時釋放鎖,否則會出現死鎖,通常在finally代碼釋放鎖? ?六、使用局部變量實現線程同步
? ? 如果使用ThreadLocal管理變量,則每一個使用該變量的線程都獲得該變量的副本,副本之間相互獨立,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產生影響。 ?? ? ThreadLocal 類的常用方法 ThreadLocal() : 創建一個線程本地變量 get() : 返回此線程局部變量的當前線程副本中的值 initialValue() : 返回此線程局部變量的當前線程的"初始值" set(T value) : 將此線程局部變量的當前線程副本中的值設置為value ? ? 例如:? ? ? ? ? 在上面例子基礎上,修改后的代碼為:? //只改Bank類,其余代碼與上同public class Bank{//使用ThreadLocal類管理共享變量accountprivate static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){@Overrideprotected Integer initialValue(){return 100;}};public void save(int money){account.set(account.get()+money);}public int getAccount(){return account.get();}} ? ? 注:ThreadLocal與同步機制? ? ? ? ? a.ThreadLocal與同步機制都是為了解決多線程中相同變量的訪問沖突問題。? ? ? ? ? b.前者采用以"空間換時間"的方法,后者采用以"時間換空間"的方式 ?七、使用阻塞隊列實現線程同步
前面5種同步方式都是在底層實現的線程同步,但是我們在實際開發當中,應當盡量遠離底層結構。 使用javaSE5.0版本中新增的java.util.concurrent包將有助于簡化開發。 本小節主要是使用LinkedBlockingQueue<E>來實現線程的同步 LinkedBlockingQueue<E>是一個基于已連接節點的,范圍任意的blocking queue。 隊列是先進先出的順序(FIFO),關于隊列以后會詳細講解~LinkedBlockingQueue 類常用方法?LinkedBlockingQueue() : 創建一個容量為Integer.MAX_VALUE的LinkedBlockingQueue put(E e) : 在隊尾添加一個元素,如果隊列滿則阻塞 size() : 返回隊列中的元素個數 take() : 移除并返回隊頭元素,如果隊列空則阻塞代碼實例:?實現商家生產商品和買賣商品的同步
?
注:BlockingQueue<E>定義了阻塞隊列的常用方法,尤其是三種添加元素的方法,我們要多加注意,當隊列滿時:
add()方法會拋出異常
offer()方法返回false
put()方法會阻塞
?
?
7.使用原子變量實現線程同步
?
需要使用線程同步的根本原因在于對普通變量的操作不是原子的。
那么什么是原子操作呢?原子操作就是指將讀取變量值、修改變量值、保存變量值看成一個整體來操作即-這幾種行為要么同時完成,要么都不完成。在java的util.concurrent.atomic包中提供了創建了原子類型變量的工具類,使用該類可以簡化線程同步。其中AtomicInteger?表可以用原子方式更新int的值,可用在應用程序中(如以原子方式增加的計數器),但不能用于替換Integer;可擴展Number,允許那些處理機遇數字類的工具和實用工具進行統一訪問。
AtomicInteger類常用方法:
AtomicInteger(int initialValue) : 創建具有給定初始值的新的
AtomicIntegeraddAddGet(int dalta) : 以原子方式將給定值與當前值相加
get() : 獲取當前值
代碼實例:
只改Bank類,其余代碼與上面第一個例子同
class Bank {private AtomicInteger account = new AtomicInteger(100);public AtomicInteger getAccount() {return account; } public void save(int money) {account.addAndGet(money);} }補充--原子操作主要有:
對于引用變量和大多數原始變量(long和double除外)的讀寫操作;
對于所有使用volatile修飾的變量(包括long和double)的讀寫操作。
總結
以上是生活随笔為你收集整理的JAVA中线程同步的方法(7种)汇总的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 区块链100问 第一问
- 下一篇: 分布式系统Paxos算法