啃碎併發(六):Java執行緒同步與實現

猿碼道發表於2018-07-11

0 前言

為何要使用Java執行緒同步? Java允許多執行緒併發控制,當多個執行緒同時操作一個可共享的資源變數時,將會導致資料不準確,相互之間產生衝突,因此加入同步鎖以避免在該執行緒沒有完成操作之前,被其他執行緒的呼叫,從而保證了該變數的唯一性和準確性。

但其併發程式設計的根本,就是使執行緒間進行正確的通訊。其中兩個比較重要的關鍵點,如下:

  1. 執行緒通訊:重點關注執行緒同步的幾種方式;
  2. 正確通訊:重點關注是否有執行緒安全問題;

Java中提供了很多執行緒同步操作,比如:synchronized關鍵字、wait/notifyAll、ReentrantLock、Condition、一些併發包下的工具類、Semaphore,ThreadLocal、AbstractQueuedSynchronizer等。本文主要說明一下這幾種同步方式的使用及優劣。

1 ReentrantLock可重入鎖

自JDK5開始,新增了Lock介面以及它的一個實現類ReentrantLock。ReentrantLock可重入鎖是J.U.C包內建的一個鎖物件,可以用來實現同步,基本使用方法如下:

public class ReentrantLockTest {

    private ReentrantLock lock = new ReentrantLock();

    public void execute() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " do something synchronize");
            try {
                Thread.sleep(5000l);
            } catch (InterruptedException e) {
                System.err.println(Thread.currentThread().getName() + " interrupted");
                Thread.currentThread().interrupt();
            }
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockTest reentrantLockTest = new ReentrantLockTest();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                reentrantLockTest.execute();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                reentrantLockTest.execute();
            }
        });
        thread1.start();
        thread2.start();
    }
}
複製程式碼

上面例子表示 同一時間段只能有1個執行緒執行execute方法,輸出如下:

Thread-0 do something synchronize
// 隔了5秒鐘 輸入下面
Thread-1 do something synchronize
複製程式碼

可重入鎖中可重入表示的意義在於 對於同一個執行緒,可以繼續呼叫加鎖的方法,而不會被掛起。可重入鎖內部維護一個計數器,對於同一個執行緒呼叫lock方法,計數器+1,呼叫unlock方法,計數器-1。

舉個例子再次說明一下可重入的意思:在一個加鎖方法execute中呼叫另外一個加鎖方法anotherLock並不會被掛起,可以直接呼叫(呼叫execute方法時計數器+1,然後內部又呼叫了anotherLock方法,計數器+1,變成了2):

public void execute() {
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + " do something synchronize");
        try {
            anotherLock();
            Thread.sleep(5000l);
        } catch (InterruptedException e) {
            System.err.println(Thread.currentThread().getName() + " interrupted");
            Thread.currentThread().interrupt();
        }
    } finally {
        lock.unlock();
    }
}

public void anotherLock() {
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + " invoke anotherLock");
    } finally {
        lock.unlock();
    }
}
複製程式碼

輸出:

Thread-0 do something synchronize
Thread-0 invoke anotherLock
// 隔了5秒鐘 輸入下面
Thread-1 do something synchronize
Thread-1 invoke anotherLock
複製程式碼

2 synchronized

synchronized跟ReentrantLock一樣,也支援可重入鎖。但是它是 一個關鍵字,是一種語法級別的同步方式,稱為內建鎖

public class SynchronizedKeyWordTest {

    public synchronized void execute() {
            System.out.println(Thread.currentThread().getName() + " do something synchronize");
        try {
            anotherLock();
            Thread.sleep(5000l);
        } catch (InterruptedException e) {
            System.err.println(Thread.currentThread().getName() + " interrupted");
            Thread.currentThread().interrupt();
        }
    }

    public synchronized void anotherLock() {
        System.out.println(Thread.currentThread().getName() + " invoke anotherLock");
    }

    public static void main(String[] args) {
        SynchronizedKeyWordTest reentrantLockTest = new SynchronizedKeyWordTest();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                reentrantLockTest.execute();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                reentrantLockTest.execute();
            }
        });
        thread1.start();
        thread2.start();
    }

}
複製程式碼

輸出結果跟ReentrantLock一樣,這個例子說明內建鎖可以作用在方法上。synchronized關鍵字也可以修飾靜態方法,此時如果呼叫該靜態方法,將會鎖住整個類

同步是一種高開銷的操作,因此應該儘量減少同步的內容。通常沒有必要同步整個方法,使用synchronized程式碼塊同步關鍵程式碼即可

synchronized跟ReentrantLock相比,有幾點侷限性

  1. 加鎖的時候不能設定超時。ReentrantLock有提供tryLock方法,可以設定超時時間,如果超過了這個時間並且沒有獲取到鎖,就會放棄,而synchronized卻沒有這種功能;
  2. ReentrantLock可以使用多個Condition,而synchronized卻只能有1個
  3. 不能中斷一個試圖獲得鎖的執行緒
  4. ReentrantLock可以選擇公平鎖和非公平鎖;
  5. ReentrantLock可以獲得正在等待執行緒的個數,計數器等;

所以,Lock的操作與synchronized相比,靈活性更高,而且Lock提供多種方式獲取鎖,有Lock、ReadWriteLock介面,以及實現這兩個介面的ReentrantLock類、ReentrantReadWriteLock類。

關於Lock物件和synchronized關鍵字選擇的考量

  1. 最好兩個都不用,使用一種java.util.concurrent包提供的機制,能夠幫助使用者處理所有與鎖相關的程式碼。
  2. 如果synchronized關鍵字能滿足使用者的需求,就用synchronized,因為它能簡化程式碼。
  3. 如果需要更高階的功能,就用ReentrantLock類,此時要注意及時釋放鎖,否則會出現死鎖,通常在finally程式碼釋放鎖。

在效能考量上來說,如果競爭資源不激烈,兩者的效能是差不多的,而當競爭資源非常激烈時(即有大量執行緒同時競爭),此時Lock的效能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇。

3 Condition條件物件

Condition條件物件的意義在於 對於一個已經獲取Lock鎖的執行緒,如果還需要等待其他條件才能繼續執行的情況下,才會使用Condition條件物件

Condition可以替代傳統的執行緒間通訊,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll()。

為什麼方法名不直接叫wait()/notify()/nofityAll()?因為Object的這幾個方法是final的,不可重寫!

public class ConditionTest {

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " run");
                    System.out.println(Thread.currentThread().getName() + " wait for condition");
                    try {
                        condition.await();
                        System.out.println(Thread.currentThread().getName() + " continue");
                    } catch (InterruptedException e) {
                        System.err.println(Thread.currentThread().getName() + " interrupted");
                        Thread.currentThread().interrupt();
                    }
                } finally {
                    lock.unlock();
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " run");
                    System.out.println(Thread.currentThread().getName() + " sleep 5 secs");
                    try {
                        Thread.sleep(5000l);
                    } catch (InterruptedException e) {
                        System.err.println(Thread.currentThread().getName() + " interrupted");
                        Thread.currentThread().interrupt();
                    }
                    condition.signalAll();
                } finally {
                    lock.unlock();
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}
複製程式碼

這個例子中thread1執行到condition.await()時,當前執行緒會被掛起,直到thread2呼叫了condition.signalAll()方法之後,thread1才會重新被啟用執行

這裡需要注意的是thread1呼叫Condition的await方法之後,thread1執行緒釋放鎖,然後馬上加入到Condition的等待佇列,由於thread1釋放了鎖,thread2獲得鎖並執行,thread2執行signalAll方法之後,Condition中的等待佇列thread1被取出並加入到AQS中,接下來thread2執行完畢之後釋放鎖,由於thread1已經在AQS的等待佇列中,所以thread1被喚醒,繼續執行。

傳統執行緒的通訊方式,Condition都可以實現。Condition的強大之處在於它可以為多個執行緒間建立不同的Condition

注意,Condition是被繫結到Lock上的,要建立一個Lock的Condition必須用newCondition()方法。

4 wait&notify/notifyAll方式

Java執行緒的狀態轉換圖與相關方法,如下:

執行緒狀態轉換圖

在圖中,紅框標識的部分方法,可以認為已過時,不再使用。上圖中的方法能夠參與到執行緒同步中的方法,如下:

  1. wait、notify、notifyAll方法:執行緒中通訊可以使用的方法。執行緒中呼叫了wait方法,則進入阻塞狀態,只有等另一個執行緒呼叫與wait同一個物件的notify方法。這裡有個特殊的地方,呼叫wait或者notify,前提是需要獲取鎖,也就是說,需要在同步塊中做以上操作

    wait/notifyAll方式跟ReentrantLock/Condition方式的原理是一樣的。

    Java中每個物件都擁有一個內建鎖,在內建鎖中呼叫wait,notify方法相當於呼叫鎖的Condition條件物件的await和signalAll方法

    public class WaitNotifyAllTest {
    
        public synchronized void doWait() {
            System.out.println(Thread.currentThread().getName() + " run");
            System.out.println(Thread.currentThread().getName() + " wait for condition");
            try {
                this.wait();
                System.out.println(Thread.currentThread().getName() + " continue");
            } catch (InterruptedException e) {
                System.err.println(Thread.currentThread().getName() + " interrupted");
                Thread.currentThread().interrupt();
            }
        }
    
        public synchronized void doNotify() {
            try {
                System.out.println(Thread.currentThread().getName() + " run");
                System.out.println(Thread.currentThread().getName() + " sleep 5 secs");
                Thread.sleep(5000l);
                this.notifyAll();
            } catch (InterruptedException e) {
                System.err.println(Thread.currentThread().getName() + " interrupted");
                Thread.currentThread().interrupt();
            }
        }
    
        public static void main(String[] args) {
            WaitNotifyAllTest waitNotifyAllTest = new WaitNotifyAllTest();
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    waitNotifyAllTest.doWait();
                }
            });
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    waitNotifyAllTest.doNotify();
                }
            });
            thread1.start();
            thread2.start();
        }
    }
    複製程式碼

    這裡需要注意的是 呼叫wait/notifyAll方法的時候一定要獲得當前執行緒的鎖,否則會發生IllegalMonitorStateException異常。

  2. join方法:該方法主要作用是在該執行緒中的run方法結束後,才往下執行。

    package com.thread.simple;
     
    public class ThreadJoin {
        public static void main(String[] args) {
            Thread thread= new Thread(new Runnable() {
                  @Override
                  public void run() {
                       System.err.println("執行緒"+Thread.currentThread().getId()+" 列印資訊");
                  }
            });
            thread.start();
     	
            try {
                thread.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.err.println("主執行緒列印資訊");	
        }
    }
    複製程式碼
  3. yield方法:執行緒本身的排程方法,使用時執行緒可以在run方法執行完畢時,呼叫該方法,告知執行緒已可以出讓CPU資源。

    public class Test1 {  
        public static void main(String[] args) throws InterruptedException {  
            new MyThread("低階", 1).start();  
            new MyThread("中級", 5).start();  
            new MyThread("高階", 10).start();  
        }  
    }  
    
    class MyThread extends Thread {  
        public MyThread(String name, int pro) {  
            super(name);// 設定執行緒的名稱  
            this.setPriority(pro);// 設定優先順序  
        }  
    
        @Override  
        public void run() {  
            for (int i = 0; i < 30; i++) {  
                System.out.println(this.getName() + "執行緒第" + i + "次執行!");  
                if (i % 5 == 0)  
                    Thread.yield();  
            }  
        }  
    }  
    複製程式碼
  4. sleep方法:通過sleep(millis)使執行緒進入休眠一段時間,該方法在指定的時間內無法被喚醒,同時也不會釋放物件鎖

    /**
     * 可以明顯看到列印的數字在時間上有些許的間隔
     */
    public class Test1 {  
        public static void main(String[] args) throws InterruptedException {  
            for(int i=0;i<100;i++){  
                System.out.println("main"+i);  
                Thread.sleep(100);  
            }  
        }  
    } 
    複製程式碼

    sleep方法告訴作業系統 至少在指定時間內不需為執行緒排程器為該執行緒分配執行時間片,並不釋放鎖(如果當前已經持有鎖)。實際上,呼叫sleep方法時並不要求持有任何鎖

    所以,sleep方法並不需要持有任何形式的鎖,也就不需要包裹在synchronized中

5 ThreadLocal

ThreadLocal是一種把變數放到執行緒本地的方式來實現執行緒同步的。比如:SimpleDateFormat不是一個執行緒安全的類,可以使用ThreadLocal實現同步,如下:

public class ThreadLocalTest {

    private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Date date = new Date();
                System.out.println(dateFormatThreadLocal.get().format(date));
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Date date = new Date();
                System.out.println(dateFormatThreadLocal.get().format(date));
            }
        });
        thread1.start();
        thread2.start();
    }
}
複製程式碼

為何SimpleDateFormat不是執行緒安全的類?具體請參考:

  1. https://blog.csdn.net/zdp072/article/details/41044059
  2. https://blog.csdn.net/zq602316498/article/details/40263083

ThreadLocal與同步機制的對比選擇

  1. ThreadLocal與同步機制都是 為了解決多執行緒中相同變數的訪問衝突問題
  2. 前者採用以 "空間換時間" 的方法,後者採用以 "時間換空間" 的方式。

6 volatile修飾變數

volatile關鍵字為域變數的訪問提供了一種免鎖機制,使用volatile修飾域相當於告訴虛擬機器該域可能會被其他執行緒更新,因此每次使用該域就要重新計算,而不是使用暫存器中的值,volatile不會提供任何原子操作,它也不能用來修飾final型別的變數

//只給出要修改的程式碼,其餘程式碼與上同
public class Bank {
    //需要同步的變數加上volatile
    private volatile int account = 100;
    public int getAccount() {
        return account;
    }
    //這裡不再需要synchronized 
    public void save(int money) {
        account += money;
    }
}
複製程式碼

多執行緒中的非同步問題主要出現在對域的讀寫上,如果讓域自身避免這個問題,則就不需要修改操作該域的方法。用final域,有鎖保護的域和volatile域可以避免非同步的問題

7 Semaphore訊號量

Semaphore訊號量被用於控制特定資源在同一個時間被訪問的個數。類似連線池的概念,保證資源可以被合理的使用。可以使用構造器初始化資源個數:

public class SemaphoreTest {

    private static Semaphore semaphore = new Semaphore(2);

    public static void main(String[] args) {
        for(int i = 0; i < 5; i ++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() + " " + new Date());
                        Thread.sleep(5000l);
                        semaphore.release();
                    } catch (InterruptedException e) {
                        System.err.println(Thread.currentThread().getName() + " interrupted");
                    }
                }
            }).start();
        }
    }
}
複製程式碼

輸出:

Thread-1 Mon Apr 18 18:03:46 CST 2016
Thread-0 Mon Apr 18 18:03:46 CST 2016
Thread-3 Mon Apr 18 18:03:51 CST 2016
Thread-2 Mon Apr 18 18:03:51 CST 2016
Thread-4 Mon Apr 18 18:03:56 CST 2016
複製程式碼

8 併發包下的工具類

8.1 CountDownLatch

CountDownLatch是一個計數器,它的構造方法中需要設定一個數值,用來設定計數的次數。每次呼叫countDown()方法之後,這個計數器都會減去1,CountDownLatch會一直阻塞著呼叫await()方法的執行緒,直到計數器的值變為0

public class CountDownLatchTest {

    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for(int i = 0; i < 5; i ++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " " + new Date() + " run");
                    try {
                        Thread.sleep(5000l);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("all thread over");
    }
}
複製程式碼

輸出:

Thread-2 Mon Apr 18 18:18:30 CST 2016 run
Thread-3 Mon Apr 18 18:18:30 CST 2016 run
Thread-4 Mon Apr 18 18:18:30 CST 2016 run
Thread-0 Mon Apr 18 18:18:30 CST 2016 run
Thread-1 Mon Apr 18 18:18:30 CST 2016 run
all thread over
複製程式碼

8.2 CyclicBarrier

CyclicBarrier阻塞呼叫的執行緒,直到條件滿足時,阻塞的執行緒同時被開啟。

呼叫await()方法的時候,這個執行緒就會被阻塞,當呼叫await()的執行緒數量到達屏障數的時候,主執行緒就會取消所有被阻塞執行緒的狀態

在CyclicBarrier的構造方法中,還可以設定一個barrierAction。在所有的屏障都到達之後,會啟動一個執行緒來執行這裡面的程式碼

public class CyclicBarrierTest {

    public static void main(String[] args) {
        Random random = new Random();
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
        for(int i = 0; i < 5; i ++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int secs = random.nextInt(5);
                    System.out.println(Thread.currentThread().getName() + " " + new Date() + " run, sleep " + secs + " secs");
                    try {
                        Thread.sleep(secs * 1000);
                        cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " " + new Date() + " runs over");
                }
            }).start();
        }
    }
}
複製程式碼

相比CountDownLatch,CyclicBarrier是可以被迴圈使用的,而且遇到執行緒中斷等情況時,還可以利用reset()方法,重置計數器,從這些方面來說,CyclicBarrier會比CountDownLatch更加靈活一些

9 使用原子變數實現執行緒同步

有時需要使用執行緒同步的根本原因在於 對普通變數的操作不是原子的。那麼什麼是原子操作呢?

原子操作就是指將讀取變數值、修改變數值、儲存變數值看成一個整體來操作 即-這幾種行為要麼同時完成,要麼都不完成

在java.util.concurrent.atomic包中提供了建立原子型別變數的工具類,使用該類可以簡化執行緒同步。比如:其中AtomicInteger以原子方式更新int的值:

class Bank {
    private AtomicInteger account = new AtomicInteger(100);

    public AtomicInteger getAccount() {
        return account;
    }

    public void save(int money) {
        account.addAndGet(money);
    }
}
複製程式碼

10 AbstractQueuedSynchronizer

AQS是很多同步工具類的基礎,比如:ReentrantLock裡的公平鎖和非公平鎖,Semaphore裡的公平鎖和非公平鎖,CountDownLatch裡的鎖等他們的底層都是使用AbstractQueuedSynchronizer完成的。

基於AbstractQueuedSynchronizer自定義實現一個獨佔鎖

public class MySynchronizer extends AbstractQueuedSynchronizer {

    @Override
    protected boolean tryAcquire(int arg) {
        if(compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    @Override
    protected boolean tryRelease(int arg) {
        setState(0);
        setExclusiveOwnerThread(null);
        return true;
    }

    public void lock() {
        acquire(1);
    }

    public void unlock() {
        release(1);
    }

    public static void main(String[] args) {
        MySynchronizer mySynchronizer = new MySynchronizer();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                mySynchronizer.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " run");
                    System.out.println(Thread.currentThread().getName() + " will sleep 5 secs");
                    try {
                        Thread.sleep(5000l);
                        System.out.println(Thread.currentThread().getName() + " continue");
                    } catch (InterruptedException e) {
                        System.err.println(Thread.currentThread().getName() + " interrupted");
                        Thread.currentThread().interrupt();
                    }
                } finally {
                    mySynchronizer.unlock();
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                mySynchronizer.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " run");
                } finally {
                    mySynchronizer.unlock();
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}
複製程式碼

11 使用阻塞佇列實現執行緒同步

前面幾種同步方式都是基於底層實現的執行緒同步,但是在實際開發當中,應當儘量遠離底層結構。本節主要是使用LinkedBlockingQueue來實現執行緒的同步。

LinkedBlockingQueue是一個基於連結串列的佇列,先進先出的順序(FIFO),範圍任意的blocking queue。

package com.xhj.thread;

import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * 用阻塞佇列實現執行緒同步 LinkedBlockingQueue的使用
 */
public class BlockingSynchronizedThread {
    /**
     * 定義一個阻塞佇列用來儲存生產出來的商品
     */
    private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
    /**
     * 定義生產商品個數
     */
    private static final int size = 10;
    /**
     * 定義啟動執行緒的標誌,為0時,啟動生產商品的執行緒;為1時,啟動消費商品的執行緒
     */
    private int flag = 0;

    private class LinkBlockThread implements Runnable {
        @Override
        public void run() {
            int new_flag = flag++;
            System.out.println("啟動執行緒 " + new_flag);
            if (new_flag == 0) {
                for (int i = 0; i < size; i++) {
                    int b = new Random().nextInt(255);
                    System.out.println("生產商品:" + b + "號");
                    try {
                        queue.put(b);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println("倉庫中還有商品:" + queue.size() + "個");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            } else {
                for (int i = 0; i < size / 2; i++) {
                    try {
                        int n = queue.take();
                        System.out.println("消費者買去了" + n + "號商品");
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println("倉庫中還有商品:" + queue.size() + "個");
                    try {
                        Thread.sleep(100);
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        BlockingSynchronizedThread bst = new BlockingSynchronizedThread();
        LinkBlockThread lbt = bst.new LinkBlockThread();
        Thread thread1 = new Thread(lbt);
        Thread thread2 = new Thread(lbt);
        thread1.start();
        thread2.start();
    }
}
複製程式碼

相關文章