java 变量锁_一张图看透java的“锁”事
Java提供了種類豐富的鎖,每種鎖因其特性的不同,在適當?shù)膱鼍跋履軌蛘宫F(xiàn)出非常高的效率。
Java中往往是按照是否含有某一特性來定義鎖,我們通過特性將鎖進行分組歸類,再使用對比的方式進行介紹,幫助大家更快捷的理解相關(guān)知識。下面給出本文內(nèi)容的總體分類目錄:
1. 樂觀鎖 VS 悲觀鎖
樂觀鎖與悲觀鎖是一種廣義上的概念,體現(xiàn)了看待線程同步的不同角度。在Java和數(shù)據(jù)庫中都有此概念對應(yīng)的實際應(yīng)用。
先說概念。對于同一個數(shù)據(jù)的并發(fā)操作,悲觀鎖認為自己在使用數(shù)據(jù)的時候一定有別的線程來修改數(shù)據(jù),因此在獲取數(shù)據(jù)的時候會先加鎖,確保數(shù)據(jù)不會被別的線程修改。Java中,synchronized關(guān)鍵字和Lock的實現(xiàn)類都是悲觀鎖。
而樂觀鎖認為自己在使用數(shù)據(jù)時不會有別的線程修改數(shù)據(jù),所以不會添加鎖,只是在更新數(shù)據(jù)的時候去判斷之前有沒有別的線程更新了這個數(shù)據(jù)。如果這個數(shù)據(jù)沒有被更新,當前線程將自己修改的數(shù)據(jù)成功寫入。如果數(shù)據(jù)已經(jīng)被其他線程更新,則根據(jù)不同的實現(xiàn)方式執(zhí)行不同的操作(例如報錯或者自動重試)。
樂觀鎖在Java中是通過使用無鎖編程來實現(xiàn),最常采用的是CAS算法,Java原子類中的遞增操作就通過CAS自旋實現(xiàn)的。
根據(jù)從上面的概念描述我們可以發(fā)現(xiàn):
- 悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時數(shù)據(jù)正確。
- 樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升。
光說概念有些抽象,我們來看下樂觀鎖和悲觀鎖的調(diào)用方式示例:
// ------------------------- 悲觀鎖的調(diào)用方式 -------------------------// synchronizedpublic synchronized void testMethod() {// 操作同步資源}// ReentrantLockprivate ReentrantLock lock = new ReentrantLock(); // 需要保證多個線程使用的是同一個鎖public void modifyPublicResources() {lock.lock();// 操作同步資源lock.unlock();}// ------------------------- 樂觀鎖的調(diào)用方式 -------------------------private AtomicInteger atomicInteger = new AtomicInteger(); // 需要保證多個線程使用的是同一個AtomicIntegeratomicInteger.incrementAndGet(); //執(zhí)行自增1通過調(diào)用方式示例,我們可以發(fā)現(xiàn)悲觀鎖基本都是在顯式的鎖定之后再操作同步資源,而樂觀鎖則直接去操作同步資源。那么,為何樂觀鎖能夠做到不鎖定同步資源也可以正確的實現(xiàn)線程同步呢?我們通過介紹樂觀鎖的主要實現(xiàn)方式 “CAS” 的技術(shù)原理來為大家解惑。
CAS全稱 Compare And Swap(比較與交換),是一種無鎖算法。在不使用鎖(沒有線程被阻塞)的情況下實現(xiàn)多線程之間的變量同步。java.util.concurrent包中的原子類就是通過CAS來實現(xiàn)了樂觀鎖。
CAS算法涉及到三個操作數(shù):
- 需要讀寫的內(nèi)存值 V。
- 進行比較的值 A。
- 要寫入的新值 B。
當且僅當 V 的值等于 A 時,CAS通過原子方式用新值B來更新V的值(“比較+更新”整體是一個原子操作),否則不會執(zhí)行任何操作。一般情況下,“更新”是一個不斷重試的操作。
之前提到j(luò)ava.util.concurrent包中的原子類,就是通過CAS來實現(xiàn)了樂觀鎖,那么我們進入原子類AtomicInteger的源碼,看一下AtomicInteger的定義:
feda866e.png
根據(jù)定義我們可以看出各屬性的作用:
- unsafe: 獲取并操作內(nèi)存的數(shù)據(jù)。
- valueOffset: 存儲value在AtomicInteger中的偏移量。
- value: 存儲AtomicInteger的int值,該屬性需要借助volatile關(guān)鍵字保證其在線程間是可見的。
接下來,我們查看AtomicInteger的自增函數(shù)incrementAndGet()的源碼時,發(fā)現(xiàn)自增函數(shù)底層調(diào)用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class,只通過class文件中的參數(shù)名,并不能很好的了解方法的作用,所以我們通過OpenJDK 8 來查看Unsafe的源碼:
// ------------------------- JDK 8 -------------------------// AtomicInteger 自增方法public final int incrementAndGet() { ?return unsafe.getAndAddInt(this, valueOffset, 1) + 1;}?// Unsafe.classpublic final int getAndAddInt(Object var1, long var2, int var4) { ?int var5; ?do { ? ? ?var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); ?return var5;}?// ------------------------- OpenJDK 8 -------------------------// Unsafe.javapublic final int getAndAddInt(Object o, long offset, int delta) { ? int v; ? do { ? ? ? v = getIntVolatile(o, offset); ? } while (!compareAndSwapInt(o, offset, v, v + delta)); ? return v;}根據(jù)OpenJDK 8的源碼我們可以看出,getAndAddInt()循環(huán)獲取給定對象o中的偏移量處的值v,然后判斷內(nèi)存值是否等于v。如果相等則將內(nèi)存值設(shè)置為 v + delta,否則返回false,繼續(xù)循環(huán)進行重試,直到設(shè)置成功才能退出循環(huán),并且將舊值返回。整個“比較+更新”操作封裝在compareAndSwapInt()中,在JNI里是借助于一個CPU指令完成的,屬于原子操作,可以保證多個線程都能夠看到同一個變量的修改值。
后續(xù)JDK通過CPU的cmpxchg指令,去比較寄存器中的 A 和 內(nèi)存中的值 V。如果相等,就把要寫入的新值 B 存入內(nèi)存中。如果不相等,就將內(nèi)存值 V 賦值給寄存器中的值 A。然后通過Java代碼中的while循環(huán)再次調(diào)用cmpxchg指令進行重試,直到設(shè)置成功為止。
CAS雖然很高效,但是它也存在三大問題,這里也簡單說一下:
下邊文章介紹:自旋鎖 VS 適應(yīng)性自旋鎖
原創(chuàng)不易,期待你的關(guān)注
總結(jié)
以上是生活随笔為你收集整理的java 变量锁_一张图看透java的“锁”事的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 代码管理 防止员工_低代码开发现形记
- 下一篇: java 全局变量 内存不回收_JAVA