1. 程式人生 > 程式設計 >Java基礎複習之旅(3)-執行緒篇

Java基礎複習之旅(3)-執行緒篇

1. 基本概念

1.1 執行緒、程式、協程是什麼?

首先要知道,一個程式可以有多個執行緒,一個執行緒可以有多個協程。
先說說執行緒跟程式:

  • 程式是資源分配的最小單元,執行緒是CPU排程的最小單位。所有與程式相關的資源,均被記錄在PCB(印刷電路板)中。
  • 執行緒隸屬於某一個程式,共享程式的資源。執行緒只由堆疊暫存器,程式計數器和棧堆指標組成。
  • 程式有獨立的地址空間,彼此之間相互不影響,可以看做一個獨立的應用。而執行緒只是程式的執行路徑。
  • 總結成一句話就是:對作業系統來說,程式是最小的資源管理單元,執行緒的最小的執行單元。

協程平時聽的比較少,所以這裡引用維基百科的解釋:

  • 協程是計算機程式的一類元件,推廣了協作式多工的子程式,允許執行被掛起與被恢復。
  • 協程是一種比執行緒更加輕量級的存在,並且協程與執行緒最大的區別就在於。執行緒是搶佔式多工的,協程是協作式多工的。這就意味著協程提供併發性而非並行性,所以不需要加鎖的操作。

1.2 run()方法與start()方法的區別?

\quad start()方法是執行多執行緒的主方法,在start()方法內部,通過呼叫run()方法啟動這個執行緒。run()方法用以啟動一個執行緒,然後主執行緒立刻返回。該啟動的執行緒不會馬上執行,而是在等待佇列中等待CPU的排程。

1.3 程式的狀態與執行緒的狀態

\quad我們知道在作業系統中,程式有五種狀態: 新建、執行、就緒、阻塞、終止。注意這只是比較廣泛的說法,不同的作業系統對程式狀態定義也有不同。

但是在Java中,Thread類定義了六種執行緒狀態,千萬不要混淆了:

  • 1.初始(NEW):新建立了一個執行緒物件,但是還沒有呼叫start()方法。
  • 2.可執行(RUNNABLE):分為就緒狀態與執行中狀態。
    在呼叫start()方法之後執行緒就進入就緒狀態,等待CPU的排程。注意:執行緒在睡眠和掛起中恢復的時候也會進入就緒狀態。
    就緒狀態的執行緒得到了CPU的時間片,執行緒被設定為當前執行緒,開始執行run()方法,執行緒進入執行中狀態。
  • 3.阻塞(BLOCKED):表示執行緒由於鎖的原因造成了阻塞。
  • 4.等待(WAITING)比如使用Thread.sleep()方法,CPU不會給執行緒分配時間,使得執行緒處於等待狀態。處於該狀態的執行緒需要被期待物件所喚醒,否則會處於無限期的等待狀態。
  • 5.超時等待(TIME_WAITTING):與等待轉態的區別就在於,他不會無限期的等待,當達到一定的時間之後,他們會被自動的喚醒。
  • 6.終止狀態(TERMINATED):當執行緒的run()方法執行完畢時,或者主執行緒的main()執行完畢時,我們就認為這個執行緒終止了。注意:執行緒一旦終止了,就不能呼叫start()方法。

補充: 進入Thread.getState()方法可以發現在JVM中也定義了六種執行緒的狀態:

public State getState() {
        // get current thread state
        return sun.misc.VM.toThreadState(threadStatus);
    }
複製程式碼

sun.misc包中的VM類中的執行緒狀態:

private static final int JVMTI_THREAD_STATE_ALIVE = 1;
    private static final int JVMTI_THREAD_STATE_TERMINATED = 2;
    private static final int JVMTI_THREAD_STATE_RUNNABLE = 4;
    private static final int JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER = 1024;
    private static final int JVMTI_THREAD_STATE_WAITING_INDEFINITELY = 16;
    private static final int JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT = 32;
複製程式碼

1.4 Synchronized與ReentrantLock的相同點與不同點

  • 相同點在於,二者都是採用加鎖的方式實現同步,而且都是阻塞式的同步。
  • 區別在於,Synchronized是Java的關鍵字,需要JVM去實現。而ReentrantLock是JDK1.5之後提供的API層面的互斥鎖,需要lock(),unlock()方法以及try/catch去實現。

1.5 新建執行緒的幾種方法

  • 通過繼承Thread類,重寫run()方法。
  • 通過實現Callable介面。
  • 通過實現Runnable介面。

1.6 實現執行緒安全的方法

  • 使用不可變的類:Integer/String
  • 使用synchronized同步塊同步物件

\quad synchronized({Object})可以用以鎖住一個物件,同樣synchronized還可以用在方法簽名上。那麼問題來了,對於非靜態方法,synchronized很明顯是鎖住呼叫這個方法的實體,那麼對於靜態方法,鎖住的是什麼?答案是鎖住的是類在JVM中所儲存的Class物件。

  • 使用ReentrantLock,配合lock(),unLock()使用。相比於synchronized,ReentrantLock更靈活,可以在這個方法中加鎖,在其他方法中將鎖釋放。
  • 使用java.util.concurrent包下面的方法去替換在多執行緒情況下不安全的實現,比如HashMap可以替換為ConcurrentHashMap。
  • 使用Collections.synchronized{Colleation}
    private static List<Integer> list = Collections.synchronizedList(new ArrayList<>());
複製程式碼
  • 使用原子操作:AtomicInteger/AtomicLong/AtomicBoolean

1.7 wait(),notify(),notifyAll()方法

首先,他們都是Object類中的方法,接下來看下面這個例子:

public static void main(String[] args) {
        new Thread(new Thread1()).start();
        new Thread(new Thread2()).start();
        new Thread(new Thread3()).start();
    }

    static class Thread1 extends Thread {
        @Override
        public void run() {
            System.out.println("Thread1");
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            System.out.println("Thread2");
        }
    }

    static class Thread3 extends Thread {
        @Override
        public void run() {
            System.out.println("Thread3");
        }
    }
複製程式碼

這裡我開闢了三個執行緒,分別去實現自己的方法。結果很明顯,就是按順序列印結果:

Thread1
Thread2
Thread3
複製程式碼

但是當我對Thread1使用wait()方法時,

static class Thread1 extends Thread {
        @Override
        public void run() {
            synchronized (lock){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Thread1");
        }
    }
複製程式碼

Thread會交出自己所持有的鎖,讓其他程式去爭奪這個鎖,而自己就一直等待著其他執行緒的喚醒。

他就一直在這等啊,直到我們在其他執行緒中使用notify()方法將其喚醒:

static class Thread3 extends Thread {
        @Override
        public void run() {
            synchronized (lock){
                lock.notify();
            }
            System.out.println("Thread3");
        }
    }
複製程式碼

這時候,執行的順序也改變了:

Thread2
Thread3
Thread1
複製程式碼

那麼notifyAll()就是喚醒其他正在等待的執行緒,然後讓他們之間重新去爭奪鎖:

static class Thread1 extends Thread {
        @Override
        public void run() {
            synchronized (lock){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Thread1");
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            synchronized (lock){
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Thread2");
        }
    }

    static class Thread3 extends Thread {
        @Override
        public void run() {
            synchronized (lock){
                lock.notifyAll();
            }
            System.out.println("Thread3");
        }
    }
複製程式碼

結果:

Thread1
Thread2
Thread3
複製程式碼

注意:使用Object的wait(),notify(),notifyAll()方法都需要持有這個物件的監視器,就是程式碼中的synchronized()。java doc中的原話:

* This method should only be called by a thread that is the owner
* of this object's monitor.
複製程式碼

否則會報出IllegalMonitorStateException異常。

8.什麼是執行緒池?

\quad 由於Java的執行緒排程完全依賴於作業系統,所以每個執行緒都會佔用一定的資源,進而我們不可能隨心所欲的去開闢執行緒。所以,這時候就需要執行緒池了,執行緒池就是預先在記憶體中開闢一塊資源,專門用於執行緒的排程。當需要執行緒執行任務時,就通過執行緒池管理器來實現執行緒的分配。

9.有哪些建立執行緒池的方法,他們之間的區別在哪?

\quad java.util.concurrent包中的Executors類中,封裝了四種開闢執行緒池的方法。

  • newFixedThreadPool—數量固定的執行緒池
    \quad 規定最大的數量,超過這個數量後進來的執行緒任務放入等待數列中。
  • newSingleThreadExecutor—只有一個執行緒的執行緒池
    \quad 一次只能執行一個任務。
  • newCachedThreadPool-快取型執行緒池
    \quad 快取型即在核心執行緒達到最大值之前,有新的任務就線上程池中加入新的執行緒,即使有空閒的執行緒也不復用。達到最大執行緒後,再複用空閒的執行緒。沒有空餘的執行緒則新建臨時執行緒,用於處理大量短時間工作的執行緒池。
  • new ScheduledThreadPool—計劃型執行緒池
    \quad 可以設定指定的延時或定期執行任務。

10.Runnable()與Callable()的區別?

  • Runnable()是沒有返回值的,而Callable()允許有返回值。
  • Runnable()不能宣告異常,Callable()可以。 程式碼演示:
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        executorService.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return 1;
            }
        });
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(" ");
            }
        });
        System.out.println(submit.get());
        //執行緒池使用完畢後,必須關閉
        executorService.shutdown();
複製程式碼

11. 守護執行緒與非守護執行緒的區別?

\quad 程式允許完畢,jvm會等待非守護執行緒完成後關閉,但是jvm不會等待守護執行緒關閉,守護執行緒最典型的例子就是GC程式。

12. 什麼是樂觀鎖?什麼是悲觀鎖?

  • 樂觀鎖:樂觀鎖認為競爭不總是會發生,因此它不需要持有鎖,將比較-替換這兩個動作作為一個原子操作嘗試去修改記憶體中的變數,如果失敗則表示發生衝突,那麼就應該有相應的重試邏輯。
  • 悲觀鎖:悲觀鎖認為競爭總是會發生,因此每次對某資源進行操作時,都會持有一個獨佔的鎖,就像synchronized,不管三七二十一,直接上了鎖就操作資源了。

2. 死鎖問題

2.1 如何產生死鎖?

像下面這種情況鎖與鎖之間相互競爭的情況:

public class MultiplyThreadTest {
    private static final Object lock = new Object();
    private static final Object lock1 = new Object();

    public static void main(String[] args) {
        new ThreadClass1().start();
        new ThreadClass2().start();
    }

    static class ThreadClass1 extends Thread {
        @Override
        public void run() {
            synchronized (lock) {
                try {
                    Thread.sleep(0);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("Thread1");
                }
            }
        }
    }

    static class ThreadClass2 extends Thread {
        @Override
        public void run() {
            synchronized (lock1) {
                synchronized (lock) {
                    System.out.println("Thread2");
                }
            }
        }
    }
}
複製程式碼

其實我剛開始寫出的來的時候,我自己都有疑問:

  • 1.不加Thread.sleep(0)這兩個執行緒是不會產生死鎖的,為什麼?
  • 2.就算加了Thread.sleep(0),那也只是讓執行緒睡0毫秒,加了跟沒加不是一樣嘛,為什麼加上了就能產生死鎖呢?

\quad其實這地方,涉及到一個冷門的知識點,可以參考stackoverflow的回答,那就是絕大部分的作業系統的時間精度都在10ms,所以看上去是sleep(0),但由於精度問題,實際上不是睡0ms。另外一個,不加Thread.sleep(0)就不會產生死鎖的原因在於,執行緒執行這兩個簡單的方法的過程其實是非常短暫的,所以鎖之間根本來不及相互競爭。

2.2 如何排查及預防死鎖?

  • 使用jsp命令檢視哪些程式正在執行:
  • 找到執行的類對應的程式編號,顯示詳細的呼叫棧資訊,匯出到記事本中:
jstack 17424 > ~/Desktop/1.txt
複製程式碼

可以看到日誌中,有詳細的死鎖資訊。

  • 預防死鎖的原則就是:保證每個執行緒都以相同的順序拿到資源的鎖。像這個例子中兩個執行緒拿到鎖的順序就相互矛盾,所以容易產生死鎖。

3.三種方式實現生產者,消費者模型

3.1 使用wait(),notify()實現

public class ProducerConsumer1 {

    private static final Object lock = new Object();
    private static Optional<Integer> optional = Optional.empty();

    public static void main(String[] args) throws InterruptedException {
        Container container = new Container(optional);
        Producer producer = new Producer(container);
        Consumer consumer = new Consumer(container);

        producer.start();
        consumer.start();

        producer.join();
        producer.join();
    }

    public static class Producer extends Thread {
        Container container;

        public Producer(Container container) {
            this.container = container;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                synchronized (lock) {
                    while (container.getOptional().isPresent()) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    Integer integer = new Random().nextInt();
                    System.out.println("Product:" + integer);
                    container.setOptional(Optional.of(integer));
                    lock.notify();
                }
            }
        }
    }

    public static class Consumer extends Thread {
        Container container;

        public Consumer(Container container) {
            this.container = container;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                synchronized (lock) {
                    while (!container.getOptional().isPresent()) {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("Consume:" + container.getOptional().get());
                    container.setOptional(Optional.empty());
                    lock.notify();
                }
            }
        }
    }
}
複製程式碼

\quad雖然看起來程式碼挺多的,但其實就是一個思想,就是生產者在生產之前,先檢查容器中是否為空,如果不為空就等待,為空才生產。同樣,消費者也需要在容器中有值的情況下才會消費,否則就等待。

3.2 使用lock,condition實現

借鑑Condition介面中的提示:

 *   public void put(Object x) throws InterruptedException {
 *     <b>lock.lock();
 *     try {</b>
 *       while (count == items.length)
 *         <b>notFull.await();</b>
 *       items[putptr] = x;
 *       if (++putptr == items.length) putptr = 0;
 *       ++count;
 *       <b>notEmpty.signal();</b>
 *     <b>} finally {
 *       lock.unlock();
 *     }</b>
 *   }
複製程式碼

程式碼如下:

public class ProducerConsumer2 {
    private static final Lock lock = new ReentrantLock();
    private static final Condition notConsumer = lock.newCondition();
    private static final Condition notProduct = lock.newCondition();

    private static Optional<Integer> optional = Optional.empty();

    public static void main(String[] args) throws InterruptedException {
        Container container = new Container(optional);
        Producer producer = new Producer(container);
        Consumer consumer = new Consumer(container);

        producer.start();
        consumer.start();

        producer.join();
        producer.join();
    }

    public static class Producer extends Thread {
        private Container container;

        public Producer(Container container) {
            this.container = container;
        }

        @Override
        public void run() {
            lock.lock();
            try {
                while (container.getOptional().isPresent()) {
                    try {
                    //注意:是await()而不是wait()
                        notConsumer.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                int random = new Random().nextInt();
                System.out.println("Product" + random);
                container.setOptional(Optional.of(random));
                notProduct.signal();
            } finally {
                lock.unlock();
            }
        }
    }

    public static class Consumer extends Thread {

        private Container container;

        public Consumer(Container container) {
            this.container = container;
        }

        @Override
        public void run() {
            try {
                lock.lock();
                while (!container.getOptional().isPresent()) {
                    try {
                        notProduct.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(container.getOptional().get());
                container.setOptional(Optional.empty());
                notConsumer.notify();
            } finally {
                lock.unlock();
            }
        }
    }
}
複製程式碼

與第一種方法類似,這裡增加了Condition條件判斷,並用到了ReentrantLock。

3.3 使用BlockingQueue.take()/put()方法實現

public class ProducerConsumer3 {

    private static final BlockingQueue<Integer> queue = new LinkedBlockingDeque<>(1);
    private static final BlockingQueue<Integer> signal = new LinkedBlockingDeque<>(1);

    public static void main(String[] args) throws InterruptedException {
        Producer producer = new Producer(queue,signal);
        Consumer consumer = new Consumer(queue,signal);

        producer.start();
        consumer.start();

        producer.join();
        producer.join();
    }

    public static class Producer extends Thread {
        BlockingQueue<Integer> queue;
        BlockingQueue<Integer> signal;

        public Producer(BlockingQueue<Integer> queue,BlockingQueue<Integer> signal) {
            this.queue = queue;
            this.signal = signal;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                int random = new Random().nextInt();
                System.out.println("Product:" + random);
                try {
                    queue.put(random);
                    signal.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static class Consumer extends Thread {
        BlockingQueue<Integer> queue;
        BlockingQueue<Integer> signal;

        public Consumer(BlockingQueue<Integer> queue,BlockingQueue<Integer> signal) {
            this.queue = queue;
            this.signal = signal;
        }

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println("Consumer:" + queue.take());
                    signal.put(0);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
複製程式碼

\quad 這種方法就比較簡單了,不用判斷容器是否為空,因為BlockingQueue在內部為我們實現了判斷,直接拿來用就可以了。這裡需要注意一點,由於take()與put()方法幾乎是同時執行的,所以需要加一個signal標誌資訊,避免亂序。

Container容器:

public class Container {
    Optional<Integer> optional;

    public Container(Optional<Integer> optional) {
        this.optional = optional;
    }

    public Optional<Integer> getOptional() {
        return optional;
    }

    public void setOptional(Optional<Integer> optional) {
        this.optional = optional;
    }
}
複製程式碼

參考資料