拾人牙慧系列--Synchronized的理解

前言

本系列文章,將在各路大神文章的基礎(chǔ)上,總結(jié)提煉出自己的感悟,力求將大神的觀點(diǎn)總結(jié)的更加凝練,希望站在巨人的肩膀上,能看得更遠(yuǎn)

本篇引用文章

Java 并發(fā)編程系列文章(這個(gè)是第一篇,里面有其余相關(guān)的文章)

內(nèi)容提要

在Java的多線程并發(fā)編程里面,常常會(huì)遇到多個(gè)線程同時(shí)共同訪問(wèn)同一個(gè)資源的情況,本文將描述如何避免此種情況下產(chǎn)生的沖突問(wèn)題

正文

  • 1.問(wèn)題的產(chǎn)生

想必多數(shù)人都去銀行存過(guò)錢(qián),也都用銀行卡刷過(guò)卡消費(fèi)(沒(méi)有的就去試試,別抬杠哈~)。那么你有沒(méi)有想過(guò),銀行的系統(tǒng)怎么保證你在同時(shí)進(jìn)行存錢(qián)和刷卡消費(fèi)的情況下(就假設(shè)你拿著主卡去存錢(qián),女朋友拿著副卡去敗家),不會(huì)把你的賬戶余額搞錯(cuò)呢?我們來(lái)模擬一下,同時(shí)存錢(qián)+消費(fèi)的場(chǎng)景

存錢(qián)消費(fèi)行為模擬

它們對(duì)應(yīng)以下三種過(guò)程:
存錢(qián)消費(fèi)過(guò)程模擬.png

可以看到,在這個(gè)場(chǎng)景里面,小明(線程1)和女朋友(線程2)在他們各自的業(yè)務(wù)邏輯之內(nèi),用到了同一個(gè)賬號(hào)(資源),當(dāng)他們同時(shí)使用這個(gè)賬號(hào)時(shí),多數(shù)情況導(dǎo)致了賬號(hào)余額(資源的屬性)得到了錯(cuò)誤的數(shù)據(jù)變更。那么我們希望的,當(dāng)然是得到正確的數(shù)據(jù)變更,從上述過(guò)程可以發(fā)現(xiàn),數(shù)據(jù)的錯(cuò)亂,源自于不同線程對(duì)同一資源的同時(shí)操作(具體來(lái)說(shuō)是寫(xiě)操作),所以,我們的問(wèn)題,即可歸結(jié)為如何防止不同線程在同一時(shí)刻操作同一資源

  • 2.問(wèn)題的解決

如標(biāo)題所示,自然,我們要用Synchronized來(lái)解決這個(gè)問(wèn)題(當(dāng)然也有其他法子,但是不是本篇的討論范圍)

Synchronized是Java中解決并發(fā)問(wèn)題的一種最常用的方法,也是最簡(jiǎn)單的一種方法。Synchronized的作用主要有三個(gè):(1)確保線程互斥的訪問(wèn)同步代碼(2)保證共享變量的修改能夠及時(shí)可見(jiàn)(3)有效解決重排序問(wèn)題。
摘自這里--http://www.cnblogs.com/paddix/p/5367116.html

結(jié)合上面的例子來(lái)說(shuō),相當(dāng)于小明(線程1)在存錢(qián)(操作資源)之前,通過(guò)Synchronized先把賬號(hào)給霸占起來(lái)了,女朋友無(wú)法操作(也就是給資源加了鎖,并且線程1把鎖獲取到手,達(dá)到互斥的效果),當(dāng)小明存錢(qián)完畢,系統(tǒng)將余額更新到賬號(hào)里(相當(dāng)于將資源副本同步到堆內(nèi)的資源),小明不再霸占賬號(hào)(釋放鎖),女朋友(線程2)再進(jìn)行與小明一樣的流程。而不管是小明先存錢(qián),還是女朋友先消費(fèi),最終賬號(hào)的余額都會(huì)是一個(gè)正確的數(shù)額(也就是說(shuō),線程的執(zhí)行次序,并不影響資源變更的正確性,只要保證資源使用的互斥性即可)

  • 3.小小的例子

光說(shuō)不練假把式,來(lái)實(shí)操一下,我們把他們的存儲(chǔ)-消費(fèi)過(guò)程,用代碼實(shí)現(xiàn)一次
賬號(hào)類

public class Account {
    String name;
    int balance;

    public Account(String name) {
        this.name = name;
        balance = 0;
    }

    public void saveMoneyNotSync(String user, int money) {
        balance += money;
        System.out.println(user + "存入" + money + "元");
    }

    public int withdrawMoneyNotSync(String user, int money) {
        balance -= money;
        System.out.println(user + "取出" + money + "元");
        return money;
    }

    /**
     * 1.這種用法,實(shí)際上是將new 出來(lái)的Account對(duì)象鎖定,一旦線程獲取了對(duì)象鎖,那么該對(duì)象的其他非靜態(tài)方法將無(wú)法被其他線程使用
     * 2.如果 synchronized 用在靜態(tài)方法上,那么鎖定的就不是對(duì)象,而是這個(gè)類的Class,一旦線程獲取了Class鎖,那么該類的其他靜態(tài)方法將無(wú)法被其他線程使用
     */
    public synchronized void saveMoney(String user, int money) {
        balance += money;
        System.out.println(user + "存入" + money + "元");
    }

    public synchronized int withdrawMoney(String user, int money) {
        balance -= money;
        System.out.println(user + "取出" + money + "元");
        return money;
    }

    public int getBalance() {
        System.out.println(name + "的賬戶余額為:" + balance);
        return balance;
    }
}

小明線程,用于模擬一次存錢(qián)過(guò)程

public class MingThread extends Thread {
    Account account;

    public MingThread(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        super.run();
        account.saveMoneyNotSync("小明", 100);
    }
}

女朋友線程,用于模擬一次取錢(qián)過(guò)程

public class GirlFirThread extends Thread {
    Account account;

    public GirlFirThread(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        super.run();
        account.withdrawMoneyNotSync("小紅", 100);
    }
}

生活線程,模擬同時(shí)進(jìn)行存錢(qián)和消費(fèi),由于同時(shí)進(jìn)行一次的情況,很難出現(xiàn)錯(cuò)誤,因此我們來(lái)模擬存取10000次的情景,小明往賬戶里面存錢(qián)10000次,一次100,女朋友取錢(qián)10000次,一次100。

public class Life {
    public static void main(String[] args) throws InterruptedException {
        final Account account = new Account("愛(ài)的賬號(hào)");

        Thread guangZhou = new Thread() {
            @Override
            public void run() {
                super.run();
                for (int i = 0; i < 10000; i++) {
                    MingThread mingThread = new MingThread(account);
                    mingThread.start();
                }
            }
        };

        Thread shenZhen = new Thread() {
            @Override
            public void run() {
                super.run();
                for (int i = 0; i < 10000; i++) {
                    GirlFirThread girlFirThread = new GirlFirThread(account);
                    girlFirThread.start();
                }
            }
        };

        guangZhou.start();
        shenZhen.start();

        Thread.sleep(1500);
        account.getBalance();
    }
}

如果按照理想情況,最后賬戶余額應(yīng)該是0,我們來(lái)看看幾組執(zhí)行結(jié)果






顯然,雖然也有正確的時(shí)候,但是多數(shù)情況下,無(wú)法得到正確的數(shù)值。你可以再去試試加了synchronized的方法,無(wú)論你怎么調(diào)用,總能得到正確的答案(懶得截圖了,一試便知)

衍生問(wèn)題

既然討論了Synchronized,必然要再討論一下跟線程同步相關(guān)的一些問(wèn)題。我們來(lái)著重討論以下幾個(gè)方法:
wait() notify() sleep() join()

wait()

wait()是Object的方法,作用是使當(dāng)前線程進(jìn)入等待,來(lái)看看官方文檔:

Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object. In other words, this method behaves exactly as if it simply performs the call wait(0).

散裝翻譯:使當(dāng)前線程進(jìn)入等待(我的理解是哪個(gè)線程調(diào)用這個(gè)方法,就使哪個(gè)線程進(jìn)入等待),直到其他的線程通過(guò)notify()喚醒該線程,或者使用notifyAll()喚醒全部線程

The current thread must own this object's monitor. The thread releases ownership of this monitor and waits until another thread notifies threads waiting on this object's monitor to wake up either through a call to the notify method or the notifyAll method. The thread then waits until it can re-obtain ownership of the monitor and resumes execution.

散裝翻譯:調(diào)用wait()的線程,必須擁有對(duì)應(yīng)Object對(duì)象的監(jiān)視器,因?yàn)閣ait()會(huì)釋放這個(gè)監(jiān)視器(如果你都沒(méi)有,你釋放個(gè)啥?),然后如果這個(gè)線程被喚醒,會(huì)阻塞等待獲取監(jiān)視器的所有權(quán),再繼續(xù)執(zhí)行

As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:

散裝翻譯:和wait(time)方法一樣,等待狀態(tài)可能被打斷,或者也可能有虛假喚醒(不太懂,可能是線程自己就醒了,但是卻沒(méi)有得到監(jiān)視器的狀態(tài)?),所以wait()方法應(yīng)該在一個(gè)隊(duì)列里面使用。如下:

   synchronized (obj) {
         while (<condition does not hold>)
             obj.wait();
         ... // Perform action appropriate to condition
     }

我們來(lái)討論一下wait()的應(yīng)用場(chǎng)景,假設(shè)有一個(gè)線程(ThreadA)對(duì)某個(gè)對(duì)象(Object)進(jìn)行處理,但是Object的某一個(gè)屬性需要從另一個(gè)線程(ThreadB)之中獲取,ThreadB將數(shù)據(jù)給到Object之后,再通知ThreadA進(jìn)行后續(xù)處理,所以當(dāng)ThreadA需要暫停自己的操作,等待其他線程的時(shí)候,wait()就派上用場(chǎng)了

這實(shí)際上有點(diǎn)像工廠組裝產(chǎn)品,假設(shè)有兩個(gè)車間共同完成一個(gè)產(chǎn)品的組裝流程,它們不一定誰(shuí)先拿到產(chǎn)品(Product),但是Product必須先由車間一組裝好零件,車間二才可以繼續(xù)。我們可以用代碼來(lái)模擬這一過(guò)程

Product主要代碼

public class Product {
    public void setComponents1(String components1) {
        System.out.println("產(chǎn)品" + name + "開(kāi)始組裝");
        System.out.println("零件- - -" + components1 + "- - -組裝中");
        try {
            //模擬組裝耗時(shí)
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.components1 = components1;
    }

    public void setComponents2(String components2) {
        System.out.println("零件- - -" + components2 + "- - -組裝中");
        try {
            //模擬組裝耗時(shí)
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.components2 = components2;
        System.out.println("產(chǎn)品" + name + "組裝完成");

    }

    public boolean isComplete() {
        return components1 != null && !components1.equals("") && components2 != null && !components2.equals("");
    }
}

車間1

public class Step1 extends Step {
    @Override
    public void machining() {
        synchronized (product) {
            System.out.println("車間1開(kāi)始工作");
            product.setComponents1("車輪子");
            System.out.println("車間1工作完畢");
            //組裝完成,喚醒需要被喚醒的線程
            product.notify();
        }
    }
}

車間2

public class Step2 extends Step {
    @Override
    public void machining() {
        System.out.println("車間2開(kāi)始工作");
        synchronized (product) {
            while (product.getComponents1() == null || product.getComponents1().equals("")) {
                try {
                    //如果第一個(gè)零件沒(méi)有組裝完畢,就等待
                    product.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            product.setComponents2("車身");
        }
        System.out.println("車間2工作完畢");
    }
}
執(zhí)行結(jié)果

從結(jié)果可以看到,車間2開(kāi)始工作之后,由于車間1還沒(méi)完成組裝工作,車間2進(jìn)入等待,直到車間1完成組裝,才被重新喚醒

sleep()

Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers. The thread does not lose ownership of any monitors.

散裝翻譯:讓線程暫停執(zhí)行一定的毫秒數(shù),暫停時(shí)間的精度取決于系統(tǒng)計(jì)時(shí)器和調(diào)度程序的精度和準(zhǔn)確性。sleep()方法不會(huì)釋放鎖

sleep()方法算是非常常用了,wait()和sleep()一樣,都會(huì)使調(diào)用的線程進(jìn)入等待/暫停,wait()會(huì)把鎖釋放,其它線程可以使用相應(yīng)的資源,但是sleep()不行,線程暫停期間,其他線程也不能使用這個(gè)資源,舉個(gè)栗子

先定義一個(gè)超簡(jiǎn)單的類

public class DoSomething {
    public void doSomething(String thing) {
        System.out.println(thing);
    }
}

用wait()的線程,很簡(jiǎn)單,打印十次,每次wait(1000),注意在循環(huán)期間,doSomething是被鎖住的

public class WaitThread extends Thread {
    DoSomething doSomething;
    public WaitThread(DoSomething doSomething) {
        this.doSomething = doSomething;
    }
    @Override
    public void run() {
        super.run();
        synchronized (doSomething) {
            for (int i = 0; i < 10; i++) {
                doSomething.doSomething(Thread.currentThread().getName() + "---WaitThread is working");
                try {
                    doSomething.wait(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

用sleep()的線程,同樣,打印十次,每次sleep(1000),循環(huán)期間,doSomething同樣鎖住

public class SleepThread extends Thread {
    DoSomething doSomething;
    public SleepThread(DoSomething doSomething) {
        this.doSomething = doSomething;
    }
    @Override
    public void run() {
        super.run();
        synchronized (doSomething) {
            for (int i = 0; i < 10; i++) {
                doSomething.doSomething(Thread.currentThread().getName() + "---SleepThread is working");
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

先試試用wait()

        DoSomething doSomething = new DoSomething();
        new WaitThread(doSomething).start();
        new WaitThread(doSomething).start();
        new WaitThread(doSomething).start();

執(zhí)行結(jié)果
wait執(zhí)行結(jié)果

可以看到,只要一wait,doSomething對(duì)象的鎖就被其他線程持有,所以它們可以交替執(zhí)行

再看看用sleep()

        DoSomething doSomething = new DoSomething();
        new SleepThread(doSomething).start();
        new SleepThread(doSomething).start();
        new SleepThread(doSomething).start();

執(zhí)行結(jié)果
sleep執(zhí)行結(jié)果

可以看到,sleep()之后,其他線程并不能執(zhí)行打印,即是說(shuō)沒(méi)有獲取doSomething的對(duì)象鎖,也就說(shuō)明sleep()并不釋放鎖

notify()

notify()用于喚醒正在等待當(dāng)前線程持有的object鎖的線程,相當(dāng)于一個(gè)提醒功能。打個(gè)比方,公司只有一個(gè)微波爐,當(dāng)我在使用微波爐的時(shí)候,其他人只能等待,那么當(dāng)我快使用完畢的時(shí)候,我可以提醒某人:“小李,微波爐我快用好了,你準(zhǔn)備”,這個(gè)提醒,就是notify(),至于小李能不能在一眾大漢之中搶到微波爐,那我就管不了了,我只負(fù)責(zé)提醒。看下文檔

Wakes up a single thread that is waiting on this object's monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation. A thread waits on an object's monitor by calling one of the wait methods.

散裝翻譯:任意喚醒一個(gè)正在等待當(dāng)前線程持有的對(duì)象鎖的線程

The awakened thread will not be able to proceed until the current thread relinquishes the lock on this object. The awakened thread will compete in the usual manner with any other threads that might be actively competing to synchronize on this object; for example, the awakened thread enjoys no reliable privilege or disadvantage in being the next thread to lock this object.

散裝翻譯:并不是線程被喚醒了,就一定能馬上拿到對(duì)象鎖,而是首先需要當(dāng)前線程放棄這個(gè)鎖,其次需要和其他等待該鎖的線程進(jìn)行競(jìng)爭(zhēng)

This method should only be called by a thread that is the owner of this object's monitor. A thread becomes the owner of the object's monitor in one of three ways:

By executing a synchronized instance method of that object.
By executing the body of a synchronized statement that synchronizes on the object.
For objects of type Class, by executing a synchronized static method of that class.
Only one thread at a time can own an object's monitor.

散裝翻譯:只有擁有了對(duì)象鎖的線程,才能調(diào)用這個(gè)對(duì)象的notify()方法。
有三種獲取鎖的方法:鎖方法,鎖代碼塊,鎖Class。
同一時(shí)刻,只能有一個(gè)線程擁有某個(gè)對(duì)象的鎖

同樣的,我們來(lái)實(shí)操一下,通過(guò)下面的例子,來(lái)展示notify()可以喚醒線程,以及notify()本身并不具備釋放鎖的功能

先定義一個(gè)Service類,用于進(jìn)行wait和notify

public class Service {
    public void testWait(Object lock) {
        synchronized (lock) {
            System.out.println("begin wait by==" + new SimpleDateFormat("mm:ss").format(new Date()));
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end wait by==" + new SimpleDateFormat("mm:ss").format(new Date()));
        }
    }

    public void testNotify(Object lock) {
        synchronized (lock) {
            System.out.println("begin notify by==" + new SimpleDateFormat("mm:ss").format(new Date()));
            lock.notify();
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                lock.wait(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end notify by==" + new SimpleDateFormat("mm:ss").format(new Date()));
        }
    }
}

testWait()方法很簡(jiǎn)單,獲取到鎖之后,打印當(dāng)前時(shí)間,然后等待,被喚醒并重新獲得鎖之后,繼續(xù)打印當(dāng)前時(shí)間,這樣,可以知道中間等待了多久
testNotify()復(fù)雜一些,獲取到鎖之后,打印當(dāng)前時(shí)間,然后notify(),為了證明notify()并不釋放鎖,我們特地讓當(dāng)前線程sleep2秒,再等待(wait()也就意味著讓出鎖)。如果說(shuō)notify()是釋放鎖的,那么testWait()方法里面第二次打印的時(shí)間,應(yīng)該和第一次打印的時(shí)間一致;反之,應(yīng)該是隔了兩秒鐘。一秒鐘之后,testNotify()所在線程再次喚醒,繼續(xù)執(zhí)行sleep5秒,最后輸出

試一試

public class WaitThread extends Thread {
    @Override
    public void run() {
        super.run();
        Service service = new Service();
        service.testWait(lock);
    }
}
public class NotifyThread extends Thread {
    @Override
    public void run() {
        super.run();
        Service service = new Service();
        service.testNotify(lock);
    }
}
        Object lock = new Object();
        WaitThread waitThread = new WaitThread(lock);
        NotifyThread notifyThread = new NotifyThread(lock);

        waitThread.start();
        Thread.sleep(10);
        notifyThread.start();

執(zhí)行結(jié)果



可以看到,end wait這一句,確實(shí)和begin wait相差了兩秒

join(long millis)

join()方法用于使當(dāng)前線程等待,直到子線程執(zhí)行完畢(如果設(shè)置了時(shí)間限制,則至多等待設(shè)置的時(shí)間這么久),當(dāng)前線程才繼續(xù)執(zhí)行,看文檔

Waits at most millis milliseconds for this thread to die. A timeout of 0 means to wait forever.
This implementation uses a loop of this.wait calls conditioned on this.isAlive. As a thread terminates the this.notifyAll method is invoked. It is recommended that applications not use wait, notify, or notifyAll on Thread instances.

散裝翻譯:等待子線程結(jié)束(在本線程消亡之前),如果設(shè)置時(shí)間n毫秒,則至多等待n毫秒(子線程如果在n毫秒前結(jié)束,則等待<n毫秒),如果n=0,則無(wú)限期等待。
join()的內(nèi)部實(shí)現(xiàn),是在一個(gè)循環(huán)里面,以isAlive()為判斷條件,去調(diào)用wait(),當(dāng)有子線程終結(jié),子線程會(huì)調(diào)用notifyAll(),喚醒等待的線程
所以,一般情況下,不建議使用Thread本身的wait(), notify(), 或者 notifyAll()方法(因?yàn)榭赡軙?huì)打亂Thread自身的喚醒邏輯)

簡(jiǎn)單的例子

    public class Test extends Thread {
        @Override
        public void run() {
            super.run();
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                try {
                    sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

不join()的情況下:

        new Test().start();
        System.out.println("main==========");
主線程自己先執(zhí)行了

join()一下:

        Test test = new Test();
        test.start();
        test.join();
        System.out.println("main==========");
主線程會(huì)等待子線程結(jié)束去喚醒它

本篇內(nèi)容到此結(jié)束,感謝收看~~

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

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