去年去阿里面試,被問到java 多線程,我是這樣手撕面試官的

1.多線程的基本概念

1.1進程與線程

程序:是爲完成特定任務,用某種語言編寫的一組指令的集合,即一段靜態代碼,靜態對象。java

進程:是程序的一次執行過程,或是正在運行的一個程序,是一個動態的過程,每一個程序都有一個獨立的內存空間算法

線程:是進程中的一個執行路徑,共享一個內存空間,線程之間能夠自由切換,併發執行. 一個進程最少有一個線程編程

線程其實是在進程基礎之上的進一步劃分,一個進程啓動以後,裏面的若干執行路徑又能夠劃分紅若干個線程緩存

1.2並行與併發

併發:指兩個或多個事件在同一個時間段內發生。安全

並行:指兩個或多個事件在同一時刻發生(同時發生)。bash

1.3同步與異步

同步:排隊執行 , 效率低可是安全.多線程

異步:同時執行 , 效率高可是數據不安全.併發

1.4線程的調度

分時調度(時間片):全部線程輪流使用 CPU 的使用權,平均分配每一個線程佔用 CPU 的時間 搶佔式調度:高優先級的線程搶佔CPU異步

Java使用的爲搶佔式調度。ide

CPU使用搶佔式調度模式在多個線程間進行着高速的切換。對於CPU的一個核新而言,某個時刻,只能執行一個線程,而 CPU的在多個線程間切換速度相對咱們的感受要快,看上去就是 在同一時刻運行。 其實,多線程程序並不能提升程序的運行速度,但可以提升程序運行效率,讓CPU的 使用率更高。

1.5線程的優先級

Java線程有優先級,優先級高的線程會得到較多的運行機會。

java線程的優先級用整數表示,取值範圍是1~10,Thread類有如下三個靜態常量:

static int MAX_PRIORITY           線程能夠具備的最高優先級,取值爲10。 static int MIN_PRIORITY           線程能夠具備的最低優先級,取值爲1。 static int NORM_PRIORITY           分配給線程的默認優先級,取值爲5。

Thread類的setPriority()和getPriority()方法分別用來設置和獲取線程的優先級。 主線程的默認優先級爲Thread.NORM_PRIORITY。 setPriority(int newPriority):改變線程的優先級 高優先級的線程要搶佔低優先級的線程的cpu的執行權。可是僅是從機率上來講的,高優先級的線程更有可能被執行。並不意味着只有高優先級的線程執行完之後,低優先級的線程才執行。

2.三種多線程的建立方式

2.1 繼承於Thread類

1.建立一個集成於Thread類的子類 (經過ctrl+o(override)輸入run查找run方法) 2.重寫Thread類的run()方法 3.建立Thread子類的對象 4.經過此對象調用start()方法

public class MyThread extends Thread{
    /*
     run方法就是線程要執行的任務方法
     */
    @Override
    public void run() {
        //這裏的代碼就是一條新的執行路徑
        //這個執行路徑的觸發方法,不是調用run方法,而是經過Thread對象的start()來起啓動任務
       for (int i=0;i<10;i++){
           System.out.println("大大大"+i);
       }
    }
}
 
 
 
 
 public static void main(String[] args) {
        MyThread m = new MyThread();
        m.start();
        for (int i=0;i<10;i++){
            System.out.println("小星星"+i);
        }
複製代碼

2.2  實現Runable接口方式

1.建立一個實現了Runable接口的類 2.實現類去實現Runnable中的抽象方法:run() 3.建立實現類的對象 4.將此對象做爲參數傳遞到Thread類中的構造器中,建立Thread類的對象 5.經過Thread類的對象調用start()

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //線程的任務
        for (int i=0;i<10;i++){
            System.out.println("牀前明月光"+i);
 
        }
 
    }
}
 
 
 
 
       //1.  建立一個任務對象
        MyRunnable r = new MyRunnable();
        //2.  建立一個線程,併爲其分配一個任務
        Thread t = new Thread(r);
        //3.   執行這個線程
        t.start();
        for (int i=0;i<10;i++){
            System.out.println("疑是地上霜"+i);
複製代碼

實現Runnable 與 繼承Thread 相比有以下優點

1.經過建立任務,而後給線程分配的方式來實現多線程,更適合多個線程同時執行相同的任務 2.能夠避免單繼承帶來的侷限性 3.任務與線程自己是分離的,提升了程序的健壯性 4.後續學習的線程池技術,接受Runnable接口的任務,而不接受Thread類型的線程

main方法其實也是一個線程。在java中因此的線程都是同時啓動的,至於何時,哪一個先執行,徹底看誰先獲得CPU的資源。在java中,每次程序運行至少啓動2個線程。一個是main線程,一個是垃圾收集線程。由於每當使用java命令執行一個類的時候,實際上都會啓動一個JVM,每個jVM實習在就是在操做系統中啓動了一個進程。

2.3 實現Callable接口方式

1.建立一個實現callable的實現類  2.實現call方法,將此線程須要執行的操做聲明在call()中  3.建立callable實現類的對象  4.將callable接口實現類的對象做爲傳遞到FutureTask的構造器中,建立FutureTask的對象  5.將FutureTask的對象做爲參數傳遞到Thread類的構造器中,建立Thread對象,並調用start方法啓動(經過FutureTask的對象調用方法get獲取線程中的call的返回值)

接口定義
//Callable接口
public interface Callable<V> {
 V call() throws Exception;
}
 
 
 
1. 編寫類實現Callable接口 , 實現call方法
class XXX implements Callable<T> {
@Override
     public <T> call() throws Exception {
       return T;
     }
}
2. 建立FutureTask對象 , 並傳入第一步編寫的Callable類對象
FutureTask<Integer> future = new FutureTask<>(callable);
3. 經過Thread,啓動線程
new Thread(future).start();
複製代碼

Runnable與Callable的異同

相同點:都是接口                均可以編寫多線程程序                 都採用Thread.start()啓動線程

不一樣點:Runnable沒有返回值;Callable能夠返回執行結果                Callable接口的call()容許拋出異常;Runnable的run()不能拋出

Callable還會獲取返回值——Callalble接口支持返回執行結果,須要調用FutureTask.get()獲得,此方法會阻塞主進程的繼續往下執行,若是不調用不會阻塞。

3.線程安全問題

線程安全問題是指,多個線程對同一個共享數據進行操做時,線程沒來得及更新共享數據,從而致使另外線程沒獲得最新的數據,從而產生線程安全問題。

三種安全鎖:

3.1同步代碼塊

使用同步監視器(鎖) Synchronized(同步監視器){ //須要被同步的代碼 }

說明:

操做共享數據的代碼(全部線程共享的數據的操做的代碼)(視做衛生間區域(全部人共享的廁所)),即爲須要共享的代碼(同步代碼塊,在同步代碼塊中,至關因而一個單線程,效率低) 共享數據:多個線程共同操做的數據,好比公共廁所就類比共享數據 同步監視器(俗稱:鎖):任何一個的對象均可以充當鎖。(可是爲了可讀性通常設置英文成lock)當鎖住之後只能有一個線程能進去(要求:多個線程必需要共用同一把鎖,好比火車上的廁所,同一個標誌表示有人)

3.2同步方法

使用同步方法,對方法進行synchronized關鍵字修飾。將同步代碼塊提取出來成爲一個方法,用synchronized關鍵字修飾此方法。對於runnable接口實現多線程,只須要將同步方法用synchronized修飾而對於繼承自Thread方式,須要將同步方法用static和synchronized修飾,由於對象不惟一(鎖不惟一)

3.3顯示鎖

Lock 子類 ReentrantLock

3.4公平鎖與非公平鎖

顯示鎖 的fair參數爲true 就表示是公平鎖   先到先得

public static void main(String[] args) {
        //線程不安全
        //同步代碼塊 和 同步方法 都屬於隱式鎖
        //解決方案3.顯示鎖 Lock 子類 ReentrantLock
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
 
    }
    static class Ticket implements Runnable{
        private int count = 10;
        // 票數
        //顯示鎖 l : fair參數爲true  就表示是公平鎖
        private Lock l = new ReentrantLock(true);
        @Override
        public void run() {
            while (true) {
                l.lock();  //鎖住
                if (count > 0) {
                    System.out.println("正在準備賣票");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count--;  //賣票
                    System.out.println(Thread.currentThread().getName() + "出票成功,餘票" + count);
                }else {
                    break;
                }
                l.unlock();//開鎖
            }
        }
    }
複製代碼

3.5出現死鎖問題

出現死鎖之後,不會出現提示,只是全部線程都處於阻塞狀態,沒法繼續

死鎖的解決辦法:

1.減小同步共享變量 2.採用專門的算法,多個線程之間規定前後執行的順序,規避死鎖問題 3.減小鎖的嵌套。

4.線程通訊問題

通訊常見方法:

這三種方法只能在同步代碼塊或同步方法中使用。

線程通訊的應用:生產者/消費者問題   1.是不是多線程問題?是的,有生產者線程和消費者線程(多線程的建立,四種方式)   2.多線程問題是否存在共享數據? 存在共享數據----產品(同步方法,同步代碼塊,lock鎖)   3.多線程是否存在線程安全問題? 存在----都對共享數據產品進行了操做。(三種方法)   4.是否存在線程間的通訊,是,若是生產多了到20時,須要通知中止生產(wait)。(線程之間的通訊問題,須要wait,notify等)

5.線程生命週期

線程生命週期的階段    描述

新建    當一個Thread類或其子類的對象被聲明並建立時,新生的線程對象處於新建狀態 就緒    處於新建狀態的線程被start後,將進入線程隊列等待CPU時間片,此時它已具有了運行的條件,只是沒分配到CPU資源 運行    當就緒的線程被調度並得到CPU資源時,便進入運行狀態,run方法定義了線程的操做和功能 阻塞    在某種特殊狀況下,被人爲掛起或執行輸入輸出操做時,讓出CPU並臨時終止本身的執行,進入阻塞狀態 死亡    線程完成了它的所有工做或線程被提早強制性地停止或出現異常致使結束

6.線程池   ExecutorService

6.1 緩存線程池

/**
  * 緩存線程池.
  * (長度無限制)
  * 執行流程:
  *   1. 判斷線程池是否存在空閒線程
  *   2. 存在則使用
  *   3. 不存在,則建立線程 並放入線程池, 而後使用
  */
 ExecutorService service = Executors.newCachedThreadPool();
 //向線程池中 加入 新的任務
 service.execute(new Runnable() {
   @Override
   public void run() {
     System.out.println("線程的名稱:"+Thread.currentThread().getName());
   }
 });
 service.execute(new Runnable() {
   @Override
   public void run() {
     System.out.println("線程的名稱:"+Thread.currentThread().getName());
   }
 });
 service.execute(new Runnable() {
   @Override
   public void run() {
     System.out.println("線程的名稱:"+Thread.currentThread().getName());
   }
 });
複製代碼

6.2 定長線程池

/**
  * 定長線程池.
  * (長度是指定的數值)
  * 執行流程:
  *   1. 判斷線程池是否存在空閒線程
  *   2. 存在則使用
  *   3. 不存在空閒線程,且線程池未滿的狀況下,則建立線程 並放入線程池, 而後使用
  *   4. 不存在空閒線程,且線程池已滿的狀況下,則等待線程池存在空閒線程
  */
 ExecutorService service = Executors.newFixedThreadPool(2);
 service.execute(new Runnable() {
   @Override
   public void run() {
     System.out.println("線程的名稱:"+Thread.currentThread().getName());
   }
 });
 service.execute(new Runnable() {
   @Override
   public void run() {
     System.out.println("線程的名稱:"+Thread.currentThread().getName());
   }
 });
複製代碼

6.3 單線程線程池

效果與定長線程池 建立時傳入數值1 效果一致.
 /**
  * 單線程線程池.
  * 執行流程:
  *   1. 判斷線程池 的那個線程 是否空閒
  *   2. 空閒則使用
  *   4. 不空閒,則等待 池中的單個線程空閒後 使用
  */
 ExecutorService service = Executors.newSingleThreadExecutor();
 service.execute(new Runnable() {
   @Override
   public void run() {
     System.out.println("線程的名稱:"+Thread.currentThread().getName());
   }
 });
 service.execute(new Runnable() {
   @Override
   public void run() {
     System.out.println("線程的名稱:"+Thread.currentThread().getName());
   }
 });
複製代碼

6.4 週期性任務定長線程池

public static void main(String[] args) {
 /**
  * 週期任務 定長線程池.
  * 執行流程:
  *   1. 判斷線程池是否存在空閒線程
  *   2. 存在則使用
  *   3. 不存在空閒線程,且線程池未滿的狀況下,則建立線程 並放入線程池, 而後使用
  *   4. 不存在空閒線程,且線程池已滿的狀況下,則等待線程池存在空閒線程
  *
  * 週期性任務執行時:
  *   定時執行, 當某個時機觸發時, 自動執行某任務 .
   */
 ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
 /**
  * 定時執行
  * 參數1.  runnable類型的任務
  * 參數2.  時長數字
  * 參數3.  時長數字的單位
  */
 /*service.schedule(new Runnable() {
   @Override
   public void run() {
     System.out.println("倆人相視一笑~ 嘿嘿嘿");
   }
 },5,TimeUnit.SECONDS);
 */
 /**
  * 週期執行
  * 參數1.  runnable類型的任務
  * 參數2.  時長數字(延遲執行的時長)
  * 參數3.  週期時長(每次執行的間隔時間)
  * 參數4.  時長數字的單位
  */
 service.scheduleAtFixedRate(new Runnable() {
   @Override
   public void run() {
     System.out.println("倆人相視一笑~ 嘿嘿嘿");
   }
 },5,2,TimeUnit.SECONDS);
}
複製代碼

7.Lambda 表達式

Lambda 體現的是函數式編程思想

Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("hhh");
    }
});
t.start();
 
 
 
 
Thread t = new Thread(() -> {
    System.out.println("hhh");
});
t.start();
複製代碼

這個表達式就是省略了中間的接口功能用表達式代替,保留了參數和方法部分。

8.小總結

線程同步的目的是爲了保護多個線程反問一個資源時對資源的破壞。 線程同步方法是經過鎖來實現,每一個對象都有切僅有一個鎖,這個鎖與一個特定的對象關聯,線程一旦獲取了對象鎖,其餘訪問該對象的線程就沒法再訪問該對象的其餘非同步方法 對於靜態同步方法,鎖是針對這個類的,鎖對象是該類的Class對象。靜態和非靜態方法的鎖互不干預。一個線程得到鎖,當在一個同步方法中訪問另外對象上的同步方法時,會獲取這兩個對象鎖。 對於同步,要時刻清醒在哪一個對象上同步,這是關鍵。 編寫線程安全的類,須要時刻注意對多個線程競爭訪問資源的邏輯和安全作出正確的判斷,對「原子」操做作出分析,並保證原子操做期間別的線程沒法訪問競爭資源。 當多個線程等待一個對象鎖時,沒有獲取到鎖的線程將發生阻塞。 死鎖是線程間相互等待鎖鎖形成的,在實際中發生的機率很是的小。真讓你寫個死鎖程序,不必定好使,呵呵。可是,一旦程序發生死鎖,程序將死掉。

相關文章
相關標籤/搜索