多線程鎖的分類學(xué)習(xí)

1. 公平鎖和非公平鎖

  • 定義:
    • 公平鎖:多個(gè)線程按照申請鎖的順序來獲取鎖,按照FIFO規(guī)則從等待隊(duì)列中拿到等待線程獲取相應(yīng)鎖
    • 非公平鎖:多個(gè)線程并不是按照申請鎖的順序來獲取鎖,有可能出現(xiàn)后申請鎖的線程先申請到鎖崔泵。在高
      并發(fā)環(huán)境下,非公平鎖有可能造成 優(yōu)先級反轉(zhuǎn) 或者 饑餓 的現(xiàn)象。如果非公平鎖搶占失敗,就要繼續(xù)采取類似公平鎖的機(jī)制癌蚁。非公平鎖的優(yōu)點(diǎn)在于吞吐量大。
  • 常見的非公平鎖:
    • ReentrantLock可以通過指定構(gòu)造函數(shù)的boolean類型來獲取公平/非公平鎖兜畸,默認(rèn)情況下是非公平鎖
    • 對于Synchronized而言努释,也是一種非公平鎖

2 可重入鎖(遞歸鎖)

  • 定義:可重入鎖的定義要類比遞歸的定義來理解。指在同一個(gè)線程外層函數(shù)獲得鎖之后咬摇,內(nèi)層遞歸函數(shù)仍然能夠獲取該鎖的代碼伐蒂,
    即進(jìn)入內(nèi)層函數(shù)時(shí)會自動獲取鎖。也就是說肛鹏,線程可以進(jìn)入任何一個(gè)它已經(jīng)擁有的鎖所同步的代碼塊逸邦。一個(gè)同步方法內(nèi)部仍然存在一個(gè)同步方法,那么可以進(jìn)入內(nèi)層同步方法在扰,且內(nèi)存同步方法和外層同步方法持有的是同一把鎖缕减。

具體看一個(gè)案例來理解可重入鎖:synchronized就是可重入鎖,現(xiàn)在問題是synchronized塊中能夠使用System.out.println()方法健田?

public void println(String x) {
    // println方法內(nèi)部使用了synchronized
    synchronized (this) {
        print(x);
        newLine();
    }
}

/**
 * 演示可重入鎖
 *
 * @author sherman
 */
public class LockReentrantDemo1 {
    public static void main(String[] args) {
        // 程序正常運(yùn)行輸出:hello
        new LockReentrantDemo1().lockReentrant();
    }

    public synchronized void lockReentrant() {
        /**
         * 注意這個(gè)println方法內(nèi)部就使用了synchronized關(guān)鍵字烛卧,鎖住了this
         * 即synchronized塊中仍然能夠使用synchronized關(guān)鍵字 -> 可重入的
         */
        System.out.println("hello");
    }
}

可重入鎖的意義有一點(diǎn)類似于事務(wù)的傳播行為(一個(gè)方法運(yùn)行在另一個(gè)開啟事務(wù)的方法中,那么當(dāng)前方法的事務(wù)行為是什么樣的妓局?)总放,類比來說可重入鎖意義就是:一個(gè)synchronized(鎖)塊運(yùn)行在另一個(gè)synchronized(塊)中,那么當(dāng)前synchronized的具體表現(xiàn)行為是什么好爬,是直接中斷局雄?還是阻塞等待?又或者是正常執(zhí)行存炮,因?yàn)閮蓚€(gè)synchronized鎖住的是同一個(gè)對象炬搭?

可重入鎖的含義就是最后一種:正常執(zhí)行,因?yàn)榭芍厝腈i穆桂,鎖的是同一個(gè)對象宫盔。

  • 典型的可重入鎖:ReentrantLock & synchronized關(guān)鍵字
  • 作用:最大作用就是防止死鎖,因?yàn)槎鄬忧短椎逆i享完,其實(shí)鎖的是同一個(gè)對象灼芭,另一個(gè)含義就是:嵌套方法持有的是同一把鎖

具體示例:

/**
 * 可重入鎖演示
 *
 * @author sherman
 */
// 演示ReentrantLock是可重入的
class ShareResouce implements Runnable {
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        get();
    }

    private void get() {
        lock.lock();
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + ": get()");
            set();
        } finally {
            lock.unlock();
        }
    }

    private void set() {
        lock.lock();
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + ": set()");
        } finally {
            lock.unlock();
        }
    }
}

public class LockReentrantDemo2 {
    // outer()和inner()方法演示synchronized是可重入的
    private synchronized void outer() {
        System.out.println(Thread.currentThread().getName() + ": outer method()");
        inner();
    }

    // outer()和inner()方法演示synchronized是可重入的
    private synchronized void inner() {
        System.out.println(Thread.currentThread().getName() + ": inner method()");
    }

    public static void main(String[] args) {
        // 驗(yàn)證synchronized是可重入的
        LockReentrantDemo2 lrd = new LockReentrantDemo2();
        new Thread(lrd::outer, "thread-1").start();
        new Thread(lrd::outer, "thread-2").start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 驗(yàn)證ReentrantLock是可重入的
        System.out.println("===================");
        new Thread(new ShareResouce(), "thread-3").start();
        new Thread(new ShareResouce(), "thread-4").start();
    }
}

補(bǔ)充:

在使用ReentrantLock類演示可重入鎖時(shí),lock.lock()和lock.unlock()數(shù)量一定要匹配般又,否則:

  • 當(dāng)lock.lock()數(shù)量 > lock.unlock():程序一直運(yùn)行
  • 當(dāng)lock.lock()數(shù)量 < lock.unlock():拋出java.lang.IllegalMonitorStateException異常

3 自旋鎖(SpinLock)

自旋鎖嘗試獲取鎖的線程不會立即阻塞彼绷,而是采用循環(huán)的方式去嘗試獲取鎖,這樣的好處是避免線程上下文切換的消耗茴迁,缺點(diǎn)是如果一直自旋會消耗CPU:

/**
 * 自旋鎖演示
 *
 * @author sherman
 */
public class LockSpin {
    AtomicReference<Thread> ar = new AtomicReference<>();

    private void lock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + ": come in!");
        while (!ar.compareAndSet(null, thread)) {

        }
    }

    private void unlock() {
        Thread thread = Thread.currentThread();
        ar.compareAndSet(thread, null);
        System.out.println(thread.getName() + ": get out!");
    }

    public static void main(String[] args) throws InterruptedException {
        LockSpin lockSpin = new LockSpin();
        new Thread(() -> {
            lockSpin.lock();
            try {
                Thread.sleep(8000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lockSpin.unlock();
        }, "線程A").start();

        // 保證線程A先進(jìn)行獲取到鎖寄悯,讓線程B之后自旋
        Thread.sleep(1000);

        new Thread(() -> {
            lockSpin.lock();
            lockSpin.unlock();
        }, "線程B").start();
    }
}

4 讀寫鎖

  • 寫鎖(獨(dú)占鎖):指該鎖一次只能被一個(gè)線程所持有,ReentrantLock和Synchronized都是獨(dú)占鎖

  • 讀鎖(共享鎖):指該鎖可以被多個(gè)線程所持有

  • 讀鎖的共享鎖可保證并發(fā)讀是非常高效的堕义,讀寫猜旬、寫讀、寫寫的過程都是互斥的

    /**

    • 演示讀寫鎖

    • @author sherman
      */
      class Cache {
      private volatile HashMap<String, Object> map = new HashMap<>();
      private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

      public Object get(String key) {
      lock.readLock().lock();
      Object res = null;
      try {
      System.out.println(Thread.currentThread().getName() + ": 正在讀取+++");
      Thread.sleep(100);
      res = map.get(key);
      System.out.println(Thread.currentThread().getName() + ": 讀取完成---");
      } catch (InterruptedException e) {
      e.printStackTrace();
      } finally {
      lock.readLock().unlock();
      }
      return res;
      }

      public void put(String key, Object value) {
      lock.writeLock().lock();
      try {
      System.out.println(Thread.currentThread().getName() + ": 正在寫入>>>");
      Thread.sleep(1000);
      map.put(key, value);
      System.out.println(Thread.currentThread().getName() + ":寫入完成<<<");
      } catch (InterruptedException e) {
      e.printStackTrace();
      } finally {
      lock.writeLock().unlock();
      }
      }
      }

    public class LockReadWrite {
    public static void main(String[] args) {
    Cache cache = new Cache();

        // 寫入操作是被一個(gè)線程獨(dú)占的倦卖,一旦寫線程開始
        // 其它線程必須等待其完成后才能繼續(xù)執(zhí)行
        for (int i = 0; i < 10; i++) {
            final int tmp = i;
            new Thread(() -> cache.put(tmp + "", tmp + ""), String.valueOf(i)).start();
        }
    
        // 讀操作可以被多個(gè)線程持有
        // 其它線程不必等待當(dāng)前讀操作完成才操作
        for (int i = 0; i < 10; i++) {
            final int tmp = i;
            new Thread(() -> cache.get(tmp + ""), String.valueOf(i)).start();
        }
    }
    

    }

5 CountDownLatch

CountDownLatch是一個(gè)計(jì)數(shù)器閉鎖昔馋,它通過一個(gè)初始化定時(shí)器latch,在latch的值被減到0之前糖耸,其它線程都會被await()方法阻塞秘遏。

以模擬火箭發(fā)射過程解釋CountDownLatch使用:

/**
 * CountDownLatch模擬火箭發(fā)射過程:
 * 火箭發(fā)射之前需要十個(gè)線程進(jìn)行前期檢查工作,每個(gè)線程耗時(shí)0-4s嘉竟,
 * 只有10個(gè)線程對應(yīng)的檢查工作全部完成后邦危,火箭才能發(fā)射
 *
 * @author sherman
 */

public class CountDownLatchDemo implements Runnable {
    public static final int TASK_NUMBERS = 10;
    private static CountDownLatch cdl = new CountDownLatch(TASK_NUMBERS);

    public static void main(String[] args) throws InterruptedException {
        CountDownLatchDemo countDownLatchDemo = new CountDownLatchDemo();
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.submit(countDownLatchDemo);
        }
        cdl.await();
        System.out.println("檢查工作檢查完畢:fire!");
        executorService.shutdown();
    }

    @Override
    public void run() {
        try {
            // 模擬火箭發(fā)射前的各種檢查工作
            int millis = new Random().nextInt(5000);
            Thread.sleep(millis);
            System.out.println(Thread.currentThread().getName() + ":檢查完畢! 耗時(shí):" + millis + "ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 每次檢查完畢后都將計(jì)數(shù)器減1
            cdl.countDown();
        }
    }
}

6 CyclicBarrier

CyclicBarrier是可循環(huán)使用的屏障,它的功能是:讓一組線程到達(dá)一個(gè)屏障(同步點(diǎn))時(shí)被阻塞舍扰,直到最后一個(gè)線程到達(dá)屏障時(shí)倦蚪,屏障才會被打開,所有被屏障阻塞的方法都會被打開边苹。

A synchronization aid that allows a set of threads to all wait for each other to reach a **common

barrier point**. CyclicBarriers are useful in programs involving a fixed sized party of threads that

must occasionally wait for each other. The barrier is called cyclic because it can be re-used

after the waiting threads are released.

示例:模擬集齊七顆龍珠才能召喚神龍:

/**
 * CyclicBarrier模擬集齊七顆龍珠才能召喚神龍
 * 設(shè)置common barrier point為7陵且,每個(gè)線程收集到七顆龍珠之前都會被阻塞
 * 每個(gè)線程都到達(dá)common barrier point時(shí)候才會召喚神龍
 *
 * @author sherman
 */
public class CyclicBarrierDemo implements Runnable {
    private static CyclicBarrier cb = new CyclicBarrier(7, () -> System.out.println("召喚神龍"));

    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + ": 到達(dá)同步點(diǎn)(收集到一個(gè)龍珠)!");
            cb.await();
            System.out.println(Thread.currentThread().getName() + ": 阻塞結(jié)束,繼續(xù)執(zhí)行!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        CyclicBarrierDemo cbd = new CyclicBarrierDemo();
        ExecutorService executorService = Executors.newFixedThreadPool(7);
        for (int i = 0; i < 7; i++) {
            try {
                Thread.sleep(new Random().nextInt(2000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            executorService.submit(cbd);
        }
        executorService.shutdown();
    }
}

7 Semaphore

Semaphore信號量主要有兩個(gè)目的:

  • 用于多個(gè)共享資源的互斥使用;
  • 用于并發(fā)數(shù)量的控制(是synchronized的加強(qiáng)版慕购,當(dāng)并發(fā)數(shù)量為1時(shí)就退化成synchronized)聊疲;

主要方法:

  • Semaphore(int permits):構(gòu)造函數(shù),允許控制的并發(fā)數(shù)量沪悲;
  • acquire():請求一個(gè)信號量获洲,導(dǎo)致信號量的數(shù)量減1;
  • release():釋放一個(gè)信號量殿如,信號量加1贡珊;

示例:使用Semaphore模擬請車位過程(3個(gè)車位,10輛車):

/**
 * 使用Semaphore模擬搶車位過程(3個(gè)車位涉馁,10輛車)
 * 任意時(shí)刻只有3輛車持有線程
 *
 * @author sherman
 */
public class SemaphoreDemo {
    public static void main(String[] args) {
        // 模擬三個(gè)車位门岔,十輛車
        // 任意時(shí)刻只有三輛車持有車位
        Semaphore semaphore = new Semaphore(3);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + ": 搶到車位");
                    // 每輛車占有車位[3,8]秒時(shí)間
                    Thread.sleep((new Random().nextInt(6) + 3) * 1000);
                    System.out.println(Thread.currentThread().getName() + ": 釋放車位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }).start();
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市烤送,隨后出現(xiàn)的幾起案子寒随,更是在濱河造成了極大的恐慌,老刑警劉巖胯努,帶你破解...
    沈念sama閱讀 222,729評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件牢裳,死亡現(xiàn)場離奇詭異,居然都是意外死亡叶沛,警方通過查閱死者的電腦和手機(jī)蒲讯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來灰署,“玉大人判帮,你說我怎么就攤上這事「然” “怎么了晦墙?”我有些...
    開封第一講書人閱讀 169,461評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長肴茄。 經(jīng)常有香客問我晌畅,道長,這世上最難降的妖魔是什么寡痰? 我笑而不...
    開封第一講書人閱讀 60,135評論 1 300
  • 正文 為了忘掉前任抗楔,我火速辦了婚禮,結(jié)果婚禮上拦坠,老公的妹妹穿的比我還像新娘束莫。我一直安慰自己结啼,他們只是感情好畦幢,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評論 6 398
  • 文/花漫 我一把揭開白布庸蔼。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪勺良。 梳的紋絲不亂的頭發(fā)上绰播,一...
    開封第一講書人閱讀 52,736評論 1 312
  • 那天,我揣著相機(jī)與錄音郑气,去河邊找鬼幅垮。 笑死腰池,一個(gè)胖子當(dāng)著我的面吹牛尾组,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播示弓,決...
    沈念sama閱讀 41,179評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼讳侨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了奏属?” 一聲冷哼從身側(cè)響起跨跨,我...
    開封第一講書人閱讀 40,124評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎囱皿,沒想到半個(gè)月后勇婴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,657評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嘱腥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評論 3 342
  • 正文 我和宋清朗相戀三年耕渴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片齿兔。...
    茶點(diǎn)故事閱讀 40,872評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡橱脸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出分苇,到底是詐尸還是另有隱情添诉,我是刑警寧澤,帶...
    沈念sama閱讀 36,533評論 5 351
  • 正文 年R本政府宣布医寿,位于F島的核電站栏赴,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏靖秩。R本人自食惡果不足惜须眷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盆偿。 院中可真熱鬧柒爸,春花似錦、人聲如沸事扭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至今野,卻和暖如春葡公,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背条霜。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評論 1 274
  • 我被黑心中介騙來泰國打工催什, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人宰睡。 一個(gè)月前我還...
    沈念sama閱讀 49,304評論 3 379
  • 正文 我出身青樓蒲凶,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拆内。 傳聞我的和親對象是個(gè)殘疾皇子旋圆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 多線程狀態(tài) 新建(NEW):新創(chuàng)建了一個(gè)線程對象。 可運(yùn)行(RUNNABLE):線程對象創(chuàng)建后麸恍,其他線程(比如ma...
    奇點(diǎn)一氪閱讀 659評論 1 8
  • 1. 計(jì)算機(jī)系統(tǒng) 使用高速緩存來作為內(nèi)存與處理器之間的緩沖灵巧,將運(yùn)算需要用到的數(shù)據(jù)復(fù)制到緩存中,讓計(jì)算能快速進(jìn)行抹沪;當(dāng)...
    AI喬治閱讀 542評論 0 12
  • 參考鏈接:http://smallbug-vip.iteye.com/blog/2275743 在多線程開發(fā)的過程...
    時(shí)之令閱讀 1,559評論 2 5
  • 樂觀鎖認(rèn)為讀多寫少刻肄,遇到并發(fā)的可能性低,拿數(shù)據(jù)的時(shí)候認(rèn)為別人不會修改融欧,所以不上鎖敏弃,但是更新數(shù)據(jù)時(shí),會判斷一下有沒有...
    繁星追逐閱讀 384評論 0 0
  • 目前蹬癌,多線程編程可以說是在大部分平臺和應(yīng)用上都需要實(shí)現(xiàn)的一個(gè)基本需求权她。本系列文章就來對 Java 平臺下的多線程編...
    業(yè)志陳閱讀 479評論 0 0