Java鎖的種類以及辨析鎖作為并發(fā)共享數(shù)據(jù),保證一致性的工具,在JAVA平臺有多種實現(xiàn)(如 synchronized 和 ReentrantLock等等 ) 。這些已經(jīng)寫好提供的鎖為我們開發(fā)提供了便利,但是鎖的具體性質(zhì)以及類型卻很少被提及。本系列文章將分析JAVA中常見的鎖以及其特性,為大家答疑解惑。
1、自旋鎖
2、自旋鎖的其他種類
3、阻塞鎖
4、可重入鎖
5、讀寫鎖
6、互斥鎖
7、悲觀鎖
8、樂觀鎖
9、公平鎖
10、非公平鎖
11、偏向鎖
12、對象鎖
13、線程鎖
14、鎖粗化
15、輕量級鎖
16、鎖消除
17、鎖膨脹
18、信號量
背景
鎖作為并發(fā)共享數(shù)據(jù),保證一致性的工具,在JAVA平臺有多種實現(xiàn)(如 synchronized 和 ReentrantLock等等 ) 。這些已經(jīng)寫好提供的鎖為我們開發(fā)提供了便利,但是鎖的具體性質(zhì)以及類型卻很少被提及。
自旋鎖
自旋鎖是采用讓當前線程不停地的在循環(huán)體內(nèi)執(zhí)行實現(xiàn)的,當循環(huán)的條件被當前線程改變時其他前程才能進入臨界區(qū)。
自旋鎖流程:獲取自旋鎖時,如果沒有任何線程保持該鎖,那么將立即得到鎖;如果在獲取自旋鎖時鎖已經(jīng)有保持者,那么獲取鎖操作將自旋在那里,直到該自旋鎖的保持者釋放了鎖。
簡單實現(xiàn)原理的代碼如下:
1234567891011121314151617 | /** * 自旋鎖原理簡單示例*/public class SpinLock { private AtomicReference sign = new AtomicReference<>(); // 獲取鎖 public void lock() { Thread current = Thread.currentThread(); while (!sign.compareAndSet(null, current)) { } } // 釋放鎖 public void unlock() { Thread current = Thread.currentThread(); sign.compareAndSet(current, null); }} |
要理解以上代碼,我們要先弄清楚AtomicReference的作用。
AtomicReference:位于java.util.concurrent.atomic包下。從包名就可知道它的大致作用:在并發(fā)環(huán)境中保證引用對象的原子操作。
查看AtomicReference源碼:
12345678910111213141516171819202122232425262728293031323334353637383940414243 | package java.util.concurrent.atomic;import java.util.function.UnaryOperator;import java.util.function.BinaryOperator;import sun.misc.Unsafe;/** * An object reference that may be updated atomically. See the {@link * java.util.concurrent.atomic} package specification for description * of the properties of atomic variables. * @since 1.5 * @author Doug Lea * @param The type of object referred to by this reference */public class AtomicReference implements java.io.Serializable { private static final long serialVersionUID = -1848883965231344442L; private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicReference.class.getDeclaredField('value')); } catch (Exception ex) { throw new Error(ex); } } private volatile V value; ...(省略) /** * Atomically sets the value to the given updated value * if the current value {@code ==} the expected value. * @param expect the expected value * @param update the new value * @return {@code true} if successful. False return indicates that * the actual value was not equal to the expected value. */ public final boolean compareAndSet(V expect, V update) { return unsafe.compareAndSwapObject(this, valueOffset, expect, update); } ...(省略) |
發(fā)現(xiàn)AtomicReference實現(xiàn)的基本原理是使用volatile關(guān)鍵字和Unsafe類來保證其可見性和原子性。(PS:在此暫不作擴展閱讀Unsafe類)
我們重點關(guān)注AtomicReference.compareAndSet()這個自旋鎖用到的方法。從方法注釋和方式實現(xiàn),可以理解:這個方法的意思就是當當前的值==(注意是雙等號)期望的值(即傳入的第一個參數(shù))時,把當前值更新為新值(即傳入的第二個參數(shù)),并且返回true,否則返回false。
再回過頭,看之前自旋鎖的代碼,就很好理解了。一開始AtomicReference中的值為null,當有線程獲得鎖后,將值更新為該線程。當其他線程進入被鎖的方法時,由于sign.compareAndSet(null, current)始終返回的是false,導(dǎo)致while循環(huán)體一直在運行,知道獲得鎖的線程調(diào)用unlock方法,將當前持有線程重新設(shè)置為null:sign.compareAndSet(current, null)其他線程才可獲得鎖。
阻塞鎖
阻塞鎖,與自旋鎖不同,改變了線程的運行狀態(tài)。阻塞鎖,可以說是讓線程進入阻塞狀態(tài)進行等待,當獲得相應(yīng)的信號(喚醒,時間) 時,才可以進入線程的準備就緒狀態(tài),準備就緒狀態(tài)的所有線程,通過競爭,進入運行狀態(tài)。
阻塞鎖和自旋鎖最大的區(qū)別就在于,當獲取鎖是,如果鎖有持有者,當前線程是進入阻塞狀態(tài),等待當前線程結(jié)束而被喚醒的。
簡單實現(xiàn)原理的代碼如下:
123456789101112131415161718192021222324 | /** * 阻塞鎖原理簡單示例 * * @author zacard * @since 2016-01-13 22:02 */public class BlockLock { private AtomicReference sign = new AtomicReference<>(); // 獲取鎖 public void lock() { Thread current = Thread.currentThread(); if (!sign.compareAndSet(null, current)) { LockSupport.park(); } } // 釋放鎖 public void unlock() { Thread current = Thread.currentThread(); sign.compareAndSet(null, current); LockSupport.unpark(current); }} |
要理解以上代碼,我們要先弄清楚LockSupport的作用。
LockSupport:位于java.util.concurrent.locks包下(又是j.u.c)。同樣,從包名和類名即可知道其作用:提供并發(fā)編程中的鎖支持。
還是先查看下LockSupport的源碼:
1234567 | public class LockSupport { private LockSupport() {} // Cannot be instantiated. private static void setBlocker(Thread t, Object arg) { // Even though volatile, hotspot doesn't need a write barrier here. UNSAFE.putObject(t, parkBlockerOffset, arg); } ...(省略) |
又是sun.misc.Unsafe這個類,在此我們不得不先擴展研究下這個Unsafe類的作用和原理了。
sun.misc.Unsafe:有個稱號叫做魔術(shù)類。因為他能直接操作內(nèi)存等一些復(fù)雜操作。包括直接修改內(nèi)存值,繞過構(gòu)造器,直接調(diào)用類方法等。當然,他主要提供了CAS(compareAndSwap)原子操作而被我們熟知。
查看Unsafe類源碼:
12345678910111213141516171819 | public final class Unsafe { private static final Unsafe theUnsafe; ...(省略) private Unsafe() { } @CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); if(!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException('Unsafe'); } else { return theUnsafe; } } ...(省略) |
根據(jù)代碼可知:Unsafe是final類,意味著我們不能通過繼承來使用或改變這個類的方法。然后構(gòu)造器是私有的,也不能實例化。但是他自己保存了一個靜態(tài)私有不可改變的實例“theUnsafe”,并且只提供了一個靜態(tài)方法getUnsafe()來獲取這個類的實例。
但是這個getUnsafe方法確有個限制:注意if語句里的判斷,他表示如果不是受信任的類調(diào)用,會直接拋出異常。顯然,我們平常編寫的類都是不受信任的!
但是,我們有反射!既然他已經(jīng)持有了一個實例,就能通過反射強行竊取這個私有的實例。
代碼如下:
123456789 | public void getUnsafe() { try { Field field = Unsafe.class.getDeclaredField('theUnsafe'); field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); }} |
Unsafe類的方法基本都是native關(guān)鍵字修飾的,也就是說這些方法都是原生態(tài)方法,方法對應(yīng)的實現(xiàn)不是在當前文件,而是在用其他語言(如C和C++)實現(xiàn)的文件中。這也就是為什么Unsafe能夠直接操作內(nèi)存等一些特權(quán)功能的原因。
回過頭看下LockSupport中park()和uppark()這2個方法的作用。
LockSupport.unpark():
123456789101112131415 | /** * Makes available the permit for the given thread, if it * was not already available. If the thread was blocked on * {@code park} then it will unblock. Otherwise, its next call * to {@code park} is guaranteed not to block. This operation * is not guaranteed to have any effect at all if the given * thread has not been started. * * @param thread the thread to unpark, or {@code null}, in which case * this operation has no effect */public static void unpark(Thread thread) { if (thread != null) UNSAFE.unpark(thread);} |
根據(jù)方法注釋:對于給定線程,將許可證設(shè)置為可用狀態(tài)。如果這個線程是因為調(diào)用park()而處于阻塞狀態(tài),則清除阻塞狀態(tài)。反之,這個線程在下次調(diào)用park()時,將保證不被阻塞。
LockSupport.park():
12345 | /** * Disables the current thread for thread scheduling purposes unless the * permit is available. * * |
If the permit is available then it is consumed and the call returns * immediately; otherwise * the current thread becomes disabled for thread scheduling * purposes and lies dormant until one of three things happens: * *
*
Some other thread invokes {@link #unpark unpark} with the * current thread as the target; or * *
Some other thread {@linkplain Thread#interrupt interrupts} * the current thread; or * *
The call spuriously (that is, for no reason) returns. *
* *
This method does not report which of these caused the * method to return. Callers should re-check the conditions which caused * the thread to park in the first place. Callers may also determine, * for example, the interrupt status of the thread upon return. * * @param blocker the synchronization object responsible for this * thread parking * @since 1.6 */ public static void park(Object blocker) { Thread t = Thread.currentThread(); setBlocker(t, blocker); UNSAFE.park(false, 0L); setBlocker(t, null); }
根據(jù)注釋:除非許可證是可用的,不然將當前線程的調(diào)度設(shè)置為不可用。當許可是可用時,方法會立即返回,不會阻塞,反之就會阻塞當前線程直到下面3件事發(fā)生:
o其他線程調(diào)用了unpark(此線程)
o其他線程interrupts(終止)了此線程
o調(diào)用時發(fā)生未知原因的返回
重入鎖
重入鎖,也叫做遞歸鎖,指的是同一線程外層函數(shù)獲得鎖之后 ,內(nèi)層遞歸函數(shù)仍然有獲取該鎖的代碼,但不受影響。在JAVA環(huán)境下ReentrantLock和synchronized都是重入鎖。
測試代碼如下:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950 | /** * 測試ReentrantLock和synchronized */@Testpublic void testReentrantLock() { // ReentrantLock test for (int i = 0; i < 3; i++) { new Thread(new Runnable() { ReentrantLock lock = new ReentrantLock(); public void get() { lock.lock(); System.out.println('ReentrantLock:' + Thread.currentThread().getId()); set(); lock.unlock(); } public void set() { lock.lock(); System.out.println('ReentrantLock:' + Thread.currentThread().getId()); lock.unlock(); } @Override public void run() { get(); } }).start(); } // synchronized test for (int i = 0; i < 3; i++) { new Thread(new Runnable() { public synchronized void get() { System.out.println('synchronized:' + Thread.currentThread().getId()); set(); } public synchronized void set() { System.out.println('synchronized:' + Thread.currentThread().getId()); } @Override public void run() { get(); } }).start(); }} |
2段代碼的輸出一致:都會重復(fù)輸出當前線程id2次。
可重入鎖最大的作用是避免死鎖。以自旋鎖作為例子:
123456789101112131415161718192021222324 | /** * 自旋鎖原理簡單示例 * * @author zacard * @since 2016-01-13 21:40 */public class SpinLock { private AtomicReference sign = new AtomicReference<>(); // 獲取鎖 public void lock() { Thread current = Thread.currentThread(); while (!sign.compareAndSet(null, current)) { } } // 釋放鎖 public void unlock() { Thread current = Thread.currentThread(); sign.compareAndSet(current, null); }} |
o若有同一線程兩調(diào)用lock(),會導(dǎo)致第二次調(diào)用lock位置進行自旋,產(chǎn)生了死鎖說明這個鎖并不是可重入的。(在lock函數(shù)內(nèi),應(yīng)驗證線程是否為已經(jīng)獲得鎖的線程)
o若1問題已經(jīng)解決,當unlock()第一次調(diào)用時,就已經(jīng)將鎖釋放了。實際上不應(yīng)釋放鎖
自旋鎖避免死鎖的方法(采用計數(shù)次統(tǒng)計):
12345678910111213141516171819202122232425262728293031323334353637383940 | /** * 自旋鎖改進 * * @author Guoqw * @since 2016-01-14 14:11 */public class SpinLockImprove { private AtomicReference owner = new AtomicReference<>(); private int count = 0; /** * 獲取鎖 */ public void lock() { Thread current = Thread.currentThread(); if (current == owner.get()) { count++; return; } while (!owner.compareAndSet(null, current)) { } } /** * 釋放鎖 */ public void unlock() { Thread current = Thread.currentThread(); if (current == owner.get()) { if (count != 0) { count--; } else { owner.compareAndSet(current, null); } } }} |
改進后自旋鎖即為重入鎖的簡單實現(xiàn)。