java死磕多線程(synchronized,Lock對比分析)

線程安全問題

雖然多線程編程極大地提高了效率,但是也會帶來一定的隱患。舉一個例子:我們要兩個線程修改并交替打印變量a

public class VolatileDemo {
    int a = 0;

    public void addNum() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        a++;//注意這里
    }

    public static void main(String[] args) {
        final VolatileDemo volatileDemo = new VolatileDemo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    volatileDemo.addNum();
                    System.out.println("num=" + volatileDemo.a);
                }

            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    volatileDemo.addNum();
                    System.out.println("num=" + volatileDemo.a);
                }

            }
        }).start();
    }
}

還是這個例子,兩個線程同時操作一個變量a,但是打印出來的a的最終結(jié)果一定會是200嗎?答案不是。可能每次的結(jié)果都不一樣。最終結(jié)果小于等于200。這就是最經(jīng)典的一個線程安全問題了。兩個線程操作一個變量可能兩個線程同事拿到了變量的副本假設(shè)a=1,線程一,和線程二同時修改了變量副本兩個線程中a=2了,這時候線程1和線程二再次將變量刷新到主內(nèi)存中,等于說兩個操作打印出同一個數(shù)字了,按照我們的意愿,這時候主內(nèi)存中變量a應(yīng)該等于3,但是還是等于2。

這個就是線程安全問題,即多個線程同時訪問一個資源時,會導(dǎo)致程序運(yùn)行結(jié)果并不是想看到的結(jié)果。。

  • 由于每個線程執(zhí)行的過程是不可控的,所以很可能導(dǎo)致最終的結(jié)果與實際上的愿望相違背或者直接導(dǎo)致程序出錯。

如何解決線程安全問題?

基本上所有的并發(fā)模式在解決線程安全問題時,都采用“序列化訪問臨界資源”的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱作同步互斥訪問。

通常來說,是在訪問臨界資源的代碼前面加上一個鎖,當(dāng)訪問完臨界資源后釋放鎖,讓其他線程繼續(xù)訪問。

在Java中,提供了兩種方式來實現(xiàn)同步互斥訪問:synchronized和Lock。

synchronized

  • synchronized用于多線程設(shè)計,有了synchronized關(guān)鍵字,多線程程序的運(yùn)行結(jié)果將變得可以控制。synchronized關(guān)鍵字用于保護(hù)共享數(shù)據(jù)。

  • synchronized實現(xiàn)同步的機(jī)制:synchronized依靠"鎖"機(jī)制進(jìn)行多線程同步,"鎖"有2種,一種是對象鎖,一種是類鎖。

互斥鎖

概念

能到達(dá)到互斥訪問目的的鎖。
舉個例子:假設(shè)我和老王要去上廁所,但是廁所只有一個,于是我進(jìn)了廁所,這時候老王就要像個SB一樣在門口等待了。

在Java中,可以使用synchronized關(guān)鍵字來標(biāo)記一個方法或者代碼塊,當(dāng)某個線程調(diào)用該對象的synchronized方法或者訪問synchronized代碼塊時,這個線程便獲得了該對象的鎖,其他線程暫時無法訪問這個方法,只有等待這個方法執(zhí)行完畢或者代碼塊執(zhí)行完畢,這個線程才會釋放該對象的鎖,其他線程才能執(zhí)行這個方法或者代碼塊。

synchronized的使用

synchronized可以修飾一個方法

在這個時候,synchronized獲取的是該類的對象鎖。等于說我同一個對象中的兩個方法被synchronized修飾,那么這兩個方法都是互斥的。第一個線程再訪問的第一個方法的時候,第二個線程也必須要等待第一個線程完成了,才能訪問第二個方法。

 public synchronized void addNum() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        a++;//注意這里
    }

那么上面的代碼加上一個synchronized。運(yùn)行結(jié)果就一定了,最終打印出來的結(jié)果是200了。

舉例:

public class TestSynchronized {
    public synchronized void method1() throws InterruptedException {
        System.out.println("method1 begin at:" + System.currentTimeMillis());
        Thread.sleep(6000);
        System.out.println("method1 end at:" + System.currentTimeMillis());
    }
    public synchronized void method2() throws InterruptedException {
        while(true) {
            System.out.println("method2 running");
            Thread.sleep(200);
        }
    }
    static TestSychronized instance = new TestSychronized();
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    instance.method1();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for(int i=1; i<4; i++) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread1 still alive");
                }                    
            }
        });
        
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    instance.method2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        thread1.start();
        thread2.start();    
        
    }
}

運(yùn)行結(jié)果:thread2一直等到thread1中的method1執(zhí)行完了之后才執(zhí)行method2,說明method1和method2互斥

synchronized {修飾代碼塊}的作用不僅于此,synchronized void method{}整個函數(shù)加上synchronized塊,效率并不好。在函數(shù)內(nèi)部,可能我們需要同步的只是小部分共享數(shù)據(jù),其他數(shù)據(jù),可以自由訪問,這時候我們可以用 synchronized(表達(dá)式){//語句}更加精確的控制。

synchronized可以修飾代碼塊

當(dāng)修飾代碼塊的時候鎖的就是傳入的對象,this只的是當(dāng)前類。
如下:

public  void addNum() {
    synchronized(this){
           try {
              Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
             }
            a++;
        }
   }

synchronized修飾靜態(tài)方法

synchronized還可以修飾靜態(tài)方法,這個時候鎖的對象就是類對象,下面兩種例子效果都相同:

public void addNum(){
   synchronized(Obl.class)
   }
}
 public static synchronized void addNum() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        a++;//注意這里
    }

例子:

public class TestSychronized {
    public synchronized static void method1() throws InterruptedException {
        System.out.println("method1 begin at:" + System.currentTimeMillis());
        Thread.sleep(6000);
        System.out.println("method1 end at:" + System.currentTimeMillis());
    }
    public synchronized static void method2() throws InterruptedException {
        while(true) {
            System.out.println("method2 running");
            Thread.sleep(200);
        }
    }
    static TestSychronized instance1 = new TestSychronized();
    static TestSychronized instance2 = new TestSychronized();
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    instance1.method1();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for(int i=1; i<4; i++) {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread1 still alive");
                }                    
            }
        });
        
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    instance2.method2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        thread1.start();
        thread2.start();    
        
    }
}

運(yùn)行結(jié)果:thread2一直等到thread1中的method1執(zhí)行完了之后才執(zhí)行method2,說明method1和method2互斥

總結(jié)

synchronized是java中的一個關(guān)鍵字,也就是說是Java語言內(nèi)置的特性。synchronized既能保證原子性,又能保證一致性。synchronized鎖的同一個對象的時候,其他線程不能訪問該對象中的其他的synchronized修飾的方法或者代碼塊,他們是互斥的。雖然synchronized可以保證線程的同步,但是在訪問線程非常多的情況下,性能低下。

如果一個代碼塊被synchronized修飾了,當(dāng)一個線程獲取了對應(yīng)的鎖,并執(zhí)行該代碼塊時,其他線程便只能一直等待,等待獲取鎖的線程釋放鎖,而這里獲取鎖的線程釋放鎖只會有兩種情況:

  1. 獲取鎖的線程執(zhí)行完了該代碼塊,然后線程釋放對鎖的占有;

  2. 線程執(zhí)行發(fā)生異常,此時JVM會讓線程自動釋放鎖。

那么如果這個獲取鎖的線程由于要等待IO或者其他原因(比如調(diào)用sleep方法)被阻塞了,但是又沒有釋放鎖,其他線程便只能干巴巴地等待,試想一下,這多么影響程序執(zhí)行效率。

因此就需要有一種機(jī)制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間或者能夠響應(yīng)中斷),通過Lock就可以辦到。

Lock

lock是一個接口,它有如下方法:

public interface Lock {
    void lock();//加鎖
    void lockInterruptibly() throws InterruptedException;//加可中斷鎖
    boolean tryLock();//加鎖成功返回true,失敗返回false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//加鎖成功返回true,失敗等待一段時間后若仍無法加鎖則返回false,可響應(yīng)中斷
    void unlock();//解鎖
    Condition newCondition(); //返回一個Condition,利用它可以如和Synchronizd配合使用的wait()和notify()一樣對線程阻塞和喚醒,不同的是一個lock可以有多個condition.
}

Lock方法

lock()

用來獲取鎖。如果鎖已被其他線程獲取,則等待。

Lock lock = ...;
if(lock.tryLock()) {
 try{
     //處理任務(wù)
 }catch(Exception ex){

 }finally{
     lock.unlock();   //釋放鎖
 } 
}else {
//如果不能獲取鎖,則直接做其他事情
}
tryLock()

用來獲取鎖。如果鎖已被其他線程獲取,則返回false,否則返回true。不會進(jìn)行等待。

Lock lock = ...;
if(lock.tryLock()) {
 try{
     //處理任務(wù)
 }catch(Exception ex){

 }finally{
     lock.unlock();   //釋放鎖
 } 
}else {
//如果不能獲取鎖,則直接做其他事情
}
tryLock(long time, TimeUnit unit)

與tryLock()方法類似,只不過區(qū)別在于這個方法在拿不到鎖時會等待一定的時間,在時間期限之內(nèi)如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內(nèi)拿到了鎖,則返回true。

lockInterruptibly()

當(dāng)通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應(yīng)中斷,即中斷線程的等待狀態(tài)。也就使說,當(dāng)兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那么對線程B調(diào)用threadB.interrupt()方法能夠中斷線程B的等待過程。

public void method() throws InterruptedException {
        lock.lockInterruptibly();
        try {  
             //.....
        }
        finally {
         lock.unlock();
        }  
   }

ReentrantLock(Lock接口實現(xiàn)類)

是一個獨占鎖,與sychronized類似

ReadWriteLock

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}

一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進(jìn)行讀操作。下面的ReentrantReadWriteLock實現(xiàn)了ReadWriteLock接口。

ReentrantReadWriteLock(ReadWriteLock實現(xiàn)類)

  • ReentrantReadWriteLock里面提供了很多豐富的方法,不過最主要的有兩個方法:readLock()和writeLock()用來獲取讀鎖和寫鎖。

  • ReentrantReadWriteLock里面的鎖主體就是一個Sync,也就是FairSync或者NonfairSync,所以說實際上只有一個鎖,只是在獲取讀取鎖和寫入鎖的方式上不一樣。

  • ReentrantReadWriteLock里面有兩個類:ReadLock/WriteLock,這兩個類都是Lock的實現(xiàn)。

  • 如果有一個線程已經(jīng)占用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。但是如果其他線程要申請讀鎖,那么不需要等待依然能申請的到讀鎖。這很大的優(yōu)化了性能。

  • 如果有一個線程已經(jīng)占用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。

小結(jié)

ReentrantReadWriteLock相比ReentrantLock的最大區(qū)別是:ReentrantReadWriteLock的讀鎖是共享鎖,任何線程都可以獲取,而寫鎖是獨占鎖。ReentrantLock不論讀寫,是獨占鎖。

Lock和synchronized的選擇

Lock和synchronized有以下幾點不同:

  1. Lock是一個接口,而synchronized是Java中的關(guān)鍵字,synchronized是內(nèi)置的語言實現(xiàn);
  2. synchronized在發(fā)生異常時,會自動釋放線程占有的鎖,因此不會導(dǎo)致死鎖現(xiàn)象發(fā)生;而Lock在發(fā)生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現(xiàn)象,因此使用Lock時需要在finally塊中釋放鎖;
  3. Lock可以讓等待鎖的線程響應(yīng)中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應(yīng)中斷;
  4. 通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
  5. Lock可以提高多個線程進(jìn)行讀操作的效率。
    在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當(dāng)競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠(yuǎn)遠(yuǎn)優(yōu)于synchronized。所以說,在具體使用時要根據(jù)適當(dāng)情況選擇。

鎖的概念相關(guān)介紹

  1. 可重入鎖

如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖

  1. 可中斷鎖

可中斷鎖:顧名思義,就是可以interrupt()中斷的鎖。
在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。
如果某一線程A正在執(zhí)行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由于等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。
在前面演示lockInterruptibly()的用法時已經(jīng)體現(xiàn)了Lock的可中斷性。

  1. 公平鎖

公平鎖即盡量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖,當(dāng)這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進(jìn)行的。這樣就可能導(dǎo)致某個或者一些線程永遠(yuǎn)獲取不到鎖。
在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。
而對于ReentrantLock和ReentrantReadWriteLock,它默認(rèn)情況下是非公平鎖,但是可以設(shè)置為公平鎖。

  1. 讀寫鎖

讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。
正因為有了讀寫鎖,才使得多個線程之間的讀操作不會發(fā)生沖突,提高了程序的性能。
ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現(xiàn)了這個接口。
可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。

最后編輯于
?著作權(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ù)。

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

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