Java多線程基礎整理

多線程複習

在這裏插入圖片描述

1.進程和線程

1.1定義

(1)進程:操作系統中一個程序的執行週期

(2)線程:進程中的一個任務。一個進程中可以包含n個線程(main()->主線程)

1.2進程與線程的關係

  • 每個進程擁有自己的一整套變量,是操作系統中資源分配的最小的單位。

​ 線程依託於進程存在,多個線程共享進程的資源,os中任務調度的基本單位。

  • 啓動、撤銷一個進程的開銷要比啓動、撤銷以一個線程大的多(線程輕量級
  • 進程間通信遠比線程間通信複雜的多;
  • 沒有進程就沒有線程,進程一旦終止,其內的線程全部取消;

1.3線程的狀態

在這裏插入圖片描述

2.多線程實現

Java中創建線程的四種方式:

(1)繼承Thread類

(2)實現Runnable接口

(3)實現Callable接口

(4)使用線程池(推薦

2.1繼承Thread類

java.lang.Thread是線程操作的核心類

2.1.1出現版本

JDK1.0

2.1.2實現方法

直接繼承Thread類而後覆寫類中的run()方法(相當於主方法,一個線程的入口就是run方法)

無論哪種方式實現多線程,線程啓動一律調用Thread類提供的start()方法。

2.1.3start()方法解析

1.首先檢查線程狀態是否爲0(線程默認狀態爲0表示爲啓動),如果線程已經啓動再次調用start()方法會拋出IllegalThreadStateException異常。所以一個線程的start()方法只能調用一次

2.private native void start0();通過start0()方法真正將線程啓動

3.JVM調用start0()方法進行資源分配與系統調度,**準備好資源啓動線程後回調run()**來執行線程的具體任務!

2.1.4線程啓動調用方法流程

start()->start0()->JVM_StartThread()->run_method_name()->run()

2.1.5侷限性

單繼承問題

2.2實現Runnable接口

java.lang.Runnable

2.2.1出現版本

JDK1.0

2.2.2實現方法

Java中Thread類本身也實現了Runnable接口,與用戶自定義的線程類共同組成代理設計模式

其中Thread類實現輔助操作,包括線程的資源調度等任務;自定義線程類完成真實業務·

在這裏插入圖片描述

實現Runnable接口,覆寫run()

public Thread(Runnable target){}

public static void main(String[] args) {
        MyThread1 mt1 = new MyThread1();
        MyThread1 mt2 = new MyThread1();
        MyThread1 mt3 = new MyThread1();
        Thread t1 = new Thread(mt1);
        Thread t2 = new Thread(mt2);
        Thread t3 = new Thread(mt3);
        t1.start();
        t2.start();
        t3.start();
}

2.2.3繼承Thread類與實現Runnable接口區別

  • 繼承Thread類有單繼承侷限,相對而言實現Runnable接口更加靈活,並且Thread類本身也實現了Runnable輔助真實業務類
  • 實現Runnable接口可以更好的實現程序共享的概念(Thread類也可以,)

2.3實現Callable接口

java.util.concurrent

2.3.1出現版本

JDK1.5

2.3.2實現方法

實現Callable接口,覆寫call()方法

Future:可接收call()方法的返回值

在這裏插入圖片描述

public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1.產生Callable對象
        MyThread3 mt = new MyThread3();
        //2.產生FutureTask對象
        FutureTask<String> ft = new FutureTask<>(mt);
        new Thread(ft).start();
        new Thread(ft).start();
        new Thread(ft).start();
        //3.接收call()返回值,FutureTask的get()繼承自Future接口中的get()
        System.out.println(ft.get());
}

2.3.3實現Runnable接口與實現Callable接口的區別

  • 實現Callable接口,線程執行任務後有返回值;而實現Runnable接口,線程執行後沒有返回值

3.多線程常用方法

3.1線程的命名與取得

  • 取得當前JVM正在執行的線程對象:public static native Thread currentThread();
  • 線程命名:
    • 創建線程時設置名稱:public Thread (Runnable target,String name)
    • 設置線程名稱:public final synchronized void setName(String name)
  • 取得線程名:public final String getName()

3.2線程休眠

3.2.1定義

讓線程暫緩執行,等到了預計時間再回復執行

public static native void sleep(long millis) throws InterruptedException{};

3.2.2狀態變換

運行狀態---->阻塞狀態---->休眠結束---->就緒狀態

3.2.3注意

(1)線程休眠會立即交出CPU,讓CPU去執行其他任務。線程休眠不會釋放對象鎖

(2)多個線程進入一個方法,不是同時休眠的;

3.3線程讓步

3.3.1定義

暫停當前正在執行的線程對象,並執行其他線程

public static native void yield();

3.3.2狀態變換

運行狀態<---->就緒狀態

3.3.3注意

(1)yield()會讓當前線程交出CPU,但不一定會立即交出。yield()交出CPU後只能讓擁有相同優先級的線程有獲取CPU的機會不會釋放對象鎖

3.4線程等待

3.4.1定義

等待該線程終止。

若線程1需要等待線2執行完畢後再恢復執行,可以在線程1中調用線程2的join();在哪個線程調用,哪個線程阻塞,等待線程執行完畢再恢復執行

3.4.2狀態轉換

運行狀態->阻塞狀態->就緒狀態

3.4.3注意

(1)join()方法只是對Object類wait()做了一層包裝而已;

(2)交出cpu,釋放對象鎖

3.5線程停止

3.5.1三種方式停止線程

  • 設置標記位,可以使線程正常退出(推薦)

  • 使用stop方法強制讓線程退出,但是該方法不安全已經被@Deprecated

  • 使用Thread類提供的interrupt()中斷線程(就是系統設置了一個標記位,直接使用即可)

    interrupt方法只是將線程狀態改爲中斷狀態而已,它不會中斷一個正在運行的線程;

3.5.2設置標記位

class MyThread02 implements Runnable{
    private int books = 0;
    private boolean flag = true;

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        while (flag == true){
            System.out.println(Thread.currentThread().getName()+"賣出書號:"+(books--));
        }
        System.out.println("線程停止");
    }
}

public class test02 {
    public static void main(String[] args) throws InterruptedException {
        MyThread02 mt1 = new MyThread02();
        Thread t1 = new Thread(mt1);
        t1.start();
        Thread.sleep(1000);
        mt1.setFlag(false);
    }
}

3.5.3stop方法

強行關閉線程,不管有沒有執行完循環(不推薦使用)

廢棄的原因:不安全,stop()會解除由線程獲得的所有鎖定,當在一個線程對象上調用了stop(),這個線程對象所運行的線程就會立即停止,可能會產生不完整數據信息

3.5.4interrupt()方法

調用interrupt()

  • 如果在線程中沒有調用wait() sleep() join(),則標記線程(isInterrupt = true);

    並不會真正中斷線程,只是簡單的將線程的狀態置爲interrupt而已,我們可以根據此狀態來進一步確定如何處理線程;

    Thread類提供的public boolean isInterrupted()可以檢測當前線程狀態是否爲中斷狀態

  class MyThread1 implements Runnable{
      boolean flag = true;
      @Override
      public void run() {
          int i = 0;
          while (flag){
              boolean bool = Thread.currentThread().isInterrupted();
              if(bool){
                  System.out.println("線程進入中斷狀態");
                  return;
              }
              System.out.println("當前線程狀態爲"+bool);
              System.out.println(Thread.currentThread().getName()+","+(i++));
          }
          System.out.println("線程停止");
      }
  }
  
  public class threadInterrupt {
      public static void main(String[] args) throws InterruptedException {
          MyThread1 mt1 = new MyThread1();
          Thread thread = new Thread(mt1);
          thread.start();
          Thread.sleep(1000);
          thread.interrupt();
      }
  }

一秒後設置線程狀態爲中斷狀態,調用isInterrupt()返回值爲true,進入if語句,執行操作,輸出線程進入中斷狀態;

  • 如果在線程中調用了wait() sleep() join(),當調用interrupt()會拋出InterruptedException異常,同時將線程狀態還原(isInterrupted = false)
class MyThread1 implements Runnable{
    boolean flag = true;
    @Override
    public void run() {
        int i = 0;
        while (flag){
            try {
                Thread.sleep(200);
                boolean bool = Thread.currentThread().isInterrupted();
                System.out.println("當前線程狀態爲"+bool);
                System.out.println(Thread.currentThread().getName()+","+(i++));
            } catch (InterruptedException e) {
                System.out.println("拋出中斷異常");
                return;
            }
        }
        System.out.println("線程停止");
    }
}

public class threadInterrupt {
    public static void main(String[] args) throws InterruptedException {
        MyThread1 mt1 = new MyThread1();
        Thread thread = new Thread(mt1);
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

輸出:一秒後中斷

當前線程狀態爲false
Thread-0,0
當前線程狀態爲false
Thread-0,1
當前線程狀態爲false
Thread-0,2
當前線程狀態爲false
Thread-0,3
拋出中斷異常

3.6線程的優先級

3.6.1定義

線程的優先級越高越有可能先執行

3.6.2常用方法

  • 設置優先級:public final void setPriority(int newPriority)
  • 取得優先級:public final int getPriority()
  • 最高優先級:public final static int MAX_PRIORITY = 10
  • 中等優先級:public final static int NORM_PRIORITY = 5
  • 最低優先級:public final static int MIN_PRIORITY = 1

3.6.3注意

  • 主方法是一箇中等優先級
  • 線程具有繼承性(比如在A線程中啓動B線程,那麼B和A的優先級將是一樣的)

3.7守護線程(後臺線程)

(1)java中的兩種線程:用戶線程和守護線程

(2)判斷線程種類的方法:isDaemon();返回true,是守護線程;返回false,是用戶線程

(3)設置守護線程:setDaemon()

(4)典型的守護線程:垃圾回收線程

(5)只有當最後一個非守護線程結束時,守護線程纔會隨着JVM一同停止工作

(6)設置線程A爲守護線程,必須在start()前執行

(7)java中啓動的線程默認爲用戶線程,包括main線程

4.線程的同步和死鎖

多線程編程3大問題

分工:線程的分工

同步:線程間通信(線程合作)

互斥:多線程併發時,某一時刻只能有一個線程訪問共享資源

一個鎖保護一個相應資源,不同鎖保護的不同的對象/資源

java中鎖的實現

(1)內建鎖:synchronized關鍵字

(2)Lock

4.1核心問題

每一個線程對象輪番搶佔資源帶來的問題

4.2同步處理

4.2.1synchronized關鍵字

處理同步的兩種模式:同步代碼塊、同步方法

  • 同步代碼塊:要使用同步代碼塊必須要設置一個鎖定的對象(類對象),一般可以鎖當前對象this
  • 同步方法:在方法上添加synchronized關鍵字,表示此方法在任意時刻只能有一個線程進入
4.2.2兩種模式
同步代碼塊

(1)鎖類的實例對象

​ synchronized(this){}

(2)鎖類對象(class對象)----全局鎖

synchronized(類名稱.class){}—鎖的是當前類的反射對象(全局唯一)

(3)任意實例對象

​ String lock = “”;

​ synchronized(lock){}

同步方法
  • 普通方法+synchronized

    鎖的是當前對象

  • synchronized+靜態方法

    鎖的是的類對象----全局鎖,效果等同於同步代碼塊的鎖類對象

栗子

1.銀行賬戶

如何保護毫無關係的資源?使用多把鎖鎖住不同的資源

class Account{
	int sal;//餘額
	String password;//密碼
	
	private Object salLock = new Object();
	private Object passwordLock = new Object();
	
	public int getMoney(){
		synchronized(salLock){}
	}
	public int setMoney(){
		synchronized(salLock){}
	}
	
	public String getPassword(){
		synchronized(passwordLock){}
	}
	public void setPassword(){
		synchronized(passwordLock){}
	}
}

此時可以同時取錢和查看密碼;

2.轉賬

由於轉賬涉及兩個賬戶間的sal操作,因此需要將兩個賬戶同時鎖定。由於方法上的synchronized只能鎖一個對象,因此鎖不住轉賬操作

如何保護有關係的屬性?使用一把鎖鎖住多個資源

A->B 100

A -= 100

B += 100

public void zhuanzhang(Account target){
	synchronized(Accout.class){
		this.sal -= 100;
		target.sal += 100;
	}
}

此時可是實現A->B 100; B -> A 200 轉賬操作

4.2.3synchronized底層實現

在使用synchronized時必須保證鎖定對象必須爲Object以及其子類對象

synchronized使用的是JVM層級別的MonitorEnter和MonitorExit實現

這兩個指令都必須獲取對象的同步監視器Monitor

對象鎖機制

monitorenter:

當執行monitorenter時;

(1)如果目標鎖對象的monitor計數器爲0,表示此對象沒有被任何其他對象所持有。此時JVM會將該鎖對象的持有線程設置爲當前線程,並且將計數器+1;

(2)如果目標鎖對象的計數器不爲0,判斷鎖對象的持有線程是否是當前線程,如果是,再次將計數器,則+1;如果鎖對象的持有線程不是當前線程,已經被其他線程佔用,當前線程需要阻塞等待,直至持有線程釋放鎖。

monitorexit:

當執行monitorexit時;

JVM會將鎖對象的計數器-1.當計數器值減爲0時,代表該鎖對象已經被釋放

  • 執行同步代碼塊後首先要執monitorenter指令退出時執行monitorexit指令

  • 使用內建鎖(synchronized)進行同步關鍵在於要獲取指定鎖對象的monitor對象

  • 當線程獲取monitor後才能繼續向下執行,否則就只能等待。這個獲取過程是互斥的,即同一時刻只有一個線程能夠獲取到對象monitor;

  • 通常一個monitorenter指令會包含若干個monitorexit指令

    原因:JVM需要確保鎖在正常執行路徑以及異常執行路徑上都能夠正確的解鎖

  • 當使用synchronized標記方法時,編譯後字節碼中方法的訪問標記多了一個ACC_SYNCHRONIZED.

    該標記表示,進入該方法時,JVM需要進行monitorenter操作,退出該方法時,無論是否正常返回,JVM均需要進行monitorexit操作。

可重入鎖

當執行MonitorEnter時,對象的monitor計數器值不爲0,但是持有線程恰好是當前線程,此時將Monitor計數器值+1,當前線程繼續進入同步方法或代碼塊

可重入性:當線程A拿到對象鎖後,可以進入對應類的所有方法

class MyThreads implements Runnable{

    @Override
    public void run() {
        test1();
    }

    public synchronized void test1(){
        //保證第一個線程進入test1()
        if(Thread.currentThread().getName().equals("A"))
        {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("線程A進入同步方法test1()");
            test2();
        }
    }

    public synchronized void test2(){
        System.out.println(Thread.currentThread().getName()+"進入同步方法test2()");
    }
}
public class TestMyThread {
    public static void main(String[] args) throws InterruptedException {
        MyThreads myThreads = new MyThreads();
        Thread threadA = new Thread(myThreads,"A");
        threadA.start();
    }


}

out:線程A進入同步方法test1()
    A進入同步方法test2()

互斥:test1()只允許進入線程A,while循環一直進行,線程B永遠進不了test1(),更進不了test2()

class MyThreads implements Runnable{

    @Override
    public void run() {
        test1();
        test2();
    }

    public synchronized void test1(){
        //保證第一個線程進入test1()
        if(Thread.currentThread().getName().equals("A"))
        {
           while (true){
               System.out.println("線程A進入同步方法test1()");
           }
        }
    }

    public synchronized void test2(){
        if(Thread.currentThread().getName().equals("B")){
            System.out.println("線程B進入同步方法test2()");
        }
    }
}
public class TestMyThread {
    public static void main(String[] args) throws InterruptedException {
        MyThreads myThreads = new MyThreads();
        Thread threadA = new Thread(myThreads,"A");
        Thread threadB = new Thread(myThreads,"B");
        threadA.start();
        threadB.start();
    }
}
out:線程A進入同步方法test1()無限循環下去
4.2.4synchronized的優化(瞭解)

synchronized:互斥

優化讓每個線程通過同步代碼塊時速度提高

4.2.4.1CAS操作

(1)定義

CAS(無鎖操作):Compare And Swap-樂觀鎖

悲觀鎖(JDK1.6之前的內建鎖):假設每一次執行同步代碼塊均會產生衝突,所以當線程獲取鎖成功,會阻塞其他嘗試獲取該鎖的線程;之前synchronized獲取鎖失敗,將線程掛起

樂觀鎖(Lock):假設所有線程訪問共享資源時不會出現衝突,既然不會出現衝突自然就不會阻塞其他線程。線程不會出現阻塞狀態。

CAS(無鎖操作),使用CAS叫做比較交換來判斷是否出現衝突,出現衝突就重試當前操作,直到不衝突爲止

(2)操作過程

一般來說,CAS交換過程:(V,O,N)

V:內存中地址存放的實際值

O:預期值(舊值)(我以爲它是啥值)

N:更新後的值(我期望它是啥值)

當執行CAS後,

(1)如果V == O,即舊值與內存中實際值相等,表示上次修改該值後沒有任何線程再次修改此值,此時可以成功的將內存中的值修改爲N;

(2)如果V != O,表示該內存中的值已經被其他線程做了修改,此時返回最新的值V,再次嘗試修改變量;

當多個線程使用CAS操作同一個變量時,只有一個線程會成功,併成功更新變量值,其餘線程均會失敗。失敗線程會重新嘗試或將線程掛起(阻塞)

元老級的內建鎖(synchronized)最主要的問題:當存在線程競爭的情況下會出現線程阻塞以及喚醒帶來的性能問題,對應互斥同步(阻塞同步),效率很低。

而CAS並不是武斷的將線程掛起,會嘗試若干次CAS操作,並非進行耗時的掛起與喚醒操作,因此稱爲非阻塞式同步

(3)CAS問題

  • ABA問題

    解決思路:沿用數據庫的樂觀鎖機制,添加版本號1A->2B->3A

    JDK1.5提供atomicAtomicStampedReference類來解決CAS的ABA問題

  • 自旋(CAS)會浪費大量的處理器資源(CPU)

    與線程阻塞相比,自旋會浪費大量的CPU資源。因爲此時線程仍處於運行狀態,只不過跑的是無用指令,期望在無用指令時,鎖能被釋放出來。

    解決思路:自適應自旋

    根據以往自旋等待時能否獲取到鎖,來動態調整自旋的時間(循環嘗試數量)

    (1)如果在上一次自旋時獲取到鎖,則此次自旋時間稍微變長一點;

    (2)如果在上一次自旋結束還沒有獲取到鎖,此次自旋時間稍微短一點;

  • 公平性

    處於阻塞狀態的線程無法立刻競爭被釋放的鎖;而處於自旋狀態的線程很有可能先獲取到鎖

    內建鎖synchronized無法實現公平性

    Lock體系可以實現公平鎖

4.2.4.2Java對象頭

JDK1.6之後對內建鎖做了優化(新增偏向鎖、輕量級鎖)

Java對象頭–Java對象的對象頭。

Java對象頭裏的markword裏默認的存放的對象的Hashcode、分代年齡和鎖標記位

4.2.4.3鎖的4種狀態

鎖狀態在對象頭mark word中

無鎖狀態0 01(第一個0特指無鎖狀態)

偏向鎖1 01(第一個1特指該鎖是偏向鎖)

輕量級鎖 00

重量級鎖(JDK1.6之前)10

這四種狀態隨着競爭狀態逐漸升級,鎖可以升級但不可以降級,爲了提高獲得鎖與釋放鎖的效率

  • 無鎖:只有一個線程不存在競爭

  • 偏向鎖:JDK1.6之後默認的synchronized

    最樂觀的鎖,從始至終只有一個線程請求一把鎖

    引入原因:HotSpot作者發現,大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一個線程多次獲得,

    爲了讓鎖的代價更低而引入了偏向鎖

    偏向鎖獲取

    當一個線程訪問同步代碼塊或者同步方法並獲取鎖時,會在對象頭和線程棧幀中的鎖記錄中記錄存儲偏向鎖的線程ID。偏向鎖複製對象頭中的線程ID到棧幀中

    以後該線程再次進入同步塊時不再需要CAS來加鎖和解鎖,只需簡單測試一下對象頭的mark word中偏向鎖線程ID是否是當前線程ID

    如果成功,表示線程已經獲取到鎖直接進入代碼塊運行。

    如果測試失敗,檢查當前偏向鎖狀態是否爲0,

    (1)如果爲0,採用CAS操作將偏向鎖字段設置爲1,並且更新自己的線程ID到mark word字段中。

    (2)如果爲1,表示此時偏向鎖已經被別的線程獲取。則此線程不斷嘗試使用CAS(自旋)獲取偏向鎖或者將偏向鎖撤銷,升級爲輕量級鎖(升級概率較大)

    偏向鎖的撤銷

    偏向鎖使用一種等待競爭出現才釋放鎖的機制,當有其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放偏向鎖

    小tips:偏向鎖的撤銷開銷較大,需要等待線程進入全局安全點safepoint(當前線程在CPU上沒有執行任何有用字節碼)。

    偏向鎖從JDK6之後默認開啓,但是它在應用程序啓動幾秒後才激活。

    -XX:BiasedLockingStartupDelay = 0,將延遲關閉,JVM一啓動就激活偏向鎖

    -XX:-UseBiasedLocking=false,關閉偏向鎖,程序默認進入輕量級鎖。

  • 輕量級鎖

    輕量級鎖:多個線程在不同時間段請求同一把鎖,也就是基本不存在鎖競爭

    針對這種狀況,JVM採用輕量級鎖來避免線程的阻塞以及喚醒。

    加鎖:

    1.線程在執行同步代碼塊之前,JVM先在當前線程的棧幀中創建用於存儲鎖記錄的空間,並將對象頭的mark word字段直接複製到此空間內(輕量級鎖複製的markword字段(包括對象),偏向鎖只複製鎖對象頭中的線程ID)

    2.然後線程嘗試使用CAS將對象頭的markword替換爲指向鎖記錄的指針(指向當前線程),

    3.如果成功,表示獲取到輕量級鎖;

    如果失敗,表示其他線程競爭輕量級鎖,當前線程便使用自旋(CAS)不斷嘗試。

    釋放:

    解鎖時,會使用CAS將複製的mark word替換回對象頭

    如果成功,表示沒有競爭發生

    如果失敗,表示當前鎖存在競爭,進一步膨脹爲重量級鎖

  • 重量級鎖

    JDK1.6之前synchronized都是重量級鎖,將線程阻塞掛起

    重量級鎖會阻塞、喚醒請求加鎖的線程。針對的是多個線程同一時刻競爭同一把鎖的情況,JVM採用自適應自旋,來避免線程在面對非常小的同步代碼塊時,仍會被阻塞以及喚醒

鎖總結:

重量級鎖:會阻塞、喚醒請求加鎖的線程。針對的是多個線程同一時刻競爭同一把鎖的情況,JVM採用自適應自旋,來避免線程在面對非常小的同步代碼塊時,仍會被阻塞以及喚醒。

輕量級鎖:採用CAS操作,將鎖對象的標記字段替換爲指向線程的指針,存儲着鎖對象原本的標記字段。針對的是多個線程在不同時間段申請同一把鎖的情況。此後運行中會繼續採用CAS操作來加鎖(因爲是多個線程,可能在下一個時間會有新的線程獲取鎖,所以需要在該線程下一次加鎖再次進行CAS操作)。

偏向鎖:只會在第一次請求時採用CAS操作,在鎖對象的mark work字段中記錄下當前線程的ID,此後運行中持有偏向鎖的線程不再有加鎖過程)。針對的是鎖僅會被同一線程持有。

4.2.4.4死鎖

死鎖一旦出現,整個程序就會中斷執行,過多的同步會造成死鎖,對於資源的上鎖一定要注意不要成“環”。

class Pen{
    private String pen = "筆";

    public String getPen() {
        return pen;
    }
}

class Book{
    private String book = "本子";

    public String getBook() {
        return book;
    }
}

public class DeadLock {
    private static Pen pen = new Pen();
    private static Book book = new Book();

    public static void main(String[] args) {
        new DeadLock().testDeadLock();
    }

    private void testDeadLock() {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (pen){
                    System.out.println(Thread.currentThread().getName()+":我有筆,就不給你");
                    synchronized (book){
                        System.out.println(Thread.currentThread().getName()+":把你的本子給我");
                    }
                }
            }
        },"pen");
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (book){
                    System.out.println(Thread.currentThread().getName()+":我有本子,就不給你");
                    synchronized (pen){
                        System.out.println(Thread.currentThread().getName()+":把你的筆給我");
                    }
                }
            }
        },"book");

        thread1.start();
        thread2.start();
    }
}

解釋:當thread1線程啓動後,鎖了pen對象,thread2線程啓動後,只能鎖book對象,需要等thread1釋放pen對象後,thread2才能鎖pen對象;需要等thread2釋放book對象後,thread1才能鎖book對象。

4.3ThreadLocal

4.3.1定義

用於提供線程局部變量,在多線程環境可以保證各個線程的變量獨立於其他線程裏的變量

相當於線程的private static ,爲每一個線程創建一個單獨的變量副本

4.3.2ThreadLocal與同步機制的區別

ThreadLocal:保證多線程環境下數據的獨立性

同步機制:保證多線程環境下數據的一致性

4.3.3ThreadLocal的簡單實現
public class testThreadLocal {
    private static String string;
    private static ThreadLocal<String> threadStr = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
       string = "thread-main";
       threadStr.set("ThreadLocal-main");
       Thread thread = new Thread(new Runnable() {
           @Override
           public void run() {
               System.out.println(threadStr.get());  //1.out:null
               string = "thread-A";
               threadStr.set("ThreadLocal-A");
               System.out.println(threadStr.get());  //2.out:ThreadLocal-A
           }
       });
       thread.start();
       thread.join();
       System.out.println("=======================");
       System.out.println(string);  //3.out:thread-A
       System.out.println(threadStr.get());  //4.out:ThreadLocal-main
    }
}

//輸出1:此時thread.str爲new ThreadLocal<>()的副本,並且線程thread中並沒有賦值給threadStr
//輸出2:thread中threadStr被賦值爲ThreadLocal-A
//輸出3:變量string在線程中值已經改變
//輸出4:threadStr在主線程中被賦值爲ThreadLocal-main,即ThreadLocal類爲每一個線程創建一個變量副本
4.3.4給ThreadLocal類的對象設置默認參數
private static ThreadLocal<String> threadStr = ThreadLocal.withInitial(()-> {
    return "hello";
});
4.3.5ThreadLocalMap詳解

每個線程的ThreadLocalMap屬於自己,ThreadLocalMap中維護的值也是屬於線程自己的。這就保證了ThreadLocal類型的變量在每個線程中是獨立的,在多線程環境下是不會相互影響的。

存儲對象Entry

Entry用於保存一個鍵值對

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

Entry使用弱引用的原因:

避免ThreadLocal沒有被回收而導致的內存泄露

4.3.6ThreadLocal的防護措施

調用ThreadLocal的get(),set(),remove()的時候都會清除當前線程中所有key爲null的value,這樣可以降低內存泄露的概率。所以我們在使用ThreadLocal的時候,每次用完ThreadLocal都調用remove()方法,清除數據,防止內存泄漏。

5.Lock體系

5.1面試題-synchronized和Lock

面1:Java中實現線程"鎖"的方式:synchronized和Lock

面2:synchronized和Lock的關係

共同點:

synchronized和Lock都是對象鎖,都支持可重入鎖

synchronized和Lock

區別:

1.synchronized是JVM級別的鎖,屬於Java中的關鍵字,使用synchronized,加鎖和解鎖都是隱式的

Lock是Java層面的鎖,加鎖和解鎖都需要顯示使用

2.Lock可以提供一些synchronized不具備的而功能,如響應中斷、隨時獲取、非阻塞式獲取、公平鎖、共享鎖(讀寫鎖)

3.synchronized的等待隊列只有一個;而同一個Lock可以擁有多個等待隊列(多個Condition對象隊列),可以進一步提高效率,減少線程阻塞與喚醒帶來的開銷(喚醒了不該喚醒的線程)

獲取一個lock鎖的Condition的隊列-lock.newCondition():產生一個新的Condition隊列

面3:到底用synchronized還是Lock?

1.若無特殊的應用場景,推薦使用synchronized,其使用方便(隱式的加減鎖),並且由於synchronized是JVM層面的實現,在之後的JDK還有對其優化的空間;

2.若要使用公平鎖、讀寫鎖、超時獲取鎖等特殊場景,纔會考慮使用Lock;

5.2產生原因

解決死鎖問題

死鎖發生的四個必要條件:只有當以下四個條件同時滿足時,程序纔會死鎖

  • 互斥:共享資源X與Y只能被一個線程佔用(不能破壞)

  • 佔有且等待:線程T1已經擁有共享資源X,在等待共享資源Y的時候不釋放X

  • 不可搶佔:其他線程不能搶佔T1線程所持有的資源X

  • 循環等待:線程A與線程B相互等待對方已佔有的資源

synchronized獲取鎖:阻塞式獲取鎖,獲取不到線程就一直阻塞下去

破壞不可搶佔條件的方法:

1.響應中斷 void lockInterruptibly() throws InterruptedException;

2.支持超時 boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

3.非阻塞式獲取鎖,線程若獲取不到鎖,線程直接退出 boolean tryLock();

5.3Lock接口常用的方法

  • void lock():阻塞式獲取鎖,相當於synchronized關鍵字
  • boolean tryLock():非阻塞式獲取鎖,獲取成功繼續執行任務返回true;否則返回false線程直接退出
  • void lockInterruptibly() throws InterruptException:獲取鎖時響應中斷
  • boolean tryLock(long time,TimeUnit unit) throws InterruptException:獲取鎖時支持超時,在超時內或爲中斷的情況下能獲取鎖
  • Condition newCondition():獲取綁定到該lock對象的等待隊列,每當調用一次就產生一個新的等待隊列

Lock體系使用的格式

try{

	lock.lock();//加鎖

}finally{

	lock.unlock();//解鎖

}

Lock實現的賣票問題

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class MyThread implements Runnable{
    private int ticket = 100;
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        for(int i = 0;i < 100;i++){
            try{
                //等同於synchronized(this)
                lock.lock();
                if(ticket > 0){
                    System.out.println(Thread.currentThread().getName()+"還剩下"+(ticket--)+"票");
                }
            }finally {
                lock.unlock();
            }
        }
    }
}

public class testLock {
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        new Thread(mt).start();
        new Thread(mt).start();
        new Thread(mt).start();
    }
}

Lock實現的響應中斷

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


class MyThread1 implements Runnable{
    private Lock lock = new ReentrantLock();
    @Override
    public void run() {
        try{
            while (true){
                lock.lockInterruptibly();
            }
        }catch (InterruptedException e){
            System.out.println("線程響應中斷");
            return;
        }finally {
            lock.unlock();
        }
    }
}

public class TaskInterrupt {
    public static void main(String[] args) throws InterruptedException {
        MyThread1 mt = new MyThread1();
        Thread thread = new Thread(mt);
        thread.start();
        Thread.sleep(3000);
        thread.interrupt();
    }
}

輸出:3秒後輸出響應中斷

Lock實現的支持超時獲取鎖問題

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class TaskTime implements Runnable{
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        test();
    }
    public void test(){
        try{
            if(lock.tryLock(1,TimeUnit.SECONDS)){
                System.out.println(Thread.currentThread().getName()+"獲取鎖成功");
                Thread.sleep(2000);
            }else{
                System.out.println(Thread.currentThread().getName()+"獲取鎖失敗");
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

public class LockTime {
    public static void main(String[] args) {
        TaskTime taskTime = new TaskTime();
        Thread thread1 = new Thread(taskTime,"線程A");
        Thread thread2 = new Thread(taskTime,"線程B");
        thread1.start();
        thread2.start();
    }
}

輸出:一個線程獲取鎖成功,一個獲取鎖失敗;在1秒內,假如線程A獲取鎖成功,睡2秒後釋放鎖,線程B無法在規定的1秒內獲取鎖,所以線程B獲取鎖失敗;

若將睡眠時間改爲0.5秒,假如線程B獲取鎖成功,休眠0.5秒後釋放鎖,線程A還可以在規定的1秒內獲取鎖;

5.4AQS

5.5Lock與AQS的關係

Lock提供獲取鎖成功與否的狀態給AQS,AQS根據此狀態來確定是否將線程置入AQS中

5.6公平鎖和非公平鎖

(只有Lock可以實現公平鎖Lock默認非公平鎖

公平鎖:等待時間最長的線程優先獲得鎖

非公平鎖:獲得鎖的線程不可預估

5.7讀寫鎖

共享鎖:同一時刻,可以有多個線程擁有鎖(WIFI密碼、百度網盤密碼)

讀寫鎖是共享鎖的一種實現,

讀鎖共享:多個線程可以同時拿到讀鎖進行訪問,當寫線程拿到寫鎖開始工作時,所有讀線程全部阻塞

寫鎖獨佔:任意一個時刻,只有一個線程可以拿到寫鎖

讀讀-不互斥

讀寫、寫寫-互斥

Lock中的ReentrantReadWriteLock實現讀寫鎖

5.8Condition接口

Lock體系的線程通信方式,類比Object wait()與notify()

常用方法:

await():釋放lock鎖,將線程置入等待隊列阻塞

signal():隨機喚醒一個處於等待狀態的線程

signalAll():喚醒所有等待線程

6.生產者與消費者模型

6.1wait()方法

死等直到被喚醒或中斷

6.1.1定義

使當前線程立刻停止運行,處於等待狀態(WAIT),並將當前線程置入鎖對象的等待隊列中,直到被通知(notify())或被中斷爲止

6.1.2使用條件

只能在同步方法或同步代碼塊中使用,必須是內建鎖

wait()調用後立刻釋放對象鎖

wait()被喚醒的notify()必須是同一個鎖對象

  • public final void wait()throws InterruptedException{}----死等,直到被喚醒或被中斷,調用了wait(),而後再調用interrupt()會拋出中斷異常
  • public final native void wait(long timeout)throws InterruptedException;----超時等待。若在規定時間內未被喚醒,則線程退出。單位:毫秒
  • public final voidwait(long timeout, int nanos) throws InterruptedException

在2的基礎上增加了納秒控制

6.2notify()方法

6.2.1定義

喚醒處於等待狀態的線程

6.2.2使用條件

必須在同步方法或同步代碼塊中使用,必須是內建鎖,用來喚醒等待該對象的其他線程。如果有多個線程等待,隨機挑選一個喚醒;

6.2.3notify()與wait()最大的不同

notify()方法調用後,當前線程不會立馬釋放對象鎖,要等待當前線程的notify()執行完畢後再釋放鎖(wait()調用後立馬釋放對象鎖)

6.3notifyAll()

喚醒所有處於等待狀態的線程

6.4線程由運行態->阻塞

(1)調用sleep(),立刻交出CPU,不釋放鎖

(2)線程調用阻塞式IO(BIO)方法

(3)線程獲取鎖失敗進入阻塞狀態

(4)線程調用wait()

(5)線程調用suspend(),將線程掛起

每個鎖對象都有兩個隊列。一個稱爲同步隊列,存儲獲取鎖失敗的線程。另一個稱爲等待隊列,存儲調用wait()等待的線程

線程喚醒實際上是將處於等待隊列的線程移動到同步隊列中競爭鎖

7.線程池

7.1線程池優點

  • 降低資源消耗

    通過重複利用已創建的線程降低線程創建與銷燬帶來的損耗

  • 提高響應速度

    當任務到達時,無需等待線程創建就可以立即執行

  • 提高線程的可管理性

    使用線程池可以統一進行線程分配、調度與監控

7.2線程池的繼承關係

(Executor的框架圖)
在這裏插入圖片描述

  • ExecutorService(普通調度池接口)

    • Future submit(Callable task|Runnable task)
    • void execute(Runnable command);

    版本:JDK1.5

  • ScheduledExcutorService(定時調度池核心接口)

    • //延遲delay個時間單位後開始執行–可以傳入Runnable/Callable接口的對象
      schedule(Runnable command,long delay, TimeUnit unit);
      schedule(Callable callable,long delay, TimeUnit unit);
    • //延遲initialDelay個時間單位後開始執行,並且每隔period個時間單位就執行一次;–只能傳入Runnable接口的對象
      scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit);

7.3線程池執行流程

(ThreadPoopExecutor類創建的線程池)

I.判斷核心線程池是否已滿,如果未滿,創建一個新的線程執行任務;反之,判斷是否有空閒線程,有的話將任務分配給空閒線程,反之(即核心線程池已滿並且都在執行任務),執行步驟2(創建線程需要獲得全局鎖)

II.判斷工作隊列(阻塞隊列BlockingQueue)是否已滿。如果工作隊列未滿,將任務存儲在工作隊列中等待空閒線程調度。如果已滿,執行步驟3

III.判斷當前線程池線程數量是否已達到最大值,如果沒有,則創建新的線程來執行任務。否則,執行步驟4(創建線程需要獲得全局鎖)

IIII.調用飽和策略來處理此任務(默認爲拋出異常給用戶AbortPolicy)

在這裏插入圖片描述

7.4線程池的使用

7.4.1手工創建線程池

通過new一個ThreadPoolExecutor就可以實現自定義線程池

ThreadPoolExecutor(int corePoolSize,
                   int maximumPoolSize,
                   long keepAliveTime,
                   TimeUnit unit,
                   BlockingQueue<Runnable> workQueue,
                   RejectedExecutionHandler handler)

1)corePoolSize:核心線程池大小

當提交一個任務到線程池時,只要核心線程池未達到corePoolSize,創建新線程來執行任務;

調用preStartAllCoreThreads(),線程池會提前創建並啓動所有核心線程。

2)BlockingQueue<Runnable> workQueue**:任務隊列

保存等待執行任務的阻塞隊列。

  • ArrayBlockingQueue:基於數組有界阻塞隊列,按照FIFO原則對元素進行排序
  • LinkedBlockingQueue:基於鏈表的無界阻塞隊列,按照FIFO原則排列元素。吞吐量要高於ArrayBlockingQueue.內置線程池newFixedThreadPool(固定大小線程池)就採用此隊列。
  • SynchronousQueue:一個不存儲元素的阻塞隊列(無界隊列),每個插入操作需要等待另一個線程的移除操作,否則插入操作一直處於阻塞狀態。吞吐量通常高於LinkedBlockingQueue.內置線程池newCachedThreadPool(緩存線程池)就採用此隊列
  • PriorityBlockingQueue:具有優先級的阻塞隊列

3)maximumPoolSize:線程池最大數量

4)keepAliveTime:線程保持活動的時間(線程池的工作線程空閒後,保持存活的時間)

如果任務多並且每個任務執行的時間比較短,可以調大此參數來提高線程利用率。

5)RejectedExecutionHandler handler(飽和策略):

-AbortPolicy:無法處理新任務拋出異常(JDK默認採用此策略)

-CallerRunsPolicy:使用調用者線程(創建該線程池的線程)所在線程來處理任務

-DisCardOldestPolicy:丟棄隊列中最近的一個任務並執行當前任務

-DisCardPolicy:不處理任務,丟棄任務,也不報異常

注意:

(1)調用Future接口的get()方法會阻塞其他線程,直到取得當前線程執行完畢後的返回值;

(2)FutureTask可以保證多線程場景下,任務只會被執行一次,其他線程不再執行此任務;(多個線程規律交替執行)

(3)線程池中的線程被包裝爲Worker工作線程,具備可重複執行任務的能力;

7.4.2向線程池提交任務

execute():提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功

submit():提交需要返回值的任務,線程池會返回一個future類型的對象,由此對象的get()獲取返回值,

get():阻塞當前線程直到任務完成,處理線程異步時使用

線程池實現賣票問題:

import java.util.concurrent.*;

class CallableTest implements Callable<String>{
    private int ticket = 20;

    @Override
    public String call() throws Exception {
        for(int i = 0; i < 20;i++){
            if (ticket > 0){
                System.out.println(Thread.currentThread().getName()+"還剩下"+(ticket--)+"票");
            }
        }
        return "票賣完了。。";
    }
}

public class threadPool {
    public static void main(String[] args) {
        ExecutorService executorService =
                new ThreadPoolExecutor(2,3,60,
                        TimeUnit.SECONDS,new LinkedBlockingQueue<>());
        CallableTest callableTest = new CallableTest();
        for(int i = 0; i < 5;i++){
            executorService.submit(callableTest);
        }
        executorService.shutdown();
    }
}

7.5內置四大線程池

7.5.1固定大小線程池

FixedThreadPool

應用場景:用於爲了滿足資源管理的需求,而需要限制當前線程數量的應用,適用於負載較重的服務器

MyThread myThread = new MyThread();
ExecutorService executorService = Executors.newFixedThreadPool(4);
for(int  i = 0;i < 5;i++){
    Future future = executorService.submit(myThread);
}

Executors.newFixedThreadPool()返回值解析:

return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());

1.corePoolSize = maxPoolSize

2.採用無界阻塞隊列LinkedBlockingQueue,永遠不會使用飽和策略

7.5.2單線程池

SingleThreadExecutor

使用場景:需要保證順序的執行各個任務,並且在任意時間點,不會有多個線程是活動的

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
7.5.3緩衝線程池

CachedThreadPool-根據需求創建線程的線程池(大小無界的線程池)

使用場景:適用於執行很多的短期異步任務的小程序,或者負載較輕的服務器

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

corePoolSize = 0;

maximumPoolSizes = Integer.MAX_VALUE(無界)

線程池的工作隊列:沒有容量的SynchronousQueue

主線程提交任務的速度高於線程處理任務的速度的話,CachedThreadPool會一直創建新的線程,極端情況下,會因爲創建過多線程而耗盡CPU和內存資源

當任務提交速度>線程執行速度,會不斷創建線程

當線程的執行速度>任務提交速度,只會創建若干個有限線程
在這裏插入圖片描述

7.5.4定時調度池

newScheduledThreadPool()

主要用來在給定的延遲之後運行任務,或者定期執行任務,功能與Timer類似,但是Timer對應的是單個後臺線程,SchedualedThreadPoolExcecutor可以在構造函數中指定多個對應的後臺線程數

ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService
public ScheduledThreadPoolExecutor(int corePoolSize,ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), threadFactory);
}

ScheduledThreadPoolExecutor爲了實現週期性的執行任務,對ThreadPoolExecutor做了三方面的修改

i.獲取DelayQueue作爲任務隊列

ii.獲取任務的方式不同

iii.執行週期任務後,增加了額外的處理

7.6合理配置線程池

配置核心池以及最大線程池線程數量(N–電腦核數)

CPU密集型任務(大數運算):NCPU + 1

IO密集型任務(讀寫文件):2 * NCPU

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