ロック
記事ディレクトリ
1.ロック
1.1 概要と機能
- 共有リソースへのアクセスを制御するツールです。
- ロックと同期、これら 2 つは最も一般的なロックで、どちらもスレッド セーフを実現できますが、使用方法と機能は大きく異なります。
- ロックは同期の代わりには使用されませんが、同期が不適切であるか、状況を満たしていない場合は使用されません。より高度な使用方法を提供します。
- 通常、Lock では 1 つのスレッドのみが共有リソースにアクセスできますが、 ReadWriteLock のReadLockなど、一部の特別な実装クラスでは同時アクセスが許可されます。
1.2. ロックはなぜ必要ですか?
- 多くのシナリオでは同期だけでは十分ではありません
- 効率が低い: ロックの解放がほとんどなく、ロックを取得しようとするときにタイムアウトを設定できず、ロックを取得しようとしているスレッドを中断できません。
- 柔軟性が十分ではない (読み取り/書き込みロックの方が柔軟です):ロックと解放のタイミングは 1 つであり、各ロックには 1 つの条件(特定のオブジェクト) しかなく、多くの場合十分ではありません。
- ロックが正常に取得されたかどうかを知る方法はありません。
1.3 一般的なロック方法
-
ロックを取得するために、Lock で 4 つのメソッドが宣言されています。
-
ロック()
- これはロックを取得する最も一般的な方法です。他のスレッドによってロックが取得されている場合は、待機します。
- synchronized のように例外が発生しても自動的にロックを解放しない(jvm が自動的に解放する)ため、最終的にロックを解放する必要があります。
- lock() メソッドは中断できません。中断されると、大きな隠れた危険がもたらされます。デッドロックに陥ると、lock() は永久に待機することになります。
-
tryLock()
- ロックの取得を試行するために使用されます。現在のロックが他のスレッドによって占有されていない場合、取得は成功し、true が返され、それ以外の場合は false が返されます。
-
tryLock(長時間,TimeUnit単位)
-
先ほどの関数と同様に、タイムアウトが発生した場合は諦めます。
-
コードデモ
-
/** * 描述: TODO 用tryLock来避免死锁 */ public class TryLockDeadlock implements Runnable { int flag = 1; static Lock lock1 = new ReentrantLock(); static Lock lock2 = new ReentrantLock(); public static void main(String[] args) { TryLockDeadlock r1 = new TryLockDeadlock(); TryLockDeadlock r2 = new TryLockDeadlock(); r1.flag = 1; r1.flag = 0; new Thread(r1).start(); new Thread(r2).start(); } @Override public void run() { for (int i = 0; i < 100; i++) { if (flag == 1) { try { if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) { try { System.out.println("线程1获取到了锁1"); Thread.sleep(new Random().nextInt(1000)); if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) { try { System.out.println("线程1获取到了锁2"); System.out.println("线程1成功获取到了两把锁"); break; } finally { lock2.unlock(); } } else { System.out.println("线程1获取锁2失败,已重试"); } } finally { lock1.unlock(); Thread.sleep(new Random().nextInt(1000)); } } else { System.out.println("线程1获取锁1失败,已重试"); } } catch (InterruptedException e) { e.printStackTrace(); } } if (flag == 0) { try { if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) { try { System.out.println("线程2获取到了锁2"); Thread.sleep(new Random().nextInt(1000)); if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) { try { System.out.println("线程2获取到了锁1"); System.out.println("线程2成功获取到了两把锁"); break; } finally { lock1.unlock(); } } else { System.out.println("线程2获取锁1失败,已重试"); } } finally { lock2.unlock(); Thread.sleep(new Random().nextInt(1000)); } } else { System.out.println("线程2获取锁2失败,已重试"); } } catch (InterruptedException e) { e.printStackTrace(); } } } } }
スレッド 1 はロック 1 を取得しました。
スレッド 2 はロック 2 を取得しました
。スレッド 2 はロック 1 の取得に失敗し、再試行しました。スレッド 1 はロック2 を取得しました。スレッド 1 は 2 つのロックを正常に取得しました。スレッド 2はロック 2 を取得しました。スレッド 2ロック 1 とスレッド 2 を取得しました。2 つのロックを正常に取得しました
-
-
ロック中断()
-
tryLock() メソッドの時間を無限に設定するのと同じです。ロックの待機中にスレッドが中断される可能性があります(時間は無限であるため)。
-
コードデモ
-
/** * 描述: TODO 在获取锁的过程中被打断 */ public class LockInterruptibly implements Runnable { private Lock lock = new ReentrantLock(); public static void main(String[] args) { LockInterruptibly lockInterruptibly = new LockInterruptibly(); Thread thread0 = new Thread(lockInterruptibly); Thread thread1 = new Thread(lockInterruptibly); thread0.start(); thread1.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //模拟线程1被打断 thread1.interrupt(); } @Override public void run() { System.out.println(Thread.currentThread().getName() + "尝试获取锁"); try { lock.lockInterruptibly(); try { System.out.println(Thread.currentThread().getName() + "获取到了锁"); Thread.sleep(5000); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + "睡眠期间被中断了"); } finally { lock.unlock(); System.out.println(Thread.currentThread().getName() + "释放了锁"); } } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + "获得锁期间被中断了"); } } }
-
-
1.4 可視性の保証
- Lock のロック解除には、同期と同じメモリ セマンティクスがあります。つまり、次のスレッドがロックされた後は、前のスレッドがロック解除される前に発生したすべての操作を確認できます。
2. ロックの分類
2.1 楽観的ロックと悲観的ロック
ReenTrantLockは、ミューテックス ロックおよびリエントラント ロックです。
相互排他的な同期ロックは悲観的ロックとも呼ばれ、非相互排他的同期は楽観的ロックとも呼ばれます。
2.1.1 非相互排他同期ロック(楽観ロック)はなぜ生まれるのか?
- 相互排他的な同期ロックの欠点。
- ブロックとウェイクアップによって生じるパフォーマンス上の欠点。
- 永続的なブロック: ロックを保持しているスレッドが永続的にブロックされると、無限ループやデッドロックなどのアクティビティの問題が発生します。そうすれば、スレッドがロックを解放するのを待っている悲惨なスレッドは決して実行できなくなります。
- 優先度の逆転: ブロック優先度が高く、ロック保持の優先度が相対的に低い場合、逆転が発生し、ブロックされたスレッドの優先度が低くなります。
2.1.2 楽観的ロックと悲観的ロックとは何ですか?
-
性格面からの分析
- 楽観的な人は、エラーの可能性は常に非常に小さいと信じており、問題が発生した場合は修正します。
- 悲観的な人は、間違いは当たり前のことであり、すべてを確実に行う必要があると信じています。
-
リソースがロックされているかどうかの観点からの分類
-
悲観的ロック: 現在のリソースがロックされていない場合、他のリソースがそのリソースを奪い合うため、不正確なデータ結果が発生します。したがって、結果の正確性を保証するために、悲観的ロックはデータを取得して変更するたびにデータをロックし、他の人がデータを変更できないようにします。
Java の一般的な悲観的ロックは、同期およびロック関連のクラスです。
-
-
オプティミスティック ロック: 操作の処理中に他のスレッドが干渉しないと考えられるため、オブジェクトはロックされません。
更新するときは、自分がデータを修正したときに他の人がデータを修正したかどうかを比較します。変更されていない場合は、データを正常に操作および変更しているのはあなただけであることを意味します。変更されている場合は、諦める、エラーを報告する、再試行するなどの戦略を選択してください。
オプティミスティック ロックは通常、CAS アルゴリズムを使用して実装されます。典型的な例は、アトミック クラスと並行コンテナです。同じことが GIT コード管理にも当てはまります (コードが変更されると、送信の失敗が促されます)。
2.1.3 一般的な悲観的ロックと楽観的ロックの実装
-
悲観的なロック
/** * 描述: TODO Lock不会像synchronized一样,异常的时候自动释放锁,所以最佳实践是,finally中释放锁,以便保证发生异常的时候锁一定被释放 */ public class MustUnlock { //最为典型的锁实现 private static Lock lock = new ReentrantLock(); public static void main(String[] args) { lock.lock(); try{ //获取本锁保护的资源 System.out.println(Thread.currentThread().getName()+"开始执行任务"); }finally { lock.unlock(); } } }
-
楽観的ロック
/** * 描述: TODO 简单的乐观锁 */ public class PessimismOptimismLock { int a; public static void main(String[] args) { //原子整型 AtomicInteger atomicInteger = new AtomicInteger(); atomicInteger.incrementAndGet(); } public synchronized void testMethod() { a++; } }
2.1.4 コストの比較
-
悲観的ロック: 元のコストは楽観的ロックよりも高くなりますが、クリティカル セクションのロック保持時間が悪化したとしても、完全に (消費されるリソースは固定されているため)、ミューテックス ロックのコストには影響しません。
同時書き込みが多い状況や、隣接ロックの保持時間が比較的長い状況に適しています。悲観的ロックにより、大量の無駄なスピンやその他の消費を回避できます (ロックを取得できない場合は独自に待機します)。)。典型的な状況:
1. クリティカルセクションに IO 操作がある
2. クリティカルセクションのコードが複雑、またはループが多い
3.クリティカルセクションでの熾烈な競争(高い同時実行性)
-
逆に、楽観的ロックのコストは最初は小さいですが、スピン時間が長かったり、試行し続けたりすると、どんどんリソースが消費されます。
同時書き込みがほとんどなく(必要なときにいつでもロックを取得できます)、そのほとんどが読み取りであるシナリオに適しています。ロックしないと、読み取りパフォーマンスが大幅に向上します。
2.2 リエントラントロックと非リエントラントロック
-
ユースケースとして ReentrantLock を取り上げます。
-
映画館の座席予約の場合 ( 2 つのスレッドで同時にチケットを予約することはできません)
/** * 模拟预定座位 * @author tyeerth * @date 2020/9/15 - 15:16 */ public class CinemaBookedSeat { private static ReentrantLock lock = new ReentrantLock(); public static void bookSeat(){ //获取锁 lock.lock(); try { System.out.println(Thread.currentThread().getName()+"正在获取座位"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"获取座位完成"); }finally { lock.unlock(); } } public static void main(String[] args) { new Thread(() -> bookSeat()).start(); new Thread(() -> bookSeat()).start(); new Thread(() -> bookSeat()).start(); } }
2.2.1 リエントラントとは何ですか?
-
同じスレッドが同じロックを複数回取得できることを意味します
-
メリット:デッドロックを回避できる
注: 同じロックが 2 つのメソッドをロックする場合、スレッドはいずれかのメソッドにアクセスしてロックを取得します。リエントラントロックの場合、現在のロックを解除せずに2番目のメソッドにアクセスできますが、2番目のロックを取得したい場合はデッドロックが発生します。非再入可能ロックの場合は、まず現在のロックを解放してから、2 番目の方法でロックを取得する必要があります。
コードデモ1では、何回再入力したかがわかり、再度ロックを取得する際に解除する必要がありません。
/**
* 描述: TODO 演示可重入锁
*/
public class GetHoldCount {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
System.out.println(lock.getHoldCount());
lock.lock();
//拿到锁就加一
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
}
}
演算結果
0
1
2
3
2
1
0
コードデモ 2
/**
* 描述: TODO 演示资源在上锁的情况下,对其递归处理4次
*/
public class RecursionDemo {
private static ReentrantLock lock = new ReentrantLock();
private static void accessResource() {
lock.lock();
try {
System.out.println("已经对资源进行了处理");
if (lock.getHoldCount()<5) {
System.out.println("获取锁"+lock.getHoldCount()+"次");
accessResource();
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
accessResource();
}
}
リソースが処理されました
ロックが 1 回取得されました
リソースが処理されました ロックが
2 回取得されました
リソースが処理さ
れました ロックが 3 回
取得されました リソースが処理されました ロックは
4 回取得されました。
リソースは処理されました。
2.2.2 リエントラントロックのソースコード解析
- リエントラント ロック ReentranLock と非リエントラント ロック ThreadPoolExecutor の Worker クラス。