【并發技術14】線程同步工具Semaphore的使用
Semaphore 通常用于限制可以訪問某些資源(物理或邏輯的)線程數目,我們可以自己設定最大訪問量。它有兩個很常用的方法是?acquire()?和?release(),分別是獲得許可和釋放許可。
官方JDK上面對 Semaphore 的解釋是這樣子的:
Semaphore 通常用于限制可以訪問某些資源(物理或邏輯的)線程數目,我們可以自己設定最大訪問量。它有兩個很常用的方法是?acquire()?和?release(),分別是獲得許可和釋放許可。
官方JDK上面對 Semaphore 的解釋是這樣子的:
一個計數信號量。從概念上講,信號量維護了一個許可集。如有必要,在許可可用前會阻塞每一個?acquire(),然后再獲取該許可。每個?release()?添加一個許可,從而可能釋放一個正在阻塞的獲取者。但是,不使用實際的許可對象,Semaphore 只對可用許可的號碼進行計數,并采取相應的行動。拿到信號量的線程可以進入代碼,否則就等待。通過?acquire()?和?release()?獲取和釋放訪問許可。
我的解釋是這樣子的:
Semaphore 相當于一個廁所,我在造的時候可以想造幾個坑就造幾個坑,假如現在我就造了3個坑,現在有10個人想要來上廁所,那么每次就只能3個人上,誰最先搶到誰就進去,出來了一個人后,第4個人才能進去,這個就限制了上廁所的人數了,就這個道理。每個人上廁所之前都先?acquire()?一下,如果有坑,就可以進入,沒有就被阻塞,在外面等;上完廁所后,會?release()?一下,釋放一個坑出來,以保證下一個人?acquire()?的時候有坑。
我覺得我的解釋比官方的要好。
1. Semaphore基本使用
這Semaphore 在限制資源訪問量的問題上用處很大,比如限制一個文件的并發訪問次數等,它的原理很好理解。下面寫一個 Semphore 的示例代碼:
public?class?SemaphoreTest?{????public?static?void?main(String[]?args)?{????????????ExecutorService?service?=?Executors.newCachedThreadPool();//使用并發庫,創建緩存的線程池????????final?Semaphore?sp?=?new?Semaphore(3);//創建一個Semaphore信號量,并設置最大并發數為3????????//availablePermits()?//用來獲取當前可用的訪問次數????????System.out.println("初始化:當前有"?+?(3?-?sp.availablePermits()?+?"個并發"));????????//創建10個任務,上面的緩存線程池就會創建10個對應的線程去執行????????for?(int?index?=?0;?index?10;?index++)?{????????????final?int?NO?=?index;??//記錄第幾個任務????????????Runnable?run?=?new?Runnable()?{??//具體任務????????????????public?void?run()?{??????????????????????try?{??????????????????????????????????????????????????sp.acquire();??//?獲取許可?????????????????????????System.out.println(Thread.currentThread().getName()?????????????????????????????+?"獲取許可"?+?"("+NO+"),"?+?"剩余:"?+?sp.availablePermits());??????????????????????????Thread.sleep(1000);??????????????????????????//?訪問完后記得釋放?,否則在控制臺只能打印3條記錄,之后線程一直阻塞????????????????????????sp.release();??//釋放許可????????????????????????System.out.println(Thread.currentThread().getName()?????????????????????????????+?"釋放許可"?+?"("+NO+"),"?+?"剩余:"?+?sp.availablePermits());??????????????????????}?catch?(InterruptedException?e)?{??????????????????????}??????????????????}??????????????};??????????????service.execute(run);??//執行任務????????}?????????????????service.shutdown();?//關閉線程池????}}
代碼結構很容易理解,10個任務,每次最多3個線程去執行任務,其他線程被阻塞。可以通過打印信息來看線程的執行情況:
初始化:當前有0個并發
pool-1-thread-1獲取許可(0),剩余:1
pool-1-thread-3獲取許可(2),剩余:0
pool-1-thread-2獲取許可(1),剩余:1
pool-1-thread-1釋放許可(0),剩余:3
pool-1-thread-4獲取許可(3),剩余:1
pool-1-thread-5獲取許可(4),剩余:1
pool-1-thread-2釋放許可(1),剩余:3
pool-1-thread-3釋放許可(2),剩余:3
pool-1-thread-6獲取許可(5),剩余:0
pool-1-thread-4釋放許可(3),剩余:2
pool-1-thread-9獲取許可(8),剩余:0
pool-1-thread-5釋放許可(4),剩余:2
pool-1-thread-6釋放許可(5),剩余:2
pool-1-thread-8獲取許可(7),剩余:0
pool-1-thread-7獲取許可(6),剩余:2
pool-1-thread-8釋放許可(7),剩余:2
pool-1-thread-10獲取許可(9),剩余:2
pool-1-thread-7釋放許可(6),剩余:2
pool-1-thread-9釋放許可(8),剩余:2
pool-1-thread-10釋放許可(9),剩余:3
從結果中看,前三個為什么剩余的不是3,2,1呢?包括下面,每次釋放的時候剩余的量好像也不對,其實是對的,只不過線程運行太快,前三個是這樣子的:因為最大訪問量是3,所以前三個在打印語句之前都執行完了aquire()方法了,或者有部分執行了,從上面的結果來看,線程1是第一個進去的,線程2第二個進去,然后線程1和2開始打印,所以只剩1個了,接下來線程3進來了,打印只剩0個了。后面釋放的時候也是,打印前可能有不止一個釋放了。
2. Semaphore同步問題
我從網上查了一下,有些人說 Semaphore 實現了同步功能,我覺得不對,因為我自己寫了個測試代碼試了,并不會自己解決并發問題,如果多個線程操作同一個數據,還是需要自己同步一下的。然后我查了一下官方 JDK 文檔(要永遠相信官方的文檔),它里面是這樣說的:
獲得一項前,每個線程必須從信號量獲取許可,從而保證可以使用該項。該線程結束后,將項返回到池中并將許可返回到該信號量,從而允許其他線程獲取該項。注意,調用?acquire()?時無法保持同步鎖,因為這會阻止將項返回到池中。信號量封裝所需的同步,以限制對池的訪問,這同維持該池本身一致性所需的同步是分開的。
這這段官方的解釋就很明確了,然后我就明白了網上有些人說的實現了同步的意思是信號量本身封裝所需的同步,也就是說我拿到了一個,別人就無法拿到了,我釋放了別人才能拿到(就跟我舉的廁所的坑一樣),但是我拿到了之后去操作公共數據的時候,針對這個數據操作的同步 Semaphore 就不管了,這就需要我們自己去同步了。下面寫一個同步的測試代碼:
public?class?SemaphoreTest2?{????private?static?int?data?=?0;????public?static?void?main(String[]?args)?{????????ExecutorService?service?=?Executors.newCachedThreadPool();????????final?Semaphore?sp?=?new?Semaphore(3);????????System.out.println("初始化:當前有"?+?(3?-?sp.availablePermits()?+?"個并發"));????????//?10個任務????????for?(int?index?=?0;?index?10;?index++)?{????????????final?int?NO?=?index;????????????Runnable?run?=?new?Runnable()?{????????????????public?void?run()?{????????????????????try?{????????????????????????//?獲取許可????????????????????????sp.acquire();????????????????????????System.out.println(Thread.currentThread().getName()????????????????????????????????+?"獲取許可"?+?"("?+?NO?+?"),"?+?"剩余:"?+?sp.availablePermits());????????????????????????//實現同步????????????????????????synchronized(SemaphoreTest2.class)?{????????????????????????????System.out.println(Thread.currentThread().getName()????????????????????????????????????+?"執行data自增前:data="?+?data);????????????????????????????data++;????????????????????????????System.out.println(Thread.currentThread().getName()????????????????????????????????????+?"執行data自增后:data="?+?data);????????????????????????}????????????????????????sp.release();????????????????????????System.out.println(Thread.currentThread().getName()????????????????????????????????+?"釋放許可"?+?"("?+?NO?+?"),"?+?"剩余:"?+?sp.availablePermits());????????????????????}?catch?(InterruptedException?e)?{????????????????????}????????????????}????????????};????????????service.execute(run);????????}????????service.shutdown();????}}
看一下運行結果(部分)
初始化:當前有0個并發
pool-1-thread-2獲取許可(1),剩余:0
pool-1-thread-2執行data自增前:data=0
pool-1-thread-3獲取許可(2),剩余:0
pool-1-thread-1獲取許可(0),剩余:0
pool-1-thread-2執行data自增后:data=1
pool-1-thread-3執行data自增前:data=1
pool-1-thread-3執行data自增后:data=2
pool-1-thread-1執行data自增前:data=2
pool-1-thread-7獲取許可(6),剩余:1
pool-1-thread-3釋放許可(2),剩余:2
pool-1-thread-1執行data自增后:data=3
從結果可以看出,每個線程在操作數據的前后,是不會受其他線程的影響的,但是其他線程可以獲取許可,獲取許可了之后就被阻塞在外面,等待當前線程操作完 data 才能去操作。當然也可以在當前線程操作 data 的時候,其他線程釋放許可,因為這完全不沖突。那如果把上面同步代碼塊去掉,再試試看會成什么亂七八糟的結果(部分):
初始化:當前有0個并發
pool-1-thread-3獲取許可(2),剩余:0
pool-1-thread-2獲取許可(1),剩余:0
pool-1-thread-3執行data自增前:data=0
pool-1-thread-2執行data自增前:data=0
pool-1-thread-1獲取許可(0),剩余:0
pool-1-thread-3執行data自增后:data=1
pool-1-thread-2執行data自增后:data=2
pool-1-thread-7獲取許可(6),剩余:0
pool-1-thread-1執行data自增前:data=2
pool-1-thread-8獲取許可(7),剩余:0
pool-1-thread-7執行data自增前:data=2
pool-1-thread-2釋放許可(1),剩余:1
pool-1-thread-7執行data自增后:data=4
從結果中看,已經很明顯了,線程2和3都進去了,然后初始 data 都是0,線程3自增了一下,打印出1是沒問題的,但是線程2呢?也自增了一下,卻打印出了2。也就是說,線程2在操作數據的前后,數據已經被線程3修改過了,再一次證明 Semaphere 并沒有實現對共有數據的同步,在操作公共數據的時候,需要我們自己實現。
Semaphere 中如果設置信號量為1的話,那就說明每次只能一個線程去操作任務,那這樣的話也就不存在線程安全問題了,所以如果設置信號量為1的話,就可以去掉那個 synchronized,不過效率就不行了。
來源:微信公眾號
任務調度
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。