JAVA多線程之synchronized、lock、volatile

synchronized譬涡、lock的簡介

假設(shè)一個Integer類型的全局變量i同時被A闪幽,B,C三個線程訪問涡匀,A線程主要是給i做加1的操作盯腌,B線程主要是給i做減1的操作,C線程主要是讀取i的值并打印出來陨瘩。那么問題來了腕够,C線程打印的i值是沒有變了,還是已經(jīng)減1舌劳,或者已經(jīng)加1呢帚湘?

這里就涉及到線程同步的問題,線程同步是多個線程按照預(yù)定的先后次序來運行甚淡,Java中可以通過synchronized或者lock來實現(xiàn)線程的同步大诸,下面將主要介紹synchronized、lock的用法以及兩者的區(qū)別材诽。

1.synchronized

synchronized是Java中的關(guān)鍵字,使用synchronized能夠防止多個線程同時進入并訪問程序的臨界區(qū)(程序的某個方法或者代碼塊)恒傻。synchronized可以修飾方法或者代碼塊脸侥,當(dāng)A線程訪問被synchronized修飾的方法或者代碼塊時,A線程就獲取該對象的鎖盈厘,此時如果B線程想訪問該臨界區(qū)睁枕,就必須等待A線程執(zhí)行完畢并釋放該對象的鎖。

1)synchronized method():被synchronized修飾后沸手,該方法就變成了一個同步方法外遇,其作用范圍就是整個方法,而作用對象要分兩種情況來考慮契吉。

情況一:該方法是非靜態(tài)方法跳仿,其作用對象就是調(diào)用該方法的對象;

情況二:該方法是靜態(tài)方法捐晶,其作用對象就是調(diào)用該方法的所有類的實例對象菲语。

2)synchronized ():括號里可以是類或者對象。

synchronized(className.class):作用對象是訪問該代碼塊的該類所有對象惑灵,當(dāng)某個線程已經(jīng)在訪問該代碼塊時山上,其它該類的所有對象都不能訪問該代碼塊。

synchronized(object):是給object加鎖英支,其他線程訪問synchronized (object)同步代碼塊時將會被阻塞(同一個類的不同對象可以訪問該代碼塊)佩憾。

synchronized(this):作用對象是當(dāng)前對象,其他線程訪問該對象的同步方法塊時將會被阻塞(同一個類的不同對象可以訪問該代碼塊)。

下面給出一個簡單例子妄帘,通過synchronized關(guān)鍵字楞黄,兩個線程交替地輸出ABABABAB字符串,代碼如下:

public class PrintAB {
    private   final Object object = new Object();
    private  boolean flag = false;
    
    public static void main(String[] args) {
        PrintAB printA = new PrintAB();
        MyRunnable1 myRunnable1 = printA.new MyRunnable1();
        MyRunnable2 myRunnable2 = printA.new MyRunnable2();
        Thread thread1 = new Thread(myRunnable1);
        Thread thread2 = new Thread(myRunnable2);
        thread1.start();
        thread2.start();
    }
    
    public class MyRunnable1 implements Runnable {
        @Override
        public void run() {
            while (true) {
                synchronized (object) {
                    if (flag) {
                        try {
                            object.wait();
                        } catch (InterruptedException e) {

                        }
                    }
                    System.out.print('A');
                    flag = true;
                    object.notify();
                }
            }
        }
        }

        public class MyRunnable2 implements Runnable {
            @Override
            public void run() {
                while (true) {
                    synchronized (object) {
                        if (!flag) {
                            try {
                                object.wait();
                            } catch (InterruptedException e) {

                            }
                        }
                        System.out.print('B');
                        flag = false;
                        object.notify();
                    }

                }
            }
        }

    }

輸出結(jié)果是:

ABABABABABAB

synchronized的更為詳細的介紹可以參考Java多線程干貨系列synchronized

2.Lock

synchronized是Java語言的關(guān)鍵字寄摆,是內(nèi)置特性谅辣,而ReentrantLock是一個類(實現(xiàn)Lock接口的類),通過該類可以實現(xiàn)線程的同步婶恼。Lock是一個接口桑阶,源碼很簡單,主要是聲明了四個方法:

public interface Lock {

void lock();

void lockInterruptibly() throws InterruptedException;

boolean tryLock();

boolean tryLock(long var1, TimeUnit var3) throws InterruptedException;

void unlock();

Condition newCondition();

}

Lock一般的使用如下:

Lock lock= ...;//獲取鎖

lock.lock();

try{

//處理任務(wù)

}catch(Exception e){

}finally{

lock.unlock();//釋放鎖

}

lock()勾邦、tryLock()蚣录、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的,unLock()方法是用來釋放鎖的眷篇,其放在finally塊里執(zhí)行萎河,可以保證鎖一定被釋放,newCondition方法下面會做介紹(通過該方法可以生成一個Condition對象蕉饼,而Condition是一個多線程間協(xié)調(diào)通信的工具類)虐杯。

Lock接口的主要方法介紹:

lock():獲取不到鎖就不罷休,否則線程一直處于block狀態(tài)昧港。

tryLock():嘗試性地獲取鎖擎椰,不管有沒有獲取到都馬上返回,拿到鎖就返回true创肥,不然就返回false 达舒。

tryLock(long time, TimeUnit unit):如果獲取不到鎖,就等待一段時間叹侄,超時返回false巩搏。

lockInterruptibly():該方法稍微難理解一些,在說該方法之前趾代,先說說線程的中斷機制贯底,每個線程都有一個中斷標志,不過這里要分兩種情況說明:

1) 線程在sleep撒强、wait或者join丈甸, 這個時候如果有別的線程調(diào)用該線程的 interrupt()方法,此線程會被喚醒并被要求處理InterruptedException尿褪。

2)如果線程處在運行狀態(tài)睦擂, 則在調(diào)用該線程的interrupt()方法時,不會響應(yīng)該中斷杖玲。

lockInterruptibly()和上面的第一種情況是一樣的顿仇, 線程在獲取鎖被阻塞時,如果調(diào)用lockInterruptibly()方法,該線程會被喚醒并被要求處理InterruptedException臼闻。下面給出一個響應(yīng)中斷的簡單例子:

public class Test{

public static void main(String[] args){

MyRunnable myRunnable = new Test().new MyRunnable();

Thread thread1 = new Thread(myRunnable,"thread1");

Thread thread2 = new Thread(myRunnable,"thread2");

thread1.start();

thread2.start();

thread2.interrupt();

}

public class MyRunnable implements Runnable{

private Lock lock=new ReentrantLock();

@Override

public synchronized void run() {

try{

lock.lockInterruptibly();

System.out.println(Thread.currentThread().getName() +"獲取了鎖");

Thread.sleep(5000);

}catch(InterruptedException e) {

e.printStackTrace();

System.out.println(Thread.currentThread().getName() +"響應(yīng)中斷");

}

finally{

lock.unlock();

System.out.println(Thread.currentThread().getName() +"釋放了鎖");

}

}

}

}

輸出結(jié)果是:

thread1獲取了鎖

thread1釋放了鎖

thread2響應(yīng)中斷

thread2在響應(yīng)中斷后鸿吆,在finally塊里執(zhí)行unlock方法時,會拋出java.lang.IllegalMonitorStateException異常(因為thread2并沒有獲取到鎖述呐,只是在等待獲取鎖的時候響應(yīng)了中斷惩淳,這時再釋放鎖就會拋出異常)。

上面簡單介紹了ReentrantLock的使用乓搬,下面具體介紹使用ReentrantLock的中的newCondition方法實現(xiàn)一個生產(chǎn)者消費者的例子思犁。

生產(chǎn)者、消費者
例子:兩個線程A进肯、B激蹲,A生產(chǎn)牙刷并將其放到一個緩沖隊列中,B從緩沖隊列中購買(消費)牙刷(說明:緩沖隊列的大小是有限制的)江掩,這樣就會出現(xiàn)如下兩種情況学辱。
1)當(dāng)緩沖隊列已滿時,A并不能再生產(chǎn)牙刷环形,只能等B從緩沖隊列購買牙刷策泣;
2)當(dāng)緩沖隊列為空時,B不能再從緩沖隊列中購買牙刷抬吟,只能等A生產(chǎn)牙刷放到緩沖隊列后才能購買萨咕。

public class ToothBrushDemo  {
    public static  void main (String[] args){
        final ToothBrushBusiness toothBrushBusiness =
                new ToothBrushDemo().new ToothBrushBusiness();
        new Thread(new Runnable() {
            @Override
            public void run() {
                executeRunnable(toothBrushBusiness, true);
            }
        }, "牙刷生產(chǎn)者1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                executeRunnable(toothBrushBusiness, false);

            }
        }, "牙刷消費者1").start();



    }

    //循環(huán)執(zhí)行50次
    public static void executeRunnable(ToothBrushBusiness toothBrushBusiness,
                                       boolean isProducer){
        for(int i = 0 ; i < 50 ; i++) {
            if (isProducer) {
                toothBrushBusiness.produceToothBrush();
            } else {
                toothBrushBusiness.consumeToothBrush();
            }
        }

    }


    public class ToothBrushBusiness {
        //定義一個大小為10的牙刷緩沖隊列
        private GoodQueue<ToothBrush> toothBrushQueue = new GoodQueue<ToothBrush>(new ToothBrush[10]);
        private int number = 1;
        private final ReentrantLock lock = new ReentrantLock();
        private final Condition notEmpty =  lock.newCondition();
        private final Condition notFull =  lock.newCondition();
        public ToothBrushBusiness() {

        }

        //生產(chǎn)牙刷
        public void produceToothBrush(){
            lock.lock();
            try {
                //牙刷緩沖隊列已滿,則生產(chǎn)牙刷線程等待
                while (toothBrushQueue.isFull()){
                    notFull.await();
                }
                ToothBrush toothBrush = new ToothBrush(number);
                toothBrushQueue.enQueue(toothBrush);
                System.out.println("生產(chǎn): " + toothBrush.toString());
                number++;
                //牙刷緩沖隊列加入牙刷后,喚醒消費牙刷線程
                notEmpty.signal();
            }
            catch (InterruptedException e){
                e.printStackTrace();
            } catch (GoodQueueException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }


        }

        //消費牙刷
        public void consumeToothBrush(){
            lock.lock();
            try {
                //牙刷緩沖隊列為空,則消費牙刷線程等待
                while (toothBrushQueue.isEmpty()){
                    notEmpty.await();
                }
                ToothBrush toothBrush = toothBrushQueue.deQueue();
                System.out.println("消費: " + toothBrush.toString());
                //從牙刷緩沖隊列取出牙刷后,喚醒生產(chǎn)牙刷線程
                notFull.signal();
            }
            catch (InterruptedException e){
                e.printStackTrace();
            } catch (GoodQueueException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }

    }

    public class ToothBrush {
        private int number;

        public ToothBrush(int number) {
            this.number = number;
        }

        @Override
        public String toString() {
            return "牙刷編號{" +
                    "number=" + number +
                    '}';
        }
    }
}

這里緩沖隊列的大小設(shè)成了10,定義了一個可重入鎖lock拗军,兩個狀態(tài)標記對象notEmpty任洞,notFull蓄喇,分別用來標記緩沖隊列是否為空发侵,是否已滿。
1)當(dāng)緩沖隊列已滿時妆偏,調(diào)用notFull.await方法用來阻塞生產(chǎn)牙刷線程刃鳄。
2)當(dāng)緩沖隊列為空時,調(diào)用notEmpty.await方法用來阻塞購買牙刷線程钱骂。
3)notEmpty.signal用來喚醒消費牙刷線程叔锐,notFull.signal用來喚醒生產(chǎn)牙刷線程。

Object和Conditon對應(yīng)關(guān)系如下:

              Object      Condition 
休眠             wait        await 
喚醒特定線程      notify      signal 
喚醒所有線程     notifyAll   signalAll 

對于同一個鎖见秽,我們可以創(chuàng)建多個Condition愉烙,就是多個監(jiān)視器的意思。在不同的情況下使用不同的Condition解取,Condition是被綁定到Lock上的步责,要創(chuàng)建一個Lock的Condition必須用newCondition()方法。
Lock鎖的介紹
ReentrantLock(可重入鎖)是唯一實現(xiàn)了Lock接口的類,并且ReentrantLock提供了更多的方法蔓肯。

synchronized和ReentrantLock都是可重入鎖遂鹊,可重入性舉個簡單的例子,當(dāng)一個線程執(zhí)行到某個synchronized方法時蔗包,比如說method1秉扑,而在method1中會調(diào)用另外一個synchronized方法method2,此時線程不必重新去申請鎖调限,而是可以直接執(zhí)行方法method2舟陆。

上面的響應(yīng)中斷的例子已經(jīng)地使用到了ReentrantLock,下面來介紹另外一種鎖旧噪,可重入讀寫鎖ReentrantReadWriteLock吨娜,該類實現(xiàn)了ReadWriteLock接口,該接口的源碼如下:

public interface ReadWriteLock {

Lock readLock();

Lock writeLock();

}

ReadWriteLock接口只有獲取讀鎖和寫鎖的方法淘钟,而ReentrantReadWriteLock是實現(xiàn)了ReadWriteLock接口宦赠,接著對其應(yīng)用場景做簡單介紹。

應(yīng)用場景:
假設(shè)一個共享的文件米母,其屬性是可讀勾扭,如果某個時間有100個線程在同時讀取該文件,如果通過synchronized或者Lock來實現(xiàn)線程的同步訪問铁瞒,那么有個問題來了妙色,當(dāng)這100個線程的某個線程獲取到了鎖后,其它的線程都要等該線程釋放了鎖才能進行讀操作慧耍,這樣就會造成系統(tǒng)資源和時間極大的浪費身辨,而ReentrantReadWriteLock正好解決了這個問題。下面給一個簡單的例子芍碧,并根據(jù)代碼以及輸出結(jié)果做簡要說明:

public classTest{

public static voidmain(String[] args){

MyRunnable myRunnable = newTest().new MyRunnable();

Thread thread1 = new Thread(myRunnable,"thread1");

Thread thread2 = new Thread(myRunnable,"thread2");

Thread thread3 = new Thread(myRunnable,"thread3");

thread1.start();

thread2.start();

thread3.start();

}

public class MyRunnable implementsRunnable{

private ReadLocklock =new ReentrantReadWriteLock().readLock();

@Override

public synchronized void run() {

try{

lock.lock();

inti=0;

while(i<5) {

System.out.println(Thread.currentThread().getName() +"正在進行讀操作");

i++;

}

System.out.println(Thread.currentThread().getName()+"讀操作完畢");

}

finally{

lock.unlock();

}

}

}

}

輸出結(jié)果:

thread1正在進行讀操作

thread1正在進行讀操作

thread1正在進行讀操作

thread1正在進行讀操作

thread1正在進行讀操作

thread1讀操作完畢

thread3正在進行讀操作

thread3正在進行讀操作

thread3正在進行讀操作

thread3正在進行讀操作

thread3正在進行讀操作

thread3讀操作完畢

thread2正在進行讀操作

thread2正在進行讀操作

thread2正在進行讀操作

thread2正在進行讀操作

thread2正在進行讀操作

thread2讀操作完畢

從輸出結(jié)果可以看出煌珊,三個線程并沒有交替輸出,這是因為這里只是讀取了5次泌豆,但將讀取次數(shù)i的值改成一個較大的數(shù)值如100000時定庵,輸出結(jié)果就會交替的出現(xiàn)。

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可以提高多個線程進行讀操作的效率掩完。

Lock和synchronized比較主要是參考Java Lock和synchronized的區(qū)別

volatile

當(dāng)一個變量被定義成volatile類型之后硼砰,該變量對所有的線程是可見的且蓬,這里的可見性指的是當(dāng)一個線程修改了該變量的值,那么新值對其它線程來說是立即可知的题翰。
雖然被volatile修飾的變量具有可見性恶阴,但是基于volatile變量的運算在并發(fā)下并不是安全的,因為Java里面的有些運算并非原子操作豹障,下面舉例說明:

public static volatile int index = 0;
    public static  void main(String[] args){
        for(int i = 0; i < 3; i ++ ){
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                  for(int i = 0 ; i < 10 ; i++){
                      index ++;
                  }
                }
            });
            thread.start();
            increase();

        }
        System.out.println("index:" + index);
    }

輸出結(jié)果:

index:20

上面的代碼冯事,我們直觀的反應(yīng),輸出的index應(yīng)該是30血公,而不應(yīng)該是20昵仅。
其實不然,++操作不是一個原子操作累魔,index++指令編譯成字節(jié)碼包含兩個操作:取值摔笤,加操作。這里index被修飾成volatile垦写,保證了此時從內(nèi)存中取得的index值是正確的吕世,但有可能其它線程通過加操作將index值加1,之前從內(nèi)存中取得的index值過期了梯澜,這時候我們執(zhí)行加1操作后將一個較小的index值同步到內(nèi)存中寞冯。
volatile除了讓變量具有可見性外渴析,還有一個更為重要的語義:禁止指令重排優(yōu)化晚伙,保證變量的賦值操作跟程序代碼中的執(zhí)行順序是一致的。JVM往往會對代碼進行優(yōu)化俭茧,這些優(yōu)化操作可能會造成程序指令在執(zhí)行時會出現(xiàn)亂序咆疗,而volatile能夠屏蔽掉JVM中必要的代碼優(yōu)化。

總結(jié)

很多東西覺得不用筆系統(tǒng)地寫下來母债,過段時間找回就會花一定的時間午磁,于是就寫在簡書上尝抖。同時也歡迎大家指正(其中有些部分是引用了其它的文章,大家也可以做下參考)迅皇。

附:Markdown簡明教程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末昧辽,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子搅荞,更是在濱河造成了極大的恐慌,老刑警劉巖框咙,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件咕痛,死亡現(xiàn)場離奇詭異,居然都是意外死亡喇嘱,警方通過查閱死者的電腦和手機茉贡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來者铜,“玉大人腔丧,你說我怎么就攤上這事∽餮蹋” “怎么了悔据?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長俗壹。 經(jīng)常有香客問我科汗,道長,這世上最難降的妖魔是什么绷雏? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任头滔,我火速辦了婚禮,結(jié)果婚禮上涎显,老公的妹妹穿的比我還像新娘坤检。我一直安慰自己,他們只是感情好期吓,可當(dāng)我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布早歇。 她就那樣靜靜地躺著,像睡著了一般讨勤。 火紅的嫁衣襯著肌膚如雪箭跳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天潭千,我揣著相機與錄音谱姓,去河邊找鬼。 笑死刨晴,一個胖子當(dāng)著我的面吹牛屉来,可吹牛的內(nèi)容都是我干的路翻。 我是一名探鬼主播,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼茄靠,長吁一口氣:“原來是場噩夢啊……” “哼茂契!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起慨绳,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤账嚎,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后儡蔓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體郭蕉,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年喂江,在試婚紗的時候發(fā)現(xiàn)自己被綠了召锈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡获询,死狀恐怖涨岁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情吉嚣,我是刑警寧澤梢薪,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站尝哆,受9級特大地震影響秉撇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜秋泄,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一琐馆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧恒序,春花似錦瘦麸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至喊巍,卻和暖如春屠缭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背玄糟。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工勿她, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留袄秩,地道東北人阵翎。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓逢并,卻偏偏與公主長得像,于是被迫代替她去往敵國和親郭卫。 傳聞我的和親對象是個殘疾皇子砍聊,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容