JavaSE--多線程

1.併發與並行

●併發:指兩個或多個事件在同一時間段內發生。(交替執行)
●並行:指兩個或多個事件在同一時刻發生。(同時發生)
即,併發就是你洗完澡後再聽歌,並行就是你一邊洗澡一邊聽歌。

2.進程與線程

(線程<進程)
●進程:程序的執行過程。(可在任務管理器查看)
●線程:進程中的一個執行單元。
一個程序運行後至少有一個進程,一個進程中可以包含多個線程。

舉例:Word的使用。
	每次打開一個Word就相當於啓動了一個進程,
	在這個進程上又有許多其他程序在執行(例如:拼寫檢查,自動更正等),這些就是一個個線程。
	如果Word關閉了,這些線程就會全部消失。但是如果這些線程消失了,Word不一定會消失。

線程一定得依附於進程才能夠存在。
“同時”執行是線程給人的感覺,在線程之間實際上是輪換執行。

3.線程調度

(1)分時調度:
所有線程輪流使用CPU的使用權,平均分配每個線程佔用CPU的時間。
(2)搶佔式調度:
優先讓優先級高的線程使用CPU,如果線程的優先級相同,那麼會隨機選擇一個(線程隨機性)。java使用的爲搶佔式調度。
●設置線程的優先級:
打開任務管理器>選擇希望設置優先級的進程>右鍵>轉到詳細信息>設置優先級

4.多線程的實現:

在Java中要想實現多線程的程序,必須依靠一個線程的主體類,即主線程(執行主方法(main)的線程)。然後此類繼承Thread類或實現Runnable接口。
在這裏插入圖片描述

1)繼承Thread類

java.lang.Thread是操作線程的類,任何類只需要繼承Thread類就可以成爲一個線程的主類。

Thread類下的兩個重要方法:
run()start()方法。
線程執行體:run()。(線程需要完成的任務)
線程的起點:start()。(線程的啓動,啓動後執行的方法體是run()方法定義的代碼)
程序的起點:main()。

//1.創建一個Thread類的子類
public class MyThread extends Thread{
    //2.重寫Thread類中的run方法,設置線程任務
    @Override
    public void run(){
        for (int i = 0; i < 20; i++) {
            System.out.println("run:"+i);
        }
    }
}
public class doMain {
    public static void main(String[] args) {
        //3.創建Thread類的子類對象
        MyThread mt = new MyThread();
        //4.調用Thread類中的start(),開啓新的線程,執行run()
        mt.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("main:"+i);
        }
    }
}

隨機性打印結果的原因:
在這裏插入圖片描述
多線程內存圖解:
在這裏插入圖片描述
Thread類的常用方法:

getName() 取得線程名字
setName() 設置線程名字
currentThread() 取得線程名字
sleep(long millitime) 使當前正在執行的程序以指定的毫秒數暫定

Thread.currentThread().getName() 和 this.getName()的區別

2)實現Runnable接口:

爲了避免單繼承侷限的問題,我們可以使用Runnable接口來實現多線程。
要啓動多線程,就一定需要通過Thread類中的start()方法,但是Runnable接口中沒有提供可以被繼承的start()方法。這時就需要借住Thread類中提供的有參構造方法:

public Thread(Runnable target) 此方法可以接收一個Runnable接口對象
//MyThread是實現了Runnable接口的子類
MyThread mt = new MyThread();
MyThread mt2 = new MyThread();
new Thread(mt).start();
new Thread(mt2).start();

3)實現Runnable接口的好處:

①避免單繼承侷限;
②降低程序的耦合性,方便解耦。即把設置線程任務(實現類中重寫run())和開啓新線程(Thread類對象調用start())進行了分離。

4)多線程的兩種實現方式及區別:

●它們的實現都需要一個線程的主類,都必須在子類中覆寫run()方法,都必須調用Thread類中的start()方法來開啓線程。

Thread類是Runnable接口的子類,而使用Runnable接口可以避免單繼承侷限,方便解耦,並且可以更加方便地實現數據共享的概念。
public class Thread extends Object implements Runnable

●它們的結構:

Runnable接口 Thread類
class MyThread implements Runnable{} class MyThread extends Thread{}
new Thread(mt).start(); mt.start();

5)使用匿名內部類實現多線程的創建:

ublic class doMain {
    public static void main(String[] args) {
        //1.線程的父類是Thread
        new Thread(){
            @Override
            public void run(){
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+">--"+"a");
                }
            }
        }.start();

        //2.線程的接口Runnable
        new Thread(new Runnable(){
                @Override
                public void run() {
                    for (int i = 0; i < 20; i++) {
                        System.out.println(Thread.currentThread().getName()+"-->"+"b");
                    }
                }
            }).start();
    }
}

5.線程的操作狀態:

要想實現多線程,必須在主線程中創建新的線程對象。任何線程一般具有以下五種狀態:
在這裏插入圖片描述

  1. 創建狀態
    新線程對象處於新建狀態時
  2. 就緒狀態
    新建線程對象後,調用該線程的start()方法就可以啓動線程。當線程啓動時,線程進入就緒狀態。此時,線程將進入線程隊列排隊,等待CPU服務,這表明它已經具備了運行條件。
  3. 運行狀態
    當就緒的線程被調度並獲得CPU資源時,便進入運行狀態。此時,自動調用該線程對象的run()方法,run()方法定義了線程的操作和功能
  4. 堵塞狀態
    在某種特殊情況下,被人爲掛起或執行輸入輸出操作時,將讓出 CPU 並臨時中止自己的執行,進入阻塞狀態。在可執行狀態下,如果調用sleep()、suspend()、wait()等方法,線程都將進入堵塞狀態。堵塞時,線程不能進入排隊隊列,只有當引起堵塞的原因被消除後,線程纔可以進入就緒狀態。
  5. 終止狀態
    線程調用stop()或run()方法執行結束後,就處於終止狀態。處於終止狀態的線程不具有繼續運行的功能。

6.線程的休眠與優先級:

1)線程的休眠:
是讓程序執行速度變慢一些。在Thread類中線程休眠操作方法爲:
public static void sleep(long millis) throws InterruptedException
設置的休眠單位是毫秒(ms)。

2)線程的優先級:
對高優先級,使用優先調度的搶佔式策略。哪個線程的優先級高,哪個線程就有可能被執行。

  1. MAX_PRIORITY : 10
  2. MIN _PRIORITY:1
  3. NORM_PRIORITY:5

線程優先級操作方法:

  1. setPriority(int p) :設置線程的優先級
  2. getPriority() :取得線程優先級

7.線程安全問題(同步與死鎖):

先來解釋不同步遇到的問題:
如果分成三個窗口賣100張票,假如不同步的話,就有可能出現三個窗口賣重票、錯票的情況。(多個線程操作同一資源可能出現的情況,因爲前面的線程還沒完成操作,其它線程也進來操作車票。(搶佔))

實現三個窗口來賣票的程序:

//實現賣票程序
public class RunnableImpl implements Runnable{
    //定義一個多線程共享的票源
    private int ticket = 100;
    //設置線程任務:賣票
    @Override
    public void run() {
        //先判斷票是否存在
        while(true){
        if (ticket>0) {
            //票存在,賣票
            System.out.println(Thread.currentThread().getName() + "->>正在賣第" + ticket + "張票");
            ticket--;
            }
        }else {
            break;
        }
    }
}
	
public class doMain {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }
}

解決方法:通過同步操作來解決。

  1. 同步操作:一個代碼塊中的多個操作在同一時間段內只能由一個線程進行,其他線程要等待此線程完成後纔可以繼續執行。
    在這裏插入圖片描述

以下有三種方式完成同步操作:
其實就是添加上圖中的“鎖”。

1)同步代碼塊:

synchronized(this){
      //需要被同步操作的代碼
}

關於this的解釋:

  1. 在實現Runnable接口創建多線程的方式中,我們可以使用this充當所,代替手動new一個對象,因爲後面我們只創建一個線程的對象。
  2. 在繼承Thread類創建多線程的方式中,慎用this,考慮我們的this是不是唯一的。我們可以使用當前類來充當這個是鎖。synchronized (類名.class)

使用同步代碼塊完成同步操作:
主要有變化的在run()方法裏,doMain類不變。

//實現賣票程序
public class RunnableImpl implements Runnable{
    //定義一個多線程共享的票源
    private int ticket = 100;
    //設置線程任務:賣票
    @Override
    public void run() {
        while(true){
        //先判斷票是否存在
            synchronized (this){
                if (ticket>0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //票存在,賣票
                    System.out.println(Thread.currentThread().getName() + "->>正在賣第" + ticket + "張票");
                    ticket--;
                }else {
                    break;
                }
            }
        }
    }
}

2)同步方法:

利用synchronized定義的方法。

//實現賣票程序
public class RunnableImpl implements Runnable{
    //定義一個多線程共享的票源
    private int ticket = 100;
    //設置線程任務:賣票
    @Override
    public void run() {
        while(true){
            sale();
        }
    }
    public synchronized void sale(){
        //先判斷票是否存在
        synchronized (this){
            if (ticket>0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //票存在,賣票
                System.out.println(Thread.currentThread().getName() + "->>正在賣第" + ticket + "張票");
                ticket--;
            }
        }
    }
}

補充:

  1. 非靜態同步方法,鎖對象:this
  2. 靜態同步方法,鎖對象:RunnableImpl.class

3)Lock鎖:

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

Lock接口中的方法:

void Lock() 獲取鎖
void unlock() 釋放鎖

Lock接口的實現類:ReentrantLock

java.util.concurrent.locks.ReentrantLock implements Lock

使用Lock鎖完成同步操作:
三步走。1.創建ReentrantLock對象 2.獲取鎖 3.釋放鎖

//實現賣票程序
public class RunnableImpl implements Runnable{
    //定義一個多線程共享的票源
    private int ticket = 100;
    //1.創建ReentrantLock對象
    Lock lk = new ReentrantLock();
    //設置線程任務:賣票
    @Override
    public void run() {
        while(true){
            //2.在可能出現線程安全的代碼前調用Lock接口中的Lock方法獲取鎖.
            lk.lock();
            if (ticket>0) {
                try {
                    Thread.sleep(10);
                    //票存在,賣票
                    System.out.println(Thread.currentThread().getName() + "->>正在賣第" + ticket + "張票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //3.在可能出現線程安全的代碼後釋放鎖。
                    lk.unlock();
                }
            }
        }
    }
}

4)比較 synchronized 與 Lock:

  1. synchronized是自動釋放鎖(顯示),lock需要手動釋放和關閉鎖(隱式)。
  2. 使用Lock鎖,JVM將花費較少的時間來調度線程,性能更好。並且具有更好的擴展性(提供更多的子類)
  3. Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖

5)總結:

加入同步後明顯比不加入同步慢許多,所以同步的代碼性能低,但是數據安全性高。

6)常見面試題分析:

  1. 同步和異步有什麼區別。什麼情況下使用?
    如果一塊數據要在多個線程間共享,則必須進行同步存取。當應用程序在對象上調用了一個需要花費很長時間來執行的方法,並且不希望讓程序等待方法的返回時,那麼就應該用異步編程,在很多情況下采用異步途徑往往更有效率。

  2. abstract的method是否可以同時是static,是否可以同時是native、synchronized?
    method、static、native、synchronized都不能和“abstract”同時聲明方法。

  3. 當一個線程進入一個對象的synchronized方法後,其他線程是否可訪問此對象的其他方法?
    不能訪問,一個對象操作一個synchronized方法只能由一個線程訪問。(其他線程等待)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章