<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Java多線程詳解——一篇文章搞懂Java多線程

          共 57594字,需瀏覽 116分鐘

           ·

          2021-05-02 06:59

          點擊上方藍色字體,選擇“標星公眾號”

          優(yōu)質(zhì)文章,第一時間送達

            作者 |  13roky

          來源 |  urlify.cn/7zuYfm

          1. 基本概念

          • 程序(program)

            程序是為完成特定任務(wù)、用某種語言編寫的一組指令的集合。即指一段靜態(tài)的代碼(還沒有運行起來),靜態(tài)對象。

          • 進程(process)

            進程是程序的一次執(zhí)行過程,也就是說程序運行起來了,加載到了內(nèi)存中,并占用了cpu的資源。這是一個動態(tài)的過程:有自身的產(chǎn)生、存在和消亡的過程,這也是進程的生命周期。

            進程是系統(tǒng)資源分配的單位,系統(tǒng)在運行時會為每個進程分配不同的內(nèi)存區(qū)域。

          • 線程(thread)

            進程可進一步細化為線程,是一個程序內(nèi)部的執(zhí)行路徑。

            若一個進程同一時間并行執(zhí)行多個線程,那么這個進程就是支持多線程的。

            線程是cpu調(diào)度和執(zhí)行的單位,每個線程擁有獨立的運行棧和程序計數(shù)器(pc),線程切換的開銷小。

            一個進程中的多個線程共享相同的內(nèi)存單元/內(nèi)存地址空間——》他們從同一堆中分配對象,可以訪問相同的變量和對象。這就使得相乘間通信更簡便、搞笑。但索格線程操作共享的系統(tǒng)資源可能就會帶來安全隱患(隱患為到底哪個線程操作這個數(shù)據(jù),可能一個線程正在操作這個數(shù)據(jù),有一個線程也來操作了這個數(shù)據(jù)v)。

            • 配合JVM內(nèi)存結(jié)構(gòu)了解(只做了解即可)





              class文件會通過類加載器加載到內(nèi)存空間。

              其中內(nèi)存區(qū)域中每個線程都會有虛擬機棧和程序計數(shù)器。

              每個進程都會有一個方法區(qū)和堆,多個線程共享同一進程下的方法區(qū)和堆。

          • CPU單核和多核的理解

            單核的CPU是一種假的多線程,因為在一個時間單元內(nèi),也只能執(zhí)行一個線程的任務(wù)。同時間段內(nèi)有多個線程需要CPU去運行時,CPU也只能交替去執(zhí)行多個線程中的一個線程,但是由于其執(zhí)行速度特別快,因此感覺不出來。

            多核的CPU才能更好的發(fā)揮多線程的效率。

            對于Java應(yīng)用程序java.exe來講,至少會存在三個線程:main()主線程,gc()垃圾回收線程,異常處理線程。如過發(fā)生異常時會影響主線程。

          • Java線程的分類:用戶線程 和 守護線程

            • Java的gc()垃圾回收線程就是一個守護線程

            • 守護線程是用來服務(wù)用戶線程的,通過在start()方法前調(diào)用thread.setDaemon(true)可以吧一個用戶線程變成一個守護線程。

          • 并行和并發(fā)

            • 并行:多個cpu同時執(zhí)行多個任務(wù)。比如,多個人做不同的事。

            • 并發(fā):一個cpu(采用時間片)同時執(zhí)行多個任務(wù)。比如,渺少、多個人做同一件事。

          • 多線程的優(yōu)點

            1. 提高應(yīng)用程序的響應(yīng)。堆圖像化界面更有意義,可以增強用戶體驗。

            2. 提高計算機系CPU的利用率。

            3. 改善程序結(jié)構(gòu)。將既長又復(fù)雜的進程分為多個線程,獨立運行,利于理解和修改。

          • 何時需要多線程

            • 程序需要同時執(zhí)行兩個或多個任務(wù)。

            • 程序需要實現(xiàn)一些需要等待的任務(wù)時,如用戶輸入、文件讀寫操作、網(wǎng)絡(luò)操作、搜索等。

            • 需要一些后臺運行的程序時。

          2. 線程的創(chuàng)建和啟動

          2.1. 多線程實現(xiàn)的原理

          • Java語言的JVM允許程序運行多個線程,多線程可以通過Java中的

            java.lang.Thread

            類來體現(xiàn)。

          • Thread類的特性

            • 每個線程都是通過某個特定的Thread對象的run()方法來完成操作的,經(jīng)常吧run()方法的主體稱為線程體。

            • 通過Thread方法的start()方法來啟動這個線程,而非直接調(diào)用run()。

          2.2.多線程的創(chuàng)建,方式一:繼承于Thread類

          1. 創(chuàng)建一個繼承于Thread類的子類。

          2. 重寫Thread類的run()方法。

          3. 創(chuàng)建Thread類的子類的對象。

          4. 通過此對象調(diào)用start()來啟動一個線程。

          代碼實現(xiàn):多線程執(zhí)行同一段代碼

          package com.broky.multiThread;

          /**
           * @author 13roky
           * @date 2021-04-19 21:22
           */
          public class ThreadTest extends Thread{
              @Override
              //線程體,啟動線程時會運行run()方法中的代碼
              public void run() {
                  //輸出100以內(nèi)的偶數(shù)
                  for (int i = 0; i < 100; i++) {
                      if (i % 2 == 0){
                          System.out.println(Thread.currentThread().getName()+":\t"+i);
                      }
                  }
              }

              public static void main(String[] args) {
                  //創(chuàng)建一個Thread類的子類對象
                  ThreadTest t1 = new ThreadTest();
                  //通過此對象調(diào)用start()啟動一個線程
                  t1.start();
                  //注意:已經(jīng)啟動過一次的線程無法再次啟動
                  //再創(chuàng)建一個線程
                  ThreadTest t2 = new ThreadTest();
                  t2.start();

                  //另一種調(diào)用方法,此方法并沒有給對象命名
                  new ThreadTest().start();

                  System.out.println("主線程");
              }
          }

          多線程代碼運行圖解


          多線程執(zhí)行多段代碼

          package com.broky.multiThread.exer;

          /**
           * @author 13roky
           * @date 2021-04-19 22:43
           */
          public class ThreadExerDemo01 {
              public static void main(String[] args) {
                  new Thread01().start();
                  new Thread02().start();
              }
          }

          class Thread01 extends Thread {
              @Override
              public void run() {
                  for (int i = 0; i < 100; i++) {
                      if (i % 2 == 0) System.out.println(Thread.currentThread().getName() + ":\t" + i);
                  }
              }
          }

          class Thread02 extends Thread {
              @Override
              public void run() {
                  for (int i = 0; i < 100; i++) {
                      if (i % 2 != 0) System.out.println(Thread.currentThread().getName() + ":\t" + i);
                  }
              }
          }

          2.3.多線程的創(chuàng)建,方式一:創(chuàng)建Thread匿名子類(也屬于方法一)

          package com.broky.multiThread;

          /**
           * @author 13roky
           * @date 2021-04-19 22:53
           */
          public class AnonymousSubClass {
              public static void main(String[] args) {

                  new Thread(){
                      @Override
                      public void run() {
                          for (int i = 0; i < 100; i++) {
                              if (i % 2 == 0) System.out.println(Thread.currentThread().getName() + ":\t" + i);
                          }
                      }
                  }.start();

              }
          }

          2.4. 多線程的創(chuàng)建,方式二:實現(xiàn)Runnable接口

          1. 創(chuàng)建一個實現(xiàn)Runnable接口的類。

          2. 實現(xiàn)類去實現(xiàn)Runnable接口中的抽象方法:run()。

          3. 創(chuàng)建實現(xiàn)類的對象。

          4. 將此對象作為參數(shù)傳到Thread類的構(gòu)造器中,創(chuàng)建Thread類的對象。

          5. 通過Thread類的對象調(diào)用start()方法。

          package com.broky.multiThread;

          /**
           * @author 13roky
           * @date 2021-04-20 23:16
           */
          public class RunnableThread {
              public static void main(String[] args) {
                  //創(chuàng)建實現(xiàn)類的對象
                  RunnableThread01 runnableThread01 = new RunnableThread01();
                  //創(chuàng)建Thread類的對象,并將實現(xiàn)類的對象當做參數(shù)傳入構(gòu)造器
                  Thread t1 = new Thread(runnableThread01);
                  //使用Thread類的對象去調(diào)用Thread類的start()方法:①啟動了線程 ②Thread中的run()調(diào)用了Runnable中的run()
                  t1.start();

                  //在創(chuàng)建一個線程時,只需要new一個Thread類就可,不需要new實現(xiàn)類
                  Thread t2 = new Thread(runnableThread01);
                  t2.start();
              }
          }

          //RunnableThread01實現(xiàn)Runnable接口的run()抽象方法
          class RunnableThread01 implements Runnable {
              @Override
              public void run() {
                  for (int i = 0; i < 100; i++) {
                      if (i % 2 == 0) System.out.println(Thread.currentThread().getName() + ":\t" + i);
                  }
              }
          }

          2.4.1. 比較創(chuàng)建線程的兩種方式

          • Java中只允許單進程,以賣票程序TiketSales類來說,很有可能這個類本來就有父類,這樣一來就不可以繼承Thread類來完成多線程了,但是一個類可以實現(xiàn)多個接口,因此

            實現(xiàn)的方式?jīng)]有類的單繼承性的局限性

            ,用實現(xiàn)Runnable接口的方式來完成多線程更加實用。

          • 實現(xiàn)Runnable接口的方式天然具有共享數(shù)據(jù)的特性(不用static變量)。因為繼承Thread的實現(xiàn)方式,需要創(chuàng)建多個子類的對象來進行多線程,如果子類中有變量A,而不使用static約束變量的話,每個子類的對象都會有自己獨立的變量A,只有static約束A后,子類的對象才共享變量A。而實現(xiàn)Runnable接口的方式,只需要創(chuàng)建一個實現(xiàn)類的對象,要將這個對象傳入Thread類并創(chuàng)建多個Thread類的對象來完成多線程,而這多個Thread類對象實際上就是調(diào)用一個實現(xiàn)類對象而已。

            實現(xiàn)的方式更適合來處理多個線程有共享數(shù)據(jù)的情況。
          • 聯(lián)系:Thread類中也實現(xiàn)了Runnable接口

          • 相同點兩種方式都需要重寫run()方法,線程的執(zhí)行邏輯都在run()方法中

          2.5. 多線程的創(chuàng)建,方式三:實現(xiàn)Callable接口

          與Runnable相比,Callable功能更強大

          1. 相比run()方法,可以有返回值

          2. 方法可以拋出異常

          3. 支持泛型的返回值

          4. 需要借助FutureTask類,比如獲取返回結(jié)果

          package com.broky.multiThread;

          import java.util.concurrent.Callable;
          import java.util.concurrent.ExecutionException;
          import java.util.concurrent.FutureTask;

          /**
           * 創(chuàng)建線程的方式三:實現(xiàn)Callable接口。 ---JDK5新特性
           * 如何理解Callable比Runnable強大?
           * 1.call()可以有返回值
           * 2.call()可以拋出異常被外面的操作捕獲
           * @author 13roky
           * @date 2021-04-22 21:04
           */

          //1.創(chuàng)建一個實現(xiàn)Callable的實現(xiàn)類
          class NumThread implements Callable<Integer>{
              //2.實現(xiàn)call方法,將此線程需要執(zhí)行的操作聲明在call()中
              @Override
              public Integer call() throws Exception {
                  int sum = 0;
                  for (int i = 1; i < 100; i++) {
                      if(i%2==0){
                          System.out.println(i);
                          sum += i;
                      }
                  }
                  return sum;
              }
          }

          public class ThreadNew {
              public static void main(String[] args) {
                  //3.創(chuàng)建Callable接口實現(xiàn)類的對象
                  NumThread numThread = new NumThread();
                  //4.將此Callable接口實現(xiàn)類的對象作為參數(shù)傳遞到FutureTask構(gòu)造器中,創(chuàng)建FutureTask對象
                  FutureTask<Integer> futureTask = new FutureTask(numThread);
                  //5.將FutureTask的對象作為參數(shù)傳遞到Thread類的構(gòu)造器中,創(chuàng)建Thread對象,并調(diào)用start()
                  new Thread(futureTask).start();

                  try {
                      //6.獲取Callable中Call方法的返回值
                      Integer sum = futureTask.get();
                      System.out.println("總和為"+sum);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  } catch (ExecutionException e) {
                      e.printStackTrace();
                  }

              }
          }

          2.6. 多線程的創(chuàng)建,方式四:線程池

          背景:

          經(jīng)常創(chuàng)建和銷毀、使用量特別大的資源、比如并發(fā)情況下的線程、對性能影響很大。

          思路:

          提前創(chuàng)建好多個線程,放入線程池中,使用時直接獲取,使用完放回池中。可以避免頻繁創(chuàng)建銷毀、實現(xiàn)重復(fù)利用。類似生活中的公共交通工具。

          優(yōu)點:

          提高響應(yīng)速度(減少了創(chuàng)建新線程的時間)

          降低資源消耗(重復(fù)利用線程池中線程,不需要每次都創(chuàng)建)

          便于線程管理

          package com.broky.multiThread;

          import java.util.concurrent.ExecutorService;
          import java.util.concurrent.Executors;
          import java.util.concurrent.ThreadPoolExecutor;

          /**
           * 創(chuàng)建線程的方式四:使用線程池
           * <p>
           * 面試題:創(chuàng)建多線程有幾種方式
           *
           * @author 13roky
           * @date 2021-04-22 21:49
           */

          class NumberThread implements Runnable {
              @Override
              public void run() {
                  for (int i = 0; i < 100; i++) {
                      if (i % 2 == 0) {
                          System.out.println(Thread.currentThread().getName() + ":\t" + i);
                      }
                  }
              }
          }

          public class ThreadPool {
              public static void main(String[] args) {

                  //1.提供指定線程數(shù)量的線程池
                  ExecutorService service = Executors.newFixedThreadPool(10);
                  ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
                  //設(shè)置線程池的屬性
                  //        System.out.println(service.getClass());
                  //        service1.setCorePoolSize(15);
                  //        service1.setKeepAliveTime();

                  //2.執(zhí)行指定的線程的操作。需要提供實現(xiàn)Runnable接口或Callable接口實現(xiàn)類的對象。
                  service.execute(new NumberThread()); //適合用于Runnable
                  //        service.submit(); 適合適用于Callable
                  //關(guān)閉線程池
                  service.shutdown();
              }
          }

          3. Thread類的常用方法

          • start() : 啟動當前線程, 調(diào)用當前線程的run()方法

          • run() : 通常需要重寫Thread類中的此方法, 將創(chuàng)建的線程要執(zhí)行的操作聲明在此方法中

          • currentThread() : 靜態(tài)方法, 返回當前代碼執(zhí)行的線程

          • getName() : 獲取當前線程的名字

          • setName() : 設(shè)置當前線程的名字

          • yield() : 釋放當前CPU的執(zhí)行權(quán)

          • join() : 在線程a中調(diào)用線程b的join(), 此時線程a進入阻塞狀態(tài), 知道線程b完全執(zhí)行完以后, 線程a才結(jié)束阻塞狀態(tài)

          • stop() : 已過時. 當執(zhí)行此方法時,強制結(jié)束當前線程.

          • sleep(long militime) : 讓線程睡眠指定的毫秒數(shù),在指定時間內(nèi),線程是阻塞狀態(tài)

          • isAlive() :判斷當前線程是否存活

          4. 線程的調(diào)度

          4.1. cpu的調(diào)度策略

          • 時間片:cpu正常情況下的調(diào)度策略。即CPU分配給各個程序的時間,每個線程被分配一個時間段,稱作它的時間片,即該進程允許運行的時間,使各個程序從表面上看是同時進行的。如果在時間片結(jié)束時進程還在運行,則CPU將被剝奪并分配給另一個進程。如果進程在時間片結(jié)束前阻塞或結(jié)束,則CPU當即進行切換。而不會造成CPU資源浪費。在宏觀上:我們可以同時打開多個應(yīng)用程序,每個程序并行不悖,同時運行。但在微觀上:由于只有一個CPU,一次只能處理程序要求的一部分,如何處理公平,一種方法就是引入時間片,每個程序輪流執(zhí)行。

          • 搶占式:高優(yōu)先級的線程搶占cpu。

          4.2. Java的調(diào)度算法:

          • 同優(yōu)先級線程組成先進先出隊列(先到先服務(wù)),使用時間片策略。

          • 堆高優(yōu)先級,使用優(yōu)先調(diào)度的搶占式策略。

          線程的優(yōu)先級等級(一共有10擋)

          • MAX_PRIORITY:10

          • MIN_PRIORITY:1

          • NORM_PRIORITY:5 (默認優(yōu)先級)

          獲取和設(shè)置當前線程的優(yōu)先級

          • getPriority(); 獲取

          • setPriority(int p); 設(shè)置

          說明:高優(yōu)先級的線程要搶占低優(yōu)先級線程cpu的執(zhí)行權(quán)。但是只是從概率上講,高優(yōu)先級的線程高概率的情況下被執(zhí)行。并不意味著只有高優(yōu)先級的線程執(zhí)行完成以后,低優(yōu)先級的線程才執(zhí)行。

          5. 線程的生命周期

          • JDk中用Thread.State類定義了線程的幾種狀態(tài)

          想要實現(xiàn)多線程,必須在主線程中創(chuàng)建新的線程對象。Java語言使用Thread類及其子類的對象來表示線程,在他的一個完整的生命周期中通常要經(jīng)歷如下的五種狀態(tài)

          1. 新建:當一個Thread類或其子類的對象被聲明并創(chuàng)建時,新的線程對象處于新建狀態(tài)。

          2. 就緒:處于新建狀態(tài)的線程被start()后,將進入線程隊列等待CPU時間片,此時它已具備了運行的條件,只是沒分配到CPU資源。

          3. 運行:當就緒的線程被調(diào)度并獲得CPU資源時,便進入運行狀態(tài),run()方法定義了線程的操作和功能。

          4. 阻塞:在某種特殊情況下,被認為掛起或執(zhí)行輸入輸出操作時,讓出CPU并臨時中止自己的執(zhí)行,進入阻塞狀態(tài)。

          5. 死亡:線程完成了它的全部工作或線程被提前強制性的中止或出現(xiàn)異常倒置導致結(jié)束。

          6. 線程的同步

          6.1. 多線程的安全性問題解析

          • 線程的安全問題

            • 多個線程執(zhí)行的不確定性硬氣執(zhí)行結(jié)果的不穩(wěn)定性

            • 多個線程對賬本的共享, 會造成操作的不完整性, 會破壞數(shù)據(jù).

            • 多個線程訪問共享的數(shù)據(jù)時可能存在安全性問題

          • 線程的安全問題Demo: 賣票過程中出現(xiàn)了重票和錯票的情況 (以下多窗口售票demo存在多線程安全問題)

          package com.broky.multiThread.safeThread;

          /**
           * @author 13roky
           * @date 2021-04-21 20:39
           */
          public class SafeTicketsWindow {
              public static void main(String[] args) {
                  WindowThread ticketsThread02 = new WindowThread();
                  Thread t1 = new Thread(ticketsThread02);
                  Thread t2 = new Thread(ticketsThread02);
                  Thread t3 = new Thread(ticketsThread02);

                  t1.setName("窗口1");
                  t2.setName("窗口2");
                  t3.setName("窗口3");

                  t1.start();
                  t2.start();
                  t3.start();
              }
          }

          class WindowThread implements Runnable {
              private int tiketsNum = 100;

              public void run() {
                  while (true) {
                      if (tiketsNum > 0) {
                          try {
                              //手動讓線程進入阻塞,增大錯票概率
                              Thread.sleep(100);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                          System.out.println(Thread.currentThread().getName() + ":\t票號:" + tiketsNum);
                          /*try {
                              //手動讓線程進入阻塞,增大重票的概率
                              Thread.sleep(100);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }*/
                          tiketsNum--;
                      } else {
                          break;
                      }
                  }
              }
          }

          錯票分析:

          當票數(shù)為1的時候,三個線程中有線程被阻塞沒有執(zhí)行票數(shù)-1的操作,這是其它線程就會通過if語句的判斷,這樣一來就會造成多賣了一張票,出現(xiàn)錯票的情況。

          極端情況為,當票數(shù)為1時,三個線程同時判斷通過,進入阻塞,然后多執(zhí)行兩側(cè)賣票操作。

          重票分析

          如果t1在輸出票號22和票數(shù)-1的操作之間被阻塞,這就導致這時候t1賣出了22號票,但是總票數(shù)沒有減少。在t1被阻塞期間,如果t2運行到輸出票號時,那么t2也會輸出和t1相同的票號22.

          通過以上兩種情況可以看出,線程的安全性問題時因為多個線程正在執(zhí)行代碼的過程中,并且尚未完成的時候,其他線程參與進來執(zhí)行代碼所導致的。

          6.2. 多線程安全性問題的解決

          原理:

          當一個線程在操作共享數(shù)據(jù)的時候,其他線程不能參與進來。知道這個線程操作完共享數(shù)據(jù)的時候,其他線程才可以操作。即使當這個線程操作共享數(shù)據(jù)的時候發(fā)生了阻塞,依舊無法改變這種情況。

          在Java中,我們通過同步機制,來解決線程的安全問題。


          6.2.1. 多線程安全問題的解決方式一:同步代碼塊

          synchronized(同步監(jiān)視器){需要被同步的代碼}

          說明:

          1. 操作共享數(shù)據(jù)(多個線程共同操作的變量)的代碼,即為需要被同步的代碼。不能多包涵代碼(效率低,如果包到while前面就變成了單線程了),也不能少包含代碼

          2. 共享數(shù)據(jù):多個線程共同操作的變量。

          3. 同步監(jiān)視器:俗稱,鎖。任何一個類的對象都可以充當鎖。但是所有的線程都必須共用一把鎖,共用一個對象。

          鎖的選擇:

          1. 自行創(chuàng)建,共用對象,如下面demo中的Object對象。

          2. 使用this表示當前類的對象

            繼承Thread的方法中的鎖不能使用this代替,因為繼承thread實現(xiàn)多線程時,會創(chuàng)建多個子類對象來代表多個線程,這個時候this指的時當前這個類的多個對象,不唯一,無法當作鎖。

            實現(xiàn)Runnable接口的方式中,this可以當作鎖,因為這種方式只需要創(chuàng)建一個實現(xiàn)類的對象,將實現(xiàn)類的對象傳遞給多個Thread類對象來當作多個線程,this就是這個一個實現(xiàn)類的對象,是唯一的,被所有線程所共用的對象。

          3. 使用類當作鎖,以下面demo為例,其中的鎖可以寫為WindowThread.class, 從這里可以得出結(jié)論,類也是一個對象

          優(yōu)點:同步的方式,解決了線程安全的問題

          缺點:操作同步代碼時,只能有一個線程參與,其他線程等待。相當于時一個單線程的過程,效率低。

          Demo

          package com.broky.multiThread.safeThread;

          /**
           * @author 13roky
           * @date 2021-04-21 20:39
           */
          public class SafeTicketsWindow {
              public static void main(String[] args) {
                  WindowThread ticketsThread02 = new WindowThread();
                  Thread t1 = new Thread(ticketsThread02);
                  Thread t2 = new Thread(ticketsThread02);
                  Thread t3 = new Thread(ticketsThread02);

                  t1.setName("窗口1");
                  t2.setName("窗口2");
                  t3.setName("窗口3");

                  t1.start();
                  t2.start();
                  t3.start();
              }
          }

          class WindowThread implements Runnable {
              private int tiketsNum = 100;
              
              //由于,Runnable實現(xiàn)多線程,所有線程共用一個實現(xiàn)類的對象,所以三個線程都共用實現(xiàn)類中的這個Object類的對象。
              Object obj = new Object();
              //如果時繼承Thread類實現(xiàn)多線程,那么需要使用到static Object obj = new Object();
              
              public void run() {
                  
                  //Object obj = new Object();
                  //如果Object對象在run()方法中創(chuàng)建,那么每個線程運行都會生成自己的Object類的對象,并不是三個線程的共享對象,所以并沒有給加上鎖。
                  
                  while (true) {
                      synchronized (obj) {
                          if (tiketsNum > 0) {
                              try {
                                  //手動讓線程進入阻塞,增大安全性發(fā)生的概率
                                  Thread.sleep(100);
                              } catch (InterruptedException e) {
                                  e.printStackTrace();
                              }
                              System.out.println(Thread.currentThread().getName() + ":\t票號:" + tiketsNum + "\t剩余票數(shù):" + --tiketsNum);
                          } else {
                              break;
                          }
                      }
                  }
              }
          }

          6.3.2. 多線程安全問題的解決方式二:同步方法

          將所要同步的代碼放到一個方法中,將方法聲明為synchronized同步方法。之后可以在run()方法中調(diào)用同步方法。

          要點:

          1. 同步方法仍然涉及到同步監(jiān)視器,只是不需要我們顯示的聲明。

          2. 非靜態(tài)的同步方法,同步監(jiān)視器是:this。

          3. 靜態(tài)的同步方法,同步監(jiān)視器是:當前類本身。

          Demo

          package com.broky.multiThread.safeThread;

          /**
           * @author 13roky
           * @date 2021-04-21 22:39
           */
          public class Window02 {
              public static void main(String[] args) {
                  Window02Thread ticketsThread02 = new Window02Thread();
                  Thread t1 = new Thread(ticketsThread02);
                  Thread t2 = new Thread(ticketsThread02);
                  Thread t3 = new Thread(ticketsThread02);

                  t1.setName("窗口1");
                  t2.setName("窗口2");
                  t3.setName("窗口3");

                  t1.start();
                  t2.start();
                  t3.start();
              }
          }

          class Window02Thread implements Runnable {
              private int tiketsNum = 100;

              @Override
              public void run() {
                  while (tiketsNum > 0) {
                      show();
                  }
              }

              private synchronized void show() { //同步監(jiān)視器:this
                  if (tiketsNum > 0) {
                      try {
                          //手動讓線程進入阻塞,增大安全性發(fā)生的概率
                          Thread.sleep(100);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      System.out.println(Thread.currentThread().getName() + ":\t票號:" + tiketsNum + "\t剩余票數(shù):" + --tiketsNum);
                  }
              }
          }
          package com.broky.multiThread.safeThread;

          /**
           * @author 13roky
           * @date 2021-04-21 22:59
           */
          public class Window03 {
              public static void main(String[] args) {
                  Window03Thread t1 = new Window03Thread();
                  Window03Thread t2 = new Window03Thread();
                  Window03Thread t3 = new Window03Thread();
                  t1.setName("窗口1");
                  t2.setName("窗口2");
                  t3.setName("窗口3");
                  t1.setPriority(Thread.MIN_PRIORITY);
                  t3.setPriority(Thread.MAX_PRIORITY);
                  t1.start();
                  t2.start();
                  t3.start();
              }
          }

          class Window03Thread extends Thread {
              public static int tiketsNum = 100;

              @Override
              public void run() {
                  while (tiketsNum > 0) {
                      show();
                  }
              }

              public static synchronized void show() {//同步監(jiān)視器:Winddoe03Thread.class  不加static話同步監(jiān)視器為t1 t2 t3所以錯誤
                  if (tiketsNum > 0) {
                      try {
                          //手動讓線程進入阻塞,增大安全性發(fā)生的概率
                          Thread.sleep(100);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      System.out.println(Thread.currentThread().getName() + ":\t票號:" + tiketsNum + "\t剩余票數(shù):" + --tiketsNum);
                  }
              }
          }

          使用同步解決懶漢模式的線程安全問題

          package com.broky.multiThread.safeThread;

          /**
           * @author 13roky
           * @date 2021-04-22 7:24
           */
          public class BankTest {
          }

          class Bank {
              private Bank() {
              }

              private static Bank instance = null;

              public static Bank getInstance() {
                  //方式一:效率性差,每個等待線程都會進入同步代碼塊
                  //        synchronized (Bank.class) {
                  //            if (instance == null) {
                  //                instance = new Bank();
                  //            }
                  //        }

                  //方式二:在同步代碼塊外層在判斷一次,就防止所有線程進入同步代碼塊。
                  if (instance == null) {
                      synchronized (Bank.class) {
                          if (instance == null) {
                              instance = new Bank();
                          }
                      }
                  }
                  return instance;
              }
          }

          6.2.3. 多線程安全問題的解決方式二:Lock鎖 -JDK5.0新特性

          JDK5.0之后,可以通過實例化ReentrantLock對象,在所需要同步的語句前,調(diào)用ReentrantLock對象的lock()方法,實現(xiàn)同步鎖,在同步語句結(jié)束時,調(diào)用unlock()方法結(jié)束同步鎖

          synchronized和lock的異同:(面試題)

          1. Lcok是顯式鎖(需要手動開啟和關(guān)閉鎖),synchronized是隱式鎖,除了作用域自動釋放。
          2. Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖。
          3. 使用Lcok鎖,JVM將花費較少的時間來調(diào)度線程,性能更好。并且具有更好的拓展性(提供更多的子類)

          建議使用順序:Lock—》同步代碼塊(已經(jīng)進入了方法體,分配了相應(yīng)的資源)—》同步方法(在方法體之外)

          Demo:

          package com.broky.multiThread.safeThread;

          import java.util.concurrent.locks.ReentrantLock;

          /**
           * @author 13roky
           * @date 2021-04-22 9:36
           */
          public class SafeLock {
              public static void main(String[] args) {
                  SafeLockThread safeLockThread = new SafeLockThread();
                  Thread t1 = new Thread(safeLockThread);
                  Thread t2 = new Thread(safeLockThread);
                  Thread t3 = new Thread(safeLockThread);

                  t1.start();
                  t2.start();
                  t3.start();
              }
          }

          class SafeLockThread implements Runnable{
              private int tickets = 100;
              private ReentrantLock lock = new ReentrantLock();

              @Override
              public void run() {
                  while (tickets>0) {
                      try {
                          //在這里鎖住,有點類似同步監(jiān)視器
                          lock.lock();
                          if (tickets > 0) {
                              Thread.sleep(100);
                              System.out.println(Thread.currentThread().getName() + ":\t票號:" + tickets + "\t剩余票數(shù):" + --tickets);
                          }
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      } finally {
                          //操作完成共享數(shù)據(jù)后在這里解鎖
                          lock.unlock();
                      }
                  }
              }
          }

          6.3. 線程同步的死鎖問題

          原理:

          不同的線程分別占用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了死鎖。

          出現(xiàn)死鎖后,并不會出現(xiàn)異常,不會出現(xiàn)提示,只是所有的線程都處于阻塞狀態(tài),無法繼續(xù)。

          使用同步時應(yīng)避免出現(xiàn)死鎖。

          Java中死鎖最簡單的情況:

          一個線程T1持有鎖L1并且申請獲得鎖L2,而另一個線程T2持有鎖L2并且申請獲得鎖L1,因為默認的鎖申請操作都是阻塞的,所以線程T1和T2永遠被阻塞了。導致了死鎖。這是最容易理解也是最簡單的死鎖的形式。但是實際環(huán)境中的死鎖往往比這個復(fù)雜的多。可能會有多個線程形成了一個死鎖的環(huán)路,比如:線程T1持有鎖L1并且申請獲得鎖L2,而線程T2持有鎖L2并且申請獲得鎖L3,而線程T3持有鎖L3并且申請獲得鎖L1,這樣導致了一個鎖依賴的環(huán)路:T1依賴T2的鎖L2,T2依賴T3的鎖L3,而T3依賴T1的鎖L1。從而導致了死鎖。

          從這兩個例子,我們可以得出結(jié)論,產(chǎn)生死鎖可能性的最根本原因是:線程在獲得一個鎖L1的情況下再去申請另外一個鎖L2,也就是鎖L1想要包含了鎖L2,也就是說在獲得了鎖L1,并且沒有釋放鎖L1的情況下,又去申請獲得鎖L2,這個是產(chǎn)生死鎖的最根本原因。另一個原因是默認的鎖申請操作是阻塞的

          死鎖的解決方法:

          1. 專門的算法、原則。
          2. 盡量減少同步資源的定義。
          3. 盡量避免嵌套同步。
          package com.broky.multiThread.safeThread;

          /**
           * @author 13roky
           * @date 2021-04-22 8:34
           */
          public class DeadLock {
              public static void main(String[] args) {
                  StringBuffer s1 = new StringBuffer();
                  StringBuffer s2 = new StringBuffer();

                  new Thread() {
                      public void run() {
                          synchronized (s1) {
                              s1.append("a");
                              s2.append("1");

                              try {
                                  Thread.sleep(1000);
                              } catch (InterruptedException e) {
                                  e.printStackTrace();
                              }

                              synchronized (s2) {
                                  s1.append("b");
                                  s2.append("2");

                                  System.out.println(s1);
                                  System.out.println(s2);
                              }
                          }
                      }
                  }.start();

                  new Thread(new Runnable() {
                      public void run() {
                          synchronized (s2) {
                              s1.append("c");
                              s2.append("3");

                              try {
                                  Thread.sleep(1000);
                              } catch (InterruptedException e) {
                                  e.printStackTrace();
                              }

                              synchronized (s1) {
                                  s1.append("d");
                                  s2.append("4");

                                  System.out.println(s1);
                                  System.out.println(s2);
                              }
                          }
                      }
                  }).start();
              }
          }

          7. 線程的通信

          很多情況下,盡管我們創(chuàng)建了多個線程,也會出現(xiàn)幾乎一個線程執(zhí)行完所有操作的時候,這時候我們就需要讓線程間相互交流。

          原理:

          當一個線程執(zhí)行完成其所應(yīng)該執(zhí)行的代碼后,手動讓這個線程進入阻塞狀態(tài),這樣一來,接下來的操作只能由其他線程來操作。當其他線程執(zhí)行的開始階段,再手動讓已經(jīng)阻塞的線程停止阻塞,進入就緒狀態(tài),雖說這時候阻塞的線程停止了阻塞,但是由于現(xiàn)在正在運行的線程拿著同步鎖,所以停止阻塞的線程也無法立馬執(zhí)行。如此操作就可以完成線程間的通信。

          所用的到方法:

          wait():一旦執(zhí)行此方法,當前線程就會進入阻塞,一旦執(zhí)行wait()會釋放同步監(jiān)視器。

          notify():一旦執(zhí)行此方法,將會喚醒被wait的一個線程。如果有多個線程被wait,就喚醒優(yōu)先度最高的。

          notifyAll() :一旦執(zhí)行此方法,就會喚醒所有被wait的線程

          說明:

          這三個方法必須在同步代碼塊或同步方法中使用。

          三個方法的調(diào)用者必須是同步代碼塊或同步方法中的同步監(jiān)視器。

          這三個方法并不時定義在Thread類中的,而是定義在Object類當中的。因為所有的對象都可以作為同步監(jiān)視器,而這三個方法需要由同步監(jiān)視器調(diào)用,所以任何一個類都要滿足,那么只能寫在Object類中。

          sleep()和wait()的異同:(面試題)

          1. 相同點:兩個方法一旦執(zhí)行,都可以讓線程進入阻塞狀態(tài)。

          2. 不同點:1) 兩個方法聲明的位置不同:Thread類中聲明sleep(),Object類中聲明wait()

            2) 調(diào)用要求不同:sleep()可以在任何需要的場景下調(diào)用。wait()必須在同步代碼塊中調(diào)用。

            2) 關(guān)于是否釋放同步監(jiān)視器:如果兩個方法都使用在同步代碼塊呵呵同步方法中,sleep不會釋放鎖,wait會釋放鎖。

          Demo:

          package com.broky.multiThread;

          /**
           * @author 13roky
           * @date 2021-04-22 13:29
           */
          public class Communication {
              public static void main(String[] args) {
                  CommunicationThread communicationThread = new CommunicationThread();
                  Thread t1 = new Thread(communicationThread);
                  Thread t2 = new Thread(communicationThread);
                  Thread t3 = new Thread(communicationThread);

                  t1.start();
                  t2.start();
                  t3.start();
              }
          }

          class CommunicationThread implements Runnable {
              int Num = 1;

              @Override
              public void run() {
                  while (true) {
                      synchronized (this) {
                          notifyAll();
                          if (Num <= 100) {
                              System.out.println(Thread.currentThread().getName() + ":\t" + Num);
                              Num++;

                              try {
                                  wait();
                              } catch (InterruptedException e) {
                                  e.printStackTrace();
                              }
                          }else{
                              break;
                          }
                      }

                  }
              }
          }

          練習

          • 練習1:

          銀行有一個賬戶。

          有兩個儲戶分別向同一個賬戶存3000元,每次存1000,存3次。每次存完打印賬戶余額。

          package com.broky.multiThread.exer;

          /**
           * 練習1
           * 銀行有一個賬戶
           * 有兩個儲戶分別向同一個賬戶存3000元,每次存1000,存3次。每次存完打印賬戶余額。
           * 分析:
           * 1.是否有多個線程問題? 是,有兩個儲戶線程。
           * 2.是否有共享數(shù)據(jù)? 是,兩個儲戶向同一個賬戶存錢
           * 3.是否有線程安全問題: 有
           *
           * @author 13roky
           * @date 2021-04-22 12:38
           */
          public class AccountTest {
              public static void main(String[] args) {
                  Account acct = new Account();
                  Customer c1 = new Customer(acct);
                  Customer c2 = new Customer(acct);

                  c1.setName("儲戶1");
                  c2.setName("儲戶2");

                  c1.start();
                  c2.start();

              }
          }

          class Account {
              private double accountSum;

              public Account() {
                  this.accountSum = 0;
              }

              public Account(double accountSum) {
                  this.accountSum = accountSum;
              }

              //存錢
              public void deppsit(double depositNum) {
                  synchronized (this) {
                      if (depositNum > 0) {
                          accountSum = accountSum + depositNum;
                          try {
                              Thread.sleep(1000);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                          System.out.println(Thread.currentThread().getName() + ": 存錢成功,當前余額為:\t" + accountSum);
                      }
                  }

              }

          }

          class Customer extends Thread {
              private Account acct;

              public Customer(Account acct) {
                  this.acct = acct;
              }

              @Override
              public void run() {
                  for (int i = 0; i < 3; i++) {
                      acct.deppsit(1000);
                  }
              }
          }
          • 經(jīng)典例題:生產(chǎn)者和消費著問題

          生產(chǎn)者( Productor)將產(chǎn)品交給店員( Clerk),而消費者( (Customer)從店員處取走產(chǎn)品, 店員一次只能持有固定數(shù)量的產(chǎn)品(比如:20),如果生產(chǎn)者試圖生產(chǎn)更多的產(chǎn)品,店員會叫生產(chǎn)者停一下,如果店中有空位放產(chǎn)品了再通知生產(chǎn)者繼續(xù)生產(chǎn); 如果店中沒有產(chǎn)品了,店員會告訴消費者等一下,如果店中有產(chǎn)品了再通知消費者來取走產(chǎn)品。

          package com.broky.multiThread.exer;

          /**
           * - 經(jīng)典例題:生產(chǎn)者和消費著問題
           * 生產(chǎn)者( Productor)將產(chǎn)品交給店員( Clerk),而消費者( (Customer)從店員處取走產(chǎn)品,
           * 店員一次只能持有固定數(shù)量的產(chǎn)品(比如:20),如果生產(chǎn)者試圖生產(chǎn)更多的產(chǎn)品,店員會叫生產(chǎn)者停一下,
           * 如果店中有空位放產(chǎn)品了再通知生產(chǎn)者繼續(xù)生產(chǎn); 如果店中沒有產(chǎn)品了,店員會告訴消費者等一下,
           * 如果店中有產(chǎn)品了再通知消費者來取走產(chǎn)品。
           *
           * 分析:
           * 1.是多線程問題,可以假設(shè)多個消費這和多個生產(chǎn)者是多線程的
           * 2.存在操作的共享數(shù)據(jù),生產(chǎn)和購買時都需要操作經(jīng)銷商的庫存存量。
           * 3.處理線程安全問題。
           * 4.三個類:生產(chǎn)者,經(jīng)銷商,消費者。經(jīng)銷商被生產(chǎn)者和消費者共享。生產(chǎn)者讀取經(jīng)銷商庫存,當庫存不夠時,生產(chǎn)產(chǎn)品
           * 并發(fā)給經(jīng)銷商,操作經(jīng)銷商庫存+1。消費者讀取經(jīng)銷商庫存,當有庫存時,方可進行購買,購買完成后,經(jīng)銷商庫存-1.
           * @author 13roky
           * @date 2021-04-22 14:36
           */
          public class ProductTest {
              public static void main(String[] args) {
                  Clerk clerk = new Clerk();
                  Producer p1 = new Producer(clerk);
                  Producer p2 = new Producer(clerk);
                  p1.setName("生產(chǎn)者1");
                  p2.setName("生產(chǎn)者2");

                  Consumer c1 = new Consumer(clerk);
                  Consumer c2 = new Consumer(clerk);
                  c1.setName("消費者1");
                  c2.setName("消費者2");

                  p1.start();
                  c1.start();
              }
          }

          class Clerk {
              private int productNum;

              public Clerk() {
                  this.productNum = 0;
              }

              public int getProductNum() {
                  return productNum;
              }

              public void setProductNum(int productNum) {
                  this.productNum = productNum;
              }
          }

          class Producer extends Thread {
              private Clerk clerk;

              @Override
              public void run() {
                  System.out.println(Thread.currentThread().getName() + "開始生產(chǎn)......");

                  while(true){
                      try {
                          Thread.sleep(500);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      produce();
                  }
              }

              public Producer(Clerk clerk) {
                  if (clerk != null) {
                      this.clerk = clerk;
                  }
              }

              private void produce() {
                  synchronized (ProductTest.class) {
                      ProductTest.class.notify();
                      if (clerk.getProductNum() < 20) {
                          clerk.setProductNum(clerk.getProductNum() + 1);
                          System.out.println(Thread.currentThread().getName() + ":\t生產(chǎn)完成第 " + clerk.getProductNum() + " 個產(chǎn)品");
                      }else {
                          try {
                              ProductTest.class.wait();
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                  }
              }

          }

          class Consumer extends Thread {
              private Clerk clerk;

              @Override
              public void run() {
                  System.out.println(Thread.currentThread().getName() + "開始消費......");

                  while(true){
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      buy();
                  }
              }

              public Consumer(Clerk clerk) {
                  if (clerk != null) {
                      this.clerk = clerk;
                  }
              }

              private void buy(){
                  synchronized (ProductTest.class) {
                      ProductTest.class.notify();
                      if (clerk.getProductNum() > 0) {
                          System.out.println(Thread.currentThread().getName() + ":\t購買完成第 " + clerk.getProductNum() + " 個產(chǎn)品");
                          clerk.setProductNum(clerk.getProductNum() - 1);
                      }else {

                          try {
                              ProductTest.class.wait();
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                  }
              }
          }





          粉絲福利:Java從入門到入土學習路線圖

          ??????

          ??長按上方微信二維碼 2 秒


          感謝點贊支持下哈 

          瀏覽 62
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线观看亚洲福利 | 亚洲中文婷婷 | 伊人网大香蕉视频在线观看 | 亚洲日本视频在线播放 | 新av天堂 |