多線程(五) java的線程鎖

  在多線程中,每一個線程的執行順序,是沒法預測不可控制的,那麼在對數據進行讀寫的時候便存在因爲讀寫順序多亂而形成數據混亂錯誤的可能性。那麼如何控制,每一個線程對於數據的讀寫順序呢?這裏就涉及到線程鎖。html

什麼是線程鎖?使用鎖的目的是什麼?先看一個例子。java

   private void testSimple(){
        SimpleRunner runner = new SimpleRunner();
        pool.execute(runner);
        pool.execute(runner);
    }
    int account01 =10;
    int account02 = 0;
    class SimpleRunner implements Runnable{
        @Override
        public void run() {
            while(true){//保證兩個帳戶的總額度不變
                account01 --;
                sleep(1000);
                account02 ++;
                Console.println("account01:"+account01+"  account02:"+account02);
            }
        }
    }
    private void sleep(int time){
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

 調用testSimple()方法開啓兩個線程執行帳戶金額轉移,運行結果以下:node

account01:9  account02:2
account01:9  account02:2
account01:8  account02:4
account01:8  account02:4
account01:6  account02:6
account01:6  account02:6
account01:5  account02:7
account01:4  account02:8

 很明顯兩個帳戶的金額總和沒法保證爲10,甚至變多了。之因此發生這種情況一方面是由於++ 和--操做不是原子操做,其次兩個變量的修改也沒有保證同步進行。因爲線程的不肯定性則將致使數據嚴重混亂。下面換一種方式看看如何:數據庫

咱們修改while循環體,不使用++或者--操做,同時對操做進行加鎖:安全

 while(true){//保證兩個帳戶的總額度不變
                synchronized ("lock"){//經過synchronized鎖把兩個變量的修改進行同步
                    account01 = account01 -1;
                    account02 = account02 +1;
                    Console.println("account01:"+account01+"  account02:"+account02);
                    sleep(1000);
                }
            }

 執行結果以下:多線程

account01:9  account02:1
account01:8  account02:2
account01:7  account02:3
account01:6  account02:4
account01:5  account02:5

 如今數據就可以徹底正常了。這裏涉及到synchronized 鎖,其目的就是保證在任意時刻,只容許一個線程進行對臨界區資源(被鎖着的代碼塊)的操做併發

習慣上喜歡稱這種機制爲加鎖,爲了容易理解,能夠把這種機制理解爲一把鑰匙和被鎖着的代碼塊,只有拿到鑰匙的線程才能執行被鎖住的代碼塊。而鑰匙就是synchronized(「lock」)中的字符串對象"lock",而被鎖着的代碼塊則是{}中的代碼。app

某個線程若是想要執行代碼塊中的內容,則必需要擁有鑰匙"lock"對象。但「lock」有個特性,同一時刻只容許一個線程擁有(暫時不考慮共享鎖)。這樣就能夠保證全部的線程依次執行被鎖着的代碼塊,避免數據混亂。在這裏有一個前提條件,也就是鑰匙是對於全部線程可見的,應該設置爲全局變量且只有一個實例,不然每個線程都有一個本身的鑰匙,那麼就起不到鎖的做用了。例如:less

            while(true){
                String lock = new String("lock");//每一個線程進入run方法的時候都new一個本身的鑰匙
                synchronized (lock){
                    account01 = account01 -1;
                    account02 = account02 +1;
                    Console.println("account01:"+account01+"  account02:"+account02);
                    sleep(1000);
                }
            }

 執行結果以下:jvm

account01:8  account02:2
account01:8  account02:2
account01:6  account02:3
account01:6  account02:3
account01:5  account02:5
account01:4  account02:5

 這樣便又發生了混亂,每一個線程都有本身的鑰匙,他們隨時均可以操做臨界區資源,和沒有加鎖無任何區別。因此在多線程操做中,鎖的使用相當重要!!!

 在java中有哪些鎖?該如何進行分類呢?

一、共享鎖/排它鎖 

    共享鎖和排他鎖是從同一時刻是否容許多個線程持有該鎖的角度來劃分。
              共享鎖容許同一時刻多個線程進入持有鎖,訪問臨界區資源。而排他鎖就是一般意義上的鎖,同一時刻只容許一個線程訪問臨界資源。對於共享鎖,主要是指對數據庫讀操做中的讀鎖,在讀寫資源的時候若是沒有線程持有寫鎖和請求寫鎖,則此時容許多個線程持有讀鎖。
              在這裏理解共享鎖的時候,不是任意時刻都容許多線程持有共享鎖的,而是在某些特殊狀況下才容許多線程持有共享鎖,在某些狀況下不容許多個線程持有共享鎖,不然,若是沒有前提條件任意時刻都容許線程任意持有共享鎖,則共享鎖的存在無心義的。例如讀寫鎖中的讀鎖,只有當沒有寫鎖和寫鎖請求的時候,就能夠容許多個線程同時持有讀鎖。這裏的前提條件就是「沒有寫鎖和寫鎖請求」,而不是任意時刻都容許多線程持有共享讀鎖。
  二、悲觀鎖/樂觀鎖  
            主要用於數據庫數據的操做中,而對於線程鎖中較爲少見。
            悲觀鎖和樂觀鎖是一種加鎖思想。對於樂觀鎖,在進行數據讀取的時候不會加鎖,而在進行寫入操做的時候會判斷一下數據是否被其它線程修改過,若是修改則更新數據,若是沒有則繼續進行數據寫入操做。樂觀鎖不是系統中自帶的鎖,而是一種數據讀取寫入思想。應用場景例如:在向數據庫中插入數據的時候,先從數據庫中讀取記錄修改版本標識字段,若是該字段沒有發生變化(沒有其餘線程對數據進行寫操做)則執行寫入操做,若是發生變化則從新計算數據。
             對於悲觀鎖,不管是進行讀操做仍是進行寫操做都會進行加鎖操做。對於悲觀鎖,若是併發量較大則比較耗費資源,固然保證了數據的安全性。

 三、可重入鎖/不可重入
                這兩個概念是從同一個線程在已經持有鎖的前提下可否再次持有鎖的角度來區分的。
                對於可重入鎖,若是該線程已經獲取到鎖且未釋放的狀況下容許再次獲取該鎖訪問臨界區資源。此種狀況主要是用在遞歸調用的狀況下和不一樣的臨界區使用相同的鎖的狀況下。
                對於不可重入鎖,則不容許同一線程在持有鎖的狀況下再次獲取該鎖並訪問臨界區資源。對於不可重入鎖,使用的時候須要當心以避免形成死鎖。

 四、公平鎖/非公平鎖
                這兩個概念主要使用線程獲取鎖的順序角度來區分的。
                對於公平鎖,全部等待的線程按照按照請求鎖的前後循序分別依次獲取鎖。
                對於非公平鎖,等待線程的線程獲取鎖的順序和請求的前後不是對應關係。有多是隨機的獲取鎖,也有可能按照其餘策略獲取鎖,總之不是按照FIFO的順序獲取鎖。
                在使用ReentrantLock的時候能夠經過構造方法主動選擇是實現公平鎖仍是非公平鎖。

五、自旋鎖/非自旋鎖
                這兩種概念是從線程等待的處理機制來區分的。
                自旋鎖在進行鎖請求等待的時候不進行wait掛起,不釋放CPU資源,執行while空循環。直至獲取鎖訪問臨界區資源。適用於等待鎖時間較短的情景,若是等待時間較長,則會耗費大量的CPU資源。而若是等待時間較短則能夠節約大量的線程切換資源。
                非自旋鎖在進行鎖等待的時候會釋放CPU資源,能夠通多sleep wait 或者CPU中斷切換上下文,切換該線程。在線程等待時間較長的狀況下能夠選擇此種實現機制。
        除此以外還有一種介於二者之間的鎖機制——自適應自旋鎖。當線程進行等待的時候先進性自旋等待,在自旋必定時間(次數)以後若是依舊沒有持有鎖則掛起等待。在jvm中synchronized鎖已經使用該機制進行處理鎖等待的狀況。
在工做中能夠根據不一樣的狀況選取合適的鎖進行使用。不管使用哪一種鎖,其目的都是保證程序可以按照要求順利執行,避免數據混亂狀況的發生。

經常使用鎖的使用方法
        一、synchronized鎖:

    對於synchronized鎖首先須要明白加鎖的底層原理。每個對象實例在對象頭中都會有monitor record列表記錄持有該鎖的線程,底層通多對該列表的查詢來判斷是否已經有線程在訪問臨界區資源。JVM內部細節之一:synchronized關鍵字及實現細節(輕量級鎖Lightweight Locking)

    在使用synchronized的時候必須弄清楚誰是「鑰匙」,屬於全局變量仍是線程內局部變量,每一個加鎖的臨界區是使用的哪一個「鑰匙」對象。必須理清楚加鎖線程和「鑰匙」對象的關係!!!!

    synchronized只能夠對方法和方法中的代碼塊進行加鎖,而網上所說的「類鎖」並非對類進行加鎖,而是synchronized(XXXX.class)。synchronized是不支持對類、構造方法和靜態代碼塊進行加鎖的。

     public synchronized void showInfo01(){//這裏synchronized鎖的是this對象,也即synchronized(this)
     }
    public void showInfo02(){
        synchronized (this){//這裏的this能夠替換爲任意Object對象。注意是Object對象,基本變量不行。java中字符串是String實例,因此字符串是能夠的。
            //doSomething
        }
    }

         二、reentranLock

    synchronized加鎖機制使基於JVM層面的加鎖,而ReentrantLock是基於jdk層面的加鎖機制。ReentrantLock根據名稱能夠看出是可重入鎖,其提供的構造方法能夠指定公平鎖或非公平鎖。ReentrantLock使用起來比synchronized更加靈活、方便和高效。

    /**
     * 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) {//經過true或false來指定公平鎖或非公平鎖
        sync = fair ? new FairSync() : new NonfairSync();
    }

 下面看一下使用方法:這裏使用的是默認非公平鎖進行測試。

    private void testReentrantLock() {
        MyRunnerForReentrantLock run = new MyRunnerForReentrantLock();
        for (int i = 0; i < 10; i++) {//開啓10個線程進行測試
       sleep(10);//睡眠10ms保證線程開啓的順序可以按照1-10依次開啓
            pool.execute(run);
        }
    }
    LockTest lockTest = new LockTest();
    class MyRunnerForReentrantLock implements Runnable {
        @Override
        public void run() {
            lockTest.reEnterLock(new AtomicInteger(3));//在run方法中調用reEnterLock()方法測試重入測試
        }
    }
    class LockTest {
        ReentrantLock reentrantLock = new ReentrantLock();//使用默認的非公平鎖ReentrantLock
        private void reEnterLock(AtomicInteger time) {
            reentrantLock.lock();//加鎖
            Console.println(Thread.currentThread().getName() + "--" + time);
            try {
                if (time.get() == 0) {
                    return;
                } else {
                    time.getAndDecrement();
                    reEnterLock(time);//這裏使用遞歸來測試重入
                }
            } finally {
                reentrantLock.unlock();//釋放鎖。注意這裏在finally中釋放鎖避免加鎖代碼拋出異常致使鎖沒法釋放形成阻塞
            }
        }
}

 執行結果以下,注意線程輸出的順序.

pool-1-thread-1--3
pool-1-thread-1--2
pool-1-thread-1--1
pool-1-thread-1--0
pool-1-thread-2--3 pool-1-thread-2--2 pool-1-thread-2--1 pool-1-thread-2--0
pool-1-thread-4--3 pool-1-thread-4--2 pool-1-thread-4--1 pool-1-thread-4--0
pool-1-thread-5--3 pool-1-thread-5--2 pool-1-thread-5--1 pool-1-thread-5--0
pool-1-thread-8--3
......
......

 能夠看出同一個線程中time變量從三、二、一、0依次循環,說明線程進入了循環體,那麼線程確實是容許重入,同一個線程能夠屢次獲取該鎖。

可是注意如下線程輸出的順序卻不是由1到10.而是 pool-1-thread-一、pool-1-thread-二、pool-1-thread-四、pool-1-thread-五、pool-1-thread-8.這就是由於ReentrantLock使用的非公平鎖形成的,使用非公平鎖的線程在獲取「鑰匙」的順序上和線程開始等待的順序是沒有關係的。咱們修改一下使用公平鎖測試一下:修改如下代碼:

        ReentrantLock reentrantLock = new ReentrantLock(true);//使用公平鎖ReentrantLock

 執行結果以下:

pool-1-thread-1--3
pool-1-thread-1--2
pool-1-thread-1--1
pool-1-thread-1--0
pool-1-thread-2--3
pool-1-thread-2--2
pool-1-thread-2--1
pool-1-thread-2--0
pool-1-thread-3--3
pool-1-thread-3--2
pool-1-thread-3--1
pool-1-thread-3--0
pool-1-thread-4--3
pool-1-thread-4--2
pool-1-thread-4--1
pool-1-thread-4--0
pool-1-thread-5--3
pool-1-thread-5--2
....
....

 能夠看出線程的執行順序按照一、二、三、4的順序進行輸出。

除了上面的lock()方法外ReentrantLock還提供了兩個重載的方法tryLock。ReentrantLock在進行等待持鎖的時候不一樣於synchronized之處就在於ReentrantLock能夠中斷線程的等待,再也不等待鎖。其主要方法就是tryLock()的使用。

  tryLock被重載了兩個方法,方法簽名爲:

public boolean tryLock() {}
public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {}
後者指定了等待超時時間

官方文檔中註明了tryLock等待鎖的機制:
public boolean tryLock()
Acquires the lock only if it is not held by another thread at the time of invocation.
Acquires the lock if it is not held by another thread and returns immediately with the value true, setting the lock hold count to one. Even when this lock has been set to use a fair ordering policy, a call to tryLock() will immediately acquire the lock if it is available, whether or not other threads are currently waiting for the lock. This "barging" behavior can be useful in certain circumstances, even though it breaks fairness. If you want to honor the fairness setting for this lock, then use tryLock(0, TimeUnit.SECONDS) which is almost equivalent (it also detects interruption).
If the current thread already holds this lock then the hold count is incremented by one and the method returns true.
If the lock is held by another thread then this method will return immediately with the value false.

 

public boolean tryLock(long timeout,  @NotNull TimeUnit unit) throws InterruptedException
Acquires the lock if it is not held by another thread within the given waiting time and the current thread has not been interrupted.
Acquires the lock if it is not held by another thread and returns immediately with the value true, setting the lock hold count to one. If this lock has been set to use a fair ordering policy then an available lock will not be acquired if any other threads are waiting for the lock. This is in contrast to the tryLock() method. If you want a timed tryLock that does permit barging on a fair lock then combine the timed and un-timed forms together:
       if (lock.tryLock() ||
          lock.tryLock(timeout, unit)) {
        ...
      }
If the current thread already holds this lock then the hold count is incremented by one and the method returns true.
If the lock is held by another thread then the current thread becomes disabled for thread scheduling purposes and lies dormant until one of three things happens:
The lock is acquired by the current thread; or
Some other thread interrupts the current thread; or
The specified waiting time elapses
If the lock is acquired then the value true is returned and the lock hold count is set to one.
If the current thread:
has its interrupted status set on entry to this method; or
is interrupted while acquiring the lock,
then InterruptedException is thrown and the current thread's interrupted status is cleared.
If the specified waiting time elapses then the value false is returned. If the time is less than or equal to zero, the method will not wait at all.
In this implementation, as this method is an explicit interruption point, preference is given to responding to the interrupt over normal or reentrant acquisition of the lock, and over reporting the elapse of the waiting time.

 這裏有中文的翻譯:Java中Lock,tryLock,lockInterruptibly有什麼區別?(郭無意的回答)
       三、讀寫鎖的使用

    對於讀寫鎖的請求「鑰匙」策略以下:

        當寫鎖操做臨界區資源時,其它新過來的線程一概等待,不管是讀鎖仍是寫鎖。

        當讀鎖操做臨界區資源時,若是有讀鎖請求資源能夠當即獲取,不用等待;若是有寫鎖過來請求資源則須要等待讀鎖釋放以後纔可獲取;若是有寫鎖在等待,而後又過來的有讀鎖,則讀鎖將會等待,寫鎖將會優先獲取臨界區資源操做權限,這樣能夠避免寫線程的長期等待。

使用方法以下:

    private void testReentrantRWLock() {
        MyRunnerForReentrantRWLock run = new MyRunnerForReentrantRWLock();
        for (int i = 0; i < 10; i++) {//開啓10個線程測試

       sleep(10);//睡眠10ms保證線程開啓的順序可以按照1-10依次開啓
            pool.execute(run);
        }
    }
    AtomicInteger num = new AtomicInteger(1);//用來切換讀寫鎖測試方法
    ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock(true);//公平讀寫鎖
    private class MyRunnerForReentrantRWLock implements Runnable {
        @Override
        public void run() {
            if(num.getAndIncrement() ==3){
                lockTest.write();//調用寫鎖測試
            }else{
                lockTest.read();//調用讀鎖測試
            }
        }
    }
        public void read() {//使用讀鎖
            rwlock.readLock().lock();
            try {
                Console.println(Thread.currentThread().getName()+"------read");
          sleep(2000); }
finally { rwlock.readLock().unlock(); } } public void write() {//使用寫鎖 rwlock.writeLock().lock(); try { sleep(2000);//模擬寫操做 Console.println(Thread.currentThread().getName()+"------write"); }finally { rwlock.writeLock().unlock(); } }

 執行結果以下:

pool-1-thread-1------read
pool-1-thread-2------read
//在這裏有明顯的停頓,大約2s以後下面的直接輸出,沒有停頓
pool-1-thread-3------write
pool-1-thread-4------read
pool-1-thread-5------read
pool-1-thread-7------read
pool-1-thread-10------read
pool-1-thread-6------read
pool-1-thread-8------read
pool-1-thread-9------read

 由運行結果執行順序和時間能夠看出,在進行write的時候其它讀線程進行了等待操做,而後write釋放以後,其它讀操做同時操做臨界區資源,未發生阻塞等待。
        四、自旋鎖

    自旋鎖是在線程等待的時候經過自選while(){}空循環避免了線程掛起切換,減小了線程切換執行的時間。所以在選擇使用自旋鎖的時候儘可能保證加鎖代碼的執行時間小於等待時間,這樣就能夠避免自旋鎖大量佔用CPU空轉,同時又免去了非自旋鎖線程切換的花銷。若是加鎖代碼塊較多,此時自旋鎖就喲啊佔用太多的CPU進行空轉,此時若是發生大量線程請求鎖則會大量浪費資源。用戶能夠根據具體狀況來自定義自旋鎖的實現,能夠實現公平自旋鎖和非公平自旋鎖。

這裏有介紹自定義自旋鎖的實現方式:Java鎖的種類以及辨析(二):自旋鎖的其餘種類
    文章中介紹的很清楚了,TicketLock CLHLock 邏輯比較簡單,這裏再也不詳述,只對MCSLock的實現作一下解讀。其中原文中MCSLock的實現unlock()方法中在釋放資源解鎖下一個等待線程的機制有些問題,已經作出了修改,請注意辨別。

package com.zpj.thread.blogTest.lock;

/**
 * Created by PerkinsZhu on 2017/8/16 18:01.
 */

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class MCSLock {//這是經過鏈表實現對線程的控制的。每過來一個新的線程則把它添加到鏈表上阻塞進行while循環,當前一個線程結束以後,修改下一個線程的開關,開啓下個線程持有鎖。
    public static class MCSNode {
        volatile MCSNode next;
        volatile boolean isLocked = true;
    }
    private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<MCSNode>();//這裏保存的是當前線程的node,要理解ThreadLocal 的工做機制
    @SuppressWarnings("unused")
    private volatile MCSNode queue;
    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode.class, "queue");

    public void lock() {
        MCSNode currentNode = new MCSNode();//過來一個新線程建立一個node,同時防止在當前線程的NODE中進行保存。
        NODE.set(currentNode);//注意,這裏的NODE存儲的數據各個線程中是不共享的
        MCSNode preNode = UPDATER.getAndSet(this, currentNode);//獲取前一個node節點,並更新當前節點
        if (preNode != null) {//前一個節點存在說明有線程正在操做臨界區資源。則當前線程循環等待
            preNode.next = currentNode;//把當前節點加入到鏈表中,等待獲取資源
            while (currentNode.isLocked) {}//循環等待,直至前一個線程釋放資源,修改當前node的isLocked標誌位
        }
    }

    public void unlock() {
        MCSNode currentNode = NODE.get();//取出當前線程的node節點
        if (currentNode.next == null) {//若是沒有新的線程等待持鎖
            if (UPDATER.compareAndSet(this, currentNode, null)) {//把當前node釋放,若是成功則結束,若是失敗進入else
            } else { //設置失敗說明忽然有線程在請求臨界區資源進行等待。此時有新的線程更新了UPDATER數據。
        //***********************注意下面的邏輯,已經進行修改 【start】********************************* while (currentNode.next == null) {}//等待新加入的線程把節點加入鏈表 // 此時currentNode.next != null 這裏理應使用鎖資源,而不該該直接結束,否則等待的線程沒法獲取「鑰匙」訪問臨界區資源。因此添加如下兩行代碼釋放鎖資源 currentNode.next.isLocked = false;//釋放新添加線程的等待 currentNode.next = null;
         //********************************** end ******************************
} }
else { currentNode.next.isLocked = false;//釋放下一個等待鎖的線程 currentNode.next = null; } } }

  五、信號量實現鎖效果

  在jdk中,除了以上提供的Lock以外,還有信號量Semaphore也能夠實現加鎖特性。Semaphore是控制訪問臨界區資源的線程數量,Semaphore設置一個容許同時操做臨界區資源的閾值,若是請求的線程在閾值以內則容許全部線程同時訪問臨界區資源,若是超出設置的該閾值則掛起等待,直至有線程退出釋放以後,才容許新的資源得到操做臨界區資源的權利。若是須要把它當作鎖使用,則只須要設置該閾值爲1,即任意時刻只容許一個線程對臨界區資源進行操做便可。雖然不是鎖,但卻實現了鎖的功能——線程互斥串行。

使用示例:

Semaphore semaphore = new Semaphore(1);//同時只容許一個線程能夠訪問臨界區資源
    private void testSemaphore(){
        for(int i = 0; i<5;i++){//開啓5個線程競爭資源
            pool.execute(new SemapRunner());
        }
    }
    class SemapRunner implements Runnable{
        @Override
        public void run() {
            try {
                Console.println(Thread.currentThread().getName()+"  請求資源");
                semaphore.acquire();//請求資源
                Console.println(Thread.currentThread().getName()+"  獲取到資源");
                sleep(2000);
                Console.println(Thread.currentThread().getName()+"  釋放資源");
                semaphore.release();//釋放資源
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

 運行結果以下:

pool-1-thread-2  請求資源
pool-1-thread-4  請求資源
pool-1-thread-2  獲取到資源
pool-1-thread-5  請求資源
pool-1-thread-1  請求資源
pool-1-thread-3  請求資源
pool-1-thread-2  釋放資源
pool-1-thread-4 獲取到資源
pool-1-thread-4  釋放資源
pool-1-thread-5 獲取到資源
pool-1-thread-5  釋放資源
pool-1-thread-1 獲取到資源
pool-1-thread-1  釋放資源
pool-1-thread-3 獲取到資源
pool-1-thread-3  釋放資源

 由結果能夠看出,只有當一個線程釋放資源以後,才容許一個等待的資源獲取到資源,這樣便實現了相似加鎖的操做。

 

  在進行線程操做的過程當中須要根據實際狀況選取不一樣的鎖機制來對線程進行控制,以保證數據、執行邏輯的正確!!!不管是使用synchronized鎖仍是使用jdk提供的鎖亦或自定義鎖,都要清晰明確使用鎖的最終目的是什麼,各類鎖的特性是什麼,使用場景分別是什麼?這樣纔可以在線程中熟練運用各類鎖。

 

 

=========================================

原文連接:多線程(五) java的線程鎖 轉載請註明出處!

=========================================

 ----end

相關文章
相關標籤/搜索