readwritelock也是一個接口,提供了readlock和writelock兩種鎖的操作機制,一個資源可以被多個線程同時讀,或者被一個線程寫,但是不能同時存在讀和寫線程。
基本規則: 讀讀不互斥 讀寫互斥 寫寫互斥
問題: 既然讀讀不互斥,為何還要加讀鎖
答: 如果只是讀,是不需要加鎖的,加鎖本身就有性能上的損耗
如果讀可以不是最新數據,也不需要加鎖
如果讀必須是最新數據,必須加讀寫鎖
讀寫鎖相較于互斥鎖的優點僅僅是允許讀讀的并發,除此之外并無其他。
結論: 讀寫鎖能夠保證讀取數據的 嚴格實時性,如果不需要這種 嚴格實時性,那么不需要加讀寫鎖。
簡單實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
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( "讀數據:" +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( "寫數據: " +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(); } } |
之前我們提到的鎖都是排它鎖(同一時刻只允許一個線程進行訪問),而讀寫鎖維護了一對鎖,一個讀鎖,一個寫鎖。讀寫鎖在同一時刻允許多個線程進行讀操作,但是寫線程訪問過程中,所有的讀線程和其他寫線程均被阻塞。如此,并發性有了很大的提升。這樣,在某些讀遠遠大于寫的場景中,讀寫鎖能夠提供比排它鎖更好的并發量和吞吐量。
一個關于讀寫鎖的demo:
分析:設計一個模擬隊列,擁有一個data成員變量用于存儲數據和存取兩種操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
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(); //釋放寫鎖 } } } |
程序部分運行結果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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 |
我們可以看到打印語句結果很正常。
下面我們再來實現一個模擬緩沖區的小demo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
import java.util.hashmap; import java.util.map; import java.util.concurrent.locks.readwritelock; import java.util.concurrent.locks.reentrantreadwritelock; /* * @author vayne * * 多線程實現緩存的小demo */ class cachend { volatile map<string, string> cachmap = new hashmap<string, string>(); //加volatile關鍵字保證可見性。 readwritelock rwlock = new reentrantreadwritelock(); //這個讀寫鎖要定義在方法外面,使得每一個線程用的是同一個讀寫鎖。 public string gets(string key) //如果定義在方法內部,就是跟方法棧有關的讀寫鎖。這樣可能不是同一個鎖。 { rwlock.readlock().lock(); string value = null ; try { value = cachmap.get(key); if (cachmap.get(key) == null ) //這里要重新獲得key對應的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(); //關于這里的順序問題,下面我會提到。 } } } 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(); } } } |
運行結果:
1
2
3
4
5
6
7
8
9
|
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 ] |
上面我給出了一些注釋,其實這個代碼是很不好寫的,考慮的東西很多。下面我來講一下上面的代碼中提到的順序問題。
對于讀寫鎖我們應該了解下面的一些性質(這些性質是由源代碼得出來的,因為源代碼的設計,所以才有下列性質):
- 如果存在讀鎖,則寫鎖不能被獲取,原因在于:讀寫鎖要確保寫鎖的操作對讀鎖可見。,如果允許讀鎖在已被獲取的情況下對寫鎖的獲取,那么正在運行的其他讀線程就無法感知到當前寫線程的操作。因此,只有等待其他讀線程都釋放了讀鎖,寫鎖才能被當前線程獲取,而寫鎖一旦被獲取,則其他讀寫線程的后續訪問將會被阻塞。
- 鎖降級:指的是寫鎖降級成為讀鎖。具體操作是獲取到寫鎖之后,在釋放寫鎖之前,要先再次獲取讀鎖。這也就是上面我寫注釋提醒大家注意的地方。為什么要這樣處理呢,答案就是為了保證數據可見性。如果當前線程不獲取讀鎖而是直接釋放寫鎖,假設此刻另一個線程(記作t)獲取了寫鎖并修改了數據,那么當前線程無法感知線程t的數據更新。如果當前線程獲取讀鎖,即遵循鎖降級的步驟,則線程t將會被阻塞,知道當前線程使用數據并釋放讀鎖之后,t才能獲取寫鎖進行數據更新。
第二條對應我們上面的程序就是,如果我們添加了“demo1”對應的value值,然后釋放了寫鎖,此時在當前線程s還未獲得讀鎖時,另一個線程t又獲得了寫鎖,那么就會將s的操作給覆蓋(如果取到的值已經緩存在s中,那么t的操作就無法被s感知了,到最后依然會返回s操作的值)。
再來看一個demo:
讀寫鎖,分為讀鎖和寫鎖,多個讀鎖不互斥,讀鎖和寫鎖互斥,寫鎖與寫鎖互斥,這是jvm自己控制的,你只要上好相應的鎖即可,如果你的代碼只讀數據,可以很多人同時讀,但不能同時寫,那就上讀鎖;如果你的代碼修改數據,只能有一個人在寫,且不能同時讀取,那就上寫鎖.總之,讀的時候上讀鎖,寫的時候上寫鎖!
看如下程序: 新建6個線程,3個線程用來讀,3個線程用來寫,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
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 ; // 共享數據,只能有一個線程能寫該數據,但可以有多個線程同時讀該數據。 // 讀寫鎖 readwritelock rwl = new reentrantreadwritelock(); // 相當于讀操作 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(); } } } |
讀寫鎖功能很強大!這樣可以實現正常的邏輯,如果我們把讀寫鎖相關的代碼注釋,發現程序正準備寫的時候,就有線程讀了,發現準備讀的時候,有線程去寫,這樣不符合我們的邏輯;通過java5的新特新可以很輕松的解決這樣的問題;
查看java api reentrantreadwritelock 上面有經典(緩存)的用法,下面是doc里面的偽代碼,,它演示的是一個實體的緩存,不是緩存系統,相當于緩存代理,注意volatile的運用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
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(); } } } |
假設現在多個線程來讀了,那第一個線程讀到的數據是空的,那它就要寫就要填充數據,那么第二個第三個就應該互斥等著,一進來是來讀數據的所以上讀鎖,進來后發現數據是空的,就先把讀鎖釋放再重新獲取寫鎖,就開始寫數據,數據寫完了,就把寫鎖釋放,把讀鎖重新掛上,持有讀鎖時不能同時獲取寫鎖,但擁有寫鎖時可同時再獲取讀鎖,自己線程掛的寫鎖可同時掛讀鎖的,這就是降級,就是除了讀鎖和寫鎖外,還有讀寫鎖也叫更新鎖,就是自己即可以讀又可以寫的鎖,也就是在自己擁有寫鎖還沒釋放寫鎖時就獲取了讀鎖就降級為讀寫鎖/更新鎖,但是不能在持有讀鎖時再獲取寫鎖;
基于上面的例子,我們可以實現一個緩存系統:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
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) { } // 可做到多個線程并必的讀 讀和寫又互斥 系統性能很高 // 這就是讀寫鎖的價值 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)來判空 |
感謝大家對服務器之家的支持。
原文鏈接:https://www.cnblogs.com/pony1223/p/9428248.html