多線程: synchronized 和 Lock 入門

synchronized 鎖的三類對象

  • Class鎖, 常見有 synchronized(this.getClass()), 以及靜態方法加鎖
  • 對象鎖, 常見有 synchronized(this), 以及實例方法加鎖
  • 屬性鎖

八鎖現象

兩個線程持有同一把鎖, 後搶到鎖的線程需要等待鎖的釋放:

public class Test1 {
    public static void main(String[] args){
        Phone phone = new Phone();
        new Thread(()->{
            phone.sendSms();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            phone.call();
        }, "B").start();
    }
}

class Phone {
    private Integer num =0;

// 鎖當前的 Phone對象實例
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信");
    }

    public void call(){
    	// this 也表示當前對象實例, 所以兩個線程共用鎖
        synchronized (this) {
            System.out.println("打電話");
        }
    }
}

一個線程持有鎖, 另一個線程不持有, 無需等待鎖釋放

上例中, 去掉 call() 方法中的 synchronized 塊, 則 “打電話” 先於 “發短信”

兩個線程持有不同種類的鎖, 無需彼此等待鎖釋放

public class Test1 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendSms();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            phone.call();
        }, "B").start();
    }
}

class Phone {
    private Integer num = 0;
	
	// 鎖對象
    public synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信");
    }

    public void call() {
    // 鎖屬性
        synchronized (num) {
            System.out.println("打電話");
        }
    }
}

同一種鎖, 鎖不同的對象, 無需彼此等待

public class Test1 {
    public static void main(String[] args) {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            phone1.sendSms();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            phone2.call();
        }, "B").start();
    }
}

class Phone {
    private Integer num = 0;

// 雖然都是鎖 Phone的實例, 但是一個是 phone1, 一個是phone2, 不是同一把鎖
    public synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信");
    }

    public synchronized void call() {
        System.out.println("打電話");
    }
}

static 修飾的方法上加鎖, 等於鎖類的Class對象

public class Test1 {
    public static void main(String[] args) {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            phone1.sendSms();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            phone2.call();
        }, "B").start();
    }
}

class Phone {
    private Integer num = 0;

    public static synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信");
    }

    public static synchronized void call() {
        System.out.println("打電話");
    }
}

上例中, 兩個線程鎖的都是 Phone.Class 對象, 共用同一把鎖, 所以先 “發短信”, 再"打電話"

下面例子, 線程仍共用一把鎖, 能證明鎖 static 方法就是鎖 Class對象

public class Test1 {
    public static void main(String[] args) {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            phone1.sendSms();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            phone2.call();
        }, "B").start();
    }
}

class Phone {
    private Integer num = 0;

    public static synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信");
    }

    public void call() {
        synchronized (Phone.class) {
            System.out.println("打電話");
        }
    }
}

線程的狀態

在這裏插入圖片描述
wait() 和 sleep() 的區別

wait() 使得線程釋放鎖, 進入等待隊列, 只有被 notify() 或 notifyAll () 喚醒, 進入鎖池; sleep() 的線程不會釋放鎖

wait() 和 notify() 必須在同步塊內出現, 而 sleep() 不一定

生產者消費者模式

核心: 利用wait() 和 notify() 控制線程之間通信, 比如, 如何控制兩個線程, 使他們交替工作?

/**
 * 線程操作資源類
 */
public class Data {
    private int num = 0;

    public synchronized void increment() throws InterruptedException {
        if (num > 0) {
            wait();
        }
        System.out.println(Thread.currentThread().getName() + " =>" + (++num));
        notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        if (num <= 0) {
            wait();
        }
        System.out.println(Thread.currentThread().getName() + " =>" + (--num));
        notifyAll();
    }
}

/**
 * 生產者- 消費者模式 線程通信
 */
public class Test {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for(int i=1;i<=100;i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
            for(int i=1;i<=100;i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();
    }
}

可以看到, num>0時, A等待B將num變爲0再喚醒自己; num<=0 時, B等待A將num變爲1, 再喚醒自己; 兩個線程交替輸出0和1

虛假喚醒問題

如果, 在上例的Test 類中, 啓動如下四個線程:

/**
 * 生產者- 消費者模式 線程通信
 */
public class Test {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for(int i=1;i<=100;i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
            for(int i=1;i<=100;i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

        new Thread(() -> {
            for(int i=1;i<=100;i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();

        new Thread(() -> {
            for(int i=1;i<=100;i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}

在這裏插入圖片描述
<=0 則BD線程 等待, >0則AC 線程等待, 那 B =>-1從何而來呢? 這就涉及 虛假喚醒問題

某一時刻, 當 num==0 時, D搶到鎖, 進入等待隊列, B再搶到鎖, 進入等待隊列, 四個線程狀態如下:

等待隊列: B,D
鎖池: A,C

假設 A獲得鎖, 執行 ++num, 並用 notifyAll() 喚醒所有等待隊列中的線程, 則 BD都進入鎖池, 如果下一時刻, D搶到鎖, 執行 --num, num爲0

下一時刻, 如果B搶到鎖, 繼續執行B線程, 由於B之前停在 wait(), 此時無需做 if判斷, 可以直接 --num, 造成了 num爲負數

 public synchronized void decrement() throws InterruptedException {
      if (num <= 0) {
          wait();		// 線程被喚醒時, 從這裏往後走
      }
      System.out.println(Thread.currentThread().getName() + " =>" + (--num));
      notifyAll();
  }

解決虛假喚醒的方法

虛假喚醒的原因: wait() 在 if 代碼塊中, 喚醒時不再經過 if 的判斷

解決方法: if 替換爲 while; 或者加 else{}

Lock 鎖的基本 API

我們用 Lock 鎖的寫法, 替換上面例子的 Data 資源類

/**
 * 多線程操作資源類
 */
public class Data {
    private int num =0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();  // 將 Condition 和 Lock綁定

    public void increment(){
        lock.lock();		// 手動鎖代碼
        try {
            while (num > 0) {
                condition.await();
            }
            System.out.println(Thread.currentThread().getName() + " =>" + (++num));
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();		// 手動釋放鎖
        }
    }

    public void decrement(){
        lock.lock();
        try {
            while (num <= 0) {
                condition.await();
            }
            System.out.println(Thread.currentThread().getName() + " =>" + (--num));
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

使用 Condition 監控類, 精確喚醒線程

Lock 鎖和 synchronized 鎖的最大區別, 就在於 Lock 結合 Condition 可以精確喚醒線程

/**
 * A,B,C,A,B,C 順序執行
 */
public class Data {
    private volatile int num = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public int getNum() {
        return num;
    }

    public void printA() {
        lock.lock();
        try {
            if (num < 100 && num % 3 != 0) {
                condition1.await();
            } else{
                System.out.println(Thread.currentThread().getName() + " =>" + (++num));
                condition2.signal();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB() {
        lock.lock();
        try {
            if (num < 100 && num % 3 != 1) {
                condition2.await();
            } else{
                System.out.println(Thread.currentThread().getName() + " =>" + (++num));
                condition3.signal();
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC() {
        lock.lock();
        try {
            if (num < 100 && num % 3 != 2) {
                condition3.await();
            } else{
                System.out.println(Thread.currentThread().getName() + " =>" + (++num));
                condition1.signal();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            while (data.getNum()<100) {
                data.printA();
            }
        }, "A").start();

        new Thread(() -> {
            while (data.getNum()<100) {
                data.printB();
            }
        }, "B").start();

        new Thread(() -> {
            while (data.getNum()<100) {
                data.printC();
            }
        }, "C").start();
    }
}
發佈了18 篇原創文章 · 獲贊 3 · 訪問量 1045
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章