什么是多線程?看我多線程七十二變,你能記住嗎?

1、什么是線程

  • 單核CPU = 一個車間:一次執(zhí)行一個進程,如果執(zhí)行多個程序,會在多個進程中來回切換,執(zhí)行到進程里面會在多個線程之間來回切換。

  • 多核CPU = 一個工廠:每次可執(zhí)行多個進程;

  • 進程:一個車間為一個進程(一個運行的程序);進程是一種重量級的資源,系統(tǒng)會分配內存和CPU資源,啟動和停止慢,內存相互獨立

  • 線程:車間內一個工人為一個線程;

  • 多線程:一個進程包含多個線程;多個線程都可以共享一個進程的內存空間;

1.1、什么是多線程?
  • 多線程是在CPU切換到某個進程之后,會在多個線程之間來回切換,每個線程就會分配到一定的cpu時間,線程是CPU分配時間的單元
1.2、并行和并發(fā)
  • 并行:多個cpu同時執(zhí)行多個線程
  • 并發(fā):一個CPU同時執(zhí)行多個線程,CPU在線程之間來回切換,讓線程都能執(zhí)行(不是同時執(zhí)行)
1.3、同步和異步
  • 同步:多個指令是依次執(zhí)行的,一個指令執(zhí)行時會阻塞當前線程,其他指令必須要在該指令完成之后執(zhí)行。
  • 異步:多個線程同時指向自己的命令,一個線程執(zhí)行完后,給另一個線程通知

2、多線程的應用場景

  • 大型企業(yè)級應用都有高并發(fā)的特點,因為會大量的用戶,比如:淘寶、京東、抖音等。如果服務器是單線程,所有用戶必須排隊執(zhí)行,必須為每個用戶的每個請求,分配一個獨立的線程,完成獨立任務,相互不影響。單機版程序(如:大型游戲)需要執(zhí)行大量的任務:圖形渲染、動作控制、網(wǎng)絡通信等。需要多線程同時執(zhí)行上面任務。

3、啟動線程的方法

3.1、繼承Thread
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <100 ; i++) {
            System.out.println("hello 我是"+Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();//一個線程不能調用兩次以上的start方法 IllegalThreadStateException
    }
}

3.2、實現(xiàn)Runnable接口
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("Runnable :"+ Thread.currentThread().getName());
    }
     public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread  = new Thread(myRunnable);
        thread.start();
        //匿名內部類寫法
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名內部類"+Thread.currentThread().getName());
            }
        }).start();
        //lambda表達式寫法
        new Thread(() ->{
            System.out.println("lambda:"+Thread.currentThread().getName());
        }).start();
     }
}

3.3、實現(xiàn)Callable接口(有返回值)
public class MyCallable implements Callable<Long> {//需指定返回值類型
    @Override
    public Long call() throws Exception {
        return 1+1L;
    }

    public static void main(String[] args) {
        //創(chuàng)建MyCallable對象
        MyCallable myCallable = new MyCallable();
        //創(chuàng)建FutureTask對象
        FutureTask<Long> futureTask = new FutureTask<>(myCallable);
        //創(chuàng)建線程對象
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            System.out.println("獲得結果:"+futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

4、線程的啟動和停止

4.1、線程啟動
  • 面試題:啟動線程是執(zhí)行start方法還是run方法?
  • start()方法
  • 只有調用了start方法,系統(tǒng)才會創(chuàng)建新的線程,直接調用run方法,會在主線程中執(zhí)行
  • 注意:不要手動調用start方法,一個start方法只能被一個線程對象調用一次
4.2、線程停止
  • stop方法(強制停止,不能釋放鎖資源,可能造成死鎖等嚴重問題,禁用)
  • 等待run方法運行完
  • 通過條件中斷run的運行

5、線程的睡眠

  • Thread.sleep(毫秒)
  • 線程一旦進入睡眠,會讓出CPU
  • 使用場景:執(zhí)行大量任務時,進行一定睡眠,可以減少CPU消耗

6、后臺進程

  • 也叫守護線程,精靈線程
  • 將一個線程設置為后臺線程:setDaemon(true)
  • 守護線程的作用是為其他線程提供服務,一旦其他線程停止了,守護線程會自動停止

7、線程的合并

  • 當前線程可以合并其他線程,當其他線程操作執(zhí)行完,再執(zhí)行當前線程的操作。
  • 注意:兩個線程不能互相合并,否則會出現(xiàn)死鎖的問題

8、線程的優(yōu)先級

  • 默認情況下線程的執(zhí)行是搶占式的,沒有特定的次序,不同的優(yōu)先級獲得CPU的概率不同
  • setPriority(int) 設置優(yōu)先級,默認是5,可設置范圍1~10

9、線程的生命周期

[圖片上傳失敗...(image-f8805d-1600949547611)]

  1. 新建:對象剛被創(chuàng)建出來,還沒有調用start
  2. 就緒:調用了start,但沒有搶到CPU,不能執(zhí)行
  3. 運行:搶到了CPU,正在執(zhí)行run
  4. 阻塞:線程遇到某些情況,暫停執(zhí)行
  5. 死亡:run方法執(zhí)行完

10、線程同步問題

10.1、為什么會出現(xiàn)線程同步問題?
  • 線程是搶占式的,一個線程在執(zhí)行指令的時候,可能被其他線程中斷,可能會出現(xiàn)數(shù)據(jù)不一致的情況
  • 多個線程同時操作一個資源
10.2、線程同步問題的解決方案(上鎖機制)
  • 同步方法

    • 語法
    public synchronized 返回類型 方法名(參數(shù)){
    
    }
    
    
    • 作用是對整個方法上鎖,保證代碼的原子性,一旦一個線程進入方法后,就會持有鎖,其他線程不能進入該方法,等線程執(zhí)行完,其他線程才能進去。
    • 相關面試題:
      • StringBufferStringBuilder的區(qū)別
      • ArrayListVector的區(qū)別
      • HashMapHashtable的區(qū)別
    • 相關知識點
      • 同步方法鎖的對象是this,當前的對象
      • 鎖機制:上鎖后,JVM會啟動監(jiān)視器(Monitor),監(jiān)視進入代碼塊或方法體的線程,如果方法體或代碼塊的鎖被某個線程所有,監(jiān)視器就會拒絕其他線程進入代碼塊或方法,直到持有鎖的線程執(zhí)行完,釋放鎖,才會讓其他線程進去
      • 一旦上鎖,程序的性能會有所降低
  • 同步代碼塊

    • 語法
    public 返回類型 方法名(參數(shù)){
    ...
        synchronized(鎖對象){
        代碼
        } 
    ...
    }
    
    
    • 注意
      • 任何Java對象(Object)都可以成為鎖對象
      • 鎖對象不能是局部變量
      • 一旦線程進入代碼塊就上鎖,持有鎖對象,執(zhí)行完代碼塊后自動釋放鎖,拋異常也會釋放鎖
    • 對比同步方法和同步代碼塊
      • 同步方法更簡潔
      • 同步方法鎖粒度更大(粒度越大鎖的范圍越大),粒度越大性能越差,同步代碼塊的性能高于同步方法
  • 同步鎖

    • 在java1.5出現(xiàn),在java.util.concurrent包中
    • Lock接口,最上層接口
    • 子接口:ReadLock、WriteLockReadWriteLock
    • 常用的實現(xiàn)類:ReentrantLock重入鎖
    • 創(chuàng)建對象:Lock lock = new RentrantLock()
    • 常用方法:lock.lock()上鎖、lock.unlock()釋放鎖
    • 語法
    lock.lock();
    try{
        代碼
    }finally{
        lock.unlock();
    }
    
    

11、單例模式

  • 單例模式主要分位餓漢式和懶漢式,作用是保證一個類只有一個實例對象

    • 優(yōu)點:
      • 減少系統(tǒng)資源的消耗
      • 符合某些特殊業(yè)務的要求
  • 餓漢式

    • 一開始就創(chuàng)建對象,不管getInstance()是否被調用,內存資源都會被消耗掉
    • 系統(tǒng)中的Runtime屬于餓漢式
    public class Singleton01 {
        private static Singleton01 instance = new Singleton01();
    
        private Singleton01(){
    
        }
    
        private static Singleton01 getInstance(){
            return instance;
        }
    }
    
    
  • 懶漢式

    • 一開始不創(chuàng)建對象,getInstance()被調用時候創(chuàng)建對象,內存一開始不消耗
    • 存在線程同步問題(需要在getInstance()方法中判斷對象是否為null)
    //普通懶漢式
    public class Singleton02 {
        private static Singleton02 instance ;
    
        private Singleton02(){
    
        }
    
        private static Singleton02 getInstance(){
            if (instance == null){
                instance = new Singleton02();
            }
            return instance;
        }
    }
    
    
    //雙檢鎖懶漢式
    public class Singleton06 {
        private static Singleton06 instance ;
    
        private Singleton06(){
    
        }
    
        private static Singleton06 getInstance(){
            if (instance == null){
                synchronized (Singleton06.class){
                    if (instance == null){
                        instance = new Singleton06();
                    }
                }
            }
            return instance;
        }
    }
    
    

12、volatile關鍵字

  • 用于修飾變量,提高線程可見性,是線程同步的輕量級解決方案,能保證可見性,不能保證原子性
  • 可見性:變量的值每個線程都能直接獲取到
  • 原子性:代碼作為整體運行,不可再分
  • 變量加上volatile關鍵字后,線程直接從主內存讀取值,不是從緩存讀取,任意線程修改變量后,其他線程都可以看到
  • 變量沒有被volatile修飾
在這里插入圖片描述
  • 變量被volatile修飾
image.png

13、線程死鎖

  • 出現(xiàn)的情況:
    • 上鎖后沒有正常釋放鎖
    • 兩個線程都持有對方需要的鎖,又需要對方持有的鎖,就可能進入相互等待的情況
public class LockTest {
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                synchronized (lock1){
                    synchronized (lock2){
                        System.out.println("thread1------------"+i);
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 100; i++) {
                synchronized (lock2){
                    synchronized (lock1){
                        System.out.println("thread2------------"+i);
                    }
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}

14、線程的等待和通知

  • 通過鎖對象的調用,定義在Object類中

  • 等待wait:讓線程進入阻塞狀態(tài)

    • wait():一直等待,直到被通知
    • wait(毫秒):等待指定時長,線程可以被通知或自動喚醒
  • 通知notify:讓等待狀態(tài)的線程從阻塞狀態(tài)恢復到就緒狀態(tài)

    • notify():通知一個等待線程
    • notifyAll():通知所有等待線程
  • 注意,只有鎖對象才能調用notify或者wait方法,必須在同步代碼塊的鎖對象或同步方法的this調用。否則會出現(xiàn)IllegalMonitorStateException異常

15、wait和sleep的區(qū)別?

  • wait會釋放鎖資源,sleep不會
  • sleep是線程對象調用,wait是鎖對象調用;
  • sleep必須等睡眠時間結束,wait可以等時間結束,也可以被喚醒

16、生產(chǎn)者消費者模式

  • 不是GOF23設計模式之一,是與線程相關的設計模式

  • 作用:

    1. 解耦:讓生產(chǎn)者消費者之間解耦,生產(chǎn)者消費者之間不會直接訪問
    2. 高并發(fā):生產(chǎn)者消費者解耦,生產(chǎn)者和消費者不需要互相等待,生產(chǎn)者能處理更多的消費者請求
    3. 解決忙閑不均:解決了生產(chǎn)者速度太快消費者需求太低,或者生產(chǎn)者速度太慢,消費者需求太大的問題
  • 實現(xiàn)過程:

    1. 定義緩沖區(qū),用于存放數(shù)據(jù),緩沖區(qū)有上限
    2. 生產(chǎn)者線程將生產(chǎn)數(shù)據(jù)存入緩沖區(qū),當緩沖區(qū)滿了,讓生產(chǎn)者等待,如果沒滿,通知生產(chǎn)者繼續(xù)生產(chǎn)
    3. 消費者從緩沖區(qū)取數(shù)據(jù),當緩沖區(qū)空了,讓消費者等待,等有數(shù)據(jù),通知消費者繼續(xù)消費

17、阻塞隊列

  • 是一種集合,根據(jù)數(shù)據(jù)的個數(shù)自動產(chǎn)生阻塞
  • BlockingQueue<T> 父接口
  • 常用實現(xiàn)類:
    • LinkedBlockingQueue 鏈表結構的阻塞隊列 (插入刪除效率高)
    • ArrayBlockingQueue 數(shù)組結構的阻塞隊列(查詢效率高)
  • 常用方法
    • put() 添加數(shù)據(jù)到末尾,如果達到上線,就阻塞當前線程
    • take()從隊列頭刪除數(shù)據(jù),如果為空,就阻塞

18、線程池

18.1、線程的作用?
  • 回收線程資源,線程是一種很重要的資源,線程的創(chuàng)建和銷毀都很消耗資源,頻繁的創(chuàng)建和銷毀線程會降低程序的性能
  • 注意:線程池中的線程執(zhí)行完任務后不是直接死亡,而是回到池中,可以重復利用
18.2、線程池API
  • Executor接口

    • execute(Runnable)執(zhí)行單個線程任務
  • ExecutorService接口

    • showdown:關閉
    • shutdownNow:立刻關閉
    • submit:提交
  • ThreadPoolExecutor 線程實現(xiàn)類

  • Executors 工具類

    • 幫助創(chuàng)建不同類型的線程池

    • 主要方法:

      • ExecutorService newCachedThreadPool()長度不限的線程,不能控制并發(fā)量,速度更快
      //長度不限線程池
      public static void testCachedThreadPool(){
          ExecutorService threadPool = Executors.newCachedThreadPool();//長度不限的線程池
          for (int i = 0; i < 10; i++) {
              threadPool.execute(()->{
                  System.out.println("當前執(zhí)行的是"+Thread.currentThread().getName());
              });
              try {
                  Thread.sleep(1);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
          threadPool.shutdown();
      }
      
      
      • ExecutorService newFixedThreadPool(int) 長度固定的線程池,可以控制并發(fā)量,并發(fā)量大時,需要排隊
      //長度固定線程池
      public static void testFixedThreadPool(){
          ExecutorService threadPool = Executors.newFixedThreadPool(5);//長度不限的線程池
          for (int i = 0; i < 10; i++) {
              threadPool.execute(()->{
                  System.out.println("當前執(zhí)行的是"+Thread.currentThread().getName());
                  try {
                      Thread.sleep(3000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              });
      
          }
          threadPool.shutdown();
      }
      
      
      • ExecutorService newSingleThreadExecutor() 單線程線程池
      //單線程線程池
      public static void testSingleThreadPool(){
          ExecutorService threadPool = Executors.newSingleThreadExecutor();//長度不限的線程池
          for (int i = 0; i < 10; i++) {
              threadPool.execute(()->{
                  System.out.println("當前執(zhí)行的是"+Thread.currentThread().getName());
                  try {
                      Thread.sleep(3000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              });
      
          }
          threadPool.shutdown();
      }
      
      
      • ScheduledExecutorService newScheduledThreadPool(int)可調度的線程池,執(zhí)行線程時,可以設置執(zhí)行周期和延時
      public static void testScheduleThreadPool(){
          ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
      
              threadPool.scheduleAtFixedRate(()->{
                  System.out.println("當前執(zhí)行的是"+Thread.currentThread().getName());
              },5,1, TimeUnit.SECONDS);
          //threadPool.scheduleWithFixedDelay()
      }
      
      
  • 自定義線程池

    • 構造方法:ThreadPoolExector
    • 核心線程數(shù):corePoolSize
    • 最大線程數(shù):maximumPoolSize
    • 存活時間:keepAliveTime
    • 時間單位:timeUnit
    • 阻塞隊列,保存執(zhí)行任務(Runnable):workingQueue
    • 優(yōu)化配置
      1. 核心線程配置和最大線程數(shù)一樣,減少創(chuàng)建新線程和銷毀線程的開銷
      2. 核心線程數(shù)配置和cpu核心數(shù)相關, 核心數(shù) * N (N >= 1) N具體看任務量、執(zhí)行時間等情況
      3. 阻塞隊列使用LinkedBlockingQueue,添加和刪除任務效率高
public static void myselfThreadPool(){
        int processors = Runtime.getRuntime().availableProcessors();
        System.out.println(processors);
        int n = 2;
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(processors * n, processors * n, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
        for (int i = 0; i < 10; i++) {
            threadPool.execute(()->{
                System.out.println("當前執(zhí)行的是"+Thread.currentThread().getName());

            });

        }
        threadPool.shutdown();
    }

19、ThreadLocal

19.1、ThreadLocal是什么?
  • ThreadLocal的作用是提供線程內的局部變量,這種變量在線程的生命周期內起作用,減少同一個線程內多個函數(shù)或者組件之間一些公共變量的傳遞的復雜度。
19.2、syncronized 與 ThreadLocal的區(qū)別?
  • syncronized雖然能解決線程間變量的隔離的問題,解決的方法是當一個線程上鎖之后,拒絕其他線程訪問,當前只有一個線程在訪問這個資源,但是這種方式需要對每一個線程都加鎖,降低了程序的效率,也降低了程序的并發(fā)性
  • syncronized 以時間換空間,讓所有線程排隊訪問,只提供了一份變量,側重的是多個線程之間訪問資源的同步
  • ThreadLocal不需要加鎖,也能解決線程間變量的隔離性,可滿足程序的高并發(fā),變量只在線程的生命周期內起作用
  • ThreadLocal以空間換時間,為每一個線程都提供一份單獨的副本,實現(xiàn)訪問互不干擾,每個線程都訪問自己獨有的那份變量,使用ThreadLocal可以保證程序擁有更高的并發(fā)性。
19.3、ThreadLocal的優(yōu)點?
  • 綁定參數(shù):保存每個線程綁定的數(shù)據(jù),在需要的地方可以直接獲取,避免參數(shù)直接傳遞帶來的代碼耦合問題
  • 線程隔離:各個線程之間的數(shù)據(jù)相互隔離又具有并發(fā)性,避免同步方法帶來的性能損失
19.4、ThreadLocal使用場景
  • JDBC連接數(shù)據(jù)庫,獲取Connection的時候,為保證在高并發(fā)的情況下進行事務操作,保證在dao層getConnection()時候得到的對象和事務的Connection對象是同一個對象,就需要使用到ThreadLocal來實現(xiàn)線程之間的數(shù)據(jù)隔離,同時也不影響數(shù)據(jù)庫連接的性能。
  • 解決方案,在DBUtils中定義ThreadLocal對象,當connection為空的時候,使用數(shù)據(jù)庫連接池獲取一個連接對象,然后將這個連接對象保存到ThreadLocal,之后調用getConnection()方法返回的是ThreadLocal中的connection,這樣就實現(xiàn)了connection對象的隔離,需要注意的是,事務提交之后需要解綁當前線程綁定的連接對象,threadLocal.remove(),目的是為了避免內存泄漏
在這里插入圖片描述
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容