(二)線程的應用及挑戰

文章簡介

上一篇文章咱們瞭解了進程和線程的發展歷史、線程的生命週期、線程的優點和使用場景,這一篇,咱們從Java層面更進一步瞭解線程的使用java

內容導航

  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

繼承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);

掃碼關注公衆號

相關文章
相關標籤/搜索