1. 程式人生 > 實用技巧 >JUC-Lock鎖及常用工具類

JUC-Lock鎖及常用工具類

一、Lock簡介

在jdk1.5之前實現同步訪問一般都是通過synchronized。在Java 5之後,java.util.concurrent.locks包下提供了Lock介面來實現同步訪問。鎖實現提供了比使用同步方法和語句可以獲得的更廣泛的鎖操作。它們允許更靈活的結構,可能具有非常不同的屬性,並且可能支援多個關聯的條件物件。

二、Lock與synchronized的區別

  • 首先synchronized是java內建關鍵字,在jvm層面,Lock是個java類;

  • synchronized會自動釋放鎖,Lock需在finally中手工釋放鎖(unlock()方法釋放鎖),否則容易造成執行緒死鎖;

  • 用synchronized關鍵字的兩個執行緒1和執行緒2,如果當前執行緒1獲得鎖,執行緒2執行緒等待。如果執行緒1阻塞,執行緒2則會一直等待下去,而Lock鎖就不一定會等待下去,如果嘗試獲取不到鎖,執行緒可以不用一直等待就結束了;

  • synchronized的鎖可重入、不可中斷()、非公平,而Lock鎖可重入、可中斷、可公平可非公平。

  • Lock鎖適合大量同步的程式碼的同步問題,synchronized鎖適合程式碼少量的同步問題。

  • synchronized只關聯一個條件佇列,Lock可以關聯多個條件佇列。

    注意:syschronzied不可中斷的意思是等待獲取鎖的時候不可中斷,拿到鎖之後可中斷,沒獲取到鎖的情況下,中斷操作一直不會生效。Lock的lockInterruptibly()可以中斷是指在等鎖的過程中也可以被中斷。

    測試程式碼如下:

    public class InterruptedDemo{
    	//中斷synchronized
        private static void test5() throws InterruptedException {
            Object o1 = new Object();
            Thread thread1 = new Thread(() -> {
                System.out.println("t1 enter");
                synchronized (o1) {
                    try {
                        System.out.println("start lock t1");
                        Thread.sleep(5000);
                        System.out.println("end lock t1");
                    } catch (InterruptedException e) {
                        System.out.println("t1 interruptedException");
                        e.printStackTrace();
                    }
                }
            });
    
            Thread thread2 = new Thread(() -> {
                System.out.println("t2 enter");
                synchronized (o1) {
                    try {
                        System.out.println("start lock t2");
                        Thread.sleep(1000);
                        System.out.println("end lock t2");
                    } catch (InterruptedException e) {
                        System.out.println("t2被中斷");
                        e.printStackTrace();
                    }
                }
            });
    
            thread1.start();
            thread2.start();
    
            // 主執行緒休眠一下,讓t1,t2執行緒百分百已經啟動,避免執行緒交替導致測試結果混淆
            Thread.sleep(1000);
            // 中斷t2執行緒的執行
            thread2.interrupt();
            System.out.println("t2 interrupt...");
    
        }
        //中斷lock
        private  static void test6() throws InterruptedException {
            Lock lock = new ReentrantLock();
    
            Thread t1 = new Thread(()->{
                lock.lock();
                try {
                    System.out.println("t6獲得鎖");
                    Thread.sleep(5000);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("t6釋放鎖");
                    lock.unlock();
                }
            });
            t1.start();
    
            Thread t2 = new Thread(()->{
                System.out.println("t7準備獲得鎖");
                try {
                    lock.lockInterruptibly();
                    System.out.println("t7獲取到鎖");
                    System.out.println("t7的中斷狀態:"+Thread.currentThread().interrupted());
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            });
            t2.start();
            Thread.sleep(500);
            System.out.println("中斷執行緒t7");
            t2.interrupt();
        }
        public static void main(String[] args) throws InterruptedException {
            test5();
        }
    }
    

    test5()方法執行結果如下:即使呼叫了t2執行緒的中斷方法,也要等到t2獲取到鎖才被中斷。

    test()6方法的執行結果如下:t2執行緒沒有獲取到鎖,在等鎖的過程中就被中斷了。

三、Lock介面的API

void lock() // 如果鎖可用就獲得鎖,如果鎖不可用就阻塞直到鎖釋放
void lockInterruptibly() // 和 lock()方法相似, 但阻塞的執行緒可中斷,丟擲 java.lang.InterruptedException異常
boolean tryLock() // 非阻塞獲取鎖;嘗試獲取鎖,如果成功返回true
boolean tryLock(long timeout, TimeUnit timeUnit) //帶有超時時間的獲取鎖方法
void unlock() // 釋放鎖
Condition newCondition() //返回一個新Condition繫結到該Lock例項。 

介面中有四個方法使用來獲取鎖的,lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly(),unlock()使用來釋放鎖的,newCondition()是用來返回返回條件佇列的。Lock其實用的基本語法格式如下:

 Lock l = ...;
 l.lock();
 try {
   // access the resource protected by this lock
   //判斷,幹活,通知  
 } finally {
     //一定要將釋放鎖的操作寫在finally中,因為發生異常時不會自動釋放鎖
   l.unlock();
 }

四、ReentrantLock可重入鎖:

ReentrantLock是Lock介面的實現類,可重入指的是執行緒在獲得鎖之後,再次獲取該鎖不需要阻塞,而是直接關聯一次計數器增加重入次數。獲得了幾次鎖就要釋放幾次(synchronized也是可重入的)

  • 可重入
//對於main執行緒即使method1方法沒有釋放鎖,method2方法中同樣可以再次獲取同一把鎖,因為都是main執行緒。
public class ReentrantLockDemo {
    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        method1();
    }

    public static void method1() {
        lock.lock();
        try {
            System.out.println("execute method1");
            method2();
        } finally {
            lock.unlock();
        }
    }

    public static void method2() {
        lock.lock();
        try {
            System.out.println("execute method2");
            method3();
        } finally {
            lock.unlock();
        }
    }

    public static void method3() {
        lock.lock();
        try {
            System.out.println("execute method3");
        } finally {
            lock.unlock();
        }
    }

}

結果:

  • 可以設定超時時間
class ReentrantLockDemo3{
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            System.out.println("啟動...");
            try {
                //tryLock()1秒後還獲取不到鎖直接返回false
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    System.out.println("獲取等待 1s 後失敗,返回");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                System.out.println("獲得了鎖");
            } finally {
                lock.unlock();
            }
        }, "t1");
        lock.lock();
        System.out.println("獲得了鎖");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

結果:tryLock()嘗試獲取鎖一秒鐘,沒有成功獲取到鎖,返回false,如何沒有設定超時時間則立即返回結果。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-XSr3n6DV-1603376107092)(http://img.mcxlblog.cn/qiniu_picGoimage-20201009192641624.png)]

  • 可中斷測試上邊已有

  • 可以設定公平鎖和非公平鎖

    RreentrantLock的構造方式可以選擇實現公平鎖還是非公平鎖,預設為非公平鎖,公平鎖就是按照執行緒到來的順序獲得鎖。非公平鎖就是隨機獲取到鎖,和執行緒的到來順序無關。公平鎖可以防止飢餓現象,但是併發度低。

    //非公平鎖
    ReentrantLock lock = new ReentrantLock();
    //公平鎖
    ReentrantLock lock2 = new ReentrantLock(true);
    

    在ReentrantLock中定義了2個靜態內部類,一個是NotFairSync,一個是FairSync,分別用來實現非公平鎖和公平鎖。通過構造方法來實現公平鎖和非公平鎖。true為公平鎖,false為非公平鎖。

  • 支援多個條件變數(Condition)

    synchronized 中也有條件變數,當條件不滿足時進入 waitSet 等待。而是用notifyAll()喚醒的時候是將整個waitSet中的所有執行緒全部喚醒.

    ReentrantLock 的條件變數比 synchronized 強大之處在於,它是支援多個條件變數的(Condition),這就好比有多個waitSet,使用signalAll()喚醒時可以選擇某一個條件變數喚醒。

    class ReentrantLockDemo5 {
        public static void main(String[] args) throws InterruptedException {
            ReentrantLock lock = new ReentrantLock();
            Condition condition2 = lock.newCondition();
            Condition condition3 = lock.newCondition();
            Thread t1 = new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.lock();
                try {
                    condition2.signalAll();
                    System.out.println("我要去喚醒condition2中等待的執行緒");
                }finally {
                    lock.unlock();
                }
            });
            t1.start();
            Thread t2 = new Thread(() -> {
                lock.lock();
                try {
                    System.out.println("t2在condition2條件中等待等待");
                    condition2.await();
                    System.out.println("t2被喚醒");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            });
            t2.start();
    
            Thread t3 = new Thread(() -> {
                lock.lock();
                try {
                    System.out.println("t3在condition3條件中等待等待");
                    condition3.await();
                    System.out.println("t3被喚醒");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            });
            t3.start();
        }
    }
    

    結果:只喚醒了t2執行緒,並沒有喚醒t3執行緒

五、ReadWriteLock

 ReadWriteLock也是一個介面,在它裡面只定義了兩個方法:一個用來獲取讀鎖,一個用來獲取寫鎖,

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

ReentrantReadWriteLock

ReentrantReadWriteLock:重入讀寫鎖,它實現了ReadWriteLock介面,在這個類中維護了兩個鎖,一個是ReadLock,一個是WriteLock,他們都分別實現了Lock介面。讀寫鎖是一種適合讀多寫少的場景下解決執行緒安全問題的工具,基本原則是:讀和讀不互斥、讀和寫互斥、寫和寫互斥。也就是說涉及到影響資料變化的操作都會存在互斥。當讀操作遠遠高於寫操作時,這時候使用 讀寫鎖讀-讀 可以併發,提高效能。可以通過readLock()和writeLock()獲取讀鎖和寫鎖,

 public ReentrantReadWriteLock.WriteLock writeLock() { 
     return writerLock; 
 }
 public ReentrantReadWriteLock.ReadLock  readLock()  {
     return readerLock; 
 }
  • 讀鎖讀鎖可以併發

    public class ReentrantReadWriteLockDemo {
    
        private Object data;
    
        private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        private ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        private ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    
        public static void main(String[] args) {
            ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
            new Thread(() -> {
                demo.read();
            }, "t1").start();
            new Thread(() -> {
                demo.read();
            }, "t2").start();
        }
    
        public Object read() {
            System.out.println(Thread.currentThread().getName()+"獲取讀鎖...");
            readLock.lock();
            try {
                System.out.println(Thread.currentThread().getName()+"讀取");
                TimeUnit.SECONDS.sleep(1);
                return data;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName()+"釋放讀鎖...");
                readLock.unlock();
            }
            return null;
        }
    
        public void write() {
            System.out.println(Thread.currentThread().getName()+"獲取寫鎖...");
            writeLock.lock();
            try {
                System.out.println(Thread.currentThread().getName()+"寫入");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName()+"釋放寫鎖...");
                writeLock.unlock();
            }
        }
    }
    

    結果如下:t1執行緒獲取讀鎖,t2執行緒同樣可以獲取讀鎖

  • 讀鎖寫鎖相互阻塞

    public static void main(String[] args) {
        ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
        new Thread(() -> {
            demo.read();
        }, "t1").start();
        new Thread(() -> {
            demo.write();
        }, "t2").start();
    }

結果如下:t2獲取寫鎖期間t1執行緒並不能獲取到讀鎖,只有t2釋放寫鎖,t1才能讀取

  • 寫鎖寫鎖相互阻塞

    public static void main(String[] args) {
            ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
            new Thread(() -> {
                demo.write();
            }, "t1").start();
            new Thread(() -> {
                demo.write();
            }, "t2").start();
        }
    

    結果如下:t1獲得寫鎖期間t2不能寫入,只有t1釋放寫鎖後t2才能寫入

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-idvN43DY-1603376107099)(http://img.mcxlblog.cn/qiniu_picGoimage-20201022190444116.png)]

  • 讀鎖不支援條件變數,寫鎖支援條件變數

    public class ReentrantReadWriteLockDemo {
    
        private Object data;
    
        private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        private ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        private ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    
        public static void main(String[] args) {
            ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
            new Thread(() -> {
                demo.read();
            }, "t1").start();
            new Thread(() -> {
                demo.write();
            }, "t2").start();
        }
    
        public Object read() {
            System.out.println(Thread.currentThread().getName()+"獲取讀鎖...");
            readLock.lock();
            try {
                System.out.println(Thread.currentThread().getName()+"讀取");
                Condition condition = readLock.newCondition();
                TimeUnit.SECONDS.sleep(1);
                return data;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName()+"釋放讀鎖...");
                readLock.unlock();
            }
            return null;
        }
    
        public void write() {
            System.out.println(Thread.currentThread().getName()+"獲取寫鎖...");
            writeLock.lock();
            try {
                System.out.println(Thread.currentThread().getName()+"寫入");
                Condition condition = writeLock.newCondition();
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName()+"釋放寫鎖...");
                writeLock.unlock();
            }
        }
    }
    

    結果如下:讀鎖在獲取Condition例項時就會報錯,而寫鎖不會報錯

  • 鎖重入時持有讀鎖的情況下去獲取寫鎖,會導致獲取寫鎖永久等待

    public static void main(String[] args) {
            ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
            new Thread(() -> {
                demo.readAndWrite();
            }, "t1").start();
     
        }
     private void readAndWrite() {
            try {
                readLock.lock();
                System.out.println(Thread.currentThread().getName()+"成功獲取到讀鎖!");
                writeLock.lock();
                System.out.println(Thread.currentThread().getName()+"成功獲取到寫鎖!");
            }finally {
                writeLock.unlock();
                System.out.println(Thread.currentThread().getName()+"釋放寫鎖!");
                readLock.unlock();
                System.out.println(Thread.currentThread().getName()+"釋放讀鎖!");
            }
        }
    

    結果如下:t1獲取到讀鎖後,再次獲取寫鎖一直陷入阻塞

  • 鎖重入時持有寫鎖的情況下可以獲取讀鎖

     public static void main(String[] args) {
            ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
            new Thread(() -> {
                demo.writeAndRead();
            }, "t1").start();
        }
    
    private void writeAndRead() {
            try {
                writeLock.lock();
                System.out.println(Thread.currentThread().getName()+"成功獲取到寫鎖!");
                readLock.lock();
                System.out.println(Thread.currentThread().getName()+"成功獲取到讀鎖!");
            }finally {
                writeLock.unlock();
                System.out.println(Thread.currentThread().getName()+"釋放寫鎖!");
                readLock.unlock();
                System.out.println(Thread.currentThread().getName()+"釋放讀鎖!");
            }
        }
    

    結果如下:t1執行緒獲取到寫鎖的情況下可以成功的再次獲取到讀鎖

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-tJdZoLid-1603376107103)(http://img.mcxlblog.cn/qiniu_picGoimage-20201022192613457.png)]

六、StampedLock

stampedLock是JDK8引入的新的鎖機制,可以簡單認為是讀寫鎖的一個改進版本,讀寫鎖雖然通過分離讀和寫的功能使得讀和讀之間可以完全併發,但是讀和寫是有衝突的,如果大量的讀執行緒存在,可能會引起寫執行緒的飢餓。StampedLockReadWriteLock相比,改進之處在於:讀的過程中也允許獲取寫鎖後寫入!這樣一來,我們讀的資料就可能不一致,所以,需要一點額外的程式碼來判斷讀的過程中是否有寫入,這種讀鎖是一種樂觀鎖。StampedLock把讀分為了悲觀讀和樂觀讀,悲觀讀就等價於ReadWriteLock的讀,而樂觀讀在一個執行緒寫共享變數時,不會被阻塞,樂觀讀是不加鎖的

  • 樂觀讀,StampedLock 支援 tryOptimisticRead() 方法(樂觀讀),讀取完畢後需要做一次 戳校驗 如果校驗通過,表示這期間確實沒有寫操作,資料可以安全使用,如果校驗沒通過則表示有寫操作修改了共享變數則升級樂觀讀為悲觀讀鎖。

    public class StampedLockDemo {
        private int data;
        private final StampedLock lock = new StampedLock();
        public StampedLockDemo(int data) {
            this.data = data;
        }
        public int read(int readTime) throws InterruptedException {
            long stamp = lock.tryOptimisticRead();
            System.out.println(Thread.currentThread().getName()+"準備讀,stamp = "+stamp);
            TimeUnit.SECONDS.sleep(readTime);
            if (lock.validate(stamp)) {
                System.out.println(Thread.currentThread().getName()+"已經讀,stamp = "+stamp);
                return data;
            }
            // 將樂觀讀升級為悲觀讀
            System.out.println("樂觀讀升級為悲觀讀,stamp = "+stamp);
            try {
                stamp = lock.readLock();
                System.out.println("悲觀讀,stamp = "+stamp);
                TimeUnit.SECONDS.sleep(readTime);
                System.out.println("悲觀讀完,stamp = "+stamp+ "  資料data = "+data);
                return data;
            } finally {
                System.out.println("釋放讀鎖,stamp =  "+stamp);
                lock.unlockRead(stamp);
            }
        }
        public void write(int newData) {
            long stamp = lock.writeLock();
            System.out.println("獲取寫鎖,stamp = "+stamp);
            try {
                 TimeUnit.SECONDS.sleep(2);
                this.data = newData;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("釋放寫鎖,stamp = "+stamp);
                lock.unlockWrite(stamp);
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            StampedLockDemo stampedLockDemo = new StampedLockDemo(1);
            new Thread(() -> {
                try {
                    stampedLockDemo.read(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "t1").start();
    
            TimeUnit.SECONDS.sleep(1);
    
            new Thread(() -> {
                try {
                    stampedLockDemo.read(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "t2").start();
        }
    }
    
    

    結果如下:樂觀讀實際並沒有加鎖,只是驗證stamp

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-JelR14mg-1603376107105)(http://img.mcxlblog.cn/qiniu_picGoimage-20201022201355978.png)]

  • 悲觀讀情況演示

     public static void main(String[] args) throws InterruptedException {
            StampedLockDemo stampedLockDemo = new StampedLockDemo(1);
            new Thread(() -> {
                try {
                    stampedLockDemo.read(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "t1").start();
    
            TimeUnit.SECONDS.sleep(1);
    
            new Thread(() -> {
                stampedLockDemo.write(3);
            }, "t2").start();
        }
    

    結果如下:由於t1執行緒在樂觀讀的時候,t2執行緒修改了資料,導致t1執行緒驗證stamp時與獲取到的stamp不一致,此時t1執行緒由樂觀讀升級為悲觀讀。

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-7nYQ9c2U-1603376107106)(http://img.mcxlblog.cn/qiniu_picGoimage-20201022201857859.png)]

    注意

    StampedLock不是可重入鎖,所以不支援重入,並且StampedLock不支援條件變數,也就是沒Condition

七、Semaphore

Semaphore也叫訊號量,在JDK1.5被引入,可以用來控制同時訪問特定資源的執行緒數量,通過協調各個執行緒,以保證合理的使用資源。

Semaphore內部維護了一組虛擬的許可,許可的數量可以通過建構函式的引數指定。

  • 訪問特定資源前,必須使用acquire方法獲得許可,如果許可數量為0,該執行緒則一直阻塞,直到有可用許可。
  • 訪問資源後,使用release釋放許可。

測試程式碼如下:

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 1. 建立 semaphore 物件
        Semaphore semaphore = new Semaphore(3);
        // 2. 10個執行緒同時執行
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"獲取許可!");
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 4. 釋放許可
                    System.out.println(Thread.currentThread().getName()+"釋放許可!");
                    semaphore.release();
                }
            }).start();
        }
    }
}

結果如下:每個執行緒必需要獲得許可

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-i3BqEtfl-1603376107107)(http://img.mcxlblog.cn/qiniu_picGoimage-20201022204007388.png)]

八、CountdownLatch

CountDownLatch是一個同步工具類,它允許一個或多個執行緒一直等待,直到其他執行緒執行完後再執行或者等待時間超時。其中構造引數用來初始化等待計數值,await() 用來等待計數歸零,countDown() 用來讓計數減一,計數器count是閉鎖需要等待的執行緒數量,只能被設定一次,且CountDownLatch沒有提供任何機制去重新設定計數器count。CountDownLatch可以用來替代join();

CountdownLatch的所有方法如下,見名知意,很容易理解:

測試程式碼如下:

public class CountdownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"開始...");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            latch.countDown();
            System.out.println(Thread.currentThread().getName()+"結束...  ,count剩餘數量 = "+ latch.getCount());
        }).start();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"開始...");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            latch.countDown();
            System.out.println(Thread.currentThread().getName()+"結束...  ,count剩餘數量 = "+ latch.getCount());
        }).start();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"開始...");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            latch.countDown();
            System.out.println(Thread.currentThread().getName()+"結束...  ,count剩餘數量 = "+ latch.getCount());
        }).start();
        System.out.println("waiting...");
        latch.await();
        System.out.println("wait end...");
    }
}

結果如下:主執行緒呼叫CountDownLatch.await()方法後直到CountDownLatch的count減為0後,才開始執行。

如果設定超時時間

 latch.await(2,TimeUnit.SECONDS);

結果如下:即使count沒有減為0但是已經等待超時,主執行緒也恢復了執行

九、CyclicBarrier

CyclicBarrier迴圈柵欄也叫同步屏障,在JDK1.5被引入,可以讓一組執行緒達到一個屏障時被阻塞,直到最後一個執行緒達到屏障時,所有被阻塞的執行緒才能繼續執行。 CyclicBarrier好比一扇門,預設情況下關閉狀態,堵住了執行緒執行的道路,直到所有執行緒都就位,門才打開,讓所有執行緒一起通過。

構造方法

  1. 預設的構造方法是CyclicBarrier(int parties),其引數表示要攔截的執行緒的數量,每個執行緒呼叫await方法告訴CyclicBarrier已經到達屏障位置,執行緒被阻塞。

  2. 另外一個構造方法CyclicBarrier(int parties, Runnable barrierAction),其中barrierAction任務會在所有執行緒到達屏障後執行。

    測試程式碼如下:

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier cb = new CyclicBarrier(2,()->{
            System.out.println("所有執行緒都已就位!!!");
        }); // 個數為2時才會繼續執行
        new Thread(()->{
            System.out.println("執行緒1開始.."+new Date());
            try {
                System.out.println(Thread.currentThread().getName()+"到達屏障");
                cb.await(); // 當個數不足時,等待
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println("執行緒1繼續向下執行..."+new Date());
        }).start();
        new Thread(()->{
            System.out.println("執行緒2開始.."+new Date());
            try {
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName()+"到達屏障");
                cb.await(); // 2 秒後,執行緒個數夠2,繼續執行
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println("執行緒2繼續向下執行..."+new Date());
        }).start();
    }
}

結果如下:只有到達柵欄的執行緒達到指定個數,才行繼續執行,barrierAction執行緒也在這個時候才開始執行

如果設定超時時間且有執行緒沒有在超時時間之內到達柵欄:

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier cb = new CyclicBarrier(2,()->{
            System.out.println("所有執行緒都已就位!!!");
        }); // 個數為2時才會繼續執行
        new Thread(()->{
            System.out.println("執行緒"+Thread.currentThread().getName()+"開始.."+new Date());
            try {
                System.out.println(Thread.currentThread().getName()+"到達屏障");
                cb.await(1, TimeUnit.SECONDS); // 當個數不足時,等待
            } catch (InterruptedException | BrokenBarrierException | TimeoutException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"繼續向下執行..."+new Date());
        }).start();
        new Thread(()->{
            System.out.println("執行緒"+Thread.currentThread().getName()+"開始.."+new Date());
            try {
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName()+"到達屏障");
                cb.await(); // 2 秒後,執行緒個數夠2,繼續執行
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"繼續向下執行..."+new Date());
             System.out.println("柵欄狀態"+cb.isBroken());
        }).start();

    }
}

結果如下:由於執行緒0先到達柵欄且設定了超時時間,執行緒1沒能線上程1的超時時間內到達,此時執行緒1會丟擲超時異常,當執行緒1到達時(即呼叫await()方法)時會丟擲柵欄破碎異常,此時柵欄已經被破壞。

CyclicBarrier和CountDownLatch的區別

  • CountDownLatch 是一次性使用的,CyclicBarrier 是可迴圈利用的
  • CyclicBarrier 類似於人滿發車,CountDownLatch 類似於人走了才發車