Java多線程間的同步(一)

日常開發(fā)中往往會遇到多個線程循環(huán)操作某個對象中的某個方法或者間接處理某個對象,如果當前某個對象被多個線程同時操作的話,勢必會造成同步的問題,即線程A操作了對象object從而改變了該object的值,此刻線程A獲取到的object的值還是之前的值。那么類似這種情況下,我們通常就要用如下幾種方式去解決該問題。

1、synchronized 關(guān)鍵字

2、Lock

3、ReadWriteLock

4、其他方式

(一)synchronized

是java中表示同步代碼快的關(guān)鍵字。可以放在方法修飾符前,比如private synchronized void test(){},也可以放在方法內(nèi)部,修飾某一段特定的代碼。synchronized有一個地方需要注意,就是在給普通方法加鎖與給靜態(tài)方法加鎖機制是不一樣的。synchronized在靜態(tài)方法上表示調(diào)用前要獲得類的鎖,而在非靜態(tài)方法上表示調(diào)用此方法前要獲得對象的鎖,synchronized可作用于instance變量、object reference(對象引用)、static函數(shù)和class literals(類名稱字面常量)身上。。

如下代碼所示:

public class SynTest {   
  
private static String a="a";   
  
//等同于方法test2
public synchronized void test1(String b){ //調(diào)用前要取得SynTest 實例化后對象的鎖   
   System.out.println(a+b);   
}   
public void test2(String b){   
   synchronized (this) {//取得SynTest 實例化后對象的鎖   
    System.out.println(a+b);   
   }   
}   
//等同于方法test4
public synchronized static void test3(String b){//調(diào)用前要取得SynTest .class類的鎖   
   System.out.println(b+a);   
}   
public static void test4(String b){   
   synchronized (SynTest .class) { //取得SynTest .class類的鎖   
    System.out.println(a+b);   
}   

注意:

1、test1和test2中synchronized 鎖定的是SynTest的對象即該類的實例化對象,也可認為是object reference(對象引用), test3和test4中synchronized 鎖定的是SynTest對象所屬的類(Class,而不再是由這個Class產(chǎn)生的某個具體對象了),SynTest.class和synTest.getClass作用于同步鎖是不一樣的,不能用synTest.getClass()來達到鎖這個Class的目的。synTest指的是由SynTest類產(chǎn)生的對象。

2、對synchronized(this)的一些理解
一、當兩個并發(fā)線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內(nèi)只能有一個線程得到執(zhí)行。另一個線程必須等待當前線程執(zhí)行完這個代碼塊以后才能執(zhí)行該代碼塊。

二、然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。

三、尤其關(guān)鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。

四、當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結(jié)果,其它線程對該object對象所有同步代碼部分的訪問都被暫時阻塞。

3、synchronized 方法的缺陷:若將一個大的方法聲明為synchronized 將會大大影響效率,典型地,若將線程類的方法 run() 聲明為synchronized ,由于在線程的整個生命期內(nèi)它一直在運行,因此將導致它對本類任何 synchronized 方法的調(diào)用都永遠不會成功。當然我們可以通過將訪問類成員變量的代碼放到專門的方法中,將其聲明為synchronized ,并在主方法中調(diào)用來解決這一問題,但是 Java 為我們提供了更好的解決辦法,那就是 synchronized 塊。

4、synchronized 塊:通過 synchronized關(guān)鍵字來聲明synchronized 塊。語法如下:

synchronized(syncObject) {  
  //允許訪問控制的代碼  
}  

synchronized 塊是這樣一個代碼塊,其中的代碼必須獲得對象 syncObject (如前所述,可以是類實例或類)的鎖方能執(zhí)行,具體機制同前所述。由于可以針對任意代碼塊,且可任意指定上鎖的對象,故靈活性較高。

5、如果只是想單純的鎖某段代碼塊,當有明確的對象作為鎖是可以的,如果沒有明確的對象作為鎖,則可以創(chuàng)建一個特殊的變量。如下:

class Foo implements Runnable
{
        private byte[] lock = new byte[0]; // 特殊的instance變量
        Public void methodA() 
        {
           synchronized(lock) { //… }
        }
        //…..
}

注:零長度的byte數(shù)組對象創(chuàng)建起來將比任何對象都經(jīng)濟――查看編譯后的字節(jié)碼:生成零長度的byte[]對象只需3條操作碼,而Object lock = new Object()則需要7行操作碼。

(二)Lock

是java.util.concurrent.locks包下的接口,Lock實現(xiàn)提供了比使用synchronized方法和語句可獲得的更廣泛的鎖定操作,因為Lock可以鎖定任意一段代碼

public class LockTest {  
    public static void main(String[] args) {  
        final Outputter output = new Outputter();  
        new Thread() {  
            public void run() {  
                output.output("Alex-Jerry");  
            };  
        }.start();        
        new Thread() {  
            public void run() {  
                output.output("AI-Curry");  
            };  
        }.start();  
    }  
}  
class Outputter {  
    private Lock lock = new ReentrantLock();// 鎖對象  
    public void output(String name) {  
        // TODO 線程輸出方法  
        lock.lock();// 得到鎖  
        try {  
            for(int i = 0; i < name.length(); i++) {  
                System.out.print(name.charAt(i));  
            }  
        } finally {  
            lock.unlock();// 釋放鎖  
        }  
    }  
}  

這樣就實現(xiàn)了和sychronized一樣的同步效果,需要注意的是,用sychronized修飾的方法或者語句塊在代碼執(zhí)行完之后鎖自動釋放,而用Lock需要我們手動釋放鎖,所以為了保證鎖最終被釋放(發(fā)生異常情況),要把互斥區(qū)放在try內(nèi),釋放鎖放在finally內(nèi)。

(三)ReadWriteLock

前兩種方式雖然解決了讀和寫、寫和寫之間的互斥,但是讀和讀之間同樣也是互斥的(不同得到線程可以同步執(zhí)行讀?。?,嚴重影響了效率,這個時候就該ReadWriteLock出場了。

class RWLock{      
    private int data;// 共享數(shù)據(jù)  
    private ReadWriteLock rwl = new ReentrantReadWriteLock();     
    public void set(int data) {  
        rwl.writeLock().lock();// 取到寫鎖  
        try {  
            System.out.println(Thread.currentThread().getName() + "準備寫入數(shù)據(jù)");  
            try {  
                Thread.sleep(20);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            this.data = data;  
            System.out.println(Thread.currentThread().getName() + "寫入" + this.data);  
        } finally {  
            rwl.writeLock().unlock();// 釋放寫鎖  
        }  
    }     
    public void get() {  
        rwl.readLock().lock();// 取到讀鎖  
        try {  
            System.out.println(Thread.currentThread().getName() + "準備讀取數(shù)據(jù)");  
            try {  
                Thread.sleep(20);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            System.out.println(Thread.currentThread().getName() + "讀取" + this.data);  
        } finally {  
            rwl.readLock().unlock();// 釋放讀鎖  
        }  
    }  
}  

測試:
public class ReadWriteLockTest {  
    public static void main(String[] args) {  
        final RWLock data = new RWLock();  
        for (int i = 0; i < 3; i++) {  
            new Thread(new Runnable() {  
                public void run() {  
                    for (int j = 0; j < 5; j++) {  
                        data.set(new Random().nextInt(30));  
                    }  
                }  
            }).start();  
        }         
        for (int i = 0; i < 3; i++) {  
            new Thread(new Runnable() {  
                public void run() {  
                    for (int j = 0; j < 5; j++) {  
                        data.get();  
                    }  
                }  
            }).start();  
        }  
    }  
}  

部分測試輸出結(jié)果如下:

Thread-4準備讀取數(shù)據(jù)  
Thread-3準備讀取數(shù)據(jù)  
Thread-5準備讀取數(shù)據(jù)  
Thread-5讀取18  
Thread-4讀取18  
Thread-3讀取18  
Thread-2準備寫入數(shù)據(jù)  
Thread-2寫入6  
Thread-2準備寫入數(shù)據(jù)  
Thread-2寫入10  
Thread-1準備寫入數(shù)據(jù)  
Thread-1寫入22  

(四)其他方式

線程同步的問題,最上面說的只是一種問題,往往導致同步問題的原因有多種。如線程A和線程B,線程B要等到線程A執(zhí)行完畢之后才能進行操作,或者子線程執(zhí)行結(jié)束才能讓主線程去操作等等,這種情況下,可解決的方案很多,如:使用FutureTask+Callable代替Runnable去執(zhí)行異步任務(FutureTask 的 get 方法,能夠阻塞主線程,直到子線程初始化完成,才繼續(xù)執(zhí)行主線程邏輯),使用handler機制,操作線程的wait、notify、join、yield等等。基于這些情況,下篇文章將會逐個去分析和解決。

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

相關(guān)閱讀更多精彩內(nèi)容

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