1. 程式人生 > >【阿裏面試系列】Java線程的應用及挑戰

【阿裏面試系列】Java線程的應用及挑戰

thread線程 ram 時間 title extend 歸類 ace 定義 code

文章簡介

上一篇文章【「阿裏面試系列」搞懂並發編程,輕松應對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等技術視頻。

技術分享圖片

【阿裏面試系列】Java線程的應用及挑戰