Java多线程编程-(5)-使用Lock对象实现同步以及线程间通信
前幾篇:
Java多線程編程-(1)-線程安全和鎖Synchronized概念
Java多線程編程-(2)-可重入鎖以及Synchronized的其他基本特性
Java多線程編程-(3)-線程本地ThreadLocal的介紹與使用
Java多線程編程-(4)-線程間通信機(jī)制的介紹與使用
在《Java多線程編程-(4)-線程間通信機(jī)制的介紹與使用》已經(jīng)學(xué)習(xí)了,可以使用方法wait/notify 結(jié)合同步關(guān)鍵字synchronized實(shí)現(xiàn)同步和線程間通信,下邊介紹一種更為方便的方式實(shí)現(xiàn)同步和線程間通信的效果,那就是Lock對(duì)象。
Lock對(duì)象簡介
這里為什么說Lock對(duì)象哪?Lock其實(shí)是一個(gè)接口,在JDK1.5以后開始提供,其實(shí)現(xiàn)類常用的有ReentrantLock,這里所說的Lock對(duì)象即是只Lock接口的實(shí)現(xiàn)類,為了方便記憶或理解,都簡稱為Lock對(duì)象。
我們知道synchronized關(guān)鍵字可以實(shí)現(xiàn)線程間的同步互斥,從JDK1.5開始新增的ReentrantLock類能夠達(dá)到同樣的效果,并且在此基礎(chǔ)上還擴(kuò)展了很多實(shí)用的功能,比使用synchronized更佳的靈活。
ReentrantLock的另一個(gè)稱呼就是“重入鎖”,Reentrant的英文釋義為:重入。
何為重入鎖,前幾篇在學(xué)習(xí)synchronized的時(shí)候,也談到了重入鎖,“一個(gè)對(duì)象一把鎖,多個(gè)對(duì)象多把鎖”,可重入鎖的概念就是:自己可以獲取自己的內(nèi)部鎖。
ReentrantLock實(shí)現(xiàn)了Lock中的接口,繼承關(guān)系和方法屬性如下:
下邊,就開始一起學(xué)習(xí)一下ReentrantLock對(duì)象。
使用ReentrantLock實(shí)現(xiàn)線程同步
public class Run {
? ? public static void main(String[] args) {
? ? ? ? Lock lock = new ReentrantLock();
? ? ? ? //lambda寫法
? ? ? ? new Thread(() -> runMethod(lock), "thread1").start();
? ? ? ? new Thread(() -> runMethod(lock), "thread2").start();
? ? ? ? new Thread(() -> runMethod(lock), "thread3").start();
? ? ? ? new Thread(() -> runMethod(lock), "thread4").start();
? ? ? ? //常規(guī)寫法
? ? ? ? new Thread(new Runnable() {
? ? ? ? ? ? @Override
? ? ? ? ? ? public void run() {
? ? ? ? ? ? ? ? runMethod(lock);
? ? ? ? ? ? }
? ? ? ? }, "thread5").start();
? ? }
? ? private static void runMethod(Lock lock) {
? ? ? ? lock.lock();
? ? ? ? for (int i = 1; i <= 5; i++) {
? ? ? ? ? ? System.out.println("ThreadName:" + Thread.currentThread().getName() + (" i=" + i));
? ? ? ? }
? ? ? ? System.out.println();
? ? ? ? lock.unlock();
? ? }
}
運(yùn)行結(jié)果:
ThreadName:thread1 i=1
ThreadName:thread1 i=2
ThreadName:thread1 i=3
ThreadName:thread1 i=4
ThreadName:thread1 i=5
ThreadName:thread2 i=1
ThreadName:thread2 i=2
ThreadName:thread2 i=3
ThreadName:thread2 i=4
ThreadName:thread2 i=5
ThreadName:thread3 i=1
ThreadName:thread3 i=2
ThreadName:thread3 i=3
ThreadName:thread3 i=4
ThreadName:thread3 i=5
ThreadName:thread4 i=1
ThreadName:thread4 i=2
ThreadName:thread4 i=3
ThreadName:thread4 i=4
ThreadName:thread4 i=5
ThreadName:thread5 i=1
ThreadName:thread5 i=2
ThreadName:thread5 i=3
ThreadName:thread5 i=4
ThreadName:thread5 i=5
可以看出,當(dāng)前線程打印完畢之后釋放鎖,其他線程才可以獲取鎖然后進(jìn)行打印。線程打印的數(shù)據(jù)是分組打印的,這是因?yàn)楫?dāng)前線程已經(jīng)持有鎖,在當(dāng)前線程打印完之后才會(huì)釋放鎖,但線程之間打印的順序是隨機(jī)的。
為了進(jìn)一步說明使用ReentrantLock可以實(shí)現(xiàn)線程之間同步,測(cè)試代碼如下:
public class Run {
? ? public static void main(String[] args) {
? ? ? ? Lock lock = new ReentrantLock();
? ? ? ? new Thread(() -> runMethod(lock, 0), "thread1").start();
? ? ? ? new Thread(() -> runMethod(lock, 5000), "thread2").start();
? ? ? ? new Thread(() -> runMethod(lock, 1000), "thread3").start();
? ? ? ? new Thread(() -> runMethod(lock, 5000), "thread4").start();
? ? ? ? new Thread(() -> runMethod(lock, 1000), "thread5").start();
? ? }
? ? private static void runMethod(Lock lock, long sleepTime) {
? ? ? ? lock.lock();
? ? ? ? try {
? ? ? ? ? ? Thread.sleep(sleepTime);
? ? ? ? ? ? System.out.println("ThreadName:" + Thread.currentThread().getName());
? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? } finally {
? ? ? ? ? ? lock.unlock();
? ? ? ? }
? ? }
}
運(yùn)行結(jié)果:
ThreadName:thread1
ThreadName:thread2
ThreadName:thread3
ThreadName:thread4
ThreadName:thread5
可以看出,在sleep指定的時(shí)間內(nèi),當(dāng)調(diào)用了lock.lock()方法線程就持有了”對(duì)象監(jiān)視器”,其他線程只能等待鎖被釋放后再次爭搶,效果和使用synchronized關(guān)鍵字是一樣的。
使用Lock對(duì)象實(shí)現(xiàn)線程間通信
上述,已經(jīng)大致看了一下如何使用ReentrantLock實(shí)現(xiàn)線程之間的同步,下邊再看一下ReentrantLock是如何實(shí)現(xiàn)線程間通信的。
在前文中我們已經(jīng)知道可以使用關(guān)鍵字synchronized與wait()方法和notify()方式結(jié)合實(shí)現(xiàn)線程間通信,也就是等待/通知模式。在ReentrantLock中,是借助Condition對(duì)象進(jìn)行實(shí)現(xiàn)的。
Condition的創(chuàng)建方式如下:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Condition按字面意思理解就是條件,當(dāng)然,我們也可以將其認(rèn)為是條件進(jìn)行使用,這樣的話我們可以通過上述的代碼創(chuàng)建多個(gè)Condition條件,我們就可以根據(jù)不同的條件來控制現(xiàn)成的等待和通知。而我們還知道,在使用關(guān)鍵字synchronized與wait()方法和notify()方式結(jié)合實(shí)現(xiàn)線程間通信的時(shí)候,notify/notifyAll的通知等待的線程時(shí)是隨機(jī)的,顯然使用Condition相對(duì)靈活很多,可以實(shí)現(xiàn)”選擇性通知”。
這是因?yàn)?#xff0c;synchronized關(guān)鍵字相當(dāng)于整個(gè)Lock對(duì)象只有一個(gè)單一的Condition對(duì)象,所有的線程都注冊(cè)到這個(gè)對(duì)象上。線程開始notifAll的時(shí)候,需要通知所有等待的線程,讓他們開始競爭獲得鎖對(duì)象,沒有選擇權(quán),這種方式相對(duì)于Condition條件的方式在效率上肯定Condition較高一些。
下邊,我們首先看一個(gè)實(shí)例。
使用Lock對(duì)象和Condition實(shí)現(xiàn)等待/通知實(shí)例
主要方法對(duì)比如下:
(1)Object的wait()方法相當(dāng)于Condition類中的await()方法;?
(2)Object的notify()方法相當(dāng)于Condition類中的signal()方法;?
(3)Object的notifyAll()方法相當(dāng)于Condition類中的signalAll()方法;
首先,使用Lock的時(shí)候,和《Java多線程編程-(4)-線程間通信機(jī)制的介紹與使用》介紹的一樣,都需要先獲取鎖。
示例代碼如下:
public class LockConditionDemo {
? ? private Lock lock = new ReentrantLock();
? ? private Condition condition = lock.newCondition();
? ? public static void main(String[] args) throws InterruptedException {
? ? ? ? //使用同一個(gè)LockConditionDemo對(duì)象,使得lock、condition一樣
? ? ? ? LockConditionDemo demo = new LockConditionDemo();
? ? ? ? new Thread(() -> demo.await(), "thread1").start();
? ? ? ? Thread.sleep(3000);
? ? ? ? new Thread(() -> demo.signal(), "thread2").start();
? ? }
? ? private void await() {
? ? ? ? try {
? ? ? ? ? ? lock.lock();
? ? ? ? ? ? System.out.println("開始等待await! ThreadName:" + Thread.currentThread().getName());
? ? ? ? ? ? condition.await();
? ? ? ? ? ? System.out.println("等待await結(jié)束! ThreadName:" + Thread.currentThread().getName());
? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? } finally {
? ? ? ? ? ? lock.unlock();
? ? ? ? }
? ? }
? ? private void signal() {
? ? ? ? lock.lock();
? ? ? ? System.out.println("發(fā)送通知signal! ThreadName:" + Thread.currentThread().getName());
? ? ? ? condition.signal();
? ? ? ? lock.unlock();
? ? }
}
運(yùn)行結(jié)果:
開始等待await! ThreadName:thread1
發(fā)送通知signal! ThreadName:thread2
等待await結(jié)束! ThreadName:thread1
可以看出結(jié)果正確執(zhí)行!
使用Lock對(duì)象和多個(gè)Condition實(shí)現(xiàn)等待/通知實(shí)例
示例代碼如下:
public class LockConditionDemo {
? ? private Lock lock = new ReentrantLock();
? ? private Condition conditionA = lock.newCondition();
? ? private Condition conditionB = lock.newCondition();
? ? public static void main(String[] args) throws InterruptedException {
? ? ? ? LockConditionDemo demo = new LockConditionDemo();
? ? ? ? new Thread(() -> demo.await(demo.conditionA), "thread1_conditionA").start();
? ? ? ? new Thread(() -> demo.await(demo.conditionB), "thread2_conditionB").start();
? ? ? ? new Thread(() -> demo.signal(demo.conditionA), "thread3_conditionA").start();
? ? ? ? System.out.println("稍等5秒再通知其他的線程!");
? ? ? ? Thread.sleep(5000);
? ? ? ? new Thread(() -> demo.signal(demo.conditionB), "thread4_conditionB").start();
? ? }
? ? private void await(Condition condition) {
? ? ? ? try {
? ? ? ? ? ? lock.lock();
? ? ? ? ? ? System.out.println("開始等待await! ThreadName:" + Thread.currentThread().getName());
? ? ? ? ? ? condition.await();
? ? ? ? ? ? System.out.println("等待await結(jié)束! ThreadName:" + Thread.currentThread().getName());
? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? } finally {
? ? ? ? ? ? lock.unlock();
? ? ? ? }
? ? }
? ? private void signal(Condition condition) {
? ? ? ? lock.lock();
? ? ? ? System.out.println("發(fā)送通知signal! ThreadName:" + Thread.currentThread().getName());
? ? ? ? condition.signal();
? ? ? ? lock.unlock();
? ? }
}
運(yùn)行結(jié)果:
開始等待await! ThreadName:thread1_conditionA
開始等待await! ThreadName:thread2_conditionB
發(fā)送通知signal! ThreadName:thread3_conditionA
等待await結(jié)束! ThreadName:thread1_conditionA
稍等5秒再通知其他的線程!
發(fā)送通知signal! ThreadName:thread4_conditionB
等待await結(jié)束! ThreadName:thread2_conditionB
可以看出實(shí)現(xiàn)了分別通知。因此,我們可以使用Condition進(jìn)行分組,可以單獨(dú)的通知某一個(gè)分組,另外還可以使用signalAll()方法實(shí)現(xiàn)通知某一個(gè)分組的所有等待的線程。
公平鎖和非公平鎖
概念很好理解,公平鎖表示線程獲取鎖的順序是按照線程加鎖的順序來分配,即先進(jìn)先出,那么他就是公平的;非公平是一種搶占機(jī)制,是隨機(jī)獲得鎖,并不是先來的一定能先得到鎖,結(jié)果就是不公平的。
ReentrantLock提供了一個(gè)構(gòu)造方法,可以很簡單的實(shí)現(xiàn)公平鎖或非公平鎖,源代碼構(gòu)造函數(shù)如下:
public ReentrantLock(boolean fair) {
? ?sync = fair ? new FairSync() : new NonfairSync();
}
參數(shù):fair為true表示是公平鎖,反之為非公平鎖,這里不再寫代碼測(cè)試。
ReentrantLock的其他方法
ReentrantLock源代碼結(jié)構(gòu)如下:
方法很簡單,看到名稱就可以想到作用是什么,挑一些簡單介紹一下:
(1)getHoldCount()方法:查詢當(dāng)前線程保持此鎖定的個(gè)數(shù),也就是調(diào)用lock()的次數(shù);
(2)getQueueLength()方法:返回正等待獲取此鎖定的線程估計(jì)數(shù)目;
(3)isFair()方法:判斷是不是公平鎖;
使用ReentrantReadWriteLock實(shí)現(xiàn)并發(fā)
上述的類ReentrantLock具有完全互斥排他的效果,即同一時(shí)間只能有一個(gè)線程在執(zhí)行ReentrantLock.lock()之后的任務(wù)。
類似于我們集合中有同步類容器 和 并發(fā)類容器,HashTable(HashTable幾乎可以等價(jià)于HashMap,并且是線程安全的)也是完全排他的,即使是讀也只能同步執(zhí)行,而ConcurrentHashMap就可以實(shí)現(xiàn)同一時(shí)刻多個(gè)線程之間并發(fā)。為了提高效率,ReentrantLock的升級(jí)版ReentrantReadWriteLock就可以實(shí)現(xiàn)效率的提升。
ReentrantReadWriteLock有兩個(gè)鎖:一個(gè)是與讀相關(guān)的鎖,稱為“共享鎖”;另一個(gè)是與寫相關(guān)的鎖,稱為“排它鎖”。也就是多個(gè)讀鎖之間不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥。
在沒有線程進(jìn)行寫操作時(shí),進(jìn)行讀操作的多個(gè)線程都可以獲取到讀鎖,而寫操作的線程只有獲取寫鎖后才能進(jìn)行寫入操作。即:多個(gè)線程可以同時(shí)進(jìn)行讀操作,但是同一時(shí)刻只允許一個(gè)線程進(jìn)行寫操作。
ReentrantReadWriteLock鎖的特性:
(1)讀讀共享;?
(2)寫寫互斥;?
(3)讀寫互斥;?
(4)寫讀互斥;
ReentrantReadWriteLock實(shí)例代碼
(1)讀讀共享
public class ReentrantReadWriteLockDemo {
? ? private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
? ? public static void main(String[] args) {
? ? ? ? ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
? ? ? ? new Thread(() -> demo.read(), "ThreadA").start();
? ? ? ? new Thread(() -> demo.read(), "ThreadB").start();
? ? }
? ? private void read() {
? ? ? ? try {
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? lock.readLock().lock();
? ? ? ? ? ? ? ? System.out.println("獲得讀鎖" + Thread.currentThread().getName()
? ? ? ? ? ? ? ? ? ? ? ? + " 時(shí)間:" + System.currentTimeMillis());
? ? ? ? ? ? ? ? //模擬讀操作時(shí)間為5秒
? ? ? ? ? ? ? ? Thread.sleep(5000);
? ? ? ? ? ? } finally {
? ? ? ? ? ? ? ? lock.readLock().unlock();
? ? ? ? ? ? }
? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? }
}
執(zhí)行結(jié)果:
獲得讀鎖ThreadA 時(shí)間:1507720692022
獲得讀鎖ThreadB 時(shí)間:1507720692022
可以看出兩個(gè)線程之間,獲取鎖的時(shí)間幾乎同時(shí),說明lock.readLock().lock(); 允許多個(gè)線程同時(shí)執(zhí)行l(wèi)ock()方法后面的代碼。
(2)寫寫互斥
public class ReentrantReadWriteLockDemo {
? ? private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
? ? public static void main(String[] args) {
? ? ? ? ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
? ? ? ? new Thread(() -> demo.write(), "ThreadA").start();
? ? ? ? new Thread(() -> demo.write(), "ThreadB").start();
? ? }
? ? private void write() {
? ? ? ? try {
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? lock.writeLock().lock();
? ? ? ? ? ? ? ? System.out.println("獲得寫鎖" + Thread.currentThread().getName()
? ? ? ? ? ? ? ? ? ? ? ? + " 時(shí)間:" + System.currentTimeMillis());
? ? ? ? ? ? ? ? //模擬寫操作時(shí)間為5秒
? ? ? ? ? ? ? ? Thread.sleep(5000);
? ? ? ? ? ? } finally {
? ? ? ? ? ? ? ? lock.writeLock().unlock();
? ? ? ? ? ? }
? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? }
}
執(zhí)行結(jié)果:
獲得寫鎖ThreadA 時(shí)間:1507720931662
獲得寫鎖ThreadB 時(shí)間:1507720936662
1
2
可以看出執(zhí)行結(jié)果大致差了5秒的時(shí)間,可以說明多個(gè)寫線程是互斥的。
(3)讀寫互斥或?qū)懽x互斥
public class ReentrantReadWriteLockDemo {
? ? private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
? ? public static void main(String[] args) throws InterruptedException {
? ? ? ? ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
? ? ? ? new Thread(() -> demo.read(), "ThreadA").start();
? ? ? ? Thread.sleep(1000);
? ? ? ? new Thread(() -> demo.write(), "ThreadB").start();
? ? }
? ? private void read() {
? ? ? ? try {
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? lock.readLock().lock();
? ? ? ? ? ? ? ? System.out.println("獲得讀鎖" + Thread.currentThread().getName()
? ? ? ? ? ? ? ? ? ? ? ? + " 時(shí)間:" + System.currentTimeMillis());
? ? ? ? ? ? ? ? Thread.sleep(3000);
? ? ? ? ? ? } finally {
? ? ? ? ? ? ? ? lock.readLock().unlock();
? ? ? ? ? ? }
? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? }
? ? private void write() {
? ? ? ? try {
? ? ? ? ? ? try {
? ? ? ? ? ? ? ? lock.writeLock().lock();
? ? ? ? ? ? ? ? System.out.println("獲得寫鎖" + Thread.currentThread().getName()
? ? ? ? ? ? ? ? ? ? ? ? + " 時(shí)間:" + System.currentTimeMillis());
? ? ? ? ? ? ? ? Thread.sleep(3000);
? ? ? ? ? ? } finally {
? ? ? ? ? ? ? ? lock.writeLock().unlock();
? ? ? ? ? ? }
? ? ? ? } catch (InterruptedException e) {
? ? ? ? ? ? e.printStackTrace();
? ? ? ? }
? ? }
}
執(zhí)行結(jié)果:
獲得讀鎖ThreadA 時(shí)間:1507721135908
獲得寫鎖ThreadB 時(shí)間:1507721138908
可以看出執(zhí)行結(jié)果大致差了3秒的時(shí)間,可以說明讀寫線程是互斥的。
---------------------?
作者:徐劉根?
來源:CSDN?
原文:https://blog.csdn.net/xlgen157387/article/details/78197583?
版權(quán)聲明:本文為博主原創(chuàng)文章,轉(zhuǎn)載請(qǐng)附上博文鏈接!
總結(jié)
以上是生活随笔為你收集整理的Java多线程编程-(5)-使用Lock对象实现同步以及线程间通信的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java多线程编程-(4)-线程间通信机
- 下一篇: 牙齿矫正后固定器的作用有哪些?