Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
“可重入鎖”是指當一個線程調用 object.lock()獲取到鎖,進入臨界區后,再次調用object.lock(),仍然可以獲取到該鎖。顯然,通常的鎖都要設計成可重入的,否則就會發生死鎖。
synchronized關鍵字,就是可重入鎖。在一個synchronized方法method1()里面調用另外一個synchronized方法method2()。如果synchronized關鍵字不可重入,那么在method2()處就會發生阻塞,這顯然不可行。
在正式介紹鎖的實現原理之前,先看一下 Concurrent 包中的與互斥鎖(ReentrantLock)相關類之間的繼承層次,如下圖所示:
Lock是一個接口,其定義如下:
常用的方法是lock()/unlock()。lock()不能被中斷,對應的lockInterruptibly()可以被中斷。
ReentrantLock本身沒有代碼邏輯,實現都在其內部類Sync中:
Sync是一個抽象類,它有兩個子類FairSync與NonfairSync,分別對應公平鎖和非公平鎖。從下面的ReentrantLock構造方法可以看出,會傳入一個布爾類型的變量fair指定鎖是公平的還是非公平的,默認為非公平的。
什么叫公平鎖和非公平鎖呢?先舉個現實生活中的例子,一個人去火車站售票窗口買票,發現現場有人排隊,于是他排在隊伍末尾,遵循先到者優先服務的規則,這叫公平;如果他去了不排隊,直接沖到窗口買票,這叫作不公平。
對應到鎖的例子,一個新的線程來了之后,看到有很多線程在排隊,自己排到隊伍末尾,這叫公平;線程來了之后直接去搶鎖,這叫作不公平。默認設置的是非公平鎖,其實是為了提高效率,減少線程切換。
鎖實現的基本原理
Sync的父類AbstractQueuedSynchronizer經常被稱作隊列同步器(AQS),這個類非常重要,該類的父類是AbstractOwnableSynchronizer。
此處的鎖具備synchronized功能,即可以阻塞一個線程。為了實現一把具有阻塞或喚醒功能的鎖,需要幾個核心要素:
針對要素1和2,在上面兩個類中有對應的體現:
state取值不僅可以是0、1,還可以大于1,就是為了支持鎖的可重入性。例如,同樣一個線程,調用5次lock,state會變成5;然后調用5次unlock,state減為0。
當state=0時,沒有線程持有鎖,exclusiveOwnerThread=null;
當state=1時,有一個線程持有鎖,exclusiveOwnerThread=該線程;
當state > 1時,說明該線程重入了該鎖。
對于要素3,Unsafe類提供了阻塞或喚醒線程的一對操作原語,也就是park/unpark。
有一個LockSupport的工具類,對這一對原語做了簡單封裝:
在當前線程中調用park(),該線程就會被阻塞;在另外一個線程中,調用unpark(Thread thread),傳入一個被阻塞的線程,就可以喚醒阻塞在park()地方的線程。
unpark(Thread thread),它實現了一個線程對另外一個線程的“精準喚醒”。notify也只是喚醒某一個線程,但無法指定具體喚醒哪個線程。
針對要素4,在AQS中利用雙向鏈表和CAS實現了一個阻塞隊列。如下所示:
阻塞隊列是整個AQS核心中的核心。如下圖所示,head指向雙向鏈表頭部,tail指向雙向鏈表尾部。入隊就是把新的Node加到tail后面,然后對tail進行CAS操作;出隊就是對head進行CAS操作,把head向后移一個位置。
初始的時候,head=tail=NULL;然后,在往隊列中加入阻塞的線程時,會新建一個空的Node,讓head和tail都指向這個空Node;之后,在后面加入被阻塞的線程對象。所以,當head=tail的時候,說明隊列為空。
下面分析基于AQS,ReentrantLock在公平性和非公平性上的實現差異。
下面進入鎖的最為關鍵的部分,即acquireQueued(...)方法內部一探究竟。
先說addWaiter(...)方法,就是為當前線程生成一個Node,然后把Node放入雙向鏈表的尾部。要注意的是,這只是把Thread對象放入了一個隊列中而已,線程本身并未阻塞。
創建節點,嘗試將節點追加到隊列尾部。獲取tail節點,將tail節點的next設置為當前節點。
如果tail不存在,就初始化隊列。
在addWaiter(...)方法把Thread對象加入阻塞隊列之后的工作就要靠acquireQueued(...)方法完成。線程一旦進入acquireQueued(...)就會被無限期阻塞,即使有其他線程調用interrupt()方法也不能將其喚醒,除非有其他線程釋放了鎖,并且該線程拿到了鎖,才會從accquireQueued(...)返回。
進入acquireQueued(...),該線程被阻塞。在該方法返回的一刻,就是拿到鎖的那一刻,也就是被喚醒的那一刻,此時會刪除隊列的第一個元素(head指針前移1個節點)。
首先,acquireQueued(...)方法有一個返回值,表示什么意思呢?雖然該方法不會中斷響應,但它會記錄被阻塞期間有沒有其他線程向它發送過中斷信號。如果有,則該方法會返回true;否則,返回false。
基于這個返回值,才有了下面的代碼:
當 acquireQueued(...)返回 true 時,會調用 selfInterrupt(),自己給自己發送中斷信號,也就是自己把自己的中斷標志位設為true。之所以要這么做,是因為自己在阻塞期間,收到其他線程中斷信號沒有及時響應,現在要進行補償。這樣一來,如果該線程在lock代碼塊內部有調用sleep()之類的阻塞方法,就可以拋出異常,響應該中斷信號。
阻塞就發生在下面這個方法中:
線程調用 park()方法,自己把自己阻塞起來,直到被其他線程喚醒,該方法返回。
park()方法返回有兩種情況。
也正因為LockSupport.park()可能被中斷喚醒,acquireQueued(...)方法才寫了一個for死循環。喚醒之后,如果發現自己排在隊列頭部,就去拿鎖;如果拿不到鎖,則再次自己阻塞自己。不斷重復此過程,直到拿到鎖。
被喚醒之后,通過Thread.interrupted()來判斷是否被中斷喚醒。如果是情況1,會返回false;如果是情況2,則返回true。
說完了lock,下面分析unlock的實現。unlock不區分公平還是非公平。
上圖中,當前線程要釋放鎖,先調用tryRelease(arg)方法,如果返回true,則取出head,讓head獲取鎖。
對于tryRelease方法:
首先計算當前線程釋放鎖后的state值。
如果當前線程不是排他線程,則拋異常,因為只有獲取鎖的線程才可以進行釋放鎖的操作。
此時設置state,沒有使用CAS,因為是單線程操作。
再看unparkSuccessor方法:
release()里面做了兩件事:tryRelease(...)方法釋放鎖;unparkSuccessor(...)方法喚醒隊列中的后繼者。
上面的 lock 不能被中斷,這里的 lockInterruptibly()可以被中斷:
這里的 acquireInterruptibly(...)也是 AQS 的模板方法,里面的 tryAcquire(...)分別被 FairSync和NonfairSync實現。
主要看doAcquireInterruptibly(...)方法:
當parkAndCheckInterrupt()返回true的時候,說明有其他線程發送中斷信號,直接拋出InterruptedException,跳出for循環,整個方法返回。
tryLock()實現基于調用非公平鎖的tryAcquire(...),對state進行CAS操作,如果操作成功就拿到鎖;
如果操作不成功則直接返回false,也不阻塞。
和互斥鎖相比,讀寫鎖(ReentrantReadWriteLock)就是讀線程和讀線程之間不互斥。
讀讀不互斥,讀寫互斥,寫寫互斥
ReadWriteLock是一個接口,內部由兩個Lock接口組成。
ReentrantReadWriteLock實現了該接口,使用方式如下:
也就是說,當使用 ReadWriteLock 的時候,并不是直接使用,而是獲得其內部的讀鎖和寫鎖,然后分別調用lock/unlock。
個視圖呢?可以理解為是一把鎖,線程分成兩類:讀線程和寫線程。讀線程和寫線程之間不互斥(可以同時拿到這把鎖),讀線程之間不互斥,寫線程之間互斥。
從下面的構造方法也可以看出,readerLock和writerLock實際共用同一個sync對象。sync對象同互斥鎖一樣,分為非公平和公平兩種策略,并繼承自AQS。
同互斥鎖一樣,讀寫鎖也是用state變量來表示鎖狀態的。只是state變量在這里的含義和互斥鎖完全不同。在內部類Sync中,對state變量進行了重新定義,如下所示:
也就是把 state 變量拆成兩半,低16位,用來記錄寫鎖。但同一時間既然只能有一個線程寫,為什么還需要16位呢?這是因為一個寫線程可能多次重入。例如,低16位的值等于5,表示一個寫線程重入了5次。
高16位,用來“讀”鎖。例如,高16位的值等于5,既可以表示5個讀線程都拿到了該鎖;也可以表示一個讀線程重入了5次。
為什么要把一個int類型變量拆成兩半,而不是用兩個int型變量分別表示讀鎖和寫鎖的狀態呢?
這是因為無法用一次CAS 同時操作兩個int變量,所以用了一個int型的高16位和低16位分別表示讀鎖和寫鎖的狀態。
當state=0時,說明既沒有線程持有讀鎖,也沒有線程持有寫鎖;當state !=0時,要么有線程持有讀鎖,要么有線程持有寫鎖,兩者不能同時成立,因為讀和寫互斥。這時再進一步通過sharedCount(state)和exclusiveCount(state)判斷到底是讀線程還是寫線程持有了該鎖。
下面介紹在ReentrantReadWriteLock的兩個內部類ReadLock和WriteLock中,是如何使用state變量的。
acquire/release、acquireShared/releaseShared 是AQS里面的兩對模板方法。互斥鎖和讀寫鎖的寫鎖都是基于acquire/release模板方法來實現的。讀寫鎖的讀鎖是基于acquireShared/releaseShared這對模板方法來實現的。這兩對模板方法的代碼如下:
將讀/寫、公平/非公平進行排列組合,就有4種組合。如下圖所示,上面的兩個方法都是在Sync中實現的。Sync中的兩個方法又是模板方法,在NonfairSync和FairSync中分別有實現。最終的對應關系如下:
對于公平,比較容易理解,不論是讀鎖,還是寫鎖,只要隊列中有其他線程在排隊(排隊等讀鎖,或者排隊等寫鎖),就不能直接去搶鎖,要排在隊列尾部。
對于非公平,讀鎖和寫鎖的實現策略略有差異。
寫線程能搶鎖,前提是state=0,只有在沒有其他線程持有讀鎖或寫鎖的情況下,它才有機會去搶鎖。或者state !=0,但那個持有寫鎖的線程是它自己,再次重入。寫線程是非公平的,即writerShouldBlock()方法一直返回false。
對于讀線程,假設當前線程被讀線程持有,然后其他讀線程還非公平地一直去搶,可能導致寫線程永遠拿不到鎖,所以對于讀線程的非公平,要做一些“約束”。當發現隊列的第1個元素是寫線程的時候,讀線程也要阻塞,不能直接去搶。即偏向寫線程。
寫鎖是排他鎖,實現策略類似于互斥鎖。
1.tryLock()實現分析
lock()方法:
在互斥鎖部分講過了。tryLock和lock方法不區分公平/非公平。
2.unlock()實現分析
unlock()方法不區分公平/非公平。
讀鎖是共享鎖,其實現策略和排他鎖有很大的差異。
1.tryLock()實現分析
2.unlock()實現分析
tryReleaseShared()的實現:
因為讀鎖是共享鎖,多個線程會同時持有讀鎖,所以對讀鎖的釋放不能直接減1,而是需要通過一個for循環+CAS操作不斷重試。這是tryReleaseShared和tryRelease的根本差異所在。
Condition本身也是一個接口,其功能和wait/notify類似,如下所示:
wait()/notify()必須和synchronized一起使用,Condition也必須和Lock一起使用。因此,在Lock的接口中,有一個與Condition相關的接口:
以ArrayBlockingQueue為例。如下所示為一個用數組實現的阻塞隊列,執行put(...)操作的時候,隊列滿了,生產者線程被阻塞;執行take()操作的時候,隊列為空,消費者線程被阻塞。
可以發現,Condition的使用很方便,避免了wait/notify的生產者通知生產者、消費者通知消費者的問題。具體實現如下:
由于Condition必須和Lock一起使用,所以Condition的實現也是Lock的一部分。首先查看互斥鎖和讀寫鎖中Condition的構造方法:
首先,讀寫鎖中的 ReadLock 是不支持 Condition 的,讀寫鎖的寫鎖和互斥鎖都支持Condition。雖然它們各自調用的是自己的內部類Sync,但內部類Sync都繼承自AQS。因此,上面的代碼sync.newCondition最終都調用了AQS中的newCondition:
每一個Condition對象上面,都阻塞了多個線程。因此,在ConditionObject內部也有一個雙向鏈表組成的隊列,如下所示:
下面來看一下在await()/notify()方法中,是如何使用這個隊列的。
關于await,有幾個關鍵點要說明:
與await()不同,awaitUninterruptibly()不會響應中斷,其方法的定義中不會有中斷異常拋出,下面分析其實現和await()的區別。
可以看出,整體代碼和 await()類似,區別在于收到異常后,不會拋出異常,而是繼續執行while循環。
同 await()一樣,在調用 notify()的時候,必須先拿到鎖(否則就會拋出上面的異常),是因為前面執行await()的時候,把鎖釋放了。
然后,從隊列中取出firstWaiter,喚醒它。在通過調用unpark喚醒它之前,先用enq(node)方法把這個Node放入AQS的鎖對應的阻塞隊列中。也正因為如此,才有了await()方法里面的判斷條件:
while( ! isOnSyncQueue(node))
這個判斷條件滿足,說明await線程不是被中斷,而是被unpark喚醒的。
notifyAll()與此類似。
StampedLock是在JDK8中新增的,有了讀寫鎖,為什么還要引入StampedLock呢?
可以看到,從ReentrantLock到StampedLock,并發度依次提高。
另一方面,因為ReentrantReadWriteLock采用的是“悲觀讀”的策略,當第一個讀線程拿到鎖之后,第二個、第三個讀線程還可以拿到鎖,使得寫線程一直拿不到鎖,可能導致寫線程“餓死”。雖然在其公平或非公平的實現中,都盡量避免這種情形,但還有可能發生。
StampedLock引入了“樂觀讀”策略,讀的時候不加讀鎖,讀出來發現數據被修改了,再升級為“悲觀讀”,相當于降低了“讀”的地位,把搶鎖的天平往“寫”的一方傾斜了一下,避免寫線程被餓死。
在剖析其原理之前,下面先以官方的一個例子來看一下StampedLock如何使用。
如上面代碼所示,有一個Point類,多個線程調用move()方法,修改坐標;還有多個線程調用distanceFromOrigin()方法,求距離。
首先,執行move操作的時候,要加寫鎖。這個用法和ReadWriteLock的用法沒有區別,寫操作和寫操作也是互斥的。
關鍵在于讀的時候,用了一個“樂觀讀”sl.tryOptimisticRead(),相當于在讀之前給數據的狀態做了一個“快照”。然后,把數據拷貝到內存里面,在用之前,再比對一次版本號。如果版本號變了,則說明在讀的期間有其他線程修改了數據。讀出來的數據廢棄,重新獲取讀鎖。關鍵代碼就是下面這三行:
要說明的是,這三行關鍵代碼對順序非常敏感,不能有重排序。因為 state 變量已經是volatile,所以可以禁止重排序,但stamp并不是volatile的。為此,在validate(stamp)方法里面插入內存屏障。
首先,StampedLock是一個讀寫鎖,因此也會像讀寫鎖那樣,把一個state變量分成兩半,分別表示讀鎖和寫鎖的狀態。同時,它還需要一個數據的version。但是,一次CAS沒有辦法操作兩個變量,所以這個state變量本身同時也表示了數據的version。下面先分析state變量。
如下圖:用最低的8位表示讀和寫的狀態,其中第8位表示寫鎖的狀態,最低的7位表示讀鎖的狀態。因為寫鎖只有一個bit位,所以寫鎖是不可重入的。
初始值不為0,而是把WBIT 向左移動了一位,也就是上面的ORIGIN 常量,構造方法如下所示。
為什么state的初始值不設為0呢?看樂觀鎖的實現:
上面兩個方法必須結合起來看:當state&WBIT !=0的時候,說明有線程持有寫鎖,上面的tryOptimisticRead會永遠返回0。這樣,再調用validate(stamp),也就是validate(0)也會永遠返回false。這正是我們想要的邏輯:當有線程持有寫鎖的時候,validate永遠返回false,無論寫線程是否釋放了寫鎖。因為無論是否釋放了(state回到初始值)寫鎖,state值都不為0,所以validate(0)永遠為false。
為什么上面的validate(...)方法不直接比較stamp=state,而要比較state&SBITS=state&SBITS 呢?
因為讀鎖和讀鎖是不互斥的!
所以,即使在“樂觀讀”的時候,state 值被修改了,但如果它改的是第7位,validate(...)還是會返回true。
另外要說明的一點是,上面使用了內存屏障VarHandle.acquireFence();,是因為在這行代碼的下一行里面的stamp、SBITS變量不是volatile的,由此可以禁止其和前面的currentX=X,currentY=Y進行重排序。
通過上面的分析,可以發現state的設計非常巧妙。只通過一個變量,既實現了讀鎖、寫鎖的狀態記錄,還實現了數據的版本號的記錄。
同ReadWriteLock一樣,StampedLock也要進行悲觀的讀鎖和寫鎖操作。不過,它不是基于AQS實現的,而是內部重新實現了一個阻塞隊列。如下所示。
這個阻塞隊列和 AQS 里面的很像。
剛開始的時候,whead=wtail=NULL,然后初始化,建一個空節點,whead和wtail都指向這個空節點,之后往里面加入一個個讀線程或寫線程節點。
但基于這個阻塞隊列實現的鎖的調度策略和AQS很不一樣,也就是“自旋”。
在AQS里面,當一個線程CAS state失敗之后,會立即加入阻塞隊列,并且進入阻塞狀態。
但在StampedLock中,CAS state失敗之后,會不斷自旋,自旋足夠多的次數之后,如果還拿不到鎖,才進入阻塞狀態。
為此,根據CPU的核數,定義了自旋次數的常量值。如果是單核的CPU,肯定不能自旋,在多核情況下,才采用自旋策略。
下面以寫鎖的加鎖,也就是StampedLock的writeLock()方法為例,來看一下自旋的實現。
如上面代碼所示,當state&ABITS==0的時候,說明既沒有線程持有讀鎖,也沒有線程持有寫鎖,此時當前線程才有資格通過CAS操作state。若操作不成功,則調用acquireWrite()方法進入阻塞隊列,并進行自旋,這個方法是整個加鎖操作的核心,代碼如下:
整個acquireWrite(...)方法是兩個大的for循環,內部實現了非常復雜的自旋策略。在第一個大的for循環里面,目的就是把該Node加入隊列的尾部,一邊加入,一邊通過CAS操作嘗試獲得鎖。如果獲得了,整個方法就會返回;如果不能獲得鎖,會一直自旋,直到加入隊列尾部。
在第二個大的for循環里,也就是該Node已經在隊列尾部了。這個時候,如果發現自己剛好也在隊列頭部,說明隊列中除了空的Head節點,就是當前線程了。此時,再進行新一輪的自旋,直到達到MAX_HEAD_SPINS次數,然后進入阻塞。這里有一個關鍵點要說明:當release(...)方法被調用之后,會喚醒隊列頭部的第1個元素,此時會執行第二個大的for循環里面的邏輯,也就是接著for循環里面park()方法后面的代碼往下執行。
另外一個不同于AQS的阻塞隊列的地方是,在每個WNode里面有一個cowait指針,用于串聯起所有的讀線程。例如,隊列尾部阻塞的是一個讀線程 1,現在又來了讀線程 2、3,那么會通過cowait指針,把1、2、3串聯起來。1被喚醒之后,2、3也隨之一起被喚醒,因為讀和讀之間不互斥。
明白加鎖的自旋策略后,下面來看鎖的釋放操作。和讀寫鎖的實現類似,也是做了兩件事情:一是把state變量置回原位,二是喚醒阻塞隊列中的第一個節點。
原文鏈接:https://www.cnblogs.com/yangchen-geek/p/15489631.html
篇文章主要介紹了Python線程條件變量Condition原理解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下 Condition 對象就是條件變量,它總是與某種鎖相關聯,可以是外部傳入的鎖或是系統默認創建的鎖。當幾個條件變量共享一個鎖時,你就應該自己傳入一個鎖。這個鎖不需要你操心,Condition 類會管理它。 acquire() 和 release() 可以操控這個相關聯的鎖。其他的方法都必須在這個鎖被鎖上的情況下使用。wait() 會釋放這個鎖,阻塞本線程直到其他線程通過 notify() 或 notify_all() 來喚醒它。一旦被喚醒,這個鎖又被 wait() 鎖上。 經典的 consumer/producer 問題的代碼示例為:
import threading
import time
import logging
logging.basicConfig(level=logging.DEBUG,
format='(%(threadName)-9s) %(message)s',)
def consumer(cv):
logging.debug('Consumer thread started ...')
with cv:
logging.debug('Consumer waiting ...')
cv.acquire()
cv.wait()
logging.debug('Consumer consumed the resource')
cv.release()
def producer(cv):
logging.debug('Producer thread started ...')
with cv:
cv.acquire()
logging.debug('Making resource available')
logging.debug('Notifying to all consumers')
cv.notify()
cv.release()
if __name__=='__main__':
condition=threading.Condition()
cs1=threading.Thread(name='consumer1', target=consumer, args=(condition,))
#cs2=threading.Thread(name='consumer2', target=consumer, args=(condition,state))
pd=threading.Thread(name='producer', target=producer, args=(condition,))
cs1.start()
time.sleep(2)
#cs2.start()
#time.sleep(2)
pd.start()
以上就是本文的全部內容,希望對大家的學習有所幫助
轉自:https://www.linuxprobe.com/python-condition-parsing.html
們可以使用以下的方式去渲染html
func main() {
router :=gin.Default()
router.LoadHTMLGlob("templates/*")
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website",
})
})
router.Run(":8080")
}
在html中我們可以使用特殊的雙花括號來渲染title這個值
<html>
<h1>
{{ .title }}
</h1>
</html>
值得注意的是這種方式并不是gin特有的,而是golang特有的,它還有其他的模板語法。
{{$article :="hello"}}
也可以給變量賦值
{{$article :=.ArticleContent}}
{{funcname .arg1 .arg2}}
{{if .condition}}
{{end}}
{{if .condition1}}
{{else if .contition2}}
{{end}}
{{if not .condition}}
{{end}}
{{if and .condition1 .condition2}}
{{end}}
{{if or .condition1 .condition2}}
{{end}}
{{if eq .var1 .var2}}
{{end}}
{{if ne .var1 .var2}}
{{end}}
(less than){{if lt .var1 .var2}}
{{end}}
{{if le .var1 .var2}}
{{end}}
{{if gt .var1 .var2}}
{{end}}
{{if ge .var1 .var2}}
{{end}}
{{range $i, $v :=.slice}}
{{end}}
{{template "navbar"}}
*請認真填寫需求信息,我們會在24小時內與您取得聯系。