JUC學習筆記

来源:https://www.cnblogs.com/deyo/archive/2023/07/05/17527829.html
-Advertisement-
Play Games

## 目錄 * [1. JUC概述及回顧](#1-juc概述及回顧) * [1.1. JUC是什麼?](#11-juc是什麼) * [1.2. 進程和線程](#12-進程和線程) * [1.3. 並行和併發](#13-並行和併發) * [1.4. wait/sleep的區別](#14-waitsle ...


目錄

JavaEE_JUC

JavaEE_JUC


1. JUC概述及回顧

1.1. JUC是什麼?

在 Java 5.0 提供了 java.util.concurrent(簡稱JUC)包,在此包中增加了在併發編程中很常用的工具類。此包包括了幾個小的、已標準化的可擴展框架,並提供一些功能實用的類,沒有這些類,一些功能會很難實現或實現起來冗長乏味。

參照JDK文檔:

1.2. 進程和線程

進程:進程是一個具有一定獨立功能的程式關於某個數據集合的一次運行活動。它是操作系統動態執行的基本單元,在傳統的操作系統中,進程既是基本的分配單元,也是基本的執行單元。

線程:通常在一個進程中可以包含若幹個線程,當然一個進程中至少有一個線程,不然沒有存在的意義。線程可以利用進程所擁有的資源,在引入線程的操作系統中,通常都是把進程作為分配資源的基本單位,而把線程作為獨立運行和獨立調度的基本單位,由於線程比進程更小,基本上不擁有系統資源,故對它的調度所付出的開銷就會小得多,能更高效的提高系統多個程式間併發執行的程度。

生活實例:

​ 使用QQ,查看進程一定有一個QQ.exe的進程,我可以用qq和A文字聊天,和B視頻聊天,給C傳文件,給D發一段語言,QQ支持錄入信息的搜索。

​ 大四的時候寫論文,用word寫論文,同時用QQ音樂放音樂,同時用QQ聊天,多個進程。

​ word如沒有保存,停電關機,再通電後打開word可以恢復之前未保存的文檔,word也會檢查你的拼寫,兩個線程:容災備份,語法檢查

1.3. 並行和併發

併發:同一時刻多個線程在訪問同一個資源,多個線程對一個點

​ 例子:小米9今天上午10點,限量搶購

​ 春運搶票

​ 電商秒殺...

並行:多項工作一起執行,之後再彙總

​ 例子:泡速食麵,電水壺燒水,一邊撕調料倒入桶中

1.4. wait/sleep的區別

功能都是當前線程暫停,有什麼區別?

wait:放開手去睡,放開手裡的鎖

sleep:握緊手去睡,醒了手裡還有鎖

wait是Object的方法,sleep是thread的方法

1.5. 創建線程回顧

創建線程常用三種方式

  1. 繼承Thread:java是單繼承,資源寶貴,要用介面方式
  2. 實現Runable介面
  3. 實現Callable介面(後面講)
  4. 使用線程池(後面講)

繼承Thread抽象類

public class MyThread extends Thread
new MyThread().start();

實現Runnable介面的方式:

  1. 新建類實現runnable介面。這種方法會新增類,有更好的方法
class MyRunnable implements Runnable//新建類實現runnable介面
new Thread(new MyRunnable(), name).start // 使用Rannable實現類創建進程,name是線程名
  1. 匿名內部類。
new Thread(new Runnable() {
    @Override
    public void run() {
     // 調用資源方法,完成業務邏輯
    }
}, "your thread name").start();

1.6. lambda表達式

之前說了Runnable介面的兩種實現方式,其實還有第三種:

  1. 創建類實現Runnable介面

  2. 編寫匿名內部類實現Runnable介面

  3. lambda表達式:這種方法代碼更簡潔精煉

new Thread(() -> {

}, "your thread name").start();

1.6.1. 什麼是lambda

Lambda 是一個匿名函數,我們可以把 Lambda表達式理解為是一段可以傳遞的代碼(將代碼像數據一樣進行傳遞)。可以寫出更簡潔、更靈活的代碼。作為一種更緊湊的代碼風格,使Java的語言表達能力得到了提升。

Lambda 表達式在Java 語言中引入了一個新的語法元素和操作符。這個操作符為 “->” , 該操作符被稱為 Lambda 操作符或剪頭操作符。它將 Lambda 分為兩個部分:

  • 左側:指定了 Lambda 表達式需要的所有參數

  • 右側:指定了 Lambda 體,即 Lambda 表達式要執行的功能

1.6.2. 案例

在一個方法中調用介面中的方法:傳統寫法

interface Foo {

    public int add(int x, int y);
}

public class LambdaDemo {
    public static void main(String[] args) {
        Foo foo = new Foo() {
            @Override
            public int add(int x, int y) {
                return x + y;
            }
        };
        System.out.println(foo.add(10, 20));
    }
}

接下來,要用lambda表達式改造。其實是改造main方法

public static void main(String[] args) {

    Foo foo = (int x, int y)->{
        return x + y;
    };
    System.out.println(foo.add(10, 20));
}

改造口訣:

拷貝小括弧(),寫死右箭頭->,落地大括弧{...}

思考:如果Foo介面有多個方法,還能使用lambda表達式嗎?

1.6.3. 函數式介面

lambda表達式,必須是函數式介面,必須只有一個抽象方法,如果介面只有一個方法java預設它為函數式介面
為了正確使用Lambda表達式,需要給介面加個註解:@FunctionalInterface。如有兩個方法,立刻報錯。

Runnable介面為什麼可以用lambda表達式?

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

發現Runnable介面上有一個註解:@FunctionalInterface

並且該介面只有一個方法:run()方法

其實,函數式介面必須只有一個方法,這個描述並不准確,它還允許有default方法和靜態方法。

例如,在Foo介面中,又添加了sub方法和mul方法:

interface Foo {

    public int add(int x, int y); // 抽象方法

    default int sub(int x, int y){ // default方法
        return x - y;
    }
    public static int mul(int x, int y){ // 靜態方法
        return x * y;
    }
}

public class LambdaDemo {

    public static void main(String[] args) {

        Foo foo = (int x, int y)->{  // lambda表達式實現抽象方法
            return x + y;
        };
        System.out.println(foo.add(10, 20)); // 調用抽象方法
        System.out.println(foo.sub(30, 15)); // 調用default方法
        System.out.println(Foo.mul(10, 50)); // 通過Foo調用靜態方法
    }
}

1.6.4. 小結

lambda表達式實現介面的前提是

有且只有一個抽象方法,可以選擇@FunctionalInterface註解增強函數式介面定義

改造口訣

拷貝小括弧(形參列表),寫死右箭頭 ->,落地大括弧 {方法實現}

1.7. synchronized回顧

多線程編程:

口訣一:線程 操作 資源類

實現步驟:

  1. 創建資源類

  2. 資源類里創建同步方法、同步代碼塊

  3. 多線程調用

例子:賣票程式

創建工程,並添加了一個SaleTicket.java

內容如下:

class Ticket {

    private Integer number = 20;

    public synchronized void sale(){
        if (number <= 0) {
            System.out.println("票已售罄!!!");
            return;
        }
        try {
            System.out.println(Thread.currentThread().getName() + "開始買票,當前票數:" + number);
            Thread.sleep(200);
            System.out.println(Thread.currentThread().getName() + "買票結束,剩餘票數:" + --number);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 在main方法中創建多線程方法,測試賣票業務
public class SaleTicket {

    public static void main(String[] args) {

        Ticket ticket = new Ticket();

        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        }, "AAA").start();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        }, "BBB").start();
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        }, "CCC").start();

    }
}

1.8. synchronized的8鎖問題

看下麵這段兒代碼,回答後面的8個問題:

class Phone {

    public synchronized void sendSMS() throws Exception {
        //TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println("------sendEmail");
    }

    public void getHello() {
        System.out.println("------getHello");
    }

}

public class Lock_8 {

    public static void main(String[] args) throws Exception {

        Phone phone = new Phone();
        Phone phone2 = new Phone();

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "AA").start();

        Thread.sleep(100);

        new Thread(() -> {
            try {
                phone.sendEmail();
                //phone.getHello();
                //phone2.sendEmail();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "BB").start();
    }
}

多線程的8個問題:

  1. 標準訪問,先列印簡訊還是郵件

    同一個鎖

    image-20230704183606714

  2. 停4秒在簡訊方法內,先列印簡訊還是郵件

    同一個鎖

    image-20230704183705194

  3. 普通的hello方法,是先打簡訊還是hello

    hello無鎖:無需爭搶,直接執行

    image-20230704183903454

  4. 現在有兩部手機,先列印簡訊還是郵件

    非同一個鎖

    image-20230704184328967

  5. 兩個靜態同步方法,1部手機,先列印簡訊還是郵件

    同一個鎖

    image-20230704184711209

  6. 兩個靜態同步方法,2部手機,先列印簡訊還是郵件

    同一個鎖

    image-20230704184801430

  7. 1個靜態同步方法,1個普通同步方法,1部手機,先列印簡訊還是郵件

    非同一個鎖

    image-20230704184837575

  8. 1個靜態同步方法,1個普通同步方法,2部手機,先列印簡訊還是郵件

    非同一個鎖

    image-20230704184925846

總結:

synchronized實現同步的基礎:Java中的每一個對象都可以作為鎖。具體表現為以下3種形式:

  1. 對於普通同步方法,鎖是當前實例對象。

  2. 對於靜態同步方法,鎖是當前類的Class對象。

  3. 對於同步方法塊,鎖是Synchonized括弧里配置的對象

當一個線程試圖訪問同步代碼塊時,它首先必須得到鎖,退出或拋出異常時必須釋放鎖。

也就是說:

​ 如果一個實例對象非靜態同步方法獲取鎖後,該實例對象的其他非靜態同步方法必須等待獲取鎖的方法釋放鎖後才能獲取鎖;可是不同實例對象的非靜態同步方法因為用的是不同對象的鎖,所以毋須等待其他實例對象的非靜態同步方法釋放鎖,就可以獲取自己的鎖。

所有的靜態同步方法用的是同一把鎖——類對象本身。不管是不是同一個實例對象,只要是一個類的對象,一旦一個靜態同步方法獲取鎖之後,其他對象的靜態同步方法,都必須等待該方法釋放鎖之後,才能獲取鎖。

​ 而靜態同步方法(Class對象鎖)與非靜態同步方法(實例對象鎖)之間是不會有競態條件的。

2. Lock鎖

首先看一下JUC的重磅武器——鎖(Lock)

相比同步鎖,JUC包中的Lock鎖的功能更加強大,它提供了各種各樣的鎖(公平鎖,非公平鎖,共用鎖,獨占鎖……),所以使用起來很靈活。

翻譯過來就是:

鎖實現提供了比使用同步方法和語句可以獲得的更廣泛的鎖操作。它們允許更靈活的結構,可能具有非常不同的屬性,並且可能支持多個關聯的條件對象。

Lock是一個介面,這裡主要有三個實現:ReentrantLock、ReentrantReadWriteLock.ReadLock、ReentrantReadWriteLock.WriteLock

2.1. ReentrantLock可重入鎖

ReentrantLock使用方式參照官方文檔:

使用ReentrantLock改造賣票程式:只需改造sale()方法

class Ticket{

    private Integer number = 20;

    private ReentrantLock lock = new ReentrantLock();

    public void sale(){

        lock.lock();

        if (number <= 0) {
            System.out.println("票已售罄!");
            lock.unlock();
            return;
        }

        try {
            Thread.sleep(200);
            number--;
            System.out.println(Thread.currentThread().getName() + "買票成功,當前剩餘:" + number);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

2.1.1. 測試可重入性

可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,再進入該線程的內層方法會自動獲取鎖。Java中ReentrantLock和synchronized都是可重入鎖,可重入鎖的一個優點是可一定程度避免死鎖

例如下列偽代碼:

class A{
  public synchronized void aa(){
    ......
        bb();
        ......
  }
  public synchronized void bb(){
    ......
  }
}
A a = new A();
a.aa();

A類中有兩個普通同步方法,都需要對象a的鎖。如果是不可重入鎖的話,aa方法首先獲取到鎖,aa方法在執行的過程中需要調用bb方法,此時鎖被aa方法占有,bb方法無法獲取到鎖,這樣就會導致bb方法無法執行,aa方法也無法執行,出現了死鎖情況。可重入鎖可避免這種死鎖的發生。

class Ticket{

    private Integer number = 20;

    private ReentrantLock lock = new ReentrantLock();

    public void sale(){

        lock.lock();

        if (number <= 0) {
            System.out.println("票已售罄!");
            lock.unlock();
            return;
        }

        try {
            Thread.sleep(200);
            number--;
            System.out.println(Thread.currentThread().getName() + "買票成功,當前剩餘:" + number);
            // 調用check方法測試鎖的可重入性
            this.check();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 為了測試可重入鎖,添加檢查餘票方法
     */
    public void check(){
        lock.lock();
        System.out.println("檢查餘票。。。。");
        lock.unlock();
    }
}

可以發現程式可以正常執行。。。說明該鎖確實可重入。

AAA買票成功,當前剩餘:19
檢查餘票。。。。
AAA買票成功,當前剩餘:18
檢查餘票。。。。
AAA買票成功,當前剩餘:17
檢查餘票。。。。
AAA買票成功,當前剩餘:16
檢查餘票。。。。
AAA買票成功,當前剩餘:15
檢查餘票。。。。
AAA買票成功,當前剩餘:14
檢查餘票。。。。
AAA買票成功,當前剩餘:13
檢查餘票。。。。
BBB買票成功,當前剩餘:12
檢查餘票。。。。
BBB買票成功,當前剩餘:11
檢查餘票。。。。
BBB買票成功,當前剩餘:10
。。。。。。

2.1.2. 測試公平鎖

ReentrantLock還可以實現公平鎖。所謂公平鎖,也就是在鎖上等待時間最長的線程優先獲得鎖的使用權。通俗的理解就是誰排隊時間最長誰先執行獲取鎖。

private ReentrantLock lock = new ReentrantLock(true);

測試結果:

AAA買票成功,當前剩餘:19
檢查餘票。。。。
BBB買票成功,當前剩餘:18
檢查餘票。。。。
CCC買票成功,當前剩餘:17
檢查餘票。。。。
AAA買票成功,當前剩餘:16
檢查餘票。。。。
BBB買票成功,當前剩餘:15
檢查餘票。。。。
CCC買票成功,當前剩餘:14
。。。。。。

可以看到ABC三個線程是按順序買票成功的。

2.1.3. 限時等待

這個是什麼意思呢?也就是通過我們的tryLock方法來實現,可以選擇傳入時間參數,表示等待指定的時間,無參則表示立即返回鎖申請的結果:true表示獲取鎖成功,false表示獲取鎖失敗。我們可以將這種方法用來解決死鎖問題。

2.1.4. ReentrantLock和synchronized區別

(1)synchronized是獨占鎖,加鎖和解鎖的過程自動進行,易於操作,但不夠靈活。ReentrantLock也是獨占鎖,加鎖和解鎖的過程需要手動進行,不易操作,但非常靈活。

(2)synchronized可重入,因為加鎖和解鎖自動進行,不必擔心最後是否釋放鎖;ReentrantLock也可重入,但加鎖和解鎖需要手動進行,且次數需一樣,否則其他線程無法獲得鎖。

(3)synchronized不可響應中斷,一個線程獲取不到鎖就一直等著;ReentrantLock可以響應中斷。

2.2. ReentrantReadWriteLock讀寫鎖

​ 在併發場景中用於解決線程安全的問題,我們幾乎會高頻率的使用到獨占式鎖,通常使用java提供的關鍵字synchronized或者concurrents包中實現了Lock介面的ReentrantLock。它們都是獨占式獲取鎖,也就是在同一時刻只有一個線程能夠獲取鎖。而在一些業務場景中,大部分只是讀數據,寫數據很少,如果僅僅是讀數據的話並不會影響數據正確性(出現臟讀),而如果在這種業務場景下,依然使用獨占鎖的話,很顯然這將是出現性能瓶頸的地方。針對這種讀多寫少的情況,java還提供了另外一個實現Lock介面的ReentrantReadWriteLock(讀寫鎖)。讀寫鎖允許同一時刻被多個讀線程訪問,但是在寫線程訪問時,所有的讀線程和其他的寫線程都會被阻塞

讀寫鎖的特點:

  1. 寫寫不可併發

  2. 讀寫不可併發

  3. 讀讀可以併發

2.2.1. 重寫讀寫問題

接下來以緩存為例用代碼演示讀寫鎖,重現問題:

class MyCache{
    
    private volatile Map<String, String> cache= new HashMap<>();

    public void put(String key, String value){
        try {
            System.out.println(Thread.currentThread().getName() + " 開始寫入!");
            Thread.sleep(300);
            cache.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 寫入成功!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
        }
    }

    public void get(String key){
        try {
            System.out.println(Thread.currentThread().getName() + " 開始讀出!");
            Thread.sleep(300);
            String value = cache.get(key);
            System.out.println(Thread.currentThread().getName() + " 讀出成功!" + value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
        }
    }
}

public class ReentrantReadWriteLockDemo {

    public static void main(String[] args) {

        MyCache cache = new MyCache();

        for (int i = 1; i <= 5; i++) {
            String num = String.valueOf(i);
            // 開啟5個寫線程
            new Thread(()->{
                cache.put(num, num);
            }, num).start();
        }
        for (int i = 1; i <= 5; i++) {
            String num = String.valueOf(i);
            // 開啟5個讀線程
            new Thread(()->{
                cache.get(num);
            }, num).start();
        }
    }
}

列印結果:多執行幾次,有很大概率不會出現問題

2.2.2. 讀寫鎖的使用

改造MyCache,加入讀寫鎖:

class MyCache{
    private volatile Map<String, String> cache= new HashMap<>();
    // 加入讀寫鎖
    ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    public void put(String key, String value){
        // 加寫鎖
        rwl.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 開始寫入!");
            Thread.sleep(500);
            cache.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 寫入成功!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 釋放寫鎖
            rwl.writeLock().unlock();
        }
    }

    public void get(String key){
        // 加入讀鎖
        rwl.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 開始讀出!");
            Thread.sleep(500);
            String value = cache.get(key);
            System.out.println(Thread.currentThread().getName() + " 讀出成功!" + value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 釋放讀鎖
            rwl.readLock().unlock();
        }
    }
}

2.2.3. 鎖降級

什麼是鎖降級,鎖降級就是從寫鎖降級成為讀鎖。在當前線程擁有寫鎖的情況下,再次獲取到讀鎖,隨後釋放寫鎖的過程就是鎖降級。這裡可以舉個例子:

public void test(){
    rwlock.writeLock().lock();
    System.out.println("獲取到寫鎖。。。。");
    rwlock.readLock().lock();
    System.out.println("獲取到讀鎖----------");
    rwlock.writeLock().unlock();
    System.out.println("釋放寫鎖==============");
    rwlock.readLock().unlock();
    System.out.println("釋放讀鎖++++++++++++++++");
}

列印效果:

2.2.4. 讀寫鎖總結

  1. 支持公平/非公平策略

  2. 支持可重入

    • 同一讀線程在獲取了讀鎖後還可以獲取讀鎖

    • 同一寫線程在獲取了寫鎖之後既可以再次獲取寫鎖又可以獲取讀鎖

  3. 支持鎖降級,不支持鎖升級

  4. 讀寫鎖如果使用不當,很容易產生“饑餓”問題:
    在讀線程非常多,寫線程很少的情況下,很容易導致寫線程“饑餓”,雖然使用“公平”策略可以一定程度上緩解這個問題,但是“公平”策略是以犧牲系統吞吐量為代價的。

  5. Condition條件支持
    寫鎖可以通過newCondition()方法獲取Condition對象。但是讀鎖是沒法獲取Condition對象,讀鎖調用newCondition() 方法會直接拋出UnsupportedOperationException

3. 線程間通信

面試題:兩個線程列印

​ 兩個線程,一個線程列印1-52,另一個列印字母A-Z列印順序為12A34B...5152Z,要求用線程間通信

3.1. 回顧線程通信

先來簡單案例:

​ 兩個線程操作一個初始值為0的變數,實現一個線程對變數增加1,一個線程對變數減少1,交替10輪。

線程間通信模型:

  1. 生產者+消費者

  2. 通知等待喚醒機制

多線程編程:口訣二

  1. 判斷

  2. 幹活

  3. 去通知

代碼實現:

class ShareDataOne {
    private Integer number = 0;

    /**
     *  增加1
     */
    public synchronized void increment() throws InterruptedException {
        // 1. 判斷
        if (number != 0) {
            this.wait();
        }

        // 2. 幹活
        number++;
        System.out.println(Thread.currentThread().getName() + ": " + number);

        // 3. 通知
        this.notifyAll();
    }

    /**
     * 減少1
     */
    public synchronized void decrement() throws InterruptedException {
        // 1. 判斷
        if (number != 1) {
            this.wait();
        }

        // 2. 幹活
        number--;
        System.out.println(Thread.currentThread().getName() + ": " + number);

        // 3. 通知
        this.notifyAll();
    }
}

/**
 * 現在兩個線程,
 * 可以操作初始值為零的一個變數,
 * 實現一個線程對該變數加1,一個線程對該變數減1,
 * 交替,來10輪。
 *
 * 筆記:Java裡面如何進行工程級別的多線程編寫
 * 1 多線程變成模板(套路)-----上
 *    1.1  線程    操作    資源類
 *    1.2  高內聚  低耦合
 * 2 多線程變成模板(套路)-----中
 *    2.1  判斷
 *    2.2  幹活
 *    2.3  通知
 */
public class NotifyWaitDemo {

    public static void main(String[] args) {
        ShareDataOne shareDataOne = new ShareDataOne();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AAA").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BBB").start();

    }
}

部分列印結果:AAA和BBB交互執行,執行結果是1 0 1 0... 一共10輪

AAA: 1
BBB: 0
AAA: 1
BBB: 0
AAA: 1
BBB: 0
AAA: 1
BBB: 0
。。。。

如果換成4個線程會怎樣?

改造mian方法,加入CCC和DDD兩個線程:

    public static void main(String[] args) {
        ShareDataOne shareDataOne = new ShareDataOne();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AAA").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BBB").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "CCC").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "DDD").start();
    }

列印結果,依然會有概率是,10101010...。

但是,多執行幾次,也會出現錯亂的現象:

AAA: 1
BBB: 0
CCC: 1
AAA: 2
CCC: 3
BBB: 2
CCC: 3
DDD: 2
AAA: 3
DDD: 2
CCC: 3
BBB: 2

3.2. 虛假喚醒

換成4個線程會導致錯誤,虛假喚醒

原因:在java多線程判斷時,不能用if,程式出事出在了判斷上面。

註意,消費者被喚醒後是從wait()方法(被阻塞的地方)後面執行,而不是重新從同步塊開頭。

如下圖: 出現-1的情況分析!

解決虛假喚醒:查看API,java.lang.Object的wait方法

中斷和虛假喚醒是可能產生的,所以要用loop迴圈,if只判斷一次,while是只要喚醒就要拉回來再判斷一次。

if換成while

class ShareDataOne {
    private Integer number = 0;

    /**
     *  增加1
     */
    public synchronized void increment() throws InterruptedException {
        // 1. 判斷
        while (number != 0) {
            this.wait();
        }

        // 2. 幹活
        number++;
        System.out.println(Thread.currentThread().getName() + ": " + number);

        // 3. 通知
        this.notifyAll();
    }

    /**
     * 減少1
     */
    public synchronized void decrement() throws InterruptedException {
        // 1. 判斷
        while (number != 1) {
            this.wait();
        }

        // 2. 幹活
        number--;
        System.out.println(Thread.currentThread().getName() + ": " + number);

        // 3. 通知
        this.notifyAll();
    }
}

/**
 * 現在兩個線程,
 * 可以操作初始值為零的一個變數,
 * 實現一個線程對該變數加1,一個線程對該變數減1,
 * 交替,來10輪。
 *
 * 筆記:Java裡面如何進行工程級別的多線程編寫
 * 1 多線程編程模板(套路)-----上
 *    1.1  線程    操作    資源類
 *    1.2  高內聚  低耦合
 * 2 多線程編程模板(套路)-----中
 *    2.1  判斷
 *    2.2  幹活
 *    2.3  去通知
 * 3 多線程編程模板(套路)-----下
 *    防止虛假喚醒(while)
 */
public class NotifyWaitDemo {

    public static void main(String[] args) {
        ShareDataOne shareDataOne = new ShareDataOne();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AAA").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BBB").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "CCC").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    shareDataOne.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "DDD").start();
    }
}

再次測試,完美解決

3.3. 線程通信(Condition)

對標synchronized:

Condition:查看API,java.util.concurrent.locks

並提供了實現案例:

使用Condition實現線程通信,改造之前的代碼(只需要改造ShareDataOne):刪掉increment和decrement方法的synchronized

class ShareDataOne {
    private Integer number = 0;

    final Lock lock = new ReentrantLock(); // 初始化lock鎖
    final Condition condition = lock.newCondition(); // 初始化condition對象

    /**
     *  增加1
     */
    public void increment() throws InterruptedException {
        lock.lock(); // 加鎖
        try {
            // 1. 判斷
            while (number != 0) {
                // this.wait();
                condition.await();
            }

            // 2. 幹活
            number++;
            System.out.println(Thread.currentThread().getName() + ": " + number);

            // 3. 通知
            // this.notifyAll();
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 減少1
     */
    public void decrement() throws InterruptedException {
        lock.lock();
        try {
            // 1. 判斷
            while (number != 1) {
                // this.wait();
                condition.await();
            }

            // 2. 幹活
            number--;
            System.out.println(Thread.currentThread().getName() + ": " + number);

            // 3. 通知
            //this.notifyAll();
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

3.4. 定製化調用通信

案例:

​ 多線程之間按順序調用,實現A->B->C。三個線程啟動,要求如下:

​ AA列印5次,BB列印10次,CC列印15次

​ 接著

​ AA列印5次,BB列印10次,CC列印15次

​ 。。。列印10輪

分析實現方式:

  1. 有一個鎖Lock,3把鑰匙Condition

  2. 有順序通知(切換線程),需要有標識位

  3. 判斷標誌位

  4. 輸出線程名 + 內容

  5. 修改標識符,通知下一個

具體實現:

內容:

class ShareDataTwo {

    private Integer flag = 1; // 線程標識位,通過它區分線程切換
    private final Lock lock = new ReentrantLock();
    private final Condition condition1 = lock.newCondition();
    private final Condition condition2 = lock.newCondition();
    private final Condition condition3 = lock.newCondition();

    public void print5() {
        lock.lock();
        try {
            while (flag != 1) {
                condition1.await();
            }
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + (i + 1));
            }
            flag = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void print10() {
        lock.lock();
        try {
            while (flag != 2) {
                condition2.await();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + (i + 1));
            }
            flag = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void print15() {
        lock.lock();
        try {
            while (flag != 3) {
                condition3.await();
            }
            for (int i = 0; i < 15; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + (i + 1));
            }
            flag = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

/**
 * 多線程之間按順序調用,實現A->B->C
 * 三個線程啟動,要求如下:
 * AA列印5次,BB列印10次,CC列印15次
 * 接著
 * AA列印5次,BB列印10次,CC列印15次
 * ......來10輪
 */
public class ThreadOrderAccess {

    public static void main(String[] args) {

        ShareDataTwo sdt = new ShareDataTwo();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                sdt.print5();
            }
        }, "AAA").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                sdt.print10();
            }
        }, "BBB").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                sdt.print15();
            }
        }, "CCC").start();
    }
}

4. 併發容器類

面試題:

​ 請舉例說明集合類是不安全的。

4.1. 重現線程不安全:List

首先以List作為演示對象,創建多個線程對List介面的常用實現類ArrayList進行add操作。

內容:

public class NotSafeDemo {

    public static void main(String[] args) {

        List<String> list = new ArrayList<>();

        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

測試結果:

出現了線程不安全錯誤

ArrayList在多個線程同時對其進行修改的時候,就會拋出java.util.ConcurrentModificationException異常(併發修改異常),因為ArrayList的add及其他方法都是線程不安全的,有源碼佐證:

解決方案:

​ List介面有很多實現類,除了常用的ArrayList之外,還有Vector和SynchronizedList。

他們都有synchronized關鍵字,說明都是線程安全的。

改用Vector或者synchronizedList試試:

    public static void main(String[] args) {

        //List<String> list = new Vector<>();
        List<String> list = Collections.synchronizedList(new ArrayList<>());

        for (int i = 0; i < 200; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }

即可解決!

Vector和Synchronized的缺點:

​ vector:記憶體消耗比較大,適合一次增量比較大的情況

​ SynchronizedList:迭代器涉及的代碼沒有加上線程同步代碼

Vector:讀取加鎖!
public synchronized ListIterator<E> listIterator() {
  return new ListItr(0);
}
synchronizedList: 讀取數據:讀取數據沒有加鎖!
  public ListIterator<E> listIterator() {
  return list.listIterator(); // Must be manually synched by user
}

4.2. CopyOnWrite容器

什麼是CopyOnWrite容器

CopyOnWrite容器(簡稱COW容器)即寫時複製的容器。通俗的理解是當我們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,複製出一個新的容器,然後新的容器里添加元素,添加完元素之後,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行併發的讀,而不需要加鎖,因為當前容器不會添加任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器

​ 從JDK1.5開始Java併發包里提供了兩個使用CopyOnWrite機制實現的併發容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。

先看看CopyOnWriteArrayList類:發現它的本質就是數組

再來看看CopyOnWriteArrayList的add方法:發現該方法是線程安全的

使用CopyOnWriteArrayList改造main方法:

    public static void main(String[] args) {

        //List<String> list = new Vector<>();
        //List<String> list = Collections.synchronizedList(new ArrayList<>());
        List<String> list = new CopyOnWriteArrayList<>();

        for (int i = 0; i < 200; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }

CopyOnWrite併發容器用於讀多寫少的併發場景。比如:白名單,黑名單。假如我們有一個搜索網站,用戶在這個網站的搜索框中,輸入關鍵字搜索內容,但是某些關鍵字不允許被搜索。這些不能被搜索的關鍵字會被放在一個黑名單當中,黑名單一定周期才會更新一次。

缺點:

  1. 記憶體占用問題。 寫的時候會創建新對象添加到新容器里,而舊容器的對象還在使用,所以有兩份對象記憶體。通過壓縮容器中的元素的方法來減少大對象的記憶體消耗,比如,如果元素全是10進位的數字,可以考慮把它壓縮成36進位或64進位。或者不使用CopyOnWrite容器,而使用其他的併發容器,如ConcurrentHashMap。

  2. 數據一致性問題。 CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。所以如果你希望寫入的的數據,馬上能讀到,請不要使用CopyOnWrite容器。

4.3. 擴展類比:Set和Map

HashSet和HashMap也都是線程不安全的,類似於ArrayList,也可以通過代碼證明。

    private static void notSafeMap() {
        Map<String, String> map = new HashMap<>();

        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                map.put(String.valueOf(Thread.currentThread().getName()), UUID.randomUUID().toString().substring(0, 8));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }

    private static void notSafeSet() {
        Set<String> set = new HashSet<>();

        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
    }

都會報:ConcurrentModificationException異常信息。

Collections提供了方法synchronizedList保證list是同步線程安全的,Set和Map呢?

JUC提供的CopyOnWrite容器實現類有:CopyOnWriteArrayList和CopyOnWriteArraySet。

有沒有Map的實現:

最終實現:

public class NotSafeDemo {

    public static void main(String[] args) {
        notSafeList();
        notSafeSet();
        notSafeMap();
    }

    private static void notSafeMap() {
        //Map<String, String> map = new HashMap<>();
        //Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
        Map<String, String> map = new ConcurrentHashMap<>();

        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                map.put(String.valueOf(Thread.currentThread().getName()), UUID.randomUUID().toString().substring(0, 8));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }

    private static void notSafeSet() {
        //Set<String> set = new HashSet<>();
        Set<String> set = new CopyOnWriteArraySet<>();

        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
    }

    private static void notSafeList() {
        //List<String> list = new Vector<>();
        //List<String> list = Collections.synchronizedList(new ArrayList<>());
        List<String> list = new CopyOnWriteArrayList<>();

        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

擴展:HashSet底層數據結構是什麼?HashMap ?

​ 但HashSet的add是放一個值,而HashMap是放K、V鍵值對

5. JUC強大的輔助類

JUC的多線程輔助類非常多,這裡我們介紹三個:

  1. CountDownLatch(倒計數器)

  2. CyclicBarrier(迴圈柵欄)

  3. Semaphore(信號量)

5.1. CountDownLatch

CountDownLatch是一個非常實用的多線程式控制制工具類,應用非常廣泛。

例如:在手機上安裝一個應用程式,假如需要5個子進程檢查服務授權,那麼主進程會維護一個計數器,初始計數就是5。用戶每同意一個授權該計數器減1,當計數減為0時,主進程才啟動,否則就只有阻塞等待了。

CountDownLatch中count down是倒數的意思,latch則是門閂的含義。整體含義可以理解為倒數的門栓,似乎有一點“三二一,芝麻開門”的感覺。CountDownLatch的作用也是如此。

常用的就下麵幾個方法:

new CountDownLatch(int count) //實例化一個倒計數器,count指定初始計數
countDown() // 每調用一次,計數減一
await() //等待,當計數減到0時,阻塞線程(可以是一個,也可以是多個)並行執行

案例:6個同學陸續離開教室後值班同學才可以關門。

public class CountDownLatchDemo {

    /**
     * main方法也是一個進程,在這裡是主進程,即上鎖的同學
     *
     * @param args
     */
    public static void main(String[] args) throws InterruptedException {

        // 初始化計數器,初始計數為6
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 0; i < 6; i++) {
            new Thread(()->{
                try {
                    // 每個同學墨跡幾秒鐘
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5));
                    System.out.println(Thread.currentThread().getName() + " 同學出門了");
                    // 調用countDown()計算減1
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }

        // 調用計算器的await方法,等待6位同學都出來
        countDownLatch.await();

        System.out.println("值班同學鎖門了");
    }
}

列印結果:

同學3 出來了
同學1 出來了
同學0 出來了
同學2 出來了
同學5 出來了
同學4 出來了
值班同學鎖門了

面試:CountDownLatch 與 join 方法的區別

​ 調用一個子線程的 join()方法後,該線程會一直被阻塞直到該線程運行完畢。而 CountDownLatch 則使用計數器允許子線程運行完畢或者運行中時候遞減計數,也就是 CountDownLatch 可以在子線程運行任何時候讓 await 方法返回而不一定必須等到線程結束;另外使用線程池來管理線程時候一般都是直接添加 Runnable 到線程池這時候就沒有辦法在調用線程的 join 方法了,countDownLatch 相比 Join 方法讓我們對線程同步有更靈活的控制。

練習:秦滅六國,一統華夏。(模仿課堂案例,練習枚舉類的使用)

5.2. CyclicBarrier

從字面上的意思可以知道,這個類的中文意思是“迴圈柵欄”。大概的意思就是一個可迴圈利用的屏障。該命令只在每個屏障點運行一次。若在所有參與線程之前更新共用狀態,此屏障操作很有用

常用方法:

  1. CyclicBarrier(int parties, Runnable barrierAction) 創建一個CyclicBarrier實例,parties指定參與相互等待的線程數,barrierAction一個可選的Runnable命令,該命令只在每個屏障點運行一次,可以在執行後續業務之前共用狀態。該操作由最後一個進入屏障點的線程執行。

  2. CyclicBarrier(int parties) 創建一個CyclicBarrier實例,parties指定參與相互等待的線程數。

  3. await() 該方法被調用時表示當前線程已經到達屏障點,當前線程阻塞進入休眠狀態,直到所有線程都到達屏障點,當前線程才會被喚醒。

案例:組隊打boss過關卡游戲。

public class CyclicBarrierDemo {

    public static void main(String[] args) {

        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {

            System.out.println(Thread.currentThread().getName() + " 過關了");
        });

        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName() + " 開始第一關");
                    TimeUnit.SECONDS.sleep(new Random().nextInt(4));
                    System.out.println(Thread.

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • rust FFI 是rust與其他語言互調的橋梁,通過FFI rust 可以有效繼承 C 語言的歷史資產。本期通過幾個例子來聊聊rust與C 語言交互的具體步驟 ...
  • # 使用PowerDesigner導入表+PowerDesigner畫ER圖+PowerDesigner設置外鍵 ps: ①ER圖:就是PD中的 ==Physical Diagram== ## 一、導入表,並設置備註為PD中的顯示名稱 - 參考:https://blog.csdn.net/weixi ...
  • 本文將詳細探討如何在Python中連接全種類資料庫以及實現相應的CRUD(創建,讀取,更新,刪除)操作。我們將逐一解析連接MySQL,SQL Server,Oracle,PostgreSQL,MongoDB,SQLite,DB2,Redis,Cassandra,Microsoft Access,El ...
  • 本文將深入探討如何利用Spring Loaded熱更新技術提高開發效率,減少編譯和重啟時間。分析Spring Loaded的熱更新原理,以及實際應用過程中所需的操作和註意事項。 ...
  • Java的特點是百花齊放,不像c#或者go只有一家主導。oracle jdk收費了,沒關係,不是只有它可用。java還有很多免費的主流的jdk發行版本,記錄下來備忘。 OpenJDK - 官方網站 - 源代碼 - 文檔 - 下載地址 OpenJDK是其他所有jdk發行版的基礎,可以用於開發和測試,但 ...
  • 一、問題描述 近日在工作中遇見了一個bug,後端程式頻頻報錯 No bean named 'XXXXX' available 。對比同類程式文件,沒有發現有任何特殊之處。在網上搜索方法基本上就是掃描包配置、註解問題、路徑問題等,皆不能解決我的問題。 排查問題是發現出現問題的類命名不符合駝峰規範,按照 ...
  • ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f4928251cca8446891aafb4d014abf39~tplv-k3u1fbpfcp-zoom-1.image) 博主在瀏覽 medium 社區時,發現了一篇點贊量 1.5k 的文 ...
  • 廢話環節:看過上期文章的小伙伴現在可能還是一頭霧水,怎麼就完成了核心內容,界面呢?哎我說別急讓我先急,博主這不夜以繼日地肝出了界面部分嘛。還是老規矩,不會把所有地方都照顧到,只挑一些有代表性的內容介紹,您各位多擔待🙏。另外博主的JavaFX是跟著B站視頻速成的,指路👉:https://www.b ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...