コンピュータプログラムにおいて、ロックとは資源を排他的に利用するものであり、ロックを取得した場合のみ、対応する資源を操作することができます。
コンピュータの下部でのロックの実装は、CPU によって提供される CAS 命令 (比較および swsp) に依存します。メモリ アドレスの場合、元の値と変更しようとした値は比較され、値が正常に変更されたかどうかによってロックが捕捉されたかどうかが示されます。
jvm には、一般的に使用される 2 つのロックがあります。
synchronized は、Java によって提供されるキーワード ロックで、オブジェクトをロックできます。 、クラス、メソッド。
JDK1.6以降、同期が最適化され、バイアスロックと軽量ロックモードが追加され、同期ロックの動作ロジックは以下のようになります。 、バイアス ロックが増加します。つまり、「
バイアス ロック CAS の取得が失敗した場合は、現在のスレッドがバイアス ロックによってバイアスされているスレッドとは異なることを意味し、バイアス ロックは
アップグレードされますによってロックを取得するのが特徴です。 スピンの取得が失敗した場合、ロックは重みロックにアップグレードされ、ロックを待機しているすべてのスレッドは JVM によって一時停止されます。ロックが解放されると、 JVM からの統合通知によって起動されます。CAS ロックをもう一度試してください。失敗した場合は、ハング状態が続きます。
明らかに、偏ったロック設計の目的は、「公式 Java の観点から見ると、同じロックに対するほとんどの競合が同じスレッドで発生する」ことです。
軽量ロック設計の目的は、「短期的には、スピン CAS を通じてロック競合が得られ、短期間の CPU スピン消費量がスレッドの一時停止とウェイクアップの消費量よりも少なくなる」ことです。ReentrantLock
ReentrantLock と言えば、JUC の AQS について話さなければなりません。
キューの構築
キュー内のロックを待機する必要があるスレッドを維持します。
ヘッド ノードは常にロックを保持する (またはリソースを保持する) ノードであり、ヘッド ノードの後に待機ノードが順番に接続されます。
ヘッド ノードはロックを解放した後、待機中のノードを順番に起動し、それらのノードは再度ロックの取得を試みます。
同期ロックの最適化後、AQS の本質は同期ロックとそれほど変わりません。また、両者のパフォーマンスもそれほど変わりません。そのため、AQS の現在の特性は次のとおりです。
は Java API レベルで実装されたロックであるため、さまざまな並行ツール クラスを実装でき、操作がより柔軟になります。メカニズムは柔軟に動作するため、デッドロックが発生しにくいです。デッドロックが発生した場合、jstack はデッドロック インジケーターを表示しないため、トラブルシューティングがより困難になります。
公平なロックは実現できますが、同期すると不公平なロックになるはずです。
JavaApi 層によって実装されたロックであるため、割り込みに応答できます。
ここで、ReentrantLock が実際には JavaApi レイヤーでの同期の実装であると言えることがわかります。
共有ロック (S) と排他的ロック (X)
これらのロックには両方とも行レベルのロックとテーブル レベルが含まれますロック。
共有ロックを取得する際、データが他のトランザクションの排他ロックによりロックされている場合、共有ロックを取得できず、排他ロックが解除されるまで待つ必要があります。 インテンション ロックスコープインテンション ロックはテーブル ロックです。テーブル ロックを取得する前に、インテンション ロックがチェックされます。
テーブル ロックの共有ロックまたは排他ロックを取得する前に、テーブルの共有ロックをチェックする必要があります。
テーブル ロックとインテンション ロックの相互排他ルールは次のとおりです。X IX S IS
X 競合 競合 競合 競合IX 競合互換 競合互換
S 競合 競合互換互換性IS 競合互換性互換性互換性
#インテンション ロックの目的は、テーブル ロックを取得するときに、インテンション ロックを使用して、テーブル ロックを取得できるかどうかを迅速に判断できることです。
行レベルのロックを取得する場合、対応するインテンション ロックが最初に取得されるため、他のトランザクションはテーブル ロックを取得するときに、各行をスキャンすることなく、インテンション ロックを通じて迅速に判断できます。
特別な注意は、インテンション ロックは重ね合わせることができる、つまり、複数のロックが存在することです。たとえば、T1 トランザクションはインテンション ロック IX1 と行レベル ロック X1 を取得しますが、T2 トランザクションは依然としてインテント ロック IX2 と行レベル ロック X2 を取得するため、インテント ロックはテーブル レベルのロックが取得される前にのみチェックされます。
記錄鎖定生效在索引上,以在SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE時保護該行資料不被其他交易更改。
記錄鎖定在沒有索引時依舊會生效,因為innodb會為每個表建立一個隱藏的索引。
記錄鎖定是最基本的行鎖。
間隙鎖定生效在索引上,用於鎖定索引值後的行,防止插入,在select from table where index=? for update時會生效,例如index= 1,則會鎖定index=1索引節點相關的行,防止其他交易插入資料。
但並不會防止update語句,就算update的資料不存在。
這個鎖是記錄鎖和間隙鎖的組合,簡而言之在select from table where index=? for update時,既會有間隙鎖定防止insert,也會有記錄鎖在index上防止這一資料的update和delete。這個Next-key只是對這兩種鎖的一種概括,因為這兩種鎖在select for update時通常會一起出現。
插入意向鎖,和意向鎖類似。不過是特殊的間隙鎖,並非發生在select for update,而是在同時發生insert時產生,例如在兩個事務同時insert索引區間為[4,7]時,同時獲得該區間的意向鎖,此時事務不會阻塞,例如A:insert-5,B:insert-7,此時不會阻塞兩個事務。
插入意向鎖是一個特殊的間隙鎖,是為了防止正常間隙鎖鎖區間的情況下,insert頻繁阻塞而設計的,例如A:insert-5,B:insert-7,如果沒有插入意向鎖,那麼5和7都要去嘗試取得間隙鎖,此時第二個事務就會被阻塞,但是透過插入意向鎖,第二個事務就不會被阻塞,只有到插入的行確實衝突,才會被阻塞。
自增鎖,這個鎖很明顯是表級insert鎖,為了確保自增主鍵的表的主鍵保持原子自增。
對於鎖這個東西,大家應該多去理解各種鎖設計運行的原理和模型,這樣在加深理解後,在使用起來才會更加深入和透徹。
眾所周知,mysql的事務對防止重複插入並沒有什麼卵用,唯一索引又存在很多缺點,業務上最好不要使用,所以一般來說防止重複插入的通用做法就是使用分散式鎖,這就有一種比較常用的寫法。
final WeekendNoticeReadCountDO weekendNoticeReadCountDO = weekendNoticeReadRepositoryService.selectByNoticeId(noticeRequestDTO.getNoticeId()); if (weekendNoticeReadCountDO == null) { final String lockKey = RedisConstant.LOCK_WEEKEND_READ_COUNT_INSERT + ":" + noticeRequestDTO.getNoticeId(); ClusterLock lock = clusterLockFactory.getClusterLockRedis( RedisConstant.REDIS_KEY_PREFIX, lockKey ); if (lock.acquire(RedisConstant.REDIS_LOCK_DEFAULT_TIMEOUT)) { //double check final WeekendNoticeReadCountDO weekendNoticeReadCountDO = weekendNoticeReadRepositoryService.selectByNoticeId(noticeRequestDTO.getNoticeId()); if (weekendNoticeReadCountDO == null) { try { lock.execute(() -> { WeekendNoticeReadCountDO readCountDO = new WeekendNoticeReadCountDO(); readCountDO.setNoticeId(noticeRequestDTO.getNoticeId()); readCountDO.setReadCount(1L); readCountDO.setCreateTime(new Date()); readCountDO.setUpdateTime(new Date()); weekendNoticeReadRepositoryService.insert(readCountDO); return true; }); } catch (ApiException err) { throw err; } catch (Exception e) { log.error("插入", e); throw new ApiException(ErrorEnum.SERVER_ERROR.getCode(), "服务端出错"); } } else { weekendNoticeReadRepositoryService.noticeCountAdd(weekendNoticeReadCountDO); } } else { log.warn("redis锁获取超时,key:{}", lockKey); throw new ApiException(ErrorEnum.SERVER_ERROR.getCode(), "服务器繁忙,请稍后重试"); } }
在獲取到鎖之後,可能是經過等待才獲取到的鎖,此時上一個釋放鎖的線程可能已經插入了數據了,所以在鎖內部,依舊要再次校驗一下數據是否存在。
這種寫法適合大多數需要唯一性的寫場景。
如何避免死鎖?最簡單有效的方法就是:**不要在鎖裡再去取得鎖,簡而言之就是鎖最好單獨使用,不要套娃。
也要注意一些隱性鎖,例如資料庫。
事務A:
插入[5,7],插入意向鎖定。
select for update更新[100,150],間隙鎖定。
事務B:
select for update更新[90,120],間隙鎖定。
插入[4,6],插入意向鎖定。
此時在並發場景下,就可能會出現A持有了[5,7]的間隙鎖,在等待事務B[90,120]的間隙鎖,事務B也一樣,就死鎖了。
**
在寫業務程式碼,定義一些工具類別或快取類別的時候,很容易疏忽而發生類似的問題。
例如建立一個static緩存,沒有使用ConcurrentHashMap中的putIfAbsent等方法,也沒有加鎖去構建,導致上面的線程剛put了,下面的線程就刪掉了,或者重複構建2次緩存。
這點在Redis鎖的範例程式碼也講到了。
線程A取得到鎖,此時B,C在等待,然後A執行時間過長,導致鎖超時被自動釋放了,此時B獲取到了鎖,在快樂的執行,然後A執行完了之後,釋放鎖時沒有判斷是否還是自己持有,導致B持有的鎖被刪除了,此時C又取得到了鎖,BC同時在執行。
以上がJava および Mysql ロックに関連する知識ポイントは何ですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。