1. 程式人生 > >【阿里面試系列】Java執行緒的應用及挑戰

【阿里面試系列】Java執行緒的應用及挑戰

文章簡介

上一篇文章【「阿里面試系列」搞懂併發程式設計,輕鬆應對80%的面試場景】我們瞭解了程序和執行緒的發展歷史、執行緒的生命週期、執行緒的優勢和使用場景,這一篇,我們從Java層面更進一步瞭解執行緒的使用。關注我的技術公眾號【架構師修煉寶典】一週出產1-2篇技術文章。Q群725219329分享併發程式設計,分散式,微服務架構,效能優化,原始碼,設計模式,高併發,高可用,Spring,Netty,tomcat,JVM等技術視訊。

內容導航

  1. 併發程式設計的挑戰
  2. 執行緒在Java中的使用

併發程式設計的挑戰

引入多執行緒的目的在第一篇提到過,就是為了充分利用CPU是的程式執行得更快,當然並不是說啟動的執行緒越多越好。在實際使用多執行緒的時候,會面臨非常多的挑戰

執行緒安全問題

執行緒安全問題值的是當多個執行緒訪問同一個物件時,如果不考慮這些執行時環境採用的排程方式或者這些執行緒將如何交替執行,並且在程式碼中不需要任何同步操作的情況下,這個類都能夠表現出正確的行為,那麼這個類就是執行緒安全的
比如下面的程式碼是一個單例模式,在程式碼的註釋出,如果多個執行緒併發訪問,則會出現多個例項。導致無法實現單例的效果

public class SingletonDemo {
   private static SingletonDemo singletonDemo=null;
   private SingletonDemo(){}
    public static SingletonDemo getInstance(){
        if(singletonDemo==null){/***執行緒安全問題***/
           singletonDemo=new SingletonDemo();
        }
        return singletonDemo;
    }
}

通常來說,我們把多執行緒程式設計中的執行緒安全問題歸類成如下三個,至於每一個問題的本質,在後續的文章中我們會單獨講解

  1. 原子性
  2. 可見性
  3. 有序性

上下文切換問題

在單核心CPU架構中,對於多執行緒的執行是基於CPU時間片切換來實現的偽並行。由於時間片非常短導致使用者以為是多個執行緒並行執行。而一次上下文切換,實際就是當前執行緒執行一個時間片之後切換到另外一個執行緒,並且儲存當前執行緒執行的狀態這個過程。上下文切換會影響到執行緒的執行速度,對於系統來說意味著會消耗大量的CPU時間

減少上下文切換的方式

  1. 無鎖併發程式設計,在多執行緒競爭鎖時,會導致大量的上下文切換。避免使用鎖去解決併發問題可以減少上下文切換
  2. CAS演算法,CAS是一種樂觀鎖機制,不需要加鎖
  3. 使用與硬體資源匹配合適的執行緒數

死鎖

在解決執行緒安全問題的場景中,我們會比較多的考慮使用鎖,因為它使用比較簡單。但是鎖的使用如果不恰當,則會引發死鎖的可能性,一旦產生死鎖,就會造成比較嚴重的問題:產生死鎖的執行緒會一直佔用鎖資源,導致其他嘗試獲取鎖的執行緒也發生死鎖,造成系統崩潰

以下是死鎖的簡單案例

public class DeadLockDemo {
    //定義鎖物件
    private final Object lockA = new Object();
    private final Object lockB = new Object();
    private void deadLock(){
        new Thread(()->{
            synchronized (lockA){
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB){
                    System.out.println("Lock B");
                }
            }
        }).start();
        new Thread(()->{
            synchronized (lockB){
                synchronized (lockA){
                    System.out.println("Lock A");
                }
            }
        }).start();
    }
    public static void main(String[] args) {
        new DeadLockDemo().deadLock();
    }
}

通過jstack分析死鎖

1.首先通過jps獲取當前執行的程序的pid

6628 Jps
17588 RemoteMavenServer
19220 Launcher
19004 DeadLockDemo

2.jstack列印堆疊資訊,輸入 jstack19004, 會列印如下日誌,可以很明顯看到死鎖的資訊提示

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000000001d461e68 (object 0x000000076b310df8, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000001d463258 (object 0x000000076b310e08, a java.lang.Object),
  which is held by "Thread-1"

解決死鎖的手段
1.保證多個執行緒按照相同的順序獲取鎖
2.設定獲取鎖的超時時間,超過設定時間以後自動釋放
3.死鎖檢測

資源限制

資源限制主要指的是硬體資源和軟體資源,在開發多執行緒應用時,程式的執行速度受限於這兩個資源。硬體的資源限制無非就是磁碟、CPU、記憶體、網路;軟體資源的限制有很多,比如資料庫連線數、計算機能夠支援的最大連線數等
資源限制導致的問題最直觀的體現就是前面說的上下文切換,也就是CPU資源和執行緒資源的嚴重不均衡導致頻繁上下文切換,反而會造成程式的執行速度下降

資源限制的主要解決方案,就是缺啥補啥。CPU不夠用,可以增加CPU核心數;一臺機器的資源有限,則增加多臺機器來做叢集。

執行緒在Java中的使用

在Java中實現多執行緒的方式比較簡單,因為Java中提供了非常方便的API來實現多執行緒。
1.繼承Thread類實現多執行緒
2.實現Runnable介面
3.實現Callable介面通過Future包裝器來建立Thread執行緒,這種是帶返回值的執行緒
4.使用執行緒池ExecutorService

關注我的技術公眾號【架構師修煉寶典】一週出產1-2篇技術文章。Q群725219329分享併發程式設計,分散式,微服務架構,效能優化,原始碼,設計模式,高併發,高可用,Spring,Netty,tomcat,JVM等技術視訊。

繼承Thread類

繼承Thread類,然後重寫run方法,在run方法中編寫當前執行緒需要執行的邏輯。最後通過執行緒例項的start方法來啟動一個執行緒

public class ThreadDemo extends Thread{
    @Override
    public void run() {
        //重寫run方法,提供當前執行緒執行的邏輯
        System.out.println("Hello world");
    }
    public static void main(String[] args) {
        ThreadDemo threadDemo=new ThreadDemo();
        threadDemo.start();
    }
}

Thread類其實是實現了Runnable介面,因此Thread自己也是一個執行緒例項,但是我們不能直接用 newThread().start()去啟動一個執行緒,原因很簡單,Thread類中的run方法是沒有實際意義的,只是一個呼叫通過建構函式傳遞寄來的另一個Runnable實現類的run方法,這塊的具體演示會在Runnable介面的程式碼中看到

public
class Thread implements Runnable {
    /* What will be run. */
    private Runnable target;
    ...
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    ...

實現Runnable介面

如果需要使用執行緒的類已經繼承了其他的類,那麼按照Java的單一繼承原則,無法再繼承Thread類來實現執行緒,所以可以通過實現Runnable介面來實現多執行緒

public class RunnableDemo implements Runnable{
    @Override
    public void run() {
        //重寫run方法,提供當前執行緒執行的邏輯
        System.out.println("Hello world");
    }
    public static void main(String[] args) {
        RunnableDemo runnableDemo=new RunnableDemo();
        new Thread(runnableDemo).start();
    }
}

上面的程式碼中,實現了Runnable介面,重寫了run方法;接著為了能夠啟動RunnableDemo這個執行緒,必須要例項化一個Thread類,通過構造方法傳遞一個Runnable介面實現類去啟動,Thread的run方法就會呼叫target.run來運行當前執行緒,程式碼在上面.

實現Callable介面

在有些多執行緒使用的場景中,我們有時候需要獲取非同步執行緒執行完畢以後的反饋結果,也許是主執行緒需要拿到子執行緒的執行結果來處理其他業務邏輯,也許是需要知道執行緒執行的狀態。那麼Callable介面可以很好的實現這個功能

public class CallableDemo implements Callable<String>{
    @Override
    public String call() throws Exception {
        return "hello world";
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> callable=new CallableDemo();
        FutureTask<String> task=new FutureTask<>(callable);
        new Thread(task).start();
        System.out.println(task.get());//獲取執行緒的返回值
    }
}

在上面程式碼案例中的最後一行 task.get()就是獲取執行緒的返回值,這個過程是阻塞的,當子執行緒還沒有執行完的時候,主執行緒會一直阻塞直到結果返回

使用執行緒池

為了減少頻繁建立執行緒和銷燬執行緒帶來的效能開銷,在實際使用的時候我們會採用執行緒池來建立執行緒,在這裡我不打算展開多執行緒的好處和原理,我會在後續的文章中單獨說明。

public class ExecutorServiceDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //建立一個固定執行緒數的執行緒池
        ExecutorService pool = Executors.newFixedThreadPool(1);
        Future future=pool.submit(new CallableDemo()); 
        System.out.println(future.get());
    }
}

pool.submit有幾個過載方法,可以傳遞帶返回值的執行緒例項,也可以傳遞不帶返回值的執行緒例項,原始碼如下

/*01*/Future<?> submit(Runnable task);
/*02*/<T> Future<T> submit(Runnable task, T result);
/*03*/<T> Future<T> submit(Callable<T> task);

關注我的技術公眾號【架構師修煉寶典】一週出產1-2篇技術文章。Q群725219329分享併發程式設計,分散式,微服務架構,效能優化,原始碼,設計模式,高併發,高可用,Spring,Netty,tomcat,JVM等技術視訊。