[從零開啟 Java 多執行緒 - 1 ]:開胃小菜

一支彩筆發表於2018-09-19

前言

這是一個關於多執行緒的系列文章。在接下來的一段時間裡,文章將圍繞多執行緒進行從淺入深的連載,感興趣的朋友可以關注一下~

正文

小A:我們們聊完了概念性的東西,是不是要聊一聊實際的用法啦?

MDove:OK,接下來我們正式進入多執行緒的世界。今天我們聊一聊基本的使用和一些面試常客的方法。下一篇則重點談一談鎖。

MDove:我們都知道,在Java中開啟多執行緒。有兩種手段:一種是繼續Thread類;另外一種是實現Runable介面。(當然還可以實現Callable、Future等方式。)

小A:那繼承Thread和實現Runable有什麼不同麼?

MDove:從技術角度上來說並沒有不同,最大的不同應該算是設計上。因為我們都知道Java是單繼承,所以當你繼承了Thread勢必不能繼承其他類。因此implement介面可以實現“多繼承”的效果。

小A:那這倆種方式是怎樣寫的呢?

MDove

1.繼承 Thread 類

public class MyThread extends Thread {
    @Override
    public void run() {
        //TODO
    }
}

public static void main(String[] args) {
    MyThread thread = new MyThread();
    thread.start();
}
複製程式碼

2.實現 Runnable 介面

public static void main(String[] args) throws InterruptedException {
    new Thread(new Runnable() {
        @Override
        public void run() {
            //TODO
        }
    }).start();
}
複製程式碼

MDove:呼叫start()就標誌著執行緒的開啟,但是start()方法的呼叫後並不是立即執行多執行緒程式碼,而是使得該執行緒變為可執行態(Runnable),什麼時候執行是由作業系統決定的。

MDove:另外需要注意一點,start()不應該被重複呼叫,否則會出現java.lang.IllegalThreadStateException異常。

小A:start執行緒我會了?那停止執行緒呢?是stop麼?

停止執行緒

MDove:的確有stop()這個方法,不過已經不推薦使用了。比較正確的停止執行緒的幾種方法是:

1、使用退出標誌,使執行緒正常退出,也就是當run()方法完成後執行緒停止。

2、使用interrupt()方法中斷執行緒。

MDove:簡單寫一個思路2的demo,你應該能夠看明白:

public class MyThread extends Thread {
    @Override
    public void run() {
        try {
            for (int i=0; i<50000; i++){
                if (this.isInterrupted()) {
                    System.out.println("已經是停止狀態了!");
                    throw new InterruptedException();
                }
                System.out.println(i);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread =new MyThread();
        myThread.start();
        Thread.sleep(100);
        myThread.interrupt();
    }
}
複製程式碼

小A:通過Thread的interrupt()方法,來通知執行緒停止。然後我們通過isInterrupted()判斷是否停止執行緒,然後使用拋異常的方式停止執行緒?

MDove:沒錯,但是不止拋異常,return,break都可以滿足這個要求。

小A:OK,停止執行緒我明白了,我記得上篇文章,你用了大量的篇幅去聊執行緒安全的問題,那麼在程式碼中,我們應該怎麼做呢?

執行緒安全

MDove:OK,讓我們先模擬一個簡單的不安全的執行緒demo:

public class MyThread implements Runnable {
    private int count = 5;

    @Override
    public void run() {
        fun();
    }

    private void fun() {
        count--;
        System.out.println("執行緒:" + Thread.currentThread().getName() + "開始計算,計算結果count = " + count);
    }
}
複製程式碼
public static void main(String[] args) {
    MyThread myThread = new MyThread();
    Thread thread1 = new Thread(myThread,"執行緒1");
    Thread thread2 = new Thread(myThread,"執行緒2");
    Thread thread3 = new Thread(myThread,"執行緒3");
    Thread thread4 = new Thread(myThread,"執行緒4");
    Thread thread5 = new Thread(myThread,"執行緒5");
    thread1.start();
    thread2.start();
    thread3.start();
    thread4.start();
    thread5.start();
}
複製程式碼

MDove:結果不需多言,我相信有了上次文章的鋪墊,這個demo的問題你也能看出來吧?

小A:是結果有重複麼?沒辦法順序的執行到0?

MDove:沒錯,這裡出現的問題就是原子性的問題。因為自加或者是自減操作,真正成為指令時並非一個指令,而是3部:

  • 1、取值
  • 2、計算
  • 3、賦值

MDove:因此,在這三個步驟中,如果有多個執行緒同時訪問,那麼一定會出現非執行緒安全問題。

小A:那如何解決這個問題呢?

MDove:最直接也是最簡單的方法,使用synchronized同步關鍵字:

private synchronized void fun() {
    count--;
    System.out.println("執行緒:" + Thread.currentThread().getName() + "開始計算,計算結果count = " + count);
}
複製程式碼

列印結果: 執行緒2 計算 count = 4 執行緒3 計算 count = 3 執行緒1 計算 count = 2 執行緒4 計算 count = 1 執行緒5 計算 count = 0

MDove:當然我們也可以使用Lock:

ReentrantLock lock=new ReentrantLock();

private void fun() {
	lock.lock();
	try {
		count--;
		System.out.print("執行緒:" + Thread.currentThread().getName() + "開始計算,計算結果count = " + count);
	} finally {
		lock.unlock();
	}
}
複製程式碼

小A:那這倆者有什麼不同呢?

MDove:不要著急,下一篇文章。再讓我從位元組碼層面,好好得給你捋一捋它們的不同。當然這個題目還有其他的解法,比如volatile、AtomicInteger等手段保證可見性、原子性、有序性。

小A:volatile、AtomicInteger又是啥?

MDove:不要著急,接下來文章會好好針對volatile進行總結,畢竟是面試的常客。當然AtomicInteger也頗為重要,因為它是CAS思想的具體實現....

面試常客API

MDove:接下來,我們聊一聊一些基礎的api的作用: ####sleep() 方法: sleep() 允許 指定以毫秒為單位的一段時間作為引數,它使得執行緒在指定的時間內進入阻塞狀態,不能得到CPU 時間,指定的時間一過,執行緒重新進入可執行狀態。(不釋放鎖)

suspend() 和 resume() 方法:

suspend()使得執行緒進入阻塞狀態,並且不會自動恢復,必須其對應的resume() 被呼叫,才能使得執行緒重新進入可執行狀態。典型地,suspend() 和 resume() 被用在等待另一個執行緒產生的結果的情形。

Thread 類中的方法;不會釋放鎖;可在任何位置呼叫。

yield() 方法:

yield() 使得執行緒放棄當前分得的 CPU 時間,但是不使執行緒阻塞,即執行緒仍處於可執行狀態,隨時可能再次分得 CPU 時間。

wait() 和 notify() 方法():

兩個方法配套使用,wait() 使得執行緒進入阻塞狀態,它有兩種形式,一種允許 指定以毫秒為單位的一段時間作為引數,另一種沒有引數,前者當對應的 notify() 被呼叫或者超出指定時間時執行緒重新進入可執行狀態,後者則必須對應的 notify() 被呼叫。

Object中的方法; 會釋放鎖;這一對方法卻必須在 synchronized 方法或塊中呼叫。原因也很好理解:只有在synchronized這類同步程式碼塊中,當前執行緒才佔有鎖,才有鎖可以釋放。同理,呼叫方法的物件上的鎖必須為當前執行緒所擁有,這樣才有鎖可以釋放。因此,這一對方法呼叫必須放置在synchronized這樣的同步程式碼中。注意:若不滿足這一條件,仍能編譯,但在執行時會出現IllegalMonitorStateException異常。

interrupt():

不要以為它是中斷某個執行緒!它只是線執行緒傳送一箇中斷訊號,讓執行緒在無限等待時(如死鎖時)能丟擲丟擲,從而結束執行緒,但是如果你吃掉了這個異常,那麼這個執行緒還是不會中斷的!

MDove:這些方法可需要好好的去體會呦。不光是面試中常常遇到,更多是它們是我們操作Thread的利器。

小A:感覺執行緒還可以,不是很難。

MDove:那是因為我們目前才是最簡單的使用階段。如何高效、安全的使用執行緒,可不是隻會使用api就夠,既然synchronized可以滿足執行緒安全,那麼為什麼還需要其他花裡胡哨的各種各樣的其他同步方式。所以多執行緒程式設計需要長久的經驗支撐。我們們接下來的內容會一點點深入,希望你可以一直覺得不難呦~~

小A:嘿嘿嘿,迫不及待了~

劇終

我是一個應屆生,最近和朋友們維護了一個公眾號,內容是我們在從應屆生過渡到開發這一路所踩過的坑,以及我們一步步學習的記錄,如果感興趣的朋友可以關注一下,一同加油~

個人公眾號:IT面試填坑小分隊

相關文章