チューブとセマフォの2つの同期プリミティブは、すべての同時実行性の問題を理論的に解決できます。JavaSDKには、シナリオによるパフォーマンスの最適化と使いやすさの向上という理由で、他にも多くのツールがあります。
同時実行シナリオ:キャッシュなど、読み取りと書き込みを少なくします。Java SDKコンカレントパッケージは、読み取り/書き込みロック-ReadWriteLockを提供します。
1.読み書きロックとは何ですか?
読み取り/書き込みロックは、3つの基本原則に従います。
- 複数のスレッドが同時に共有変数を読み取ることを許可します。パフォーマンスは相互排他ロックよりも優れています。
- シェア変数を書き込むことができるスレッドは1つだけです。
- 書き込みスレッドが書き込み操作を実行している場合、この時点で読み取りスレッドは共有変数を読み取ることができません。
2.キャッシュをすばやく実装する
実装コードの例は次のとおりです。
class Cache<K, V> {
// 不是线程安全类,使用读写锁来保证线程安全
final Map<K, V> m = new HashMap<>();
// 可重入读写锁
final ReadWriteLock rwl = new ReentrantReadWriteLock();
// 读锁
final Lock r = rwl.readLock();
// 写锁
final Lock w = rwl.writeLock();
// 读缓存,try{}finally{}编程范式
V get(K key) {
r.lock();
try {
return m.get(key);
} finally {
r.unlock();
}
}
// 写缓存
V put(String key, Data v) {
w.lock();
try {
return m.put(key, v);
} finally {
w.unlock();
}
}
}
キャッシュを使用するには、まずキャッシュデータの初期化の問題を解決する必要があります。次の2つの方法があります。
- 1回限りの読み込み方法は、アプリケーションの起動時に読み込まれる少量のデータに適しています。
- オンデマンドの読み込み方法は遅延読み込みとも呼ばれます。つまり、最初にキャッシュにクエリを実行し、データベースにクエリを実行するのではなく、同時にキャッシュに保存します。
3.キャッシュのオンデマンドロードを実装する
class Cache<K, V> {
// 不是线程安全类
final Map<K, V> m = new HashMap<>();
// 可重入读写锁
final ReadWriteLock rwl = new ReentrantReadWriteLock();
// 读锁
final Lock r = rwl.readLock();
// 写锁
final Lock w = rwl.writeLock();
// 读缓存,try{}finally{}编程范式
V get(K key) {
V v = null;
// 读缓存
r.lock(); // (1)
try {
v = m.get(key); // (2)
} finally {
r.unlock(); // (3)
}
// 缓存中存在,返回
if (v != null) { // (4)
return v;
}
// 缓存中不存在,查询数据库
w.lock(); // (5)
try {
// 再次验证
// 其他线程可能已经查询过数据库
v = m.get(key); // (6)
if (v == null) { // (7)
// 查询数据库
v= 省略代码无数
m.put(key, v);
}
} finally {
w.unlock();
}
return v;
}
}
キャッシュを5で書き込むには、書き込みロックが必要ですが、コード6および7では、キャッシュが存在するかどうかを再確認する必要があるのはなぜですか?
その理由は、同時実行性の高いシナリオでは、書き込みロックを競合する複数のスレッドが存在する可能性があるためです。
- キャッシュが空で何もキャッシュされていないとします。3つのスレッドT1、T2、およびT3が同時にget()メソッドを呼び出し、keyパラメータも同じである場合を考えます。
- 次に、コードtheに対して同時に実行しますが、現時点では、スレッドT1、スレッドT1が書き込みロックを取得し、データベースにクエリを実行してキャッシュを更新し、最後に書き込みロックを解放すると、1つのスレッドのみが書き込みロックを取得できます。
- このとき、スレッドT2とT3には、T2であると想定して書き込みロックを取得できる別のスレッドがあります。再認証を使用しない場合、T2はデータベースに再度クエリを実行します。
- T2が書き込みロックを解放した後、T3もデータベースを再度クエリします。
- 実際、スレッドT1はすでにキャッシュされた値を設定しており、T2とT3についてデータベースを再度クエリする必要はありません。
- したがって、検証方法は、同時実行性の高いシナリオでデータを繰り返しクエリする問題を回避できます。
4.読み取り/書き込みロックのアップグレードとダウングレード
上記オンデマンドでロードしたサンプルコードでは、①でリードロックを取得し、③でリードロックを解除していますが、②以下の検証キャッシュを追加して、ロジックを更新することは可能ですか?詳細コードは以下の通りです。
// 读缓存
r.lock(); ①
try {
v = m.get(key); ②
if (v == null) {
w.lock();
try {
// 再次验证并更新缓存
// 省略详细代码
} finally{
w.unlock();
}
}
} finally{
r.unlock(); ③
}
最初に読み取りロックを取得し、次に書き込みロックにアップグレードします。これをロックのアップグレードと呼びます。
問題:読み取りロックが解放されていません。現時点で書き込みロックを取得すると、書き込みロックが永久に待機し、最終的に関連するスレッドがブロックされ、目覚められる機会がなくなります。
ロックのダウングレードは許可されています。次に例を示します。
class CachedData {
Object data;
volatile boolean cacheValid;
final ReadWriteLock rwl = new ReentrantReadWriteLock();
// 读锁
final Lock r = rwl.readLock();
// 写锁
final Lock w = rwl.writeLock();
void processCachedData() {
// 获取读锁
r.lock();
if (!cacheValid) {
// 释放读锁,因为不允许读锁的升级
r.unlock();
// 获取写锁
w.lock();
try {
// 再次检查状态
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 释放写锁前,降级为读锁
// 降级是可以的
r.lock(); // (1)
} finally {
// 释放写锁
w.unlock();
}
}
// 此处仍然持有读锁
try {
use(data);
} finally {
r.unlock();
}
}
}