一、悲觀鎖
顧名思義,這是一個帶有貶義意思的鎖,對程式性能方面效率等方面不是很友好,也不是很人性化的一種鎖的思想,因為這種鎖的利用資料庫本身自帶的鎖,将資源強行獨占,在高并發下對性能消耗比較大,是以比較炒蛋,實作方式有資料庫本身具備的鎖機制:共享鎖、排他鎖、更新鎖。
Java在JDK1.5之前都是靠 synchronized關鍵字保證同步的,這種通過使用一緻的鎖定協定來協調對共享狀态的通路,可以確定無論哪個線程持有共享變量的鎖,都采用獨占的方式來通路這些變量。這就是一種獨占鎖,獨占鎖其實就是一種悲觀鎖,是以可以說 synchronized 也是悲觀鎖。
1、共享鎖(Share Lock)
S鎖,也叫讀鎖,用于所有的隻讀資料操作。共享鎖是非獨占的,允許多個并發事務讀取其鎖定的資源。
- 多個事務可封鎖同一個共享頁;
- 任何事務都不能修改該頁;
- 通常是該頁被讀取完畢,S鎖立即被釋放。
例如,執行查詢語句“SELECT * FROM my_table LOCK IN SHARE MODE”時,首先鎖定第一頁,讀取之後,釋放對第一頁的鎖定,然後鎖定第二頁。這樣,就允許在讀操作過程中,修改未被鎖定的第一頁。
例如,語句“SELECT * FROM my_table HOLDLOCK”就要求在整個查詢過程中,保持對表的鎖定,直到查詢完成才釋放鎖定
2、排他鎖(Exclusive Lock)
X鎖,也叫寫鎖,表示對資料進行寫操作。如果一個事務對對象加了排他鎖,其他事務就不能再給它加任何鎖了。
- 僅允許一個事務封鎖此頁,一個事務在一行資料加上排他鎖後;
- 其他任何事務必須等到X鎖被釋放才能對該頁進行通路;
- X鎖一直到事務結束才能被釋放。
InnoDB引擎預設的修改資料語句,update,delete,insert都會自動給涉及到的資料加上排他鎖;
使用for update加排他鎖:SELECT * FROM my_table FOR UPDATE,Mysql會對查詢結果中的每行都加排他鎖,當沒有其他線程對查詢結果集中的任何一行使用排他鎖時,可以成功申請排他鎖,否則會被阻塞。
加過排他鎖的資料行其他事務種是不能修改資料的,也不能通過for update和lock in share mode鎖的方式查詢資料,但可以直接通過select ...from...查詢資料,因為普通查詢沒有任何鎖機制。
3、更新鎖
U鎖,在修改操作的初始化階段用來鎖定可能要被修改的資源,這樣可以避免使用共享鎖造成的死鎖現象
4、synchronized(同步鎖)
- 當一個方法使用synchronized修飾後,這個方法變為“同步方法”,此時該方法不允許當一個線程同時到方法内部執行代碼。在方法上直接使用synchronized,那麼同步螢幕對象就是目前方法所屬對象,即方法中看到的this。
- 靜态方法若使用synchronized修飾,靜态方法的同步螢幕對象為這個類的類對象,靜态方法裡的同步塊的同步螢幕對象也必須是這個類的類對象。
- 同步塊
synchronized(同步螢幕對象){
需要同步運作的代碼判斷
}
同步塊可以更準确的控制需要多個線程同步運作的代碼片段。有效的縮小同步範圍可以在保證并發安全的前提下盡可能的提高并發效率,但同步塊在指定同步螢幕對象(上鎖的對象)時注意,這個對象可以是java任何類型的執行個體,但是要保證需要同步運作改代碼片段的線程看到的是同一個才可以!
- synchronized作為互斥鎖的使用
當使用synchronized控制多段代碼,并且他們指定的同步螢幕對象是同一個時,這些代碼片段互為互斥的,多個線程不能同時在這些代碼片段間一起執行
二、樂觀鎖
相對悲觀鎖,一種對程式資源效率方面比較友好的一種鎖機制,也是一種鎖的思想,在資料進行送出更新的時候,才會正式對資料是否産生并發沖突進行檢測,如果發現并發沖突了,則讓傳回使用者錯誤的資訊,讓使用者決定如何去做,主要就是兩個步驟:沖突檢測和資料更新。其實作方式有一種比較典型的就是 Compare and Swap ( CAS )。CAS是項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,隻有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程并不會被挂起,而是被告知這次競争中失敗,并可以再次嘗試。
先舉個栗子:
SELECT quantity FROM items WHERE id = 1; --查詢出商品庫存資訊,結果quantity = 3
UPDATE items SET quantity WHERE id = 1 AND quantity = 3; --修改商品庫存資訊為2
以上,在更新之前,先查詢一下庫存表中目前庫存數(quantity),然後在做update的時候,以庫存數作為一個修改條件。當送出更新的時候,判斷資料庫表對應記錄的目前庫存數與第一次取出來的庫存數進行比對,如果資料庫表目前庫存數與第一次取出來的庫存數相等,則予以更新,否則認為是過期資料。
但是以上更新語句存在一個比較重要的問題,即傳說中的ABA問題。
比如說一個線程one從資料庫中取出庫存數3,這時候另一個線程two也從資料庫中取出庫存數3,并且two進行了一些操作變成了2,然後two又将庫存數變成3,這時候線程one進行CAS操作發現資料庫中仍然是3,然後one操作成功。盡管線程one的CAS操作成功,但是不代表這個過程就是沒有問題的。
version方式:
還有一個比較好的辦法可以解決ABA問題,那就是通過一個單獨的可以順序遞增的version字段,比較像svn的版本控制。
樂觀鎖每次在執行資料的修改操作時,都會帶上一個版本号,一旦版本号和資料的版本号一緻就可以執行修改操作并對版本号執行+1操作,否則就執行失敗。因為每次操作的版本号都會随之增加,是以不會出現ABA問題,因為版本号隻會增加不會減少。
除了version以外,還可以使用時間戳,因為時間戳天然具有順序遞增性。
以上SQL其實還是有一定的問題的,就是一旦遇上高并發的時候,就隻有一個線程可以修改成功,那麼就會存在大量的失敗。對于像淘寶這樣的電商網站,高并發是常有的事,總讓使用者感覺到失敗顯然是不合理的。是以,還是要想辦法減少樂觀鎖的粒度的。有一條比較好的建議,可以減小樂觀鎖力度,最大程度的提升吞吐率,提高并發能力!如下:
UPDATE items SET quantity = quantity -1 WHERE id = 1 AND quantity > 0; --修改商品庫存
以上SQL語句中,如果使用者下單數為1,則通過quantity - 1 > 0的方式進行樂觀鎖控制,update語句,在執行過程中,會在一次原子操作中自己查詢一遍quantity的值,并将其扣減掉1。
上面這個栗子隻是用簡單的sql比較靈活的實作樂觀鎖的一種方式,對于java對CAS技術的支援,在網上搜了一下比較好的文章傳送門:https://www.cnblogs.com/qjjazry/p/6581568.html