最新最全的java多線程基礎(chǔ)總結(jié)(上)

知識點

image.png

應(yīng)該了解的概念

1. 線程與進(jìn)程

進(jìn)程是指一個內(nèi)存中運行的應(yīng)用程序,每個進(jìn)程都有自己獨立的一塊內(nèi)存空間,一個進(jìn)程中可以啟動多個線程。比如在 Windows 系統(tǒng)中,一個運行的 exe 就是一個進(jìn)程。

線程是指進(jìn)程中的一個執(zhí)行流程,一個進(jìn)程中可以運行多個線程。比如java.exe 進(jìn)程中可以運行很多線程。線程總是屬于某個進(jìn)程,進(jìn)程中的多個線程共享進(jìn)程的內(nèi)存。

1.1 區(qū)別

并發(fā)性:進(jìn)程之間可以并發(fā)執(zhí)行,同一個進(jìn)程的多個線程之間也可并發(fā)執(zhí)行。

調(diào)度:線程作為調(diào)度和分配的基本單位,進(jìn)程作為擁有資源的基本單位 。

根本區(qū)別進(jìn)程是操作系統(tǒng)資源分配的基本單位,而線程是任務(wù)調(diào)度和執(zhí)行的基本單位

開銷方面:每個進(jìn)程都有獨立的代碼和數(shù)據(jù)空間(程序上下文),進(jìn)程之間切換開銷大線程可以看做輕量級的進(jìn)程,同一類線程共享代碼和數(shù)據(jù)空間,每個線程都有自己獨立的運行棧和程序計數(shù)器(PC),線程之間切換的開銷小

包含關(guān)系線程是進(jìn)程的一部分,所以線程也被稱為輕權(quán)進(jìn)程或者輕量級進(jìn)程

2. 臨界區(qū)

臨界區(qū)用來表示一種公共資源或者說是共享數(shù)據(jù),可以被多個線程使用。但是每個線程使用時,一旦臨界區(qū)資源被一個線程占有,那么其他線程必須等待。多個進(jìn)程必須互斥的對它進(jìn)行訪問

3. 同步VS異步

同步方法:調(diào)用者必須等待被調(diào)用的方法結(jié)束后,調(diào)用者后面的代碼才能執(zhí)行

異步調(diào)用:調(diào)用者不用管被調(diào)用方法是否完成,都會繼續(xù)執(zhí)行后面的代碼,當(dāng)被調(diào)用的方法完成后會通知調(diào)用者。

4. 并發(fā)與并行

并發(fā):多個任務(wù)交替進(jìn)行,(類似單個 CPU ,通過 CPU 調(diào)度算法等,處理多個任務(wù)的能力,叫并發(fā))

并行:真正意義上的“同時進(jìn)行”。(類似多個 CPU ,同時并且處理相同多個任務(wù)的能力,叫做并行)

5. 阻塞和非阻塞

阻塞和非阻塞通常用來形容多線程間的相互影響,比如一個線程占有了臨界區(qū)資源,那么其他線程需要這個資源就必須進(jìn)行等待該資源的釋放,會導(dǎo)致等待的線程掛起,這種情況就是阻塞,而非阻塞就恰好相反,它強(qiáng)調(diào)沒有一個線程可以阻塞其他線程,所有的線程都會嘗試地往前運行。

線程與多線程

1. 什么是線程

線程是操作系統(tǒng)能夠進(jìn)行運算調(diào)度的最小單位,它被包含在進(jìn)程之中,是進(jìn)程中的實際運作單位。

2. 為什么需要多線程以及出現(xiàn)的問題

CPU、內(nèi)存、I/O 設(shè)備的速度是有極大差異的,為了合理利用 CPU 的高性能,平衡這三者的速度差異,計算機(jī)體系結(jié)構(gòu)、操作系統(tǒng)、編譯程序都做出了貢獻(xiàn)

  • CPU 增加了緩存,以均衡與內(nèi)存的速度差異;// 導(dǎo)致 可見性問題
  • 操作系統(tǒng)增加了進(jìn)程、線程,以分時復(fù)用 CPU,進(jìn)而均衡 CPU 與 I/O 設(shè)備的速度差異;// 導(dǎo)致 原子性問題
  • 編譯程序優(yōu)化指令執(zhí)行次序,使得緩存能夠得到更加合理地利用。// 導(dǎo)致 有序性問題
image.png

3. 線程的特征

  1. main()方法是個天然的多線程程序 所有的Java 程序,不論并發(fā)與否,都有一個名為主線程的Thread 對象。執(zhí)行該程序時, Java虛擬機(jī)( JVM )將創(chuàng)建一個新Thread 并在該線程中執(zhí)行main()方法。這是非并發(fā)應(yīng)用程序中唯一的線程,也是并發(fā)應(yīng)用程序中的第一個線程。
  2. Java中的線程共享應(yīng)用程序中的所有資源,包括內(nèi)存和打開的文件,快速而簡單地共享信息。但是必須使用同步避免數(shù)據(jù)競爭
  3. Java中的所有線程都有一個優(yōu)先級,這個整數(shù)值介于Thread.MIN_PRIORITY(1)和Thread.MAX_PRIORITY(10)之間,默認(rèn)優(yōu)先級是Thread.NORM_PRIORITY(5)。線程的執(zhí)行順序并沒有保證,通常,較高優(yōu)先級的線程將在較低優(yōu)先級的線程之前執(zhí)行

4. 線程狀態(tài)

  • 創(chuàng)建(NEW):新創(chuàng)建了一個線程對象,但還沒有調(diào)用 start() 方法。
  • 運行(RUNNABLE):Java 線程中將就緒(ready)和運行中(running)兩種狀態(tài)籠統(tǒng)的稱為“運行”。

線程對象創(chuàng)建后,其他線程(比如 main 線程)調(diào)用了該對象的 start() 方法。該狀態(tài)的線程位于可運行線程池中,等待被線程調(diào)度選中,獲取 CPU 的使用權(quán),此時處于就緒狀態(tài)(ready)。就緒狀態(tài)的線程在獲得 CPU 時間片后變?yōu)檫\行中狀態(tài)(running)。

  • 阻塞(BLOCKED):表示線程阻塞于鎖。線程的執(zhí)行過程中由于一些原因進(jìn)入阻塞狀態(tài)比如:調(diào)用 sleep 方法、嘗試去得到一個鎖等等
  • 超時等待(TIMED_WAITING):該狀態(tài)不同于WAITING,它可以在指定的時間后自行返回。
  • 等待(WAITING):進(jìn)入該狀態(tài)的線程需要等待其他線程做出一些特定動作(通知或中斷)。
  • 終止(TERMINATED):表示該線程已經(jīng)執(zhí)行完畢。
  • 運行(running):CPU 開始調(diào)度線程,并開始執(zhí)行 run 方法
  • 消亡(dead):run 方法執(zhí)行完 或者 執(zhí)行過程中遇到了一個異常
image.png

5. 使用多線程一定快嗎?

答:不一定,因為多線程會進(jìn)行上下文切換,上下文切換會帶來開銷。

5.1 什么是上下文切換?

單核在一個時刻只能運行一個線程,當(dāng)在運行一個線程的過程中轉(zhuǎn)去運行另外一個線程,這個叫做線程上下文切換(對于進(jìn)程也是類似)。

線程上下文切換過程中會記錄 程序計數(shù)器、CPU 寄存器 的 狀態(tài)等數(shù)據(jù)。

5.2 如何減少上下文切換?

5.2.1 減少線程的數(shù)量

由于一個CPU每個時刻只能執(zhí)行一條線程,而傲嬌的我們又想讓程序并發(fā)執(zhí)行,操作系統(tǒng)只好不斷地進(jìn)行上下文切換來使我們從感官上覺得程序是并發(fā)執(zhí)的行。因此,我們只要減少線程的數(shù)量,就能減少上下文切換的次數(shù)

5.2.2 控制同一把鎖上的線程數(shù)量

多條線程共用同一把鎖,那么當(dāng)一條線程獲得鎖后,其他線程就會被阻塞;當(dāng)該線程釋放鎖后,操作系統(tǒng)會從被阻塞的線程中選一條執(zhí)行,從而又會出現(xiàn)上下文切換

因此,減少同一把鎖上的線程數(shù)量也能減少上下文切換的次數(shù)

5.2.3 采用無鎖并發(fā)編程

  • 需要并發(fā)執(zhí)行的任務(wù)是無狀態(tài)的:HASH分段

所謂無狀態(tài)是指并發(fā)執(zhí)行的任務(wù)沒有共享變量,他們都獨立執(zhí)行。對于這種類型的任務(wù)可以按照ID進(jìn)行HASH分段,每段用一條線程去執(zhí)行。

  • 需要并發(fā)執(zhí)行的任務(wù)是有狀態(tài)的:CAS算法

如果任務(wù)需要修改共享變量,那么必須要控制線程的執(zhí)行順序,否則會出現(xiàn)安全性問題。你可以給任務(wù)加鎖,保證任務(wù)的原子性與可見性,但這會引起阻塞,從而發(fā)生上下文切換;為了避免上下文切換,你可以使用CAS算法, 僅在線程內(nèi)部需要更新共享變量時使用CAS算法來更新,這種方式不會阻塞線程,并保證更新過程的安全性(具體的方式后面的文章會將)。

6. 使用多線程的缺點

6.1 上下文切換的開銷

當(dāng) CPU 從執(zhí)行一個線程切換到執(zhí)行另外一個線程的時候,它需要先存儲當(dāng)前線程的本地的數(shù)據(jù),程序指針等,然后載入另一個線程的本地數(shù)據(jù),程序指針等,最后才開始執(zhí)行。這種切換稱為“上下文切換”。CPU 會在一個上下文中執(zhí)行一個線程,然后切換到另外一個上下文中執(zhí)行另外一個線程。上下文切換并不廉價。如果沒有必要,應(yīng)該減少上下文切換的發(fā)生。

6.2 增加資源消耗

線程在運行的時候需要從計算機(jī)里面得到一些資源。 除了 CPU,線程還需要一些 內(nèi)存來維持它本地的堆棧。它也需要 占用操作系統(tǒng)中一些資源來管理線程

6.3 編程更復(fù)雜

在多線程訪問共享數(shù)據(jù)的時候,要考慮 **線程安全 **問題

7. 線程狀態(tài)的基本操作

線程在生命周期內(nèi)還有需要基本操作,而這些操作會成為線程間一種通信方式,比如使用中斷(interrupted)方式通知實現(xiàn)線程間的交互等等

7.1 interrupted

中斷可以理解為線程的一個標(biāo)志位,它表示了一個運行中的線程是否被其他線程進(jìn)行了中斷操作。中斷好比其他線程對該線程打了一個招呼。其他線程可以調(diào)用該線程的interrupt()方法對其進(jìn)行中斷操作,同時該線程可以調(diào)用 isInterrupted()來感知其他線程對其自身的中斷操作,從而做出響應(yīng)。另外,同樣可以調(diào)用Thread的靜態(tài)方法 interrupted()對當(dāng)前線程進(jìn)行中斷操作,該方法會清除中斷標(biāo)志位。需要注意的是,當(dāng)拋出InterruptedException時候,會清除中斷標(biāo)志位,也就是說在調(diào)用isInterrupted會返回false。

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        //sleepThread睡眠1000ms
        final Thread sleepThread = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                super.run();
            }
        };
        //busyThread一直執(zhí)行死循環(huán)
        Thread busyThread = new Thread() {
            @Override
            public void run() {
                while (true) ;
            }
        };
        sleepThread.start();
        busyThread.start();
        sleepThread.interrupt();
        busyThread.interrupt();
        while (sleepThread.isInterrupted()) ;
        System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
        System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
    }
}

開啟了兩個線程分別為sleepThread和BusyThread, sleepThread睡眠1s,BusyThread執(zhí)行死循環(huán)。然后分別對著兩個線程進(jìn)行中斷操作,可以看出sleepThread拋出InterruptedException后清除標(biāo)志位,而busyThread就不會清除標(biāo)志位。

另外,同樣可以通過中斷的方式實現(xiàn)線程間的簡單交互, while (sleepThread.isInterrupted()) 表示在Main中會持續(xù)監(jiān)測sleepThread,一旦sleepThread的中斷標(biāo)志位清零,即sleepThread.isInterrupted()返回為false時才會繼續(xù)Main線程才會繼續(xù)往下執(zhí)行。因此,中斷操作可以看做線程間一種簡便的交互方式。一般在結(jié)束線程時通過中斷標(biāo)志位或者標(biāo)志位的方式可以有機(jī)會去清理資源,相對于武斷而直接的結(jié)束線程,這種方式要優(yōu)雅和安全。

7.2 join

join方法可以看做是線程間協(xié)作的一種方式,很多時候,一個線程的輸入可能非常依賴于另一個線程的輸出,這就像兩個好基友,一個基友先走在前面突然看見另一個基友落在后面了,這個時候他就會在原處等一等這個基友,等基友趕上來后,就兩人攜手并進(jìn)。其實線程間的這種協(xié)作方式也符合現(xiàn)實生活。在軟件開發(fā)的過程中,從客戶那里獲取需求后,需要經(jīng)過需求分析師進(jìn)行需求分解后,這個時候產(chǎn)品,開發(fā)才會繼續(xù)跟進(jìn)。

join方法源碼關(guān)鍵是:

 while (isAlive()) {
    wait(0);
 }

線程的合并是指將某一個線程A在調(diào)用A.join()方法合并到正在運行的另一個線程B中,此時線程B處于阻塞狀態(tài)需要等到線程A執(zhí)行完畢后才開始線程B的繼續(xù)執(zhí)行

public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        TestThread t = new TestThread();
        Thread t1 = new Thread(t);
        t1.start();
        for (int i = 0; i < 100; i++) {
            /**
             * 當(dāng)main線程中的i等于50的時候,就把t1線程合并到main線程中執(zhí)行。此時main線程是處于阻塞狀態(tài)
             * 直到t1線程執(zhí)行完成后,main才開始繼續(xù)執(zhí)行
             */
            if (50==i) {
                t1.join();
            }
            System.out.println("main.."+i);
        }  
    }
}
class TestThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("join.."+i);
        }  
    }
}

7.3 sleep

public static native void sleep(long millis)方法顯然是Thread的靜態(tài)方法,很顯然它是讓當(dāng)前線程按照指定的時間休眠,其休眠時間的精度取決于處理器的計時器和調(diào)度器。需要注意的是如果當(dāng)前線程獲得了鎖,sleep方法并不會失去鎖。

public static void main(String[] args) throws InterruptedException {
    int num = 10;
    while (true) {
        System.out.println(num--);
        Thread.sleep(1000);
        if (num<=0) {
            break;             
        }
    }
}

7.4 yield

該暫停方法暫停的時候不一定就暫停了,取決于CPU,假如剛暫停CPU調(diào)度又調(diào)到了該線程那就又啟動了.....

public class YieldDemo {
    public static void main(String[] args) throws InterruptedException {
        TestThread1 t = new TestThread1();
        Thread t1 = new Thread(t);
        t1.start();
        for (int i = 0; i < 100; i++) {
            //當(dāng)main線程中的i是20的倍數(shù)時,就暫停main線程
            if (i%20==0) {
                Thread.yield();//yield寫在哪個線程體中,就暫停哪個線程。這里是在main里,就暫停main線程
                System.out.println("main線程暫停");
            }
            System.out.println("main.."+i);
        }  
    }
}
class TestThread1 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("join.."+i);
        }  
    }
}

8. 兩類線程

8.1 用戶線程(User Thread)

User和Daemon兩者幾乎沒有區(qū)別,唯一的不同之處就在于虛擬機(jī)的離開:如果 User Thread已經(jīng)全部退出運行了,只剩下Daemon Thread存在了,虛擬機(jī)也就退出了。

8.2 守護(hù)線程(Daemon Thread)

Daemon的作用是為其他線程的運行提供便利服務(wù),守護(hù)線程最典型的應(yīng)用就是 GC (垃圾回收器)

當(dāng)所有非守護(hù)線程結(jié)束時,程序也就終止,同時會殺死所有守護(hù)線程。main() 屬于非守護(hù)線程。

使用 setDaemon() 方法將一個線程設(shè)置為守護(hù)線程。

8.2.1 需要注意

  • thread.setDaemon(true)必須在thread.start()之前設(shè)置,否則會拋出一個IllegalThreadStateException異常。你不能把正在運行的常規(guī)線程設(shè)置為守護(hù)線程。
  • Daemon線程中產(chǎn)生的新線程也是Daemon的
  • 不要認(rèn)為所有的應(yīng)用都可以分配給Daemon來進(jìn)行服務(wù),比如讀寫操作或者計算邏輯。

8.2.2 守護(hù)線程的代碼實踐

  • 前臺線程是保證執(zhí)行完畢的,后臺線程還沒有執(zhí)行完畢就退出了。
public class Test {  
  public static void main(String args) {  
      Thread t1 = new MyCommon();  
      Thread t2 = new Thread(new MyDaemon());  
      t2.setDaemon(true); //設(shè)置為守護(hù)線程  
      t2.start();  
      t1.start();  
      }  
}  
class MyCommon extends Thread {  
  public void run() {  
        for (int i = 0; i < 5; i++) {  
              System.out.println("線程1第" + i + "次執(zhí)行!");  
              try {  
                  Thread.sleep(7);  
              } catch (InterruptedException e) {  
                 e.printStackTrace();  
                }  
          }  
   }  
} 
class MyDaemon implements Runnable {  
  public void run() {  
      for (long i = 0; i < 9999999L; i++) {  
      System.out.println("后臺線程第" + i + "次執(zhí)行!");  
      try {  
      Thread.sleep(7);  
      } catch (InterruptedException e) {  
      e.printStackTrace();  
      }  
  }  
 }  
}

執(zhí)行結(jié)果:


   后臺線程第0次執(zhí)行!
  線程1第0次執(zhí)行! 
  線程1第1次執(zhí)行! 
  后臺線程第1次執(zhí)行! 
  后臺線程第2次執(zhí)行! 
  線程1第2次執(zhí)行! 
  線程1第3次執(zhí)行! 
  后臺線程第3次執(zhí)行! 
  線程1第4次執(zhí)行! 
  后臺線程第4次執(zhí)行! 
  后臺線程第5次執(zhí)行! 
  后臺線程第6次執(zhí)行! 
  后臺線程第7次執(zhí)行!
  • 守護(hù)線程在退出的時候并不會執(zhí)行finnaly塊中的代碼,所以將釋放資源等操作不要放在finnaly塊中執(zhí)行,這種操作是不安全的
public class DaemonDemo {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(new Runnable() {
            @Override
            public void run() {
                //daemodThread run方法中是一個while死循環(huán)
                while (true) {
                    try {
                        System.out.println("i am alive");
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        System.out.println("finally block");
                    }
                }
            }
        });
        daemonThread.setDaemon(true);
        daemonThread.start();
        //確保main線程結(jié)束前能給daemonThread能夠分到時間片
        try {
            Thread.sleep(800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

執(zhí)行結(jié)果

i am alive
finally block
i am alive

daemodThread run方法中是一個while死循環(huán),會一直打印,但是當(dāng)main線程結(jié)束后daemonThread就會退出所以不會出現(xiàn)死循環(huán)的情況。

main線程先睡眠800ms保證daemonThread能夠擁有一次時間片的機(jī)會,也就是說可以正常執(zhí)行一次打印“i am alive”操作和一次finally塊中"finally block"操作。

緊接著main 線程結(jié)束后,daemonThread退出,這個時候只打印了"i am alive"并沒有打印finnal塊中的。

守護(hù)線程在退出的時候并不會執(zhí)行finnaly塊中的代碼,所以將釋放資源等操作不要放在finnaly塊中執(zhí)行,這種操作是不安全的

8.2.3 特點

  • 因為是守護(hù)線程,或者說是支持性線程,就意味著這個線程并不屬于程序中不可或缺的一部分。所以當(dāng)所有的非守護(hù)線程(即用戶線程)結(jié)束之后,程序就會結(jié)束,JVM退出,同時也就會殺死所有的非守護(hù)線程。所以也就意味著,守護(hù)線程不適合去訪問固有資源,比如文件,數(shù)據(jù)庫。因為隨時可能中斷。

  • 后臺線程會隨著主程序的結(jié)束而結(jié)束,但是前臺進(jìn)程則不會,或者說只要有一個前臺線程未退出,進(jìn)程就不會終止。

  • 默認(rèn)情況下,程序員創(chuàng)建的線程是用戶線程;用setDaemon(true)可以設(shè)置線程為后臺線程;而用isDaemon( )則可以判斷一個線程是前臺線程還是后臺線程;

  • jvm的垃圾回收器其實就是一個后臺線程;

  • setDaemon函數(shù)必須在start函數(shù)之前設(shè)定,否則會拋出IllegalThreadStateException異常;

8.2.4 使用場景

  • qq,飛訊等等聊天軟件,主程序是非守護(hù)線程,而所有的聊天窗口是守護(hù)線程 ,當(dāng)在聊天的過程中,直接關(guān)閉聊天應(yīng)用程序時,聊天窗口也會隨之關(guān)閉,但是不是 立即關(guān)閉,而是需要緩沖,等待接收到關(guān)閉命令后才會執(zhí)行窗口關(guān)閉操作.

  • jvm中,gc線程是守護(hù)線程,作用就是當(dāng)所有用戶自定義線以及主線程執(zhí)行完畢后, gc線程才停止

  • Web服務(wù)器中的Servlet,在容器啟動時,后臺都會初始化一個服務(wù)線程,即調(diào)度線程,負(fù)責(zé)處理http請求,然后每個請求過來,調(diào)度線程就會從線程池中取出一個工作者線程來處理該請求,從而實現(xiàn)并發(fā)控制的目的。也就是說,一個實際應(yīng)用在Java的線程池中的調(diào)度線程

8.2.5 總結(jié)

守護(hù)線程就是用來告訴JVM,我的這個線程是一個低級別的線程,不需要等待它運行完才退出,讓JVM喜歡什么時候退出就退出,不用管這個線程。

在日常的業(yè)務(wù)相關(guān)的CRUD開發(fā)中,其實并不會關(guān)注到守護(hù)線程這個概念,也幾乎不會用上。

但是如果要往更高的地方走的話,這些深層次的概念還是要了解一下的,比如一些框架的底層實現(xiàn)。

7. 線程安全

7.1 什么是線程安全?

所有的隱患都是在多個線程訪問的情況下產(chǎn)生的,也就是我們要確保在多條線程訪問的時候,我們的程序還能按照我們預(yù)期的行為去執(zhí)行.

下面代碼需要在多線程環(huán)境下的測試

Integer count = 0;

   public void getCount() {

       count ++;
       System.out.println(count);
   }
//開三個線程代碼,值會重復(fù) 所以在多線程的情況下

結(jié)論:

當(dāng)多個線程訪問某個方法時,不管你通過怎樣的調(diào)用方式、或者說這些線程如何交替地執(zhí)行,我們在主程序中不需要去做任何的同步,這個類的結(jié)果行為都是我們設(shè)想的正確行為,那么我們就可以說這個類是線程安全的。

7.2 什么時候會出現(xiàn)線程安全?

  • 線程安全問題都是由全局變量及靜態(tài)變量引起的。若每個線程中對全局變量、靜態(tài)變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;

  • 有多個線程同時執(zhí)行寫操作,一般都需要考慮線程同步,否則的話就可能出現(xiàn)線程安全的問題。

7.3 有哪些方法可以保證線程安全嗎?

7.3.1 synchronized

7.3.1.1 同步代碼塊

synchronized 關(guān)鍵字可以用于方法中的某個區(qū)塊中,表示只對這個區(qū)塊的資源實行互斥訪問。

synchronized關(guān)鍵字就是用來控制線程同步的,保證我們的線程在多線程環(huán)境下,不被多個線程同時執(zhí)行,確保我們數(shù)據(jù)的完整性,使用方法一般是加在方法上。

  • 代碼塊中的鎖對象可以是任意對象;

  • 但是必須保證多個線程使用的鎖對象是同一個;

  • 鎖對象的作用就是將同步代碼塊鎖住,只允許一個線程在同步代碼塊中執(zhí)行

      // 創(chuàng)建一個鎖對象
    Object object = new Object();
    int count = 0; // 記錄方法的命中次數(shù)
    // 創(chuàng)建同步代碼塊
            synchronized (object) {
                if (ticket > 0) {
                   count++ ;
                   int i = 1;
                   j = j + i;
                }
            }

注意

  • synchronized鎖的是括號里的對象,而不是代碼,其次,對于非靜態(tài)的synchronized方法,鎖的是對象本身也就是this
  • synchronized鎖住一個對象之后,別的線程如果想要獲取鎖對象,那么就必須等這個線程執(zhí)行完釋放鎖對象之后才可以,否則一直處于等待狀態(tài)。
  • 加synchronized關(guān)鍵字,可以讓我們的線程變得安全,但是我們在用的時候,也要注意縮小synchronized的使用范圍,如果隨意使用時很影響程序的性能,別的對象想拿到鎖,結(jié)果你沒用鎖還一直把鎖占用,這樣就有點浪費資源。

7.3.1.2 同步方法

使用 synchronized 修飾的方法,就叫做同步方法,保證A線程執(zhí)行該方法的時候,其他線程只能在方法外等著

  int count = 0; // 記錄方法的命中次數(shù)
  public synchronized void threadMethod(int j) {
      count++ ;
      int i = 1;
      j = j + i;
  }

7.3.2 同步鎖(Lock)

java.util.concurrent.locks.Lock 機(jī)制提供了比 synchronized 代碼塊和 synchronized 方法更廣泛的鎖定操作,同步代碼塊/同步方法具有的功能 Lock 都有,除此之外更強(qiáng)大,更體現(xiàn)面向?qū)ο蟆?/p>

Lock 鎖使用步驟:

  1. 在成員位置創(chuàng)建一個 ReentrantLock 對象;
  2. 在可能出現(xiàn)安全問題的代碼前調(diào)用 Lock 接口中的方法lock();
  3. 在可能出現(xiàn)安全問題的代碼前調(diào)用 Lock 接口中的方法unLock();

7.3.2.1 lock.lock()

跟synchronized不同的是,Lock獲取的所對象需要我們親自去進(jìn)行釋放,為了防止我們代碼出現(xiàn)異常,所以我們的釋放鎖操作放在finally中,因為finally中的代碼無論如何都是會執(zhí)行的。

private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子類
  private void method(Thread thread){
      lock.lock(); // 獲取鎖對象
      try {
          System.out.println("線程名:"+thread.getName() + "獲得了鎖");
          // Thread.sleep(2000);
      }catch(Exception e){
          e.printStackTrace();
      } finally {
          System.out.println("線程名:"+thread.getName() + "釋放了鎖");
          lock.unlock(); // 釋放鎖對象
      }
}

7.3.2.2 lock.tryLock()

tryLock()這個方法跟Lock()是有區(qū)別的,Lock在獲取鎖的時候,如果拿不到鎖,就一直處于等待狀態(tài),直到拿到鎖,但是tryLock()卻不是這樣的,tryLock是有一個Boolean的返回值的,如果沒有拿到鎖,直接返回false,停止等待,它不會像Lock()那樣去一直等待獲取鎖。

private void method(Thread thread){
      // lock.lock(); // 獲取鎖對象
      if (lock.tryLock()) {
          try {
              System.out.println("線程名:"+thread.getName() + "獲得了鎖");
              // Thread.sleep(2000);
          }catch(Exception e){
              e.printStackTrace();
          } finally {
              System.out.println("線程名:"+thread.getName() + "釋放了鎖");
              lock.unlock(); // 釋放鎖對象
          }
      }
  }

7.3.2.3 lock.tryLock(5,TimeUnit.SECONDS)

一種方式來控制一下,讓后面等待的線程,可以等待5秒,如果5秒之后,還獲取不到鎖,那么就停止等,其實tryLock()是可以進(jìn)行設(shè)置等待的相應(yīng)時間的。

private void method(Thread thread) throws InterruptedException {
      // lock.lock(); // 獲取鎖對象

      // 如果5秒內(nèi)獲取不到鎖對象,那就不再等待
      if (lock.tryLock(5,TimeUnit.SECONDS)) {
          try {
              System.out.println("線程名:"+thread.getName() + "獲得了鎖");
          }catch(Exception e){
              e.printStackTrace();
          } finally {
              System.out.println("線程名:"+thread.getName() + "釋放了鎖");
              lock.unlock(); // 釋放鎖對象
          }
      }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容