Java編程讀寫鎖詳解
ReadWriteLock也是一個接口,提供了readLock和writeLock兩種鎖的操作機制,一個資源可以被多個線程同時讀,或者被一個線程寫,但是不能同時存在讀和寫線程。
基本規(guī)則: 讀讀不互斥 讀寫互斥 寫寫互斥
問題: 既然讀讀不互斥,為何還要加讀鎖
答: 如果只是讀,是不需要加鎖的,加鎖本身就有性能上的損耗
如果讀可以不是最新數(shù)據(jù),也不需要加鎖
如果讀必須是最新數(shù)據(jù),必須加讀寫鎖
讀寫鎖相較于互斥鎖的優(yōu)點僅僅是允許讀讀的并發(fā),除此之外并無其他。
結(jié)論: 讀寫鎖能夠保證讀取數(shù)據(jù)的 嚴格實時性,如果不需要這種 嚴格實時性,那么不需要加讀寫鎖。
簡單實現(xiàn):
package readandwrite;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class MyTest {
private static ReentrantReadWriteLock rwl=new ReentrantReadWriteLock();
private static double data=0;
static class readClass implements Runnable{
@Override
public void run() {
rwl.readLock().lock();
System.out.println("讀數(shù)據(jù):"+data);
rwl.readLock().unlock();
}
}
static class writeClass implements Runnable{
private double i;
public writeClass(double i) {
this.i = i;
}
@Override
public void run() {
rwl.writeLock().lock();
data=i;
System.out.println("寫數(shù)據(jù): "+data);
rwl.writeLock().unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService pool=Executors.newCachedThreadPool();
for(int i=0;i<10;i++){
pool.submit(new readClass());
pool.submit(new writeClass((double)new Random().nextDouble()));
pool.submit(new writeClass((double)new Random().nextDouble()));
Thread.sleep(1000);
}
pool.shutdown();
}
}
之前我們提到的鎖都是排它鎖(同一時刻只允許一個線程進行訪問),而讀寫鎖維護了一對鎖,一個讀鎖,一個寫鎖。讀寫鎖在同一時刻允許多個線程進行讀操作,但是寫線程訪問過程中,所有的讀線程和其他寫線程均被阻塞。如此,并發(fā)性有了很大的提升。這樣,在某些讀遠遠大于寫的場景中,讀寫鎖能夠提供比排它鎖更好的并發(fā)量和吞吐量。
一個關(guān)于讀寫鎖的Demo:
分析:設(shè)計一個模擬隊列,擁有一個data成員變量用于存儲數(shù)據(jù)和存取兩種操作。
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo
{
public static void main(String[] args)
{
DefQueue queue = new DefQueue();
for (int i = 1; i < 10; i++)
{
//啟動線程進行讀操作
new Thread(new Runnable()
{
@Override
public void run()
{
while (true)
{
queue.get();
}
}
}).start();
//啟動線程進行寫操作
new Thread(new Runnable()
{
@Override
public void run()
{
while(true)
{
queue.put(new Random().nextInt(10000));
}
}
}).start();
}
}
}
class DefQueue
{
private int data;
ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void get()
{
rwLock.readLock().lock();//加讀鎖
try
{
System.out.println(Thread.currentThread().getName() + "be ready to get data");
Thread.sleep((long) (Math.random() * 1000));
System.out.println(Thread.currentThread().getName() + "get the data: " + data);
} catch (InterruptedException e)
{
e.printStackTrace();
} finally
{
rwLock.readLock().unlock();//釋放讀鎖
}
}
public void put(int data)
{
rwLock.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() + " has wrote the data: "+data);
} catch (InterruptedException e)
{
e.printStackTrace();
} finally
{
rwLock.writeLock().unlock();//釋放寫鎖
}
}
}
程序部分運行結(jié)果:
Thread-0be ready to get data Thread-0get the data: 0 Thread-1 be ready to write data Thread-1 has wrote the data: 1156 Thread-2be ready to get data Thread-2get the data: 1156 Thread-3 be ready to write data Thread-3 has wrote the data: 9784 Thread-3 be ready to write data Thread-3 has wrote the data: 4370 Thread-3 be ready to write data Thread-3 has wrote the data: 1533 Thread-4be ready to get data Thread-4get the data: 1533 Thread-5 be ready to write data Thread-5 has wrote the data: 2345 Thread-6be ready to get data Thread-6get the data: 2345 Thread-9 be ready to write data Thread-9 has wrote the data: 9463 Thread-9 be ready to write data Thread-9 has wrote the data: 9301 Thread-9 be ready to write data Thread-9 has wrote the data: 549 Thread-9 be ready to write data Thread-9 has wrote the data: 4673 Thread-9 be ready to write data
我們可以看到打印語句結(jié)果很正常。
下面我們再來實現(xiàn)一個模擬緩沖區(qū)的小Demo:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/*
* @author vayne
*
* 多線程實現(xiàn)緩存的小demo
*/
class Cachend
{
volatile Map<String, String> cachmap = new HashMap<String, String>();//加volatile關(guān)鍵字保證可見性。
ReadWriteLock rwLock = new ReentrantReadWriteLock();//這個讀寫鎖要定義在方法外面,使得每一個線程用的是同一個讀寫鎖。
public String getS(String key) //如果定義在方法內(nèi)部,就是跟方法棧有關(guān)的讀寫鎖。這樣可能不是同一個鎖。
{
rwLock.readLock().lock();
String value = null;
try
{
value = cachmap.get(key);
if (cachmap.get(key) == null)//這里要重新獲得key對應(yīng)的value值
{
rwLock.readLock().unlock();
rwLock.writeLock().lock();
try
{
if (cachmap.get(key) == null)//這里也是
{
value = "" + Thread.currentThread().getName();
cachmap.put(key, value);
System.out.println(Thread.currentThread().getName() + " put the value ::::" + value);
}
} finally
{
rwLock.readLock().lock(); //將鎖降級,這里跟下一句的順序不能反。
rwLock.writeLock().unlock();//關(guān)于這里的順序問題,下面我會提到。
}
}
} finally
{
rwLock.readLock().unlock();
}
return cachmap.get(key);
}
}
public class CachendDemo
{
public static void main(String[] args)
{
Cachend ca = new Cachend();
for (int i = 0; i < 4; i++)
{
new Thread(new Runnable()
{
@Override
public void run()
{
System.out.println(Thread.currentThread().getName()+" "+ca.getS("demo1"));
System.out.println(Thread.currentThread().getName()+" "+ca.cachmap.entrySet());
}
}).start();
}
}
}
運行結(jié)果:
Thread-0 put the value ::::Thread-0 Thread-0 Thread-0 Thread-0 [demo1=Thread-0] Thread-2 Thread-0 Thread-2 [demo1=Thread-0] Thread-3 Thread-0 Thread-3 [demo1=Thread-0] Thread-1 Thread-0 Thread-1 [demo1=Thread-0]
上面我給出了一些注釋,其實這個代碼是很不好寫的,考慮的東西很多。下面我來講一下上面的代碼中提到的順序問題。
對于讀寫鎖我們應(yīng)該了解下面的一些性質(zhì)(這些性質(zhì)是由源代碼得出來的,因為源代碼的設(shè)計,所以才有下列性質(zhì)):
- 如果存在讀鎖,則寫鎖不能被獲取,原因在于:讀寫鎖要確保寫鎖的操作對讀鎖可見。,如果允許讀鎖在已被獲取的情況下對寫鎖的獲取,那么正在運行的其他讀線程就無法感知到當(dāng)前寫線程的操作。因此,只有等待其他讀線程都釋放了讀鎖,寫鎖才能被當(dāng)前線程獲取,而寫鎖一旦被獲取,則其他讀寫線程的后續(xù)訪問將會被阻塞。
- 鎖降級:指的是寫鎖降級成為讀鎖。具體操作是獲取到寫鎖之后,在釋放寫鎖之前,要先再次獲取讀鎖。這也就是上面我寫注釋提醒大家注意的地方。為什么要這樣處理呢,答案就是為了保證數(shù)據(jù)可見性。如果當(dāng)前線程不獲取讀鎖而是直接釋放寫鎖,假設(shè)此刻另一個線程(記作T)獲取了寫鎖并修改了數(shù)據(jù),那么當(dāng)前線程無法感知線程T的數(shù)據(jù)更新。如果當(dāng)前線程獲取讀鎖,即遵循鎖降級的步驟,則線程T將會被阻塞,知道當(dāng)前線程使用數(shù)據(jù)并釋放讀鎖之后,T才能獲取寫鎖進行數(shù)據(jù)更新。
第二條對應(yīng)我們上面的程序就是,如果我們添加了“demo1”對應(yīng)的value值,然后釋放了寫鎖,此時在當(dāng)前線程S還未獲得讀鎖時,另一個線程T又獲得了寫鎖,那么就會將S的操作給覆蓋(如果取到的值已經(jīng)緩存在S中,那么T的操作就無法被S感知了,到最后依然會返回S操作的值)。
再來看一個DEMO:
讀寫鎖,分為讀鎖和寫鎖,多個讀鎖不互斥,讀鎖和寫鎖互斥,寫鎖與寫鎖互斥,這是JVM自己控制的,你只要上好相應(yīng)的鎖即可,如果你的代碼只讀數(shù)據(jù),可以很多人同時讀,但不能同時寫,那就上讀鎖;如果你的代碼修改數(shù)據(jù),只能有一個人在寫,且不能同時讀取,那就上寫鎖.總之,讀的時候上讀鎖,寫的時候上寫鎖!
看如下程序: 新建6個線程,3個線程用來讀,3個線程用來寫,
package javaplay.thread.test;
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
public static void main(String[] args) {
final Queue3 q3 = new Queue3();
for (int i = 0; i < 3; i++) {
new Thread() {
public void run() {
while (true) {
q3.get();
}
}
}.start();
new Thread() {
public void run() {
while (true) {
q3.put(new Random().nextInt(10000));
}
}
}.start();
}
}
}
class Queue3 {
private Object data = null;// 共享數(shù)據(jù),只能有一個線程能寫該數(shù)據(jù),但可以有多個線程同時讀該數(shù)據(jù)。
// 讀寫鎖
ReadWriteLock rwl = new ReentrantReadWriteLock();
// 相當(dāng)于讀操作
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();
}
}
// 相當(dāng)于寫操作
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();
}
}
}
讀寫鎖功能很強大!這樣可以實現(xiàn)正常的邏輯,如果我們把讀寫鎖相關(guān)的代碼注釋,發(fā)現(xiàn)程序正準備寫的時候,就有線程讀了,發(fā)現(xiàn)準備讀的時候,有線程去寫,這樣不符合我們的邏輯;通過Java5的新特新可以很輕松的解決這樣的問題;
查看Java API ReentrantReadWriteLock 上面有經(jīng)典(緩存)的用法,下面是doc里面的偽代碼,,它演示的是一個實體的緩存,不是緩存系統(tǒng),相當(dāng)于緩存代理,注意volatile的運用:
package javaplay.thread.test;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/*
* Sample usages. Here is a code sketch showing how to perform lock downgrading after updating a cache
* (exception handling is particularly tricky when handling multiple locks in a non-nested fashion):
*/
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
假設(shè)現(xiàn)在多個線程來讀了,那第一個線程讀到的數(shù)據(jù)是空的,那它就要寫就要填充數(shù)據(jù),那么第二個第三個就應(yīng)該互斥等著,一進來是來讀數(shù)據(jù)的所以上讀鎖,進來后發(fā)現(xiàn)數(shù)據(jù)是空的,就先把讀鎖釋放再重新獲取寫鎖,就開始寫數(shù)據(jù),數(shù)據(jù)寫完了,就把寫鎖釋放,把讀鎖重新掛上,持有讀鎖時不能同時獲取寫鎖,但擁有寫鎖時可同時再獲取讀鎖,自己線程掛的寫鎖可同時掛讀鎖的,這就是降級,就是除了讀鎖和寫鎖外,還有讀寫鎖也叫更新鎖,就是自己即可以讀又可以寫的鎖,也就是在自己擁有寫鎖還沒釋放寫鎖時就獲取了讀鎖就降級為讀寫鎖/更新鎖,但是不能在持有讀鎖時再獲取寫鎖;
基于上面的例子,我們可以實現(xiàn)一個緩存系統(tǒng):
package javaplay.thread.test;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class CacheDemo {
private Map<String, Object> cache = new HashMap<>();
public static void main(String[] args) {
}
// 可做到多個線程并必的讀 讀和寫又互斥 系統(tǒng)性能很高
// 這就是讀寫鎖的價值
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public Object getData(String key) {
rwl.readLock().lock();
Object value = null;
try {
value = cache.get(key);
if (value == null) {// 避免首次多次查詢要加synchronized
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
if (value == null) // 就算第二個第三個線程進來時也不用再寫了 跟偽代碼相同原理
value = "aaa";// 實際去query db
} finally {
rwl.writeLock().unlock();
}
rwl.readLock().lock();
}
} finally {
rwl.readLock().unlock();
}
return value;
}
}
錯誤之處:沒有把不存在的值put;要用get(key)來判空
感謝大家對腳本之家的支持。
- Java并發(fā)編程之重入鎖與讀寫鎖
- Java 讀寫鎖實現(xiàn)原理淺析
- Java并發(fā)編程之ReadWriteLock讀寫鎖的操作方法
- Java并發(fā)之搞懂讀寫鎖
- Java多線程讀寫鎖ReentrantReadWriteLock類詳解
- java并發(fā)編程中ReentrantLock可重入讀寫鎖
- Java中讀寫鎖ReadWriteLock的原理與應(yīng)用詳解
- 詳解Java?ReentrantReadWriteLock讀寫鎖的原理與實現(xiàn)
- 一文了解Java讀寫鎖ReentrantReadWriteLock的使用
- Java讀寫鎖ReadWriteLock的創(chuàng)建使用及測試分析示例詳解
- Java AQS中ReentrantReadWriteLock讀寫鎖的使用
- Java讀寫鎖ReadWriteLock原理與應(yīng)用場景詳解
相關(guān)文章
Java for循環(huán)性能優(yōu)化實現(xiàn)解析
這篇文章主要介紹了Java for循環(huán)性能優(yōu)化實現(xiàn)解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-01-01
Mybatis入門教程(四)之mybatis動態(tài)sql
這篇文章主要介紹了Mybatis入門教程(四)之mybatis動態(tài)sql的相關(guān)資料,涉及到動態(tài)sql及動態(tài)sql的作用知識,本文介紹的非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-09-09

