亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

Java多線程筆記(二):鎖與閉鎖工具類

skinner / 974人閱讀

摘要:如果釋放的次數(shù)多了,會得到一個異常反之則會導(dǎo)致當(dāng)前線程一直持有該鎖,導(dǎo)致其他線程無法進(jìn)入臨界區(qū)。,若沒有參數(shù),當(dāng)前線程會嘗試獲得鎖,如果申請鎖成功,則返回,否則立即返回。閉鎖閉鎖是一種同步工具類,可以延遲線程的進(jìn)度直到其到達(dá)終止?fàn)顟B(tài)。

為了更好地支持并發(fā)程序,“鎖”是較為常用的同步方法之一。在高并發(fā)環(huán)境下,激勵的鎖競爭會導(dǎo)致程序的性能下降。
所以我們將在這里討論一些有關(guān)于鎖使用和問題以及一些注意事項。

工具類 ReentrantLock

重入鎖可以完全替代Synchronized關(guān)鍵字,但其必須顯式的調(diào)用unlock。建議視為Synchronized的高級版,比起Synchronized關(guān)鍵字,其可定時、可輪詢并含有可中斷的鎖獲取操作,公平隊列以及非塊結(jié)構(gòu)的鎖。

/**
 * 重入鎖演示
 *
 */
public class ReeterLock implements Runnable{

    public static ReentrantLock lock = new ReentrantLock();

    public static int i = 0;

    @Override
    public void run() {
        for (int j=0;j<10000;j++){
            //手動上鎖,可以上N把,這里是為了演示
            lock.lock();
            lock.lock();
            lock.lock();
            try {
                i ++;
            } finally {
                //無論如何必須釋放鎖,上幾把 釋放幾把
                lock.unlock();
                lock.unlock();
                lock.unlock();
            }
        }
    }

    public static void main(String[] a) throws InterruptedException {
        ReeterLock rl = new ReeterLock();
        Thread t1 = new Thread(rl);
        Thread t2 = new Thread(rl);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.print(i);
    }
}

那么我們可以明顯的看到重入鎖保護(hù)著臨界區(qū)資源i,確保多線程對i操作的安全。在demo中我們也是加了3次鎖并釋放了3次鎖。

需要注意的是,如果同一線程多次獲得鎖,那么在釋放鎖的時候,也必須釋放相同次數(shù)。如果釋放的次數(shù)多了,會得到一個java.lang.IllegalMonitorStateException異常;反之則會導(dǎo)致當(dāng)前線程一直持有該鎖,導(dǎo)致其他線程無法進(jìn)入臨界區(qū)。

中斷響應(yīng):ReentrantLock.lockInterruptibly()

對于synchronized來說,如果一個線程在等待鎖,那么結(jié)果只有兩種情況,要么它獲得這把鎖繼續(xù)執(zhí)行,要么它就保持等待。而使用重入鎖,則提供另外一種可能,那就是線程可以被中斷。也就是在等待鎖,程序可以根據(jù)需要取消對鎖的請求。有些時候,這么做是非常有必要的。

lockInterruptibly()方法是一個可以對中斷進(jìn)行響應(yīng)的鎖申請動作,即在等待鎖的過程中,中斷響應(yīng)。

public class IntLock implements Runnable{

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

    /**
     * 控制加鎖順序,制造死鎖
     * @param lock
     */
    public IntLock(int lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            /**
             *  1號線程,先占用 1號鎖,再申請 2號鎖
             *  2號線程,先占用 2號鎖,再申請 1號鎖
             *  這樣就很容易造成兩個線程相互等待.
             */
            if (lock == 1){
                //加入優(yōu)先響應(yīng)中斷的鎖
                lock1.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + "  進(jìn)入...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                /**
                 * 這時候,1號線程 想要持有 2號鎖 ,但是2號線程已經(jīng)先占用了2號鎖,所以1 號線程等待.
                 * 2號線程也一樣,占用著2號鎖 不釋放,還想申請1號鎖,而1號鎖 被1號線程占用且不釋放.
                 */
                lock2.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + "  完成...");

            }else {
                lock2.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + "  進(jìn)入...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock1.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + "  完成...");
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "  被中斷,報異常...");
            e.printStackTrace();
        } finally {
            if (lock1.isHeldByCurrentThread()) {
                System.out.println(Thread.currentThread().getName() + "  釋放...");
                lock1.unlock();
            }
            if (lock2.isHeldByCurrentThread()) {
                System.out.println(Thread.currentThread().getName() + "  釋放...");
                lock2.unlock();
            }
            System.out.println(Thread.currentThread().getName() + " 線程退出...");
        }
    }

    public static void main(String[] a) throws InterruptedException {
        IntLock re1 = new IntLock(1);
        IntLock re2 = new IntLock(2);
        Thread t1 = new Thread(re1," 1 號線程 ");
        Thread t2 = new Thread(re2," 2 號線程 ");
        t1.start();
        t2.start();
        //主線程sleep 2秒,讓兩個線程相互競爭資源.造成死鎖
        Thread.sleep(2000);
        //中斷2號線程
        t2.interrupt();

        /* 執(zhí)行結(jié)果:

            1 號線程   進(jìn)入...
            2 號線程   進(jìn)入...
            2 號線程   被中斷,報異常...    // 執(zhí)行 t2.interrupt();
            java.lang.InterruptedException
            2 號線程   釋放...
            at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
            2 號線程  線程退出...
            at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
            1 號線程   完成...  // 只有1號線程能執(zhí)行完成
            at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
            1 號線程   釋放...
            at com.iboray.javacore.Thread.T3.IntLock.run(IntLock.java:55)
            1 號線程   釋放...
            at java.lang.Thread.run(Thread.java:745)
            1 號線程  線程退出...
        */


    }
}
鎖申請等待限時:ReentrantLock.tryLock

除了等待外部通之外,避免死鎖還有另外一種方法,就是限時等待,給定一個等待時間讓線程自動放棄。

tryLock(時長,計時單位),若超過設(shè)定時長還沒得到鎖就返回false,若成功獲得鎖就返回true。

tryLock(),若沒有參數(shù),當(dāng)前線程會嘗試獲得鎖,如果申請鎖成功,則返回true,否則立即返回false。這種模式不會引起線程等待,因此不會產(chǎn)生死鎖。

public class TimeLock implements Runnable{

    public static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 申請資源...");
        try {
            //申請3秒,如果獲取不到,返回false,退出.
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                System.out.println(Thread.currentThread().getName() + " 獲得資源,開始執(zhí)行...");
                //持有鎖6秒
                Thread.sleep(6000);
                System.out.println(Thread.currentThread().getName() + " 執(zhí)行完成...");
            }else {
                System.out.println(Thread.currentThread().getName() + " 申請鎖失敗...");
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " 中斷...");
            e.printStackTrace();
        }finally {
            if (lock.isHeldByCurrentThread()) {
                System.out.println(Thread.currentThread().getName() + " 釋放鎖...");
                lock.unlock();
            }
        }

    }

    public static void main(String[] a) throws InterruptedException {
        TimeLock re = new TimeLock();
        Thread t1 = new Thread(re," 1 號線程 ");
        Thread t2 = new Thread(re," 2 號線程 ");
        t1.start();
        t2.start();

        /*
        執(zhí)行結(jié)果:

            1 號線程  申請資源...
            2 號線程  申請資源...
            1 號線程  獲得資源,開始執(zhí)行...
            2 號線程  釋放鎖...  //等待了5秒后,依然申請不到鎖,就返回false
            1 號線程  執(zhí)行完成...
            1 號線程  釋放鎖...
        */
    }
}

由于占用鎖的線程會持有鎖長達(dá)6秒,故另一個線程無法在5秒的等待時間內(nèi)獲取鎖,因此,請求鎖會失敗。

公平鎖:ReentrantLock(true)

在大多數(shù)情況下,鎖的申請都是非公平的。也就是說,線程1首先請求了鎖A,接著線程2也請求了鎖A。那么當(dāng)鎖A可用時,是線程1還是線程2可以獲得鎖呢?顯然這是不一定的。系統(tǒng)只會從這個鎖的等待隊列中隨機(jī)挑選一個,因此不能保證其公平性。

公平鎖會按照實際的先后順序,保證先到先得,它不會產(chǎn)生饑餓,只要排隊,最終都可以等到資源。在創(chuàng)建重入鎖時,通過有參構(gòu)造函數(shù),傳入boolean類型的參數(shù),true表示是公平鎖。實現(xiàn)公平所必然要維護(hù)一個有序隊列,所以公平鎖的實現(xiàn)成本高,性能相對也非常低,默認(rèn)情況下,鎖是非公平的。

public class ReentrantLockExample3 implements Runnable{

    //創(chuàng)建公平鎖
    public static ReentrantLock lock = new ReentrantLock(true);

    static  int i = 0;

    @Override
    public void run() {

        for (int j = 0;j<5;j++){
            lock.lock();
            try {
                i++;
                System.out.println(Thread.currentThread().getName() + " 獲得鎖 " + i);
            } finally {
                lock.unlock();
            }
        }

    }

    public static void main(String[] a) throws InterruptedException {
        ReentrantLockExample3 re = new ReentrantLockExample3();
        Thread t1 = new Thread(re," 1 號線程 ");
        Thread t2 = new Thread(re," 2 號線程 ");
        Thread t3 = new Thread(re," 3 號線程 ");
        Thread t4 = new Thread(re," 4 號線程 ");
        t1.start();
        t2.start();
        t3.start();
        t4.start();

        /*
        執(zhí)行結(jié)果:

        1 號線程  獲得鎖 1
        2 號線程  獲得鎖 2
        3 號線程  獲得鎖 3
        4 號線程  獲得鎖 4
        1 號線程  獲得鎖 5
        2 號線程  獲得鎖 6
        3 號線程  獲得鎖 7
        4 號線程  獲得鎖 8
        .....
        4 號線程  獲得鎖 16
        1 號線程  獲得鎖 17
        2 號線程  獲得鎖 18
        3 號線程  獲得鎖 19
        4 號線程  獲得鎖 20
        */

    }
}
ReentrantLock的以上幾個重要的方法

lock() 獲取鎖,如果鎖被占用,則等待

lockInterruptibly() 獲取鎖,但優(yōu)先響應(yīng)中斷

tryLock() 嘗試獲取鎖,如果成功返回true,否則返回false。該方法不等待,立即返回。

tryLock(long time,TimeUnit unit) 在給定時間內(nèi)獲取鎖。

unlock() 釋放鎖。

就重入鎖實現(xiàn)來看,它主要集中在Java 層面。在重入鎖實現(xiàn)中,主要包含三個要素:

原子狀態(tài)。原子狀態(tài)使用CAS操作來存儲當(dāng)前鎖的狀態(tài),判斷鎖是否已經(jīng)被別的線程持有。

等待隊列。所有沒有請求成功的線程都進(jìn)入等待隊列進(jìn)行等待。當(dāng)有線程釋放鎖后,系統(tǒng)就從當(dāng)前等待隊列中喚醒一個線程繼續(xù)工作。

阻塞原語park()和unpack(),用來掛起和恢復(fù)線程。沒有得到鎖的線程將會被掛起。

重入鎖的好搭檔:Condition條件

如果大家理解了Object.wait()Object.notify()方法的話,就能很容易地理解Condition對象了。它和wait()和notify()方法的作用是大致相同的。但是wait()方法和notify()方法是和synchronized關(guān)鍵字合作使用的,而Condtion是與重入鎖相關(guān)聯(lián)的。通過Lock接口(重入鎖就實現(xiàn)了這個接口)的Condtion newCondition()方法可以生成一個與當(dāng)前重入鎖綁定的Condition實例。利用Condition對象,我們就可以讓線程在合適的時間等待,或者在某一個特定的時刻得到通知,繼續(xù)執(zhí)行。

Condition接口提供的基本方法如下:

void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();

以上方法含義如下

await()方法會使當(dāng)前線程等待,同時釋放當(dāng)前鎖,當(dāng)其他線程中使用signal()或者signalAll()方法時候,線程會重新獲得鎖并繼續(xù)執(zhí)行。或者當(dāng)線程被中斷時,也能跳出等待。這和Object.wait()方法很相似。

awaitUninterruptibly()await()方法類似,但它不會再等待過程中響應(yīng)中斷。

singal() 用于喚醒一個等待隊列中的線程。singalAll()是喚醒所有等待線程。

public class ConditionExample implements Runnable{
    public static ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();


    @Override
    public void run() {

        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " 獲取到鎖...");
            //等待
            condition.await();
            System.out.println(Thread.currentThread().getName() + " 執(zhí)行完成");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //釋放鎖
            lock.unlock();
            System.out.println(Thread.currentThread().getName() + " 釋放鎖");
        }


    }

    public static void main(String[] a) throws InterruptedException {
        ConditionExample re = new ConditionExample();
        Thread t1 = new Thread(re,"1 號線程 ");
        t1.start();
        //主線程sleep,1號線程會一直等待.直到獲取到1號線程的鎖資源,并將其喚醒.
        Thread.sleep(2000);
        //獲得鎖
        lock.lock();
        //喚醒前必須獲得當(dāng)前資源對象的鎖
        condition.signal();
        //釋放鎖
        lock.unlock();

    }
}
ReadWriteLock讀寫鎖

ReadWriteLock是JDK5中提供的讀寫分離鎖。讀寫分離鎖可以有效地幫助減少鎖競爭,以提升系統(tǒng)性能。用鎖分離的機(jī)制來提升性能非常容易理解,比如線程A1、A2、A3進(jìn)行寫操作,B1、B2、B3進(jìn)行讀操作,如果使用重入鎖或者內(nèi)部鎖,則理論上說所有讀之間、讀與寫之間、寫和寫之間都是串行操作。當(dāng)B1進(jìn)行讀取時,B2、B3則需要等待鎖。由于讀操作并不對數(shù)據(jù)的完整性造成破壞,這種等待顯然是不合理的。因此,讀寫鎖就有了發(fā)揮功能的余地。

在這種情況下,讀寫鎖運行多個線程同時讀。但是考慮到數(shù)據(jù)完整性,寫寫操作和讀寫操作間依然是需要互相等待和持有鎖的??偟膩碚f,讀寫鎖的訪問約束如下表。

非阻塞 阻塞
阻塞 阻塞

如果在系統(tǒng)中,讀的次數(shù)遠(yuǎn)遠(yuǎn)大于寫的操作,讀寫鎖就可以發(fā)揮最大的功效,提升系統(tǒng)的性能。

栗子:

public class ReadWriteLockExample {

    //創(chuàng)建普通重入鎖
    private static Lock lock = new ReentrantLock();

    //創(chuàng)建讀寫分離鎖
    private static ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();

    //創(chuàng)建讀鎖
    private static Lock readLock = rwlock.readLock();

    //創(chuàng)建寫鎖
    private static Lock writeLock = rwlock.writeLock();

    private  int value;

    public Object HandleRead(Lock lock) throws InterruptedException {
        try {
            //上鎖
            lock.lock();
            //模擬處理業(yè)務(wù)
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " Read...");
            return value;
        } finally {
            //釋放鎖
            lock.unlock();
        }
    }

    public void HandleWrite(Lock lock,int index) throws InterruptedException {
        try {
            lock.lock();
            Thread.sleep(1000);
            value = index;
            System.out.println(Thread.currentThread().getName() + " Write...");
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] a ) throws InterruptedException {
        final ReadWriteLockExample rwle = new ReadWriteLockExample();

        //創(chuàng)建讀方法
        Runnable readR = new Runnable() {
            @Override
            public void run() {
                try {
                    //rwle.HandleRead(lock); //普通鎖
                    rwle.HandleRead(readLock);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        //創(chuàng)建寫方法
        Runnable writeR = new Runnable() {
            @Override
            public void run() {
                try {
                    //rwle.HandleWrite(lock,new Random().nextInt()); //普通鎖
                    rwle.HandleWrite(writeLock,new Random().nextInt());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        //18次讀
        for (int i=0;i<18;i++){
            Thread s = new Thread(readR);
            s.start();
        }
        //2次寫
        for (int i=18;i<20;i++){
            Thread s = new Thread(writeR);
            s.start();
        }

        /**
         * 結(jié)論:
         *
         * 用普通鎖運行,大約執(zhí)行20秒左右
         *
         * 用讀寫分離鎖,大約執(zhí)行3秒左右
         *
         */

    }

}

在讀鎖和寫鎖之間的交互可以采用多種實現(xiàn)方式。ReadWriteLock中的一些可選實現(xiàn)包括:

釋放優(yōu)先:當(dāng)一個寫入操作釋放寫入鎖時,并且隊列中同時存在讀線程和寫線程,那么應(yīng)該優(yōu)先選擇哪一個線程。

讀線程插隊:如果鎖是由讀線程持有,但是寫線程還在等待,是否允許新到的讀線程獲得訪問權(quán),還是應(yīng)在寫線程后面等待?若允許的話可以提高并發(fā)性但是可能造成寫線程的饑餓。

重入性:讀取鎖和寫入鎖是否可重入。

降級和升級:若一個線程持有寫鎖可否在繼續(xù)持有寫鎖的狀態(tài)下獲取讀鎖?這可能會使寫鎖“降級”為讀鎖。讀鎖是否優(yōu)先于其它正在等待的讀線程和寫線程而升級為一個寫鎖?在大多數(shù)讀寫鎖實現(xiàn)中不支持“升級”,因為這樣容易死鎖(兩個讀線程試圖同時升級為寫鎖,那么二者都不會釋放寫鎖)。

閉鎖

閉鎖是一種同步工具類,可以延遲線程的進(jìn)度直到其到達(dá)終止?fàn)顟B(tài)。閉鎖的作用相當(dāng)于一扇門:在閉鎖到達(dá)結(jié)束狀態(tài)之前,這扇門一直是關(guān)閉的,并且沒有任何線程能通過,當(dāng)?shù)竭_(dá)結(jié)束狀態(tài)時,這扇門會打開并允許所有的線程通過。當(dāng)閉鎖到達(dá)結(jié)束狀態(tài)后,將不會再改變狀態(tài),因此這扇門將永遠(yuǎn)保持打開狀態(tài)。閉鎖可以用來確保某些活動直到其他活動都完成才繼續(xù)執(zhí)行,例如:

確保某個計算在其需要的所有資源都被初始化后才繼續(xù)執(zhí)行。二元閉鎖(包括兩個狀態(tài))可以用來表示“資源R已經(jīng)被初始化”,而所有需要R的操作都必須先在這個比鎖上等待。

確保某個服務(wù)在其依賴的所有其他服務(wù)都已經(jīng)啟動之后才啟動。每個服務(wù)都有一個相關(guān)的二元閉鎖。當(dāng)啟動服務(wù)S時,將首先在S依賴的其他服務(wù)的閉鎖上等待,在所有依賴的服務(wù)都啟動后會釋放閉鎖S,這樣其他依賴S的服務(wù)才能繼續(xù)執(zhí)行。

等待直到某個操作的所有參與者(例如,在多玩家游戲中的所有玩家)都就緒再繼續(xù)執(zhí)行。在這種情況中,當(dāng)所有的玩家都執(zhí)行就緒時,閉鎖將到達(dá)結(jié)束狀態(tài)。

CountDownLatch

CountDownLatch 就是一種靈活的閉鎖實現(xiàn),可以在上述的各種情況中使用,它可以使一個或多個線程等待一組時間發(fā)生CountDown在英文中意為倒計時,Latch為門閂。閉鎖的狀態(tài)包括一個計數(shù)器,該計數(shù)器被初始化為一個正數(shù),表示需要等待的事情數(shù)量。countDown方法遞減計數(shù)器,表示有一個事件已經(jīng)發(fā)生了,而await方法等待計數(shù)器達(dá)到零,這表示所有需要等待的事情都已經(jīng)發(fā)生。如果計數(shù)器的值是非零,那么await會一直阻塞直到計數(shù)器為零,或者等待中的線程中斷,或者等待超時。因此,這個工具通常用來控制線程等待,它可以讓某一個線程等待直到倒計時結(jié)束,再開始執(zhí)行。

CountDownLatch的構(gòu)造函數(shù)接收一個整數(shù)作為參數(shù),即當(dāng)前這個計數(shù)器的技術(shù)個數(shù)。

public CountDownLatch(int count)

下面這個簡單的示例,演示了CountDownLatch的使用。

public class CountDownLatchExample implements Runnable{

    static final CountDownLatch cdl = new CountDownLatch(10);
    static final CountDownLatchExample cdle = new CountDownLatchExample();

    @Override
    public void run() {
        try {
            Thread.sleep(new Random().nextInt(10) * 1000);
            System.out.println(Thread.currentThread().getName() + " 部件檢查完畢...");
            //一個線程完成工作,倒計時器減1
            cdl.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] a) throws InterruptedException {
        ExecutorService exec = Executors.newFixedThreadPool(10);
        for (int i=0;i<10;i++){
            exec.submit(cdle);
        }
        //等待所有線程完成,主線程才繼續(xù)執(zhí)行
        cdl.await();
        System.out.println(Thread.currentThread().getName() + " 所有檢查完成,上跑道起飛...");
        //關(guān)閉線程池
        exec.shutdown();
    }
}
FutureTask

FutureTask也可以用做閉鎖。FutureTask表示的計算是通過Callable來實現(xiàn)的,相當(dāng)于一種可生成結(jié)果的Runnable,并且可以處于以下3種狀態(tài):

等待運行(Waiting to run)

正在運行(Running)

運行完成(Completed)

Future.get的行為取決于任務(wù)的狀態(tài)。如果任務(wù)已經(jīng)完成,那么get會立即返回結(jié)果,否則get將阻塞直到任務(wù)進(jìn)入完成狀態(tài),然后返回結(jié)果或者拋出異常。FutureTask將計算結(jié)果從執(zhí)行的計算的線程傳遞到獲取這個結(jié)果的線程,而FutureTask的規(guī)劃確保了這種傳遞過程能夠?qū)崿F(xiàn)結(jié)果的安全發(fā)布。

FutureTask在ExeCutor中表示異步任務(wù),此外還可以用來表示一些時間較長的計算,這些計算可以在使用計算結(jié)果之前啟動。

信號量

技術(shù)信號量(Counting Semaphore)用來控制同時訪問某個特定資源的操作數(shù)量,或者同時執(zhí)行某個指定操作的數(shù)量。計數(shù)信號量還可以用來實現(xiàn)某種資源池,或者對容器施加邊界。

信號量為多線程協(xié)作提供了更為強(qiáng)大的控制方法。廣義上說,信號量是對鎖的擴(kuò)展。無論是內(nèi)部鎖synchronized還是重入鎖ReentrantLock,一次都只允許一個線程訪問一個資源,而信號量卻可以指定多個線程,同時訪問某一個資源。信號量主要提供了以下構(gòu)造函數(shù):

public Semaphore(int permits)
public Semaphore(int permits,boolean fair) //第二個參數(shù)可以指定是否公平

在構(gòu)造信號量對象時,必須要指定信號量的準(zhǔn)入數(shù),即同時能申請多少個許可。當(dāng)每個線程每次只申請一個許可時,這就相當(dāng)于指定了同時有多少個線程可以訪問某一個資源。信號量的主要邏輯方法有:

public void acquire() //嘗試獲得一個準(zhǔn)入的許可。若無法獲得,則線程會等待,直到有線程釋放一個許可或者當(dāng)前線程被中斷
public void acquireUninterruptibly()//和acquire()類似,但是不響應(yīng)中斷
public boolean tryAcquire()//嘗試獲得一個許可,成功true失敗fasle,不會等待,立刻返回
public boolean tryAcquire(long timeout,TimeUnit unit)
public void release()//線程訪問資源結(jié)束后,釋放一個許可,以使其他等待許可的線程進(jìn)行資源訪問

栗子:

public class SemaphoreExample implements Runnable {

    //指定信號量,同時可以有5個線程訪問資源
    public static final Semaphore s = new Semaphore(5);

    @Override
    public void run() {

        try {
            //申請信號量,也可以直接使用 s.acquire();
            if (s.tryAcquire(1500, TimeUnit.SECONDS)) {
                //模擬耗時操作
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + " 完成了任務(wù)..");
                //離開時必須釋放信號量,不然會導(dǎo)致信號量泄露——申請了但沒有釋放
                s.release();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] a) throws InterruptedException {
       //申請20個線程
        ExecutorService exec = Executors.newFixedThreadPool(20);
        final SemaphoreExample re = new SemaphoreExample();
        for (int i=0;i<20;i++){
            exec.submit(re);
        }
        exec.shutdown();
    }
}

Semaphore中管理者一組虛擬的許可(permit),許可的初始數(shù)量可通過構(gòu)造函數(shù)來指定。在執(zhí)行操作時可以先獲得許可(只要還有剩余的許可),并在使用以后釋放許可。如果沒有許可,那么acquire將阻塞直到有許可(或者被中斷或者操作超時)。release方法將返回一個許可給信號量。計算信號量的一種簡化形式是二值信號量,即初始值為1的Semaphore。二值信號量可以用做互斥體(mutex),并具備不可重入的加鎖語義:誰擁有這個唯一的許可,誰就擁有了互斥鎖。

同樣,我們也可以使用Semaphore將任何一種容器變成有界阻塞容器。

CyclicBarrier

和之前的CountDownLatch類似,它(循環(huán)柵欄)也可以實現(xiàn)線程間的技術(shù)等待,但它的功能比CountDownLatch更加復(fù)雜強(qiáng)大。它能阻塞一組線程直到某個事件發(fā)生。因此,柵欄可以用于實現(xiàn)一些協(xié)議,例如“開會一定要在xx地方集合,等其他人到了再討論下一步要做的事情”。

CyclicBarrier的使用場景也很豐富。比如,司令下達(dá)命令,要求10個士兵一起去完成一項任務(wù)。這時,就會要求10個士兵先集合報道,接著,一起雄赳赳氣昂昂地執(zhí)行任務(wù)。當(dāng)10個士兵把自己手頭的任務(wù)都執(zhí)行完成了,那么司令才能對外宣布,任務(wù)完成!

下面的栗子使用CyclicBarrier演示了上述司機(jī)命令士兵完成任務(wù)的場景。

public class CyclicBarrierExample {

    public static class Soldier implements Runnable{

        private String name;
        private CyclicBarrier cyclicBarrier;

        public Soldier(String name, CyclicBarrier cyclicBarrier) {
            this.name = name;
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            try {
                System.out.println(name + " 來報道..");
                //等待所有士兵到齊
                cyclicBarrier.await();
                doWork();
                //等待所有士兵完成任務(wù)
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }

        void doWork(){
            try {
                Thread.sleep(new Random().nextInt(10) * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name + " 任務(wù)已完成..");
        }
    }

    public static class doOrder implements Runnable{

        boolean flag;
        int n;

        public doOrder(boolean flag, int n) {
            this.flag = flag;
            this.n = n;
        }

        @Override
        public void run() {
            if (flag){
                System.out.println("司令 : 士兵 " + n +"個 任務(wù)完成");
            }else {
                System.out.println("司令 : 士兵 " + n +"個 集合完畢");
                //執(zhí)行完后 改變完成標(biāo)記.當(dāng)下一次調(diào)用doOrder時,可以進(jìn)入if
                flag = true;
            }
        }
    }

    public static void main(String[] a){
        final int n = 10;
        //是否完成了任務(wù)
        boolean flag = false;

        //創(chuàng)建10個士兵線程
        Thread[] allSoldier = new Thread[n];
        //創(chuàng)建CyclicBarrier實例
        //這里的意思是,等待10個線程都執(zhí)行完,就執(zhí)行doOrder()方法
        CyclicBarrier c = new CyclicBarrier(n, new doOrder(flag,n));
        for (int i=0;i
優(yōu)化鎖

一般我們對于鎖的優(yōu)化有以下幾個大致方向:

減少鎖的持有時間

減少鎖的請求頻率

使用帶有協(xié)調(diào)機(jī)制的獨占鎖,這些機(jī)制允許更高的并發(fā)性

鎖分段技術(shù)

在某些情況下,可以將鎖分解技術(shù)進(jìn)一步擴(kuò)展為對一組獨立對象上的鎖進(jìn)行分解,這種情況被稱為鎖分段。例如,在ConcurrentHashMap的實現(xiàn)中使用了一個包含16個鎖的數(shù)組,每個鎖保護(hù)所有散列桶的1/16,其中第N個散列桶由第(N mod 16)個鎖來保護(hù)。假設(shè)散列函數(shù)具有合理的分布性,并且關(guān)鍵字能夠均勻分布,那么這大約能把對于鎖的請求減少到原來的1/16,正是這項技術(shù)使得ConcurrentHashMap能夠支持多達(dá)16個并發(fā)的寫入器。(要使得擁有大量處理器的系統(tǒng)在高訪問量的情況下實現(xiàn)更高的并發(fā)性,還可以進(jìn)一步增加鎖的數(shù)量,但僅當(dāng)你能證明并發(fā)寫入線程的競爭足夠激烈并需要突破這個限制時,才能將鎖分段的數(shù)量超過默認(rèn)的16個。)

另一個典型的案例就是LinkedBlockingQueue的實現(xiàn)。
take()和put()方法雖然都對隊列進(jìn)行了修改操作,但由于是鏈表,因此,兩個操作分別作用于隊列的前端和末尾,理論上兩者并不沖突。使用獨占鎖,則要求在進(jìn)行take和put操作時獲取當(dāng)前隊列的獨占鎖,那么take和put就不可能真正的并發(fā),他們會彼此等待對方釋放鎖。在JDK的實現(xiàn)中,取而代之的是兩把不同的鎖,分離了take和put操作.削弱了競爭的可能性.實現(xiàn)類取數(shù)據(jù)和寫數(shù)據(jù)的分離,實現(xiàn)了真正意義上成為并發(fā)操作。

鎖分段的一個劣勢在于:與采用單個鎖來實現(xiàn)獨占訪問相比,要獲取多個鎖來實現(xiàn)獨占訪問將更加困難并且開銷更高。通常,在執(zhí)行一個操作時最多只需獲取一個鎖,但在某些情況下需要加鎖整個容器,例如當(dāng)ConcurrentHashMap需要擴(kuò)展映射范圍,以及重新計算鍵值的散列值要分布到更大的桶集合中時,就需要獲取分段鎖集合中的所有鎖。

避免熱點域

鎖分解和鎖分段技術(shù)都能提高可伸縮性,因為它們都能使不同的線程在不同的數(shù)據(jù)(或者同一個數(shù)據(jù)的不同部分)上操作,而不會相互干擾。如果程序采用鎖分段或分解技術(shù),那么一定要表現(xiàn)出在鎖上的競爭頻率高于在鎖保護(hù)的數(shù)據(jù)上發(fā)生競爭的頻率。如果一個鎖保護(hù)兩個獨立變量X和Y,并且線程A想要訪問X,而線程B想要訪問Y(這類似于在ServerStatus中,一個線程調(diào)用addUser,而另一個線程調(diào)用addQuery),那么這兩個線程不會在任何數(shù)據(jù)上發(fā)生競爭,即使它們會在同一個鎖上發(fā)生競爭。

當(dāng)每個操作都請求多個變量時,鎖的粒度將很難降低。這是在性能與可伸縮性之間相互制衡的另一個方面,一些常見的優(yōu)化措施,例如將一些反復(fù)計算的結(jié)果緩存起來,都會引入一些”熱點域“,而這些熱點域往往會限制可伸縮性。

當(dāng)實現(xiàn)HashMap時,你需要考慮如何在size方法中計算Map中的元素數(shù)量。最簡單的方法就是,在每次調(diào)用時都統(tǒng)計一次元素的數(shù)量。一種常見的優(yōu)化措施是,在插入和移除元素時更新一個計數(shù)器,雖然這在put和remove等方法中略微增加了一些開銷,以確保計數(shù)器是最新的值,但這把size方法的開銷從O(n)降低到O(1)。

在單線程或者采用完全同步的實現(xiàn)中,使用一個獨立的計算器能很好地提高類似size和isEmpty這些方法的執(zhí)行速度,但卻導(dǎo)致更難以提升實現(xiàn)的可伸縮性,因為每個修改map的操作都需要更新這個共享的計數(shù)器。即使使用鎖分段技術(shù)來實現(xiàn)散列鏈,那么在對計數(shù)器的訪問進(jìn)行同步時,也會重新導(dǎo)致在使用獨占鎖時存在的可伸縮性問題。一個看似性能優(yōu)化的措施——緩存size操作的結(jié)果,已經(jīng)變成了一個可伸縮性問題。在這種情況下,計數(shù)器也被稱為熱點域,因為每個導(dǎo)致元素數(shù)量發(fā)生變化的操作都需要訪問它。
為了避免這個問題,ConcurrentHashMap中的size將對每個分段進(jìn)行枚舉并將每個分段中的元素數(shù)量相加,而不是維護(hù)一個全局計數(shù)。為了避免枚舉每個元素,ConcurrentHashMap為每個分段都維護(hù)一個獨立的計數(shù),并通過每個分段的鎖來維護(hù)這個值。

一些替代獨占鎖的方法

第三種降低競爭鎖的影響的技術(shù)就是放棄使用獨占鎖,從而有助于使用一種友好并發(fā)的方式來管理共享狀態(tài)。例如,使用并發(fā)容器、讀-寫鎖、不可變對象以及原子變量。

ReadWriteLock實現(xiàn)了一種在多個讀取操作以及單個寫入操作情況下的加鎖規(guī)則:如果多個讀取操作都不會修改共享資源,那么這些讀取操作可以同時訪問該共享資源,但在執(zhí)行寫入操作時必須以獨占方式來獲取鎖。對于讀取操作占多數(shù)的數(shù)據(jù)結(jié)構(gòu),ReadWriteLock能夠提供比獨占鎖更高的并發(fā)性。而對于只讀的數(shù)據(jù)結(jié)構(gòu),其中包含的不變性可以完全不需要加鎖操作。

原子變量提供了一種方式來降低更新“熱點域”時的開銷,例如競態(tài)計數(shù)器、序列發(fā)生器、或者對鏈表數(shù)據(jù)結(jié)構(gòu)中頭節(jié)點的引用。原子變量類提供了在整數(shù)或者對象引用上的細(xì)粒度原子操作(因此可伸縮性更高),并使用了現(xiàn)代處理器中提供的底層并發(fā)原語(例如比較并交換)。如果在類中只包含少量的熱點域,并且這些域不會與其他變量參與到不變性條件中,那么用原子變量來替代他們能提高可伸縮性。(通過減少算法中的熱點域,可以提高可伸縮性——雖然原子變量能降低熱點域的更新開銷,但并不能完全消除。)

來自JVM的鎖優(yōu)化 鎖粗化

如果對一個鎖不停地進(jìn)行請求,同步和釋放,其本身也會消耗系統(tǒng)寶貴的資源,反而不利于性能優(yōu)化.
虛擬機(jī)在遇到需要一連串對同一把鎖不斷進(jìn)行請求和釋放操作的情況時,便會把所有的鎖操作整合成對鎖的一次請求,從而減少對鎖的請求同步次數(shù),這就是鎖的粗化。

鎖偏向

偏向鎖是一種針對加鎖操作的優(yōu)化手段,他的核心思想是:如果一個線程獲得了鎖,那么鎖就進(jìn)行偏向模式.當(dāng)這個線程再次請求鎖時,無需再做任何同步操作.這樣就節(jié)省了大量操作鎖的動作,從而提高程序性能.

因此,對于幾乎沒有鎖競爭的場合,偏向鎖有比較好的優(yōu)化效果.因為極有可能連續(xù)多次是同一個線程請求相同的鎖.而對于鎖競爭激烈的程序,其效果不佳.

使用Java虛擬機(jī)參數(shù):-XX:+UseBiasedLocking 可以開啟偏向鎖.

輕量級鎖

如果偏向鎖失敗,虛擬機(jī)并不會立即掛起線程.它還會使用一種稱為輕量級的鎖的優(yōu)化手段.輕量級鎖只是簡單的將對象頭部作為指針,指向持有鎖的線程堆棧內(nèi)部,來判斷一個線程是否持有對象鎖.如果線程獲得輕量鎖成功,則可以順利進(jìn)入臨界區(qū).如果失敗,則表示其他線程爭搶到了鎖,那么當(dāng)前線程的鎖請求就會膨脹為重量級鎖.

自旋鎖

鎖膨脹后,虛擬機(jī)為了避免線程真實的在操作系統(tǒng)層面掛起,虛擬機(jī)還做了最后的努力就是自旋鎖.如果一個線程暫時無法獲得索,有可能在幾個CPU時鐘周期后就可以得到鎖,
那么簡單粗暴的掛起線程可能是得不償失的操作.虛擬機(jī)會假設(shè)在很短時間內(nèi)線程是可以獲得鎖的,所以會讓線程自己空循環(huán)(這便是自旋的含義),如果嘗試若干次后,可以得到鎖,那么久可以順利進(jìn)入臨界區(qū),
如果還得不到,才會真實地講線程在操作系統(tǒng)層面掛起.

鎖消除

鎖消除是一種更徹底的鎖優(yōu)化,Java虛擬機(jī)在JIT編譯時,通過對運用上下文的掃描,去除不可能存在的共享資源競爭鎖,節(jié)省毫無意義的資源開銷.

我們可能會問:如果不可能存在競爭,為什么程序員還要加上鎖呢?

在Java軟件開發(fā)過程中,我們必然會用上一些JDK的內(nèi)置API,比如StringBuffer、Vector等。你在使用這些類的時候,也許根本不會考慮這些對象到底內(nèi)部是如何實現(xiàn)的。比如,你很有可能在一個不可能存在并發(fā)競爭的場合使用Vector。而周所眾知,Vector內(nèi)部使用了synchronized請求鎖,如下代碼:

public String [] createString(){
  Vector v = new Vector();
  for (int i =0;i<100;i++){
    v.add(Integer.toString(i));
  }
  return v.toArray(new String[]{});
}

上述代碼中的Vector,由于變量v只在createString()函數(shù)中使用,因此,它只是一個單純的局部變量。局部變量是在線程棧上分配的,屬于線程私有的數(shù)據(jù),因此不可能被其他線程訪問。所以,在這種情況下,Vector內(nèi)部所有加鎖同步都是沒有必要的。如果虛擬機(jī)檢測到這種情況,就會將這些無用的鎖操作去除。

鎖消除設(shè)計的一項關(guān)鍵技術(shù)是逃逸分析,就是觀察某個變量是否會跳出某個作用域(比如對Vector的一些操作).在本例中,變量v顯然沒有逃出createString()函數(shù)之外。以次為基礎(chǔ),虛擬機(jī)才可以大膽將v內(nèi)部逃逸出當(dāng)前函數(shù),也就是說v有可能被其他線程訪問。如果是這樣,虛擬機(jī)就不能消除v中的鎖操作。

逃逸分析必須在-server模式下進(jìn)行,可以使用-XX:+DoEscapeAnalysis參數(shù)打開逃逸分析。使用-XX:+EliminateLocks參數(shù)可以打開鎖消除。

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/66286.html

相關(guān)文章

  • Java并發(fā)編程筆記

    摘要:本文探討并發(fā)中的其它問題線程安全可見性活躍性等等。當(dāng)閉鎖到達(dá)結(jié)束狀態(tài)時,門打開并允許所有線程通過。在從返回時被叫醒時,線程被放入鎖池,與其他線程競爭重新獲得鎖。 本文探討Java并發(fā)中的其它問題:線程安全、可見性、活躍性等等。 在行文之前,我想先推薦以下兩份資料,質(zhì)量很高:極客學(xué)院-Java并發(fā)編程讀書筆記-《Java并發(fā)編程實戰(zhàn)》 線程安全 《Java并發(fā)編程實戰(zhàn)》中提到了太多的術(shù)語...

    NickZhou 評論0 收藏0
  • Java線程&高并發(fā)

    摘要:線程啟動規(guī)則對象的方法先行發(fā)生于此線程的每一個動作。所以局部變量是不被多個線程所共享的,也就不會出現(xiàn)并發(fā)問題。通過獲取到數(shù)據(jù),放入當(dāng)前線程處理完之后將當(dāng)前線程中的信息移除。主線程必須在啟動其他線程后立即調(diào)用方法。 一、線程安全性 定義:當(dāng)多個線程訪問某個類時,不管運行時環(huán)境采用何種調(diào)度方式,或者這些線程將如何交替執(zhí)行,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同,這個類都能表現(xiàn)出正確的行...

    SQC 評論0 收藏0
  • Java程序員工作3年,薪資為何會被應(yīng)屆生倒掛?

    摘要:同時也會關(guān)注市場上同崗位薪資,以便對企業(yè)內(nèi)部薪資結(jié)構(gòu)做出相應(yīng)調(diào)整。一般來說,相同崗位和職責(zé)的員工,薪資低于市場不超過,都屬于合理范疇,因為一個員工不會為了的薪酬而跳槽。同時,還能激勵員工自我提升,以獲得相應(yīng)技能市場所給予的報酬。 各位職場人都聽說過薪資倒掛這詞兒吧,這個情況在行業(yè)內(nèi)早就不是什...

    szysky 評論0 收藏0
  • 用CountDownLatch提升請求處理速度

    摘要:是多線程包里的一個常見工具類,通過使用它可以借助線程能力極大提升處理響應(yīng)速度,且實現(xiàn)方式非常優(yōu)雅。主線程處于狀態(tài),直到的值數(shù)減到,則主線程繼續(xù)執(zhí)行。此時必須使用線程池,并限定最大可處理線程數(shù)量,否則服務(wù)器不穩(wěn)定性會大福提升。 countdownlatch是java多線程包concurrent里的一個常見工具類,通過使用它可以借助線程能力極大提升處理響應(yīng)速度,且實現(xiàn)方式非常優(yōu)雅。今天我們...

    oujie 評論0 收藏0
  • Java 線程并發(fā)編程面試筆錄一覽

    摘要:創(chuàng)建線程的方式方式一將類聲明為的子類。將該線程標(biāo)記為守護(hù)線程或用戶線程。其中方法隱含的線程為父線程?;謴?fù)線程,已過時。等待該線程銷毀終止。更多的使當(dāng)前線程在鎖存器倒計數(shù)至零之前一直等待,除非線 知識體系圖: showImg(https://segmentfault.com/img/bVbef6v?w=1280&h=960); 1、線程是什么? 線程是進(jìn)程中獨立運行的子任務(wù)。 2、創(chuàng)建線...

    bitkylin 評論0 收藏0

發(fā)表評論

0條評論

skinner

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<