多線程簡介(全)


線程實現方式

並發與並行

  • 並發:指兩或多個事件在同一個時間段內發生
  • 並行:指兩或多個事件在同一個時刻發生(同時發生)

進程的概念

內存:所有的應用程序都需要進入到內存中執行 臨時存儲RAM

硬盤:永久存儲ROM

  • 進入到內存的程序叫進程
  • 任務管理器-->結束進程
  • 那么就把進程從內存中清除了

線程的概念

點擊一個應用程序的功能執行,就會開啟一條應用程序到cpu的執行路徑,cup就可以通過這個路徑執行功能,這個路徑有一個名字,叫線程。

線程屬於進程:是進程中的一個執行單元,負責程序的執行

線程的好處:

  1. 效率高
  2. 多線程之間互不影響

如:
單核心線程cpu

  • cpu在多個線程之間做高速的切換
  • 輪流執行多個線程
  • 效率低
  • 切換的速度塊(1/n毫秒)

4核心8線程

  • 有8個線程,可以同時執行8個線程
  • 8個線程在多個任務之間做高速的切換
  • 速度是單線程cpu的8倍(每個執行到的幾率都被提高了8倍)

線程的調度

分時調度

  • 所有線程輪流使用cpu的使用權

搶占式調度

  • 優先級高的線程使用cpu先使用,若相同,隨機選擇一個。Java使用的就是搶占式調度。

主線程

主線程:執行主(main)方法的線程

單線程程序:Java程序中只有一個線程
執行從main方法開始,從上到下依次執行

  • JVM執行main方法,main方法會進入到棧內存
  • JVM會找操作系統開辟一條main方法通向cpu的執行路徑
  • cpu就可以通過這個路徑來執行main方法
  • 而這個路徑有一個名字,叫main(主)線程

創建多線程

創建多線程程序的第一種方式:創建Thread的子類
java.lang.Thread類:是描述線程的類,我們想要實現多線程程序,就必須繼承Thread類。

實現步驟:

  1. 創建一個Thread類的子類
  2. 在Thread類的子類中重寫Thread類的run方法,設置線程任務(開啟線程要做什么?)
  3. 創建Thread類的子類對象
  4. 調用Thread類中的方法start方法,開啟新的線程,執行run方法。
  • void start() 使該線程開始執行;Java虛擬機調用該線程的run方法。
  • 結果是兩個線程並發地運行;當前線程(main線程)和另一個線程(創建地新線程,執行其run方法)。
  • 多次啟動一個線程是非法的。特別是當線程已經結束執行后,不能再重新啟動。

Java使用的就是搶占式調度。優先級高的線程使用cpu先使用,若相同,隨機選擇一個。

//1.創建一個Thread類的子類
public class MyThread extends Thread{
    //2.在Thread類的子類中重寫Thread類的run方法,設置線程任務(開啟線程要做什么?)
    @Override
    public void run(){
        for (int i = 0;i<20;i++){
            System.out.println("run:"+1);
        }
    }
}
    
public class CaiNiao{
    public static void main(String[] args){
        //3 創建Thread類的子類對象
        MyThread mt = new MyThread();
        //4.調用Thread類中的方法start方法,開啟新的線程,執行run方法。
        mt.start();
    
        //主線程會繼續執行主方法中的代碼
        for (int i = 0;i<37;i++){
            System.out.println("main:"+i);
        }
    
    }
}

創建多線程程序的第二種方法:實現Runnable接口
java.lang.Runnable

  • Runnable 接口應該由那些打算通過某一線程執行其實例的類來實現。類必須定義一個稱為run的無參數方法。

java.lang.Thread類的構造方法

  • Thread(Runnable target)分配新的 Thread對象。
  • Thread(Runnable target,String name)分配新的 Thread對象。

實現步驟:

  1. 創建一個Runnable接口的實現類
  2. 在實現類中重寫Runnable接口的run方法,設置線程任務
  3. 創建一個Runnable 接口的實現類對象
  4. 創建Thread類對象,構造方法中傳遞Runnable接口的實現類對象
  5. 調用Thread類中的方法start方法,開啟新的線程,執行run方法。

實現Runnable接口創建多線程程序的好處:
1 避免了單繼承的局限性

  • 一個類只能繼承一個類(一個人只能有一個親爹),類繼承了Thread類就不能繼承其他的類
  • 實現Runnable接口,還可以繼承其他的類,實現其他的接口

2 增強了程序的擴展性,降低了程序的耦合性(解耦)

  • 實現Runnable接口的方式,把設置線程任務和開啟新線程進行了分離(解耦)
  • 實現類中,重寫了run方法:用來設置線程任務
  • 創建Thread類對象,調用Thread類中的方法start方法,開啟新的線程,執行run方法。

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

  • 匿名:沒有名字
  • 內部類:寫在其他類內部的類

匿名內部類的作用:簡化代碼

  • 把子類繼承父類,重寫父類的方法,創建子類對象合一步完成
  • 把實現類實現類接口,重寫接口中的方法,創建實現類對象合一步完成

格式:

new 父類/接口(){
    重置父類/接口中的方法
};

//線程的父類是Thread
//new MyThread().start();

//線程的接口Runnable
//Runnable r = RunnableTmpl();//多態
//new Runnable(r).start();

Thread類的常用方法

獲取線程的名稱:

  1. 使用Thread類中的方法getName() 返回該線程的名稱。
  2. static Thread currentThread() 返回對當前正在執行的線程對象的引用。

設置線程的名稱:

  1. 使用Thread類中的方法setName(名字)
  2. void setName(String name) 改變線程的名稱,使之參數 name相同。
  • public void start():使該線程開始執行;Java虛擬機調用該線程的run方法
  • public void run():此線程要執行的任務在此處定義代碼
  • public static void sleep(long millis):使當前正在執行的線程以指定的毫秒數暫停(暫時停止執行)。

線程的安全問題

模擬賣票案例

創建三個的線程,同時開啟,對共享的票進行出售


public class RunnableImpl implementsc Runnable{
    //定義一個多線程共享的票源
    private int ticket = 100//設置線程任務:買票
    @Override
    public void run(){
        //使用死循環,讓賣票操作重復執行
        while (true){
            //先判斷票是否存在
            if(ticket>0){
                //提高安全問題出現的概率,讓程序睡眠
                try{
                    Thread.sleep(10);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                //票存在,賣票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
                ticket --;
            }
        }
    }
}


public class CaiNiao{
    public static void main(String[] args){
        //創建Runnable接口的實現類對象
        RunnableImpl run = new RunnableImpl();
        //創建Thread類對象,構造方法中傳遞Runnable接口的實現類對象
        Thread t0 = new Thread(run);        
        Thread t1 = new Thread(run);        
        Thread t2 = new Thread(run);        
        //調用start方法開啟多線程
        t0.start();
        t1.start();
        t2.start();
    }
}

這樣會導致一個結果

  • Thread-0 -->正在賣第1張票
  • Thread-1 -->正在賣第1張票
  • Thread-2 -->正在賣第0張票

解決線程安全問題的一種方案:使用同步代碼塊

格式:

格式:
    syncharonized(鎖對象){
        可能會出現線程安全問題的代碼(訪問了共享數據的代碼)        
    }

注意:

  • 通過代碼塊中的鎖對象,可以使用任意的對象
  • 但是必須要保證多個線程使用的鎖對象是同一個

鎖對象作用

  • 把同步代碼塊鎖住,只讓一個線程在同步代碼中執行
public class RunnableImpl implementsc Runnable{
    //定義一個多線程共享的票源
    private int ticket = 100//設置線程任務:買票
    @Override
    public void run(){
        //使用死循環,讓賣票操作重復執行
        while (true){
            //同步代碼塊
            syncharonized(obj){
                //先判斷票是否存在
                if(ticket>0){
                    //提高安全問題出現的概率,讓程序睡眠
                    try{
                        Thread.sleep(10);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    //票存在,賣票 ticket--
                    System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
                    ticket --;    
            }

解決線程安全問題的二種方案:使用同步方法

使用步驟:

  1. 把訪問了共享數據的代碼抽取出來,放到一個方法中
  2. 在方法上添加synchronized修飾符

格式:定義方法的格式

    修飾符 synchronized 返回值類型 方法名(參數列表){
        可能會出現線程安全問題的代碼(訪問了共享數據的代碼)
    }

解決線程安全問題的三種方案:使用Lock鎖

java.util.concurrent.Locks.Lock接口
Lock實現提供了比使用synchronized 方法和語句可獲得的更廣泛的鎖定操作。

Lock接口中的方法:

  void Lock()獲取鎖
  void unLock() 釋放鎖
java.util.concurrent.Locks.ReentrantLock implements Lock 接口

使用步驟:

  1. 在成員位置創建一個ReentrantLock對象
  2. 在可能會出現安全問題的代碼前調用Lock接口中的方法Lock獲取鎖
  3. 在可能會出現安全問題的代碼前調后用Lock接口中的方法unLock釋放鎖
//1.在成員位置創建一個ReentrantLock對象
Lock l = new ReentrantLock();

@Override
    public void run(){
        //使用死循環,讓賣票操作重復執行
        while (true){
            //2. 在可能會出現安全問題的代碼前調用Lock接口中的方法Lock獲取鎖
            l.lock();
            
            //同步代碼塊
            syncharonized(obj){
                //先判斷票是否存在
                if(ticket>0){
                    //提高安全問題出現的概率,讓程序睡眠
                    try{
                        Thread.sleep(10);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    //票存在,賣票 ticket--
                    System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
                    ticket --;    
                }
                //3.在可能會出現安全問題的代碼前調后用Lock接口中的方法unLock釋放鎖
                l.unLock();//無論程序釋放異常,鎖都會釋放
            

等待喚醒機制

線程的狀態

 

  1. NEW   至今尚未啟動的線程處於這種狀態
  2. RUNNABLE   正在Java虛擬機中執行的線程處於這種狀態
  3. BLOCKED 受阻塞並等待某個監視器鎖的線程處於這種狀態
  4. WAITING 無限期的等待另一個線程來執行某一待定操作的線程處於這種狀態
  5. TIMED_WAITNG 等待另一個線程來執行取決於指定等待時間的操作的線程處於這種狀態
  6. TERMINATED 已退出的線程處於這種狀態。
  • 阻塞狀態:具有cpu的執行資格,等待cpu空閑時執行
  • 休眠狀態:放棄cpu的執行資格,cpu空閑,也不執行

等待喚醒案例分析

public static void sleep(Long millis):使用當前正在執行的線程以指定的毫秒數暫停(暫停停止執行).

  • 毫秒數結束之后,線程繼續執行

等待喚醒案例:線程之間的通信

  • 創建一個顧客線程(消費者):告知老板要包子的中類和數量,調用wait方法,放棄cpu的執行,進入到WAITNG狀態(無限等待)
  • 創建一個老板線程(生產者):花了5秒做包子,做好包子之后,調用notify方法,喚醒顧客吃包子

注意:

  • 顧客和老板線程必須使用同步代碼塊包裹起來,保證等待和喚醒只能有一個在執行
  • 同步使用的鎖對象必須保證唯一
  • 只有鎖對象才能調用wait和notify方法

Object類中的方法

void wait()

  • 在其他線程調用此對象的notify()方法或notify() 方法前,導致當前線程等待。

void notify()

  • 喚醒在此對象監視器上等待的單個線程。
  • 會繼續執行wait方法之后的代碼
public class CaiNiao{
    public static void main(String[] args){
        //創建鎖對象,保證唯一
        Object obj = new Object();
        //創建一個顧客線程(消費者)
        new Thread(){
            @Override
            public void run(){
            //一直等着買包子
            while(true){
                    //保證等待和喚醒的線程只能有一個執行,需要使用同步技術
                    syncharonized (obj){
                        System.out.println("告知老板要的包子的種類和數量");
                        //調用wait方法,放棄cpu的執行,進入到WAITNG狀態(無限等待)
                        try{
                            obj.wait();
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                        //喚醒之后執行的代碼
                        System.out.println("包子已經做好了,開吃!");
                        System.out.println("--------------");
                    }
                }
            }
        }.start();
        
        //創建一個老板線程(生產者)
        new Thread(){
            @Override
            public void run(){
                //一直做包子
                while(true){
                    //花5秒做包子
                    try{
                        Thread.sleep(5000);//花5秒做包子
                    }catch(InterruptedException e);
                        e.printStackTrace();
                    }
                //保證等待和喚醒只能有一個在執行,需要使用同步技術
                syncharonized (obj){
                    System.out.println("花了5秒做包子,做好包子之后,調用notify方法,喚醒顧客吃包子");
                    //做好包子之后,調用notify方法,喚醒顧客吃包子
                    obj.notify();
                }
            }
        }.start();
    }
  

Object類中wait帶參方法和notify

進入到TimeWaiting(計時等待)有兩種方法

  1. 使用sleep(Long m)方法,在毫秒值結束之后,線程睡醒進入到Runnable/Blacked狀態
  2. 使用wait(Long m))方法,wait方法如果在毫秒值結束之后,還沒有被notify喚醒,就會自動醒來,線程睡醒進入到Runnable/Blacked狀態

喚醒的方法:

  1. void notify()喚醒在此對象監視器上等待的單個線程。
  2. void notifyAll()喚醒在此對象監視器上等待的所有線程。

調用wait和notify方法需要注意的細節

  1. wait方法與notify方法必須要由同一個鎖對象調用。因為:對應的鎖對象可以通過notify喚醒使用同一個鎖對象調用的wait方法后的線程
  2. wait方法與notify方法是屬於Object類的方法的。因為:鎖對象可以是任意對象,而任意對象的所屬類都是繼承了Object類的。
  3. wait方法與notify方法必須要在同步代碼塊或者是同步函數中使用。因為:必須要通過鎖對象調用這兩個方法。

線程間通信


概念:多個線程在處理同一個資源,但是處理的動作(線程的任務)卻不相同
重點:有效的利用資源

分析:需要那些類

1 資源類:包子類

  • 設置包子的屬性
  • 包子的狀態:有true 沒有false

2 生產者(包子鋪)類:是一個線程類,可以繼承Thread

  • 設置線程任務(run):生產包子
  • 對包子的狀態進行判斷

true:有包子

  • 包子鋪調用wait方法進入等待狀態

false:沒有包子

  • 包子鋪生產包子
  • 增加一些多樣性:交替生產兩種包子
  • 有兩種狀態(i%2 == 0 )
  • 包子鋪生產好了包子
  • 修改包子的狀態為true有
  • 喚醒吃貨線程,讓吃貨線程吃包子

3 消費者(吃貨)類:是一個線程類,可以繼承Thread

  • 設置線程任務(run):吃包子
  • 對包子的狀態進行判斷

false:沒有包子

  • 吃貨線程調用wait方法進入等待狀態

true:有包子

  • 吃貨吃包子
  • 吃貨吃完包子
  • 修改包子的狀態為false沒有
  • 吃貨喚醒包子鋪線程,生產包子


4 測試類:

  • 包含main方法,程序執行的入口,啟動程序
  • 創建包子對象:
  • 創建包子鋪線程,開啟,生產包子
  • 創建吃貨線程,開啟,吃包子;

注意:

  • 包子鋪線程和包子線程關系-->通信(互斥)、
  • 必須同時同步技術保證兩個線程只能有一個在執行
  • 鎖對象必須保證唯一,可以使用包子對象作為鎖對象
  • 包子鋪類和吃貨的類就需要把包子對象作為參數傳遞進來
  1. 需要在成員位置創建一個包子變量
  2. 使用帶參數的構造方法,為這個包子變量賦值

線程池

線程池概念:其實就是一個容納多個線程的容器,其中的線程可以反復使用,省去了頻繁創建線程對象的操作,無需反復創建線程而消耗過多的資源。

線程池:容器-->集合(ArrayList,HashSet,LinkedList<Thread>,HashMap)

  • 當程序第一次啟動的時候,創建多個線程,保存到一個集合中
  • 當我們想要使用線程的時候,就可以從集合中取出來線程使用
Thread t = list.remove(0);返回的是被移除的元素,(線程只能被一個任務使用)
Thread t = linked.removeFist();
  • 當我們使用完畢線程,需要把線程歸還給線程池
list.add(t);
linked.addLast(t);

在JDK1.5 之后,JDK內置了線程池,我們可以直接使用

合理利用線程池帶來的好處:

  1. 降低資源消耗
  2. 提高響應速度
  3. 提高線程的可管理性。


線程池的代碼實現:JDK1.5之后提供的

java.util.concurrent.Executors;線程池的工廠類,用來生成線程池

Executors類中的靜態方法:

static ExecutorService newFixedThreadPool(int nThreads)創建一個可重用的固定線程數的線程池

參數:

int nTherad:創建線程池中包含的線程數量

返回值:

ExecutorService接口,返回的是ExecutorService接口的實現類對象,我們可以使用ExecutorService接口接收(面向接口編程)

java.util.concurrent.ExecutorService:線程池接口

  • 用來從線程池中獲取線程,調用start方法開啟多線程,執行線程任務
submit(Runnable task)提交一個Runnable任務用於執行

關閉/銷毀線程池的方法

void shutdown()

線程池的使用步驟:

  1. 使用線程池的工廠類Executors里邊提供的靜態方法,newFixedThreadPool生產一個指定線程數量的線程池
  2. 創建一個類,實現Runnable接口,重寫了run方法:用來設置線程任務
  3. 調用ExecutorService中的方法submit,傳遞線程任務(實現類),開啟線程,執行run方法。
  4. 調用ExecutorService中的方法shotdown銷毀線程池(不建議執行)
ExecutorService es = Executors.newFixedThreadPool(2)
es.submit(new RunnableImpl());//創建了一個新的線程執行

Lambda表達式

函數式編程思想概述
---強調做什么,而不是以什么形式做

面向對象的思想:

  • 做一件事情,找一個能解決這個的事情的對象,調用對象的方法,完成事情

函數式編程思想

  • 只要能獲取到結果,誰去做的,這么做的都不重要,重視的是結果,不重視過程

傳統寫法和Lambda寫法對比

傳統寫法

public class CaiNiao{
    public static void main(String[] args){
        //匿名內部類,實現多線程
        //Runnable task = new Runnable()
        Runnable task = new Runnable(){
            @Override
            public void run(){//覆蓋重寫抽象方法
                System.out.println(Thread.currentThread().getName()+"新線程創建了");
                System.out.println("多線程任務執行!");
                
            }
        };
        new Thread(task).start();//開啟線程
    
    }
    
}

匿名內部類的好處與弊端

  • 一方面,匿名內部類可以幫助我們省去實現類的定義;
  • 另一方面,匿名內部類的語法確實太復雜了
public class CaiNiao{
    public static void main(String[] args){
        //匿名內部類,實現多線程
        //Runnable task = new Runnable()
        new Thread(new Runnable(){
            @Override
            public void run(){//覆蓋重寫抽象方法
                System.out.println(Thread.currentThread().getName()+"新線程創建了");
                System.out.println("多線程任務執行!");
            }
        }).start();//開啟線程
    
    }
    
}

Lambda寫法

()->{}

public class CaiNiao{
    public static void main(String[] args){
        //匿名內部類,實現多線程
        //Runnable task = new Runnable()
        new Thread(()->{//覆蓋重寫抽象方法
                System.out.println(Thread.currentThread().getName()+"新線程創建了");
                System.out.println("多線程任務執行!");
            }
        ).start();//開啟線程
    
    }
    
}

Lambda表達式的標准格式

由三部分組成:

  1. 一些參數
  2. 一個箭頭
  3. 一段代碼

格式:

  • (參數列表)-> {一些重寫方法的代碼}:

解釋說明格式:

  • ():接口中抽象方法的參數列表,沒有參數,就空着
  • ->:傳遞的意思,把參數傳遞給方法體{}
  • {}:重寫接口的抽象方法的方法體

Lambda表達式:是可推導,可以省略

  • 凡是根據上下文推導出來的內容,都可以省略書寫

可以省略的內容

  1. (參數列表):括號中的參數列表的數據類型,可以省略不寫
  2. (參數列表):括號中的參數如果只有一個,那么類型和()都可以省略
  3. (一些代碼):如果()中的代碼只有一行,無論是否由返回值,都可以省略({},return,分號)

注意:要省略{},return,分號必須一起省略

  • JDK1.7 版本之前,創建集合對象必須把前后的泛型都寫上
  • JDK1.7 版本之后,=號后邊的泛型可以省略,后邊的泛型可以根據前邊的泛型推導出來
    new Thread(()->{//覆蓋重寫抽象方法
        System.out.println(Thread.currentThread().getName()+"新線程創建了");
    }
).start();//開啟線程
        
    new Thread(()->System.out.println(Thread.currentThread().getName()+"新線程創建了")).start();//開啟線程

Lambda的使用前提

  1. 使用Lambda必須具有接口,且要求接口中有且僅有一個抽象方法;
  2. 使用Lambda必須具有上下文推斷。

備注:有且僅有一個抽象方法的接口,成為“函數式接口”。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM