面試阿里,字節跳動99%會被問到的java線程和線程池,看完這篇你就懂了!

前言:

最近也是在後臺收到不少小夥伴私信問我線程和線程池這一塊的問題,說本身在面試的時候總是被問到這一塊的問題,被問的很頭疼。前幾天看到後幫幾個小夥伴解決了問題,可是問的人有點多我一個個回答也回答不過來,乾脆花了一個上午時間寫了這篇文章分享給你們。話很少說,滿滿的乾貨都在下面了!java

併發與並行

併發:指兩個或多個事件在同一個時間段內發生。
在操做系統中,安裝了多個程序,併發指的是在一段時間內宏觀上有多個程序同時運行,這在單 CPU 系統中,每 一時刻只能有一道程序執行,即微觀上這些程序是分時的交替運行,只不過是給人的感受是同時運行,那是由於分 時交替運行的時間是很是短的。面試

並行:指兩個或多個事件在同一時刻發生(同時發生)。
在多個 CPU 系統中,這些能夠併發執行的程序即可以分配到多個處理器上(CPU),實現多任務並行執行,即利用每一個處理器來處理一個能夠併發執行的程序,這樣多個程序即可以同時執行。目前電腦市場上說的多核 CPU,即是多核處理器,核越多,可以並行處理的程序數量越多,這能大大的提升電腦運行的效率。安全

注意:單核處理器的計算機確定是不能並行的處理多個任務的,只能是多個任務在單個CPU上併發運行。同理,線程也是同樣的,從宏觀角度上理解線程是並行運行的,可是從微觀角度上分析倒是串行運行的,即一個線程一個線程的去運行,當系統只有一個CPU時,線程會以某種順序執行多個線程,咱們把這種狀況稱之爲線程調度。多線程

線程與進程

進程:是指一個內存中運行的應用程序,每一個進程都有一個獨立的內存空間,一個應用程序能夠同時運行多 個進程;進程也是程序的一次執行過程,是系統運行程序的基本單位;系統運行一個程序便是一個進程從創 建、運行到消亡的過程。
線程:線程是進程中的一個執行單元,負責當前進程中程序的執行,一個進程中至少有一個線程。一個進程 中是能夠有多個線程的,這個應用程序也能夠稱之爲多線程程序併發

建立線程類

Java使用 java.lang.Thread 類表明線程,全部的線程對象都必須是Thread類或其子類的實例。每一個線程的做用是完成必定的任務,實際上就是執行一段程序流即一段順序執行的代碼。Java使用線程執行體來表明這段程序流。
Java中經過繼承Thread類來建立並啓動多線程的步驟以下:ide

定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體就表明了線程須要完成的任務,所以把 run()方法稱爲線程執行體。
建立Thread子類的實例,即建立了線程對象。
調用線程對象的start()方法來啓動該線程。
首先自定義一個線程類工具

public class ThreadClass extends Thread {
    //重寫run方法
    @Override
    public void run()
    {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"正在執行"+i);
            try {
                //休眠500毫秒
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

主線程:操作系統

public class DemoTest {
    public static void main(String[] args) {
        //建立一個線程對象
        ThreadClass mythread = new ThreadClass();
        //開啓線程
        mythread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("主線程正在執行" + i);
            try {
                //休眠500毫秒
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

其實實際上咱們通常不會去繼承線程類,因爲java的單繼承特性,當咱們繼承了線程類就沒法繼承別的父類了,通常咱們是經過重寫接口來開啓線程的。線程

重寫Runnable接口

步驟以下:code

定義Runnable接口的實現類,並重寫該接口的run()方法,該run()方法的方法體一樣是該線程的線程執行體。
建立Runnable實現類的實例,並以此實例做爲Thread的target來建立Thread對象,該Thread對象纔是真正 的線程對象。
調用線程對象的start()方法來啓動線程。
首先重寫接口

public class Runnableimp implements Runnable {
    //重寫run方法
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"正在執行"+i);
            try {
                //休眠500毫秒
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

主線程:

public class DemoTest {
    public static void main(String[] args) {
        //建立一個線程對象,傳入重寫了run方法的接口對象
        Thread mythread = new Thread(new Runnableimp());
        //開啓線程
        mythread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("主線程正在執行" + i);
            try {
                //休眠500毫秒
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

執行的結果和剛纔相同。

匿名內部類方式實現線程的建立

public class DemoTest {
    public static void main(String[] args) {
        //建立一個線程對象,使用匿名內部類重寫run方法
        Thread mythread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+"正在執行"+i);
                    try {
                        //休眠500毫秒
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        //開啓線程
        mythread.start();
    }
}

使用lambda表達式

public class DemoTest {
    public static void main(String[] args) {
        //建立一個線程對象,使用lambda表達式重寫run方法
        Thread mythread = new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+"正在執行"+i);
                try {
                    //休眠500毫秒
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //開啓線程
        mythread.start();
    }
}

線程安全

線程安全問題都是由全局變量及靜態變量引發的。若每一個線程中對全局變量、靜態變量只有讀操做,而無寫操做,通常來講,這個全局變量是線程安全的;如有多個線程同時執行寫操做,通常都須要考慮線程同步, 不然的話就可能影響線程安全。

線程同步

當咱們使用多個線程訪問同一資源的時候,且多個線程中對資源有寫的操做,就容易出現線程安全問題。
要解決上述多線程併發訪問一個資源的安全性問題,Java中提供了同步機制 (synchronized)來解決。

同步代碼塊

同步代碼塊: synchronized 關鍵字能夠用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。
格式:

synchronized(同步鎖){
      須要同步操做的代碼 
      }

示例

private int num = 100;
private Object lock = new Object();
synchronized (lock)
{
      num--;
}

同步方法

同步方法:使用synchronized修飾的方法,就叫作同步方法,保證A線程執行該方法的時候,其餘線程只能在方法外 等着。

public synchronized void method()
{     
    可能會產生線程安全問題的代碼   
}

Lock鎖

java.util.concurrent.locks.Lock 機制提供了比synchronized代碼塊和synchronized方法更普遍的鎖定操做, 同步代碼塊/同步方法具備的功能Lock都有,除此以外更強大,更體現面向對象。
Lock鎖也稱同步鎖,加鎖與釋放鎖以下:

public void lock() :加同步鎖。
public void unlock() :釋放同步鎖。

Lock lock = new ReentrantLock();    
//加鎖
lock.lock();
可能會產生線程安全問題的代碼  
//釋放鎖
lock.unlock();

咱們使用線程的時候就去建立一個線程,這樣實現起來很是簡便,可是就會有一個問題:
若是併發的線程數量不少,而且每一個線程都是執行一個時間很短的任務就結束了,這樣頻繁建立線程就會大大下降系統的效率,由於頻繁建立線程和銷燬線程須要時間。
那麼有沒有一種辦法使得線程能夠複用,就是執行完一個任務,並不被銷燬,而是能夠繼續執行其餘的任務?
在Java中能夠經過線程池來達到這樣的效果。今天咱們就來詳細講解一下Java的線程池。

線程池

線程池:其實就是一個容納多個線程的容器,其中的線程能夠反覆使用,省去了頻繁建立線程對象的操做, 無需反覆建立線程而消耗過多資源。

Java裏面線程池的頂級接口是 java.util.concurrent.Executor ,可是嚴格意義上講 Executor 並非一個線程 池,而只是一個執行線程的工具。真正的線程池接口是 java.util.concurrent.ExecutorService 。
要配置一個線程池是比較複雜的,尤爲是對於線程池的原理不是很清楚的狀況下,頗有可能配置的線程池不是較優 的,所以在 java.util.concurrent.Executors線程工廠類裏面提供了一些靜態工廠,生成一些經常使用的線程池。官方建議使用Executors工程類來建立線程池對象。
newFixedThreadPool方法

public static ExecutorService newFixedThreadPool(int nThreads)

建立一個可重用固定線程數的線程池,以共享的無界隊列方式來運行這些線程。在任意點,在大多數 nThreads 線程會處於處理任務的活動狀態。若是在全部線程處於活動狀態時提交附加任務,則在有可用線程以前,附加任務將在隊列中等待。若是在關閉前的執行期間因爲失敗而致使任何線程終止,那麼一個新線程將代替它執行後續的任務(若是須要)。在某個線程被顯式地關閉以前,池中的線程將一直存在。

參數:

nThreads - 池中的線程數

返回:

新建立的線程池

拋出:

IllegalArgumentException - 若是 nThreads <= 0
獲取到了一個線程池ExecutorService 對象,那麼怎麼使用呢,在這裏定義了一個使用線程池對象的方法以下: public Future<?> submit(Runnable task):獲取線程池中的某一個線程對象,並執行。

下面的代碼經過四種方式向線程池中提交任務執行

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        //建立線程池,線程數量爲2
        ExecutorService es = Executors.newFixedThreadPool(2);
        //將任務扔到線程池的四種方式
        //使用匿名內部類,
        es.submit(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+"正在執行"+i);
                    try {
                        //休眠500毫秒
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        //使用lambda表達式
        es.submit(()->{
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+"正在執行"+i);
                    try {
                        //休眠500毫秒
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        //使用重寫的接口
        es.submit(new Runnableimp());
        //使用重寫的線程類
        es.submit(new ThreadClass());
        //啓動一次順序關閉,執行之前提交的任務,但不接受新任務
        es.shutdown();
        //主線程等待全部線程將任務執行完畢
        while (!es.isTerminated());
        System.out.println("線程執行完畢!");
    }
}

除此以外java還提供了:

newScheduledThreadPool:建立一個線程池,它可安排在給定延遲後運行命令或者按期地執行。
newSingleThreadExecutor:建立一個使用單個 worker 線程的 Executor,以無界隊列方式來運行該線程。
newSingleThreadScheduledExecutor: 建立一個單線程執行程序,它可安排在給定延遲後運行命令或者按期
地執行。

小結:

今天的分享就到這裏了,你們看完有什麼不懂的話能夠發私信問我,我看到都會回覆的。

相關文章
相關標籤/搜索