Java多線程7 Lock

Java多線程目錄

1 synchronized的缺陷

synchronized是java中的一個關(guān)鍵字,也就是說是Java語言內(nèi)置的特性。那么為什么會出現(xiàn)Lock呢?

在上面一篇文章中,我們了解到如果一個代碼塊被synchronized修飾了,當一個線程獲取了對應的鎖,并執(zhí)行該代碼塊時,其他線程便只能一直等待,等待獲取鎖的線程釋放鎖,而這里獲取鎖的線程釋放鎖只會有兩種情況:

1)獲取鎖的線程執(zhí)行完了該代碼塊,然后線程釋放對鎖的占有;

2)線程執(zhí)行發(fā)生異常,此時JVM會讓線程自動釋放鎖。

那么如果這個獲取鎖的線程由于要等待IO或者其他原因(比如調(diào)用sleep方法)被阻塞了,但是又沒有釋放鎖,其他線程便只能干巴巴地等待,試想一下,這多么影響程序執(zhí)行效率。

因此就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間或者能夠響應中斷),通過Lock就可以辦到。

再舉個例子:當有多個線程讀寫文件時,讀操作和寫操作會發(fā)生沖突現(xiàn)象,寫操作和寫操作會發(fā)生沖突現(xiàn)象,但是讀操作和讀操作不會發(fā)生沖突現(xiàn)象。

但是采用synchronized關(guān)鍵字來實現(xiàn)同步的話,就會導致一個問題:

如果多個線程都只是進行讀操作,所以當一個線程在進行讀操作時,其他線程只能等待無法進行讀操作。

因此就需要一種機制來使得多個線程都只是進行讀操作時,線程之間不會發(fā)生沖突,通過Lock就可以辦到。

另外,通過Lock可以知道線程有沒有成功獲取到鎖。這個是synchronized無法辦到的。

總結(jié)一下,也就是說Lock提供了比synchronized更多的功能。但是要注意以下幾點:

1)Lock不是Java語言內(nèi)置的,synchronized是Java語言的關(guān)鍵字,因此是內(nèi)置特性。Lock是一個類,通過這個類可以實現(xiàn)同步訪問;

2)Lock和synchronized有一點非常大的不同,采用synchronized不需要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執(zhí)行完之后,系統(tǒng)會自動讓線程釋放對鎖的占用;而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現(xiàn)死鎖現(xiàn)象。

2 Lock

2.1 ReentrantLock
public class LockTest {
    public static void main(String[] args) {

        new LockTest().init();
    }

    private void init() {
        final Outputer outputer = new Outputer();

        new Thread(new Runnable() {
            @Override
            public void run() {

                while (true) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    outputer.output("kpioneer");

                }
            }
        }).start();


        new Thread(new Runnable() {
            @Override
            public void run() {

                while (true) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    outputer.output("Jack");

                }
            }
        }).start();
    }

    static class Outputer {
        
        Lock lock = new ReentrantLock();

        public void output(String name) {
            int len = name.length();
            lock.lock();
            /**
             *
             *不管程序運行是否異常都要釋放鎖
             * 否則再也進入不到改頁面
             */
            try {
                for (int i = 0; i < len; i++) {
                    System.out.print(name.charAt(i));
                }
                System.out.println();
            } finally {
                lock.unlock();
            }
        }
    }
}
kpioneer
Jack
kpioneer
Jack
kpioneer
Jack
Jack
kpioneer
Jack
kpioneer
省略...
2.2 ReentrantReadWriteLock

實中有這樣一種場景:對共享資源有讀和寫的操作,且寫操作沒有讀操作那么頻繁。在沒有寫操作的時候,多個線程同時讀一個資源沒有任何問題,所以應該允許多個線程同時讀取共享資源;但是如果一個線程想去寫這些共享資源,就不應該允許其他線程對該資源進行讀和寫的操作了。

針對這種場景,JAVA的并發(fā)包提供了讀寫鎖ReentrantReadWriteLock,它表示兩個鎖,一個是讀操作相關(guān)的鎖,稱為共享鎖;一個是寫相關(guān)的鎖,稱為排他鎖,描述如下:

線程進入讀鎖的前提條件:

沒有其他線程的寫鎖,

沒有寫請求或者有寫請求,但調(diào)用線程和持有鎖的線程是同一個。

線程進入寫鎖的前提條件:

沒有其他線程的讀鎖

沒有其他線程的寫鎖

而讀寫鎖有以下三個重要的特性:

(1)公平選擇性:支持非公平(默認)和公平的鎖獲取方式,吞吐量還是非公平優(yōu)于公平。

(2)重進入:讀鎖和寫鎖都支持線程重進入。

(3)鎖降級:遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成為讀鎖。

public class ReadWriteLockTest {
    public static void main(String[] args) {

        final Queue q = new Queue();

        for (int i = 0; i < 3; i++) {

            new Thread() {
                @Override
                public void run() {

                    while (true) {
                        q.get();
                    }
                }
            }.start();

            new Thread() {
                @Override
                public void run() {
                    while (true) {
                        q.put(new Random().nextInt(10000));
                    }
                }
            }.start();
        }
    }
}

class Queue {

    //共享數(shù)據(jù),只能有一個線程能寫該數(shù)據(jù),但可以有多個線程同時讀該數(shù)據(jù)。
    ReadWriteLock rwl = new ReentrantReadWriteLock();
    private Object data = null;//共享數(shù)據(jù),只能有一個線程能寫數(shù)據(jù),但可以有多個線程讀該數(shù)據(jù)

    public void get() {
        //上讀鎖,其他線程只能讀不能寫
        rwl.readLock().lock();
        try {

            System.out.println(Thread.currentThread().getName() + " be ready to read data!");
            Thread.sleep((long) (Math.random() * 1000));
            System.out.println(Thread.currentThread().getName() + " have read data :" + data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwl.readLock().unlock();
        }
    }

    public void put(Object data) {
        //上寫鎖,不允許其他線程讀也不允許寫
        rwl.writeLock().lock();

        try {
            System.out.println(Thread.currentThread().getName() + " be ready to write data!");

            Thread.sleep((long) (Math.random() * 1000));

            this.data = data;
            System.out.println(Thread.currentThread().getName() + " have write data: " + data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwl.writeLock().unlock();
        }
    }
}
Thread-0 be ready to read data!
Thread-2 be ready to read data!
Thread-2 have read data :null
Thread-0 have read data :null
Thread-1 be ready to write data!
Thread-1 have write data: 4664
Thread-1 be ready to write data!
Thread-1 have write data: 1849
Thread-3 be ready to write data!
Thread-3 have write data: 75
Thread-3 be ready to write data!
Thread-3 have write data: 8222
Thread-3 be ready to write data!
Thread-3 have write data: 3056
Thread-3 be ready to write data!
Thread-3 have write data: 6114
Thread-3 be ready to write data!
Thread-3 have write data: 6376
省略...

3 鎖的相關(guān)概念介紹

在前面介紹了Lock的基本使用,這一節(jié)來介紹一下與鎖相關(guān)的幾個概念。

3.1 可重入鎖

如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明了鎖的分配機制:基于線程的分配,而不是基于方法調(diào)用的分配。舉個簡單的例子,當一個線程執(zhí)行到某個synchronized方法時,比如說method1,而在method1中會調(diào)用另外一個synchronized方法method2,此時線程不必重新去申請鎖,而是可以直接執(zhí)行方法method2。

看下面這段代碼就明白了:

class MyClass {
    public synchronized void method1() {
        method2();
    }
     
    public synchronized void method2() {
         
    }
}

上述代碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻,線程A執(zhí)行到了method1,此時線程A獲取了這個對象的鎖,而由于method2也是synchronized方法,假如synchronized不具備可重入性,此時線程A需要重新申請鎖。但是這就會造成一個問題,因為線程A已經(jīng)持有了該對象的鎖,而又在申請獲取該對象的鎖,這樣就會線程A一直等待永遠不會獲取到的鎖。

而由于synchronized和Lock都具備可重入性,所以不會發(fā)生上述現(xiàn)象。

3.2 可中斷鎖

可中斷鎖:顧名思義,就是可以相應中斷的鎖。
  在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。
  如果某一線程A正在執(zhí)行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由于等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。
  在前面演示lockInterruptibly()的用法時已經(jīng)體現(xiàn)了Lock的可中斷性。

3.3 公平鎖

公平鎖即盡量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖。

非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的。這樣就可能導致某個或者一些線程永遠獲取不到鎖。

在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。

而對于ReentrantLock和ReentrantReadWriteLock,它默認情況下是非公平鎖,但是可以設(shè)置為公平鎖。

看一下這2個類的源代碼就清楚了:

   /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        /**
         * Performs {@link Lock#lock}. The main reason for subclassing
         * is to allow fast path for nonfair version.
         */
        abstract void lock();

        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

        protected final boolean isHeldExclusively() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        final ConditionObject newCondition() {
            return new ConditionObject();
        }

        // Methods relayed from outer class

        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }

        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }

        final boolean isLocked() {
            return getState() != 0;
        }

        /**
         * Reconstitutes the instance from a stream (that is, deserializes it).
         */
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }
    /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

另外在ReentrantLock類中定義了很多方法,比如:

isFair() //判斷鎖是否是公平鎖

isLocked() //判斷鎖是否被任何線程獲取了

isHeldByCurrentThread() //判斷鎖是否被當前線程獲取了

hasQueuedThreads() //判斷是否有線程在等待該鎖

在ReentrantReadWriteLock中也有類似的方法,同樣也可以設(shè)置為公平鎖和非公平鎖。不過要記住,ReentrantReadWriteLock并未實現(xiàn)Lock接口,它實現(xiàn)的是ReadWriteLock接口。

3.4 讀寫鎖

讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。

正因為有了讀寫鎖,才使得多個線程之間的讀操作不會發(fā)生沖突。

ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現(xiàn)了這個接口。

可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。

public class CacheDemo {
    private Map<String, Object> cache = new HashMap<>();
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public static void main(String[] args) {


    }

    public Object getData(String key) {

        Object value = null;
        //首先開啟讀鎖,從緩存中去取
        readWriteLock.readLock().lock();
        try {
            value = cache.get(key);
            //如果緩存中沒有釋放讀鎖,上寫鎖
            if (value == null) {
                //對應queryDB()
                readWriteLock.readLock().unlock();
                //讀鎖必須unlock之后才能獲取寫鎖
                readWriteLock.writeLock().lock();
                try {
                    //對應queryDB()
                    value = queryDB();
                } finally {
                    //釋放寫鎖
                    readWriteLock.writeLock().unlock();
                }
                //然后再上讀鎖
                readWriteLock.readLock().lock();
            }
        } finally {
            //最后釋放讀鎖
            readWriteLock.readLock().unlock();
        }
        return value;

    }

    public Object queryDB() {
        return "aaaa";
    }
}
特別感謝:

ReentrantReadWriteLock讀寫鎖詳解
Lock和synchronized的區(qū)別和使用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容