Java多線程基礎(chǔ)——多線程實例

前言

??在之前我們講述了Java的線程模型,理解清楚了過后再我們使用的過程中才能得心應(yīng)手,防止不必要的錯誤出現(xiàn),多線程錯誤是很難復(fù)現(xiàn)的錯誤,一定要小心謹(jǐn)慎的使用。
??同時,這里講的是線程間交互,同步的問題,如果線程間不存在交互,各自用自己的局部變量工作,也不存在這些問題了。

共享變量

假如有一下場景,兩個線程依次對某一個成員變量進行操作,會出現(xiàn)什么問題呢?

public class Main {
        static int num;
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                new Thread() {
                    @Override
                    public void run() {
                        for (int j = 0; j < 1000; j++) {
                            num = j;
                        }
                        System.out.println(Thread.currentThread().getName() + ": num = " + num);
                    }
                }.start();
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("main, num = " + num);
        }
}

我們可以發(fā)現(xiàn)num的最終值是999,而且每個線程的值都是999。我們發(fā)現(xiàn)這里并沒有出現(xiàn)多線程的錯誤,其實是因為在Java里面基本類型的賦值操作是原子性的,上一節(jié)我們講過。那假如吧賦值操作改為非原子性的操作呢?,比如改為num++,會怎么樣呢?

       for (int j = 0; j < 1000; j++) {
           num ++;
       }

        //輸出
        Thread-0: num = 1889
        Thread-1: num = 2000
        Thread-2: num = 3000
        Thread-4: num = 4000
        Thread-3: num = 5000
        Thread-5: num = 6000
        Thread-9: num = 7000
        Thread-8: num = 8046
        Thread-7: num = 8046
        Thread-6: num = 9046
        main, num = 9046

具體數(shù)據(jù)肯定有差別,發(fā)現(xiàn)這里出現(xiàn)了問題,結(jié)果并不是10000,這是由于num++是非原子操作,它包括3個操作:讀取num的值,進行加1操作,把新值寫入num。假如當(dāng)前線程先讀取了num的值放入工作內(nèi)存,然后線程這是被切換到了另一個線程,另一個線程修改了num的值;在回到當(dāng)前線程繼續(xù)執(zhí)行,這是的num就不是最新的值,所以導(dǎo)致出錯。
那我們加上volatile關(guān)鍵字會怎么樣呢?

volatile static int num;

其實不用看結(jié)果我們也能知道volatile也沒用,它不能保證原子性,那么我們該怎么保證同步呢。這就輪到synchronized登場了。

synchronized

我們把Thread的run改成如下形式

        new Thread() {
            @Override
            public void run() {
                synchronized (Main.class) {
                    for (int j = 0; j < 1000; j++) {
                        num++;
                    }
                    System.out.println(Thread.currentThread().getName() + ": num = " + num);
                }
            }
        }.start();

        //輸出
        Thread-0: num = 1000
        Thread-3: num = 2000
        Thread-1: num = 3000
        Thread-4: num = 4000
        Thread-6: num = 5000
        Thread-2: num = 6000
        Thread-7: num = 7000
        Thread-8: num = 8000
        Thread-5: num = 9000
        Thread-9: num = 10000
        main, num = 10000

這就能正確的同步,synchronized實際上是一種鎖機制,括號里面是鎖的內(nèi)容,這里鎖的是當(dāng)前類的.class對象,對象在當(dāng)前進程中是單例的,只有一個。當(dāng)一個線程獲取到該鎖后,其他線程再去嘗試獲取鎖就會等待,直到持有鎖的線程運行完畢,自動釋放鎖后,再去嘗試獲取該鎖。即由synchronized保護的代碼塊每次只能由一個線程運行,這樣就保證了同步性。synchronized可作用于一段代碼或方法,既可以保證可見性,又能夠保證原子性。

線程啟動相關(guān)

在Java中,我們可以利用Thread的子類啟動線程,也可以實現(xiàn)Runnable的接口來啟動線程;Thread本質(zhì)也是實現(xiàn)了Runnable;

public class MyThread extends Thread{
   @Override
   public void run(){
       //TODO
   }
}
new MyThread().start();
public class MyRunnable implements Runnable{
   @Override
   public void run(){
       //TODO
   }
}
new MyRunnable().start();

當(dāng)然,還可以用ThreadFactory來啟動線程

ThreadFactory factory = Executors.defaultThreadFactory();
factory.newThread(new Runnable() {
    @Override
     public void run() {
         //TODO
     }
}).start();

還有一個帶返回的線程Callable+FutureTask

Callable<Integer> callable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        return new Random().nextInt(10);
    }
};
FutureTask<Integer> future = new FutureTask<Integer>(callable);
new Thread(future).start();
//獲取返回值
try {
    System.out.println(future.get());
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
}

注意啟動線程的操作是start()方法,而不是run()方法,以run()啟動的線程實際上是串行執(zhí)行的代碼,即直接執(zhí)行線程對象的run()方法,而不是啟動一個線程

總結(jié)

我們可以看出volatile雖然具有可見性但是并不能保證原子性。

性能方面,synchronized關(guān)鍵字是防止多個線程同時執(zhí)行一段代碼,就會影響程序執(zhí)行效率,而volatile關(guān)鍵字在某些情況下性能要優(yōu)于synchronized。

但是要注意volatile關(guān)鍵字是無法替代synchronized關(guān)鍵字的,因為volatile關(guān)鍵字無法保證操作的原子性。

線程其他知識

線程暫停

線程有一個sleep的靜態(tài)方法用于暫停線程,并且會阻塞,不會釋放已經(jīng)持有的鎖。單位:毫秒

Thread.sleep(1000);

線程等待

等待隊列

所有的實例對象都有一個等待隊列,它是在實例的wait方法執(zhí)行后停止操作的線程的隊列。在執(zhí)行完wait方法后,線程便會暫停操作,進入等待隊列。除非發(fā)生以下其中一種情況,否則將一直在等待隊列中休眠。反之將會退出隊列。等待隊列是一個虛擬的概念,它既不是實例中的字段,也不是用于獲取正在實例上等待的線程列表的方法。
· 有其他線程的notify方法來喚醒線程
· 有其他線程的notifyAll方法來喚醒線程
· 有其他線程的interrupt方法來喚醒線程
· wait方法超時

將線程放入等待隊列

Object obj =new Object();
new Thread(){
    @Override
    public void run(){
        synchronized(obj){
            .....
            obj.wait();//線程將進入等待隊列
            .....
        }
    }
}.start();

假如是鎖當(dāng)前對象,則wait()等同于this.wait()。

public clsss TestWait{
    public static void main(String[] args){
        TestWait test = new TestWait();
        new Thread(){
            @Override
            public void run(){
                test.testWait();
            }
        }.start();
    }
    public void testWait(){
        synchronized(TestWait.this){
            try {
                wait();//等同于this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

注意執(zhí)行wait方法必須持有鎖,線程進入等待隊列必須釋放鎖,這也是跟sleep不同的地方,sleep會阻塞不會釋放鎖。

wait.png

從等待隊列中取出線程

notify方法會將等待隊列中的一個線程取出。

public class TestWait {
    public static void main(String[] args) {
        TestWait test = new TestWait();
        new Thread() {
            @Override
            public void run() {
                test.testWait();
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                test.testNotify();
            }
        }.start();
    }

    public void testWait() {
        synchronized (TestWait.this) {
            try {
                System.out.println("進入等待隊列" + System.currentTimeMillis() / 1000);
                wait();//等同于this.wait();
                System.out.println("從等待隊列中恢復(fù)執(zhí)行" + System.currentTimeMillis() / 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void testNotify() {
        synchronized (TestWait.this) {
            notify();//等同于this.notify();
        }
    }
}
//輸出
進入等待隊列1524497926
從等待隊列中恢復(fù)執(zhí)行1524497927

那么第一個線程確實被喚醒了,并且時間差一秒。

notify.png

注意B執(zhí)行notify后A不會立即運行,而是要等B運行完后釋放鎖,A這時候去重新獲取鎖后,才能運行。

取出等待隊列的所有線程

notify只能喚醒一個線程,notify會喚醒所有在等待隊列中的線程。其他跟notify一樣。注意notify,notifyAll,wait均需要獲取鎖才能使用,否則會拋出異常java.lang. IllegalMonitorStateException
由于使用notify只能喚醒一個線程,所以處理速度比notifyAll快;但使用notify時,如果處理不好,程序邊可能終止。一般來說,使用notifyAll的代碼比notify要更為健壯

區(qū)別

wait,notify,notifyAll是Object類的方法,而不是Thread類的固有方法;sleep是Thread類的靜態(tài)方法。但由于Oeject是所有類的父類,所以wait,notify,notifyAll也可以通過Thread使用,但不建議。

線程狀態(tài)切換圖

thread_state.png
最后編輯于
?著作權(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)容

  • 本文出自 Eddy Wiki ,轉(zhuǎn)載請注明出處:http://eddy.wiki/interview-java.h...
    eddy_wiki閱讀 2,298評論 0 14
  • Java-Review-Note——4.多線程 標(biāo)簽: JavaStudy PS:本來是分開三篇的,后來想想還是整...
    coder_pig閱讀 1,772評論 2 17
  • 1.解決信號量丟失和假喚醒 public class MyWaitNotify3{ MonitorObject m...
    Q羅閱讀 1,006評論 0 1
  • 使用頻率越高的東西,越要舍得花錢。
    coco吖閱讀 201評論 0 0
  • 職場也是一個賽場,如果把職場人比做“兔子”,將職業(yè)發(fā)展目標(biāo)當(dāng)成“比賽終點”的話,有多少“兔子”能夠超越對手并順利跑...
    ElliotJu閱讀 633評論 0 1

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