Java多線程——(2)JDK併發包

1. 可重入鎖

1.1ReentrantLock的使用

public class ReentrantLockDemo implements Runnable{

    public static ReentrantLock lock = new ReentrantLock();
    public static int count = 0;
    @Override
    public void run() {
        for(int i=0; i<10000; i++) {
            //lock.lock();
            try {
                count ++;
            }finally {
                //lock.unlock();  //使用ReentrantLock的範式!  放入finally塊中使用。
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo th = new ReentrantLockDemo();
        Thread t1 = new Thread(th);
        Thread t2 = new Thread(th);
        t1.start();t2.start();
        t1.join();t2.join(); //主線程等待t1結束,主線程等待t2結束
        System.out.println(count);
    }
}

運行結果:
這裏寫圖片描述
若恢復鎖:
這裏寫圖片描述

1.1 ReentrantLock的可重入特性

線程拿了這把鎖多少個許可,就要釋放多少次。若沒有釋放完全,會導致其他線程拿不到許可。

public class ReentrantLockDemo_Re implements Runnable {

    public static ReentrantLock lock = new ReentrantLock();
    public static int count = 0;
    @Override
    public void run() {
        for(int i=0; i<10000; i++) {
            lock.lock();
            lock.lock();       //重入
            try {
                count ++;
            }finally {
                lock.unlock();   //必須釋放兩次
                lock.unlock();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo_Re th = new ReentrantLockDemo_Re();
        Thread t1 = new Thread(th);
        Thread t2 = new Thread(th);
        t1.start();t2.start();
        t1.join();t2.join(); //主線程等待t1結束,主線程等待t2結束
        System.out.println(count);
    }
}

假如只釋放了一次。那麼必然有一個線程會卡在獲取鎖的位置:
這裏寫圖片描述
這裏寫圖片描述
卡在了17行,因爲之前線程沒有釋放完,導致第二個線程無法獲取鎖,一直等待:
這裏寫圖片描述

1.2 ReentrantLock的可中斷特性

通過lockInterruptibly()加鎖,可以在死鎖或者線程長時間卡住的時候,線程調用interrupt()拋出中斷異常。

public class ReentrantLockInterruptibly implements Runnable{

    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;

    //控制加鎖順序,方便構造死鎖
    public ReentrantLockInterruptibly(int lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            if(lock == 1) {
                lock1.lockInterruptibly();          //獲取鎖1
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {}
                lock2.lockInterruptibly();          //再申請鎖2

            }else {
                lock2.lockInterruptibly();          //獲取鎖2
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {}
                lock1.lockInterruptibly();          //再申請鎖1
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            if(lock1.isHeldByCurrentThread()) lock1.unlock();
            if(lock2.isHeldByCurrentThread()) lock2.unlock();
            System.out.println(Thread.currentThread().getId()+":線程退出");
        }
    }

    public static void main(String[] args) throws InterruptedException {
         ReentrantLockInterruptibly r1 = new ReentrantLockInterruptibly(1);
         ReentrantLockInterruptibly r2 = new ReentrantLockInterruptibly(2);
         Thread t1 = new Thread(r1);
         Thread t2 = new Thread(r2);
         t1.start();t2.start();
         Thread.sleep(1000);
         //中斷其中一個線程
         DeadlockChecker.check(); //檢測到死鎖便中斷這個線程
    }

/**
 * 死鎖檢查類
 */
public class DeadlockChecker {

    private final static ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
    final static Runnable deadlockCheck = new Runnable() {

        @Override
        public void run() {
            while(true) {
                long[] deadlockedThreadIds = mbean.findDeadlockedThreads();    //找到所有死鎖
                if (deadlockedThreadIds != null) {
                    ThreadInfo[] threadInfos = mbean.getThreadInfo(deadlockedThreadIds);
                    for(Thread t : Thread.getAllStackTraces().keySet()) {
                        for(int i = 0; i < threadInfos.length; i++) {      
                            if(t.getId() == threadInfos[i].getThreadId()) {//如果是死鎖,便中斷
                                t.interrupt();  //中斷此線程
                            }
                        }
                    }//end of for
                }//end of if

                try {
                    Thread.sleep(5000);
                }catch(InterruptedException e) {
                }
            }// end of while
      }

    };

    public static void check() {
        Thread t = new Thread(deadlockCheck);
        t.setDaemon(true);   //跟業務沒有關係,只是做死鎖檢查而已.設爲守護線程
        t.start();
    }
}

這裏寫圖片描述

1.3 ReentrantLock的可限時特性

也是一種避免死鎖,或者長期等待的一種措施。

/**
 * ReentrantLock可限時特性
 * @author liq
 */
public class ReentrantLockTimeLock implements Runnable{

    public static ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        try {
            if(lock.tryLock(5, TimeUnit.SECONDS)) {   //限時鎖,超過5秒獲取鎖失敗
                Thread.sleep(6000);
            }else {
                System.out.println("get lock failed");
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            if(lock.isHeldByCurrentThread()) lock.unlock();
        }
    }
    public static void main(String[] args) {
        ReentrantLockTimeLock timeLock = new ReentrantLockTimeLock();
        //必有一個線程獲取鎖失敗,並在5秒後打印"get lock failed"
        Thread t1 = new Thread(timeLock);
        Thread t2 = new Thread(timeLock);
        t1.start();
        t2.start();
    }

}

5秒後必有一個線程獲取鎖失敗:
這裏寫圖片描述

1.4 ReentrantLock的公平鎖特性

一般來講,鎖都是非公平的,即,先來的線程不一定先獲取鎖,後來的線程也不一定後獲取鎖。如果先來的線程一直獲取不到鎖,可能會導致飢餓現象。
當然,公平鎖要處理排隊的問題,性能較差。

//源碼:構造方法

 /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

使用:

public static ReentrantLock fairLock = new ReentrantLock(true);

1.5 ReentrantLock 鎖的實現

//源碼
        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1)) //CAS獲取鎖 0狀態:表示沒有人佔用,1狀態:表示鎖已經被佔用
                setExclusiveOwnerThread(Thread.currentThread());
            else //拿鎖沒有成功
                acquire(1);//鎖申請
        }

    //鎖申請
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&   //試着再獲取一次,說不定剛被人釋放了
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //加入等待隊列當中去,之後再試着取一次
            selfInterrupt();
    }

    //加入到等待隊列
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);//每個node對應當前線程
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;//將當前節點放到隊列的尾巴上去
                return node;
            }
        }
        enq(node);
        return node; //返回當前節點
    }

/*unlock()操作*/
    public void unlock() {
        sync.release(1);
    }
 public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);  //等待隊列的頭部進行釋放
            return true;
        }
        return false;
    }
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread); //進行unpark()操作
    }

2. condition 條件

類似於Object.wait() 和 Object.notify();且都需要在同步塊中執行。
Object.wait() 和 Object.notify()結合Synchronized使用,而condition的await()和signal()都需要結合ReentrantLock 搭配使用。

await()會使當前線程等待,同時釋放當前鎖,當其他線程中使用signal()時或者signalAll()方法時,線程會重新獲得鎖並繼續執行。或者當線程被中斷時,也能跳出等待。這和Object.wait()方法很相似。
awaitUninterruptibly()與await()基本相同,但是它並不會在等待過程中響應中斷。
signal()方法用於喚醒一個在等待中的線程。相對的signalAll()方法會喚醒所有在等待中的線程。這和Object.notify()方法很類似。

爲什麼一定要在同步塊中執行(Object.wait() 和 Object.notify(); condition的await()和signal())?
【答】設想這樣一種情況:等待和通知這兩個過程不需要鎖,若,通知(喚醒)發生在了等待之前。此時,程序邏輯認爲它已經發送過了通知,預想着等待被喚醒繼續執行;但實際情況是,此時的等待還沒有發生。直到等待發生後,它一直等待被喚醒。但已經沒有線程喚醒它了,導致產生了死鎖。反過來說,正是由於這種等待和喚醒的雙方在同一個鎖上的協調,才能保證線程間的協調同步。不然,隨意喚醒,隨意等待。喚醒時也不知道有沒有線程在等待,等待時也不知道接下來有沒有線程喚醒自己,豈不是亂了套?

public class ReentrantLockCondition implements Runnable{

    public static ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();
    @Override
    public void run() {
        try {
            lock.lock();          //必須在同步塊之間等待

            condition.await();    //線程掛起,釋放當前鎖  等待主線程喚醒或被中斷。喚醒後需要先拿到鎖才能繼續向下執行
            System.out.println("Thread is going on");
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockCondition tl = new ReentrantLockCondition();
        Thread t1 = new Thread(tl);
        t1.start();
        Thread.sleep(2000);
        //通知線程t1繼續執行
        lock.lock();
        condition.signal();    //喚醒等待在condition上的掛起線程
        lock.unlock();
    }

}

3. Semaphore 信號量

信號量類似於一種有限的資源。
允許若干個線程進入臨界區,超過許可數量的線程就必須等待。
信號量的許可數量爲1的時候就相當與一把鎖。

主要API:

public void acquire() //獲取信號量
public void release() //釋放信號量
public void acquireUninterruptibly() //等待過程中不響應中斷
public boolean tryAcquire()  //僅僅試一下,拿不到就返回false
public boolean tryAcquire(long timeout, TimeUnit unit) //try多長時間
/**
 * 信號量演示
 * @author liq
 *
 */
public class SemaphoreDemo implements Runnable{

    final Semaphore semp = new Semaphore(5);  //信號量只有5個許可

    @Override
    public void run() {

        try {
            semp.acquire();  //當前線程拿到一個許可
            //模擬耗時操作
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getId() + ":done!");
        }catch(InterruptedException e) {
            e.printStackTrace();
        }finally {
            semp.release();     //釋放掉許可
        }
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newFixedThreadPool(20);
        final SemaphoreDemo demoTask = new SemaphoreDemo();
        for(int i = 0; i < 20; i++) {  //一次性提交20個任務, 一次5個信號量允許5個線程做,做完之後釋放掉信號量,其他線程才能拿到許可
            exec.submit(demoTask);
        }
    }
}

4. ReadWriteLock 讀寫鎖

JDK5中提供的讀寫分離鎖
讀-讀不互斥:讀讀之間不阻塞
讀-寫互斥:讀阻塞寫,寫也會阻塞讀
寫-寫互斥:寫寫阻塞

主要接口:

private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static readLock = readWriteLock.readLock(); //獲取讀鎖
private static Lock writeLock = readWriteLock.writeLock(); //獲取寫鎖

5. CountDownLatch 倒數計時器

是一種柵欄的機制。一個線程等待其他所有線程完成後繼續。
在火箭發射前,爲了保證萬無一失,往往還要進行各項設備、儀器的檢查。只有等所有檢查完畢後,引擎才能點火。這種場景就非常適合使用CountDownLatch。它可以使得點火線程等待所有檢查線程全部完工後,再執行。

這裏寫圖片描述
主要接口:

static final CountDownLatch end = new CountDownLatch(10); //有10個檢查項目
end.countDown(); //每個線程做完後,計時減1,減到0後,主線程停止等待。
end.await();     //所有項目(線程等待)
package ch5;

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * CountDownLatch演示
 * @author liq
 *
 */
public class CountDownLatchDemo implements Runnable {

    static final CountDownLatch end = new CountDownLatch(10);        //10個檢查任務
    @Override
    public void run() {

        try {
            //模擬檢查任務
            Thread.sleep(new Random().nextInt(10)*1000);
            System.out.println("check complete");
            end.countDown();    //每次做完一項任務,計數器減1
        }catch(InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatchDemo demoTask = new CountDownLatchDemo();
        ExecutorService pool = Executors.newFixedThreadPool(10);
        for(int i = 0; i < 10; i++) {
            pool.submit(demoTask);
        }
        //等待檢查
        end.await();   //計數器減少到0,喚醒
        //發射火箭
        System.out.println("Fire!");
        pool.shutdown();
    }
}

這裏寫圖片描述

6. CyclicBarrier 循環柵欄

計數器可以反覆使用,比如,假設我們將計數器設置爲10。那麼湊齊第一批10個線程後,計數器就會歸0,然後接着湊齊下一批10個線程。
區別於“CountDownLatch 一個線程等待其他線程”,線程之間是相互等待的

這裏寫圖片描述
主要接口:

public CyclicBarrier(int parties, Runnable barrierAction)//parties :參與者個數,比如10個項目; barrierAction:柵欄動作,就是當計數器一次計數完成後,系統會執行的動作。
await() //等待倒計時完成
package ch5;

import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * 循環柵欄演示
 * @author liq
 *
 */
public class CyclicBarrierDemo {

    public static class Soldier implements Runnable{

        private String soldier;
        private final CyclicBarrier cyclic;
        Soldier(CyclicBarrier cyclic, String soldierName){
            this.cyclic = cyclic;
            this.soldier = soldierName;
        }

        @Override
        public void run() {
            try {
                //等待所有士兵到齊
                cyclic.await();  //倒計數完成,會執行BarrierRun()動作
                doWork();
                //等待所有士兵完成工作
                cyclic.await();   //循環柵欄體現於此, 又一次使用cyclic柵欄進行倒計數
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e) {
                e.printStackTrace();
            }
        }

        void doWork() {

            //模擬doWork:休眠隨機時間
            try {
                Thread.sleep(Math.abs(new Random().nextInt()%10000));
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(soldier + ":任務完成");
        }

    }


    public static class BarrierRun implements Runnable{

        boolean flag;
        int N;
        public BarrierRun(boolean flag, int N) {
            this.flag = flag;
            this.N= N;
        }

        @Override
        public void run() {
            if(flag) {//第二次任務完成的時候會執行
                System.out.println("司令:[士兵"+ N +"個,完成任務!]");
            }else {//第一次集合完畢的時候會執行
                System.out.println("司令:[士兵"+ N +"個,集合完畢!]");   
                flag = true;
            }
        }
    }

    public static void main(String[] args) {
        final int N = 10;  //10個士兵
        Thread[] allSoldier = new Thread[N];
        boolean flag = false;
        CyclicBarrier cyclic = new CyclicBarrier(N, new BarrierRun(flag, N)); //10個士兵到達後,執行BarrierRun();
        //設置屏障點,主要是爲了執行這個方法
        System.out.println("集合隊伍");
        for(int i = 0; i < N; ++i) {
            System.out.println("士兵"+i+"報道!");
            allSoldier[i] = new Thread(new Soldier(cyclic,  "士兵" + i));
            allSoldier[i].start();

            //若有一個線程被中斷了,會拋InterruptedException;其它線程看自己永遠沒有希望繼續,會拋BrokenBarrierException
/*          if(i == 5) {
                allSoldier[0].interrupt();
            }*/
        }
    }
}

這裏寫圖片描述

7. LockSupport 鎖支持

提供比較底層的線程阻塞原語。用來將線程掛起,有點像suspend()。但有所不同。LockSupport可以毫無顧忌地使用,suspend()是不建議使用的。如果resume()發生在suspend()之前,那麼,線程將會被永遠掛起。產生”凍結”的效果。永遠醒不過來。
其設計思想類似信號量。根據許可(permit)來喚醒,unpark()發生在park()之前,線程是掛不住的。unpark()能使得一個許可可用。默認許可是不可用的,所以當park()的時候,由於許可不可用,當前線程會被掛起堵住,直到許可可用。(Disables the current thread for thread scheduling purposes unless the permit is available. )。當然,線程也可中斷使得park()返回。park()自身並不拋出異常,但它會響應中斷異常,一旦線程發生中斷,它會立即返回。
主要接口:

LockSupport.park();    //線程掛起
LockSupport.unpark(); //線程恢復

代碼示例:


import java.util.concurrent.locks.LockSupport;
/**
 * LockSupport演示
 * @author liq
 *
 */
public class LockSupportDemo {

    public static Object u = new Object();


    public static class ChangeObjectThread extends Thread{
        public ChangeObjectThread(String name) {
            super.setName(name);
        }

        @Override
        public void run() {
            synchronized (u) {
                System.out.println("in "+getName());

                //睡眠1秒,使得unpark()發生在park之前
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                LockSupport.park();     //線程掛起
            }
        }
    }

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

        ChangeObjectThread t1 = new ChangeObjectThread("t1");
        ChangeObjectThread t2 = new ChangeObjectThread("t2");

        t1.start();
        Thread.sleep(100);
        t2.start();
        //unpark()發生在park之前, t1, t2 也不會掛住
        LockSupport.unpark(t1);
        LockSupport.unpark(t2);
        t1.join();
        t2.join();
    }
}

執行結果可見,兩個線程正常執行結束。並沒有掛住。
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章