簡単な方法でJavaロックを学ぶ(1)
インターネットの潮流の下で、Javaの優れた言語機能は、さまざまな主要メーカーの熱意をもたらしました。これは必然的に大昌に入る予定の学生がしっかりしたコンピュータ基盤を持っていることを要求します。次に、さまざまなロックの基本的な知識ポイントと、Javaロックの実装と使用に焦点を当てて、学生が大規模な工場からのさまざまなトリッキーなインタビューの質問にうまく対処できるようにします。
ロックの意味
マルチCPUアーキテクチャを備えたコンピュータでは、複数のスレッドが同じコンピュータリソースを同時に操作して、データの不整合や不正な読み取りを引き起こすのを効果的に防ぐことができます。ロックはマルチスレッドのシナリオでは優れたソリューションですが、不適切に使用すると、cpuの枯渇、デッドロック、サービススループットの低下などの深刻なパフォーマンスの問題が発生する可能性があります。
Javaロックの一般的な機能
1つのポイントに注意してください。ロックには、楽観的ロックや再入可能ロックなど、複数の特性があります。次に、特性に応じて展開します。
悲観的なロックと楽観的なロック
基本概念
ペシミスティックロックとは、現在のスレッドが同期リソースを操作しているときに他のスレッドがそれを変更するというペシミスティックな信念を指します。したがって、現在のスレッドは、同期リソースが他のスレッドによって操作されないようにロックを追加する必要があります。Javaでの一般的な悲観的なロックの実装は、synchronizedとLockの実装クラスです。
楽観的ロックとは、同期リソースを操作するときに他のスレッドが現在のスレッドを変更しないため、現在のスレッドがロックされないという楽観的な見方を指します。同期リソースが更新されると、同期リソースが他のスレッドによって変更されているかどうかをアクティブにチェックします。変更されている場合は、さまざまな方法(再試行や例外のスローなど)で解決できます。変更がない場合は、直接更新します。
使用するシーン
2種類のロックは使用シナリオが異なり、誰が良いのか、誰が悪いのかを一般化することはできません。ペシミスティックロックは、主に読み取りが少なくなり、書き込みが増えるシナリオを扱います。オプティミスティックロックは、主に、読み取りが多くなり、書き込みが少なくなるシナリオを扱います。
サンプルコード
- 悲観的なロック
このデモの目的により、学生は各スレッドが1つずつ排他的に実行されていることを明確に確認できますが、スレッドが1〜10の順序で実行されることを保証するものではありません。
/**
* @author : 乌鸦
* @since : 2020/9/18
*/
public class Demo{
//同步资源
private String name;
private ReentrantLock lock = new ReentrantLock();
//悲观锁实现一
public synchronized void updateName(String name, Integer threadNum) throws InterruptedException {
//do something about name
System.out.println(threadNum + "is doing something at " + Calendar.getInstance().getTimeInMillis());
this.name = name;
//为了演示效果故休眠1s
Thread.sleep(1000);
}
//悲观锁实现二
public void setName(String name, Integer threadNum) throws InterruptedException {
//阻塞直到获取到锁
lock.lock();
try{
//do something about name
System.out.println(threadNum + " is doing something at " + Calendar.getInstance().getTimeInMillis());
this.name = name;
//为了演示效果故休眠1s
Thread.sleep(1000);
}finally{
lock.unlock();
}
}
public void setNameWithNoLock(String name, Integer threadNum)throws InterruptedException {
//do something about name
System.out.println(threadNum + " is doing something at " + Calendar.getInstance().getTimeInMillis());
this.name = name;
//为了演示效果故休眠1s
Thread.sleep(1000);
}
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
//悲观锁
final CountDownLatch latch = new CountDownLatch(10);
System.out.println("悲观锁输出结果,请注意看输出的时间戳:");
for(int i=0;i<10;i++){
final int num = i;
new Thread(() -> {
try {
demo.setName("乌鸦"+num,num);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
}).start();
}
//主要是为了hold主线程,让10个线程跑完
latch.await();
//无锁
final CountDownLatch latch2 = new CountDownLatch(10);
System.out.println("乐观锁输出结果,请注意看输出的时间戳:");
for(int i=0;i<10;i++){
final int num = i;
new Thread(() -> {
try {
demo.setNameWithNoLock("乌鸦"+num,num);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch2.countDown();
}).start();
}
//主要是为了hold主线程,让10个线程跑完
latch2.await();
}
}
=================输出结果===================
悲观锁输出结果,请注意看输出的时间戳:
0 is doing something at 1600418985619
1 is doing something at 1600418986632
2 is doing something at 1600418987637
3 is doing something at 1600418988642
4 is doing something at 1600418989646
5 is doing something at 1600418990647
6 is doing something at 1600418991651
7 is doing something at 1600418992656
8 is doing something at 1600418993660
9 is doing something at 1600418994666
乐观锁输出结果,请注意看输出的时间戳:
0 is doing something at 1600418995672
1 is doing something at 1600418995672
2 is doing something at 1600418995672
3 is doing something at 1600418995672
4 is doing something at 1600418995672
5 is doing something at 1600418995673
6 is doing something at 1600418995673
7 is doing something at 1600418995673
8 is doing something at 1600418995673
9 is doing something at 1600418995673
- 楽観的なロック
package com.example.demo;
インポートjava.util.concurrent.CountDownLatch;
インポートjava.util.concurrent.atomic.AtomicInteger;
/ **
- @author:カラス
-
@since:2020/9/18
* /
public class OptimisticLockDemo {
public AtomicInteger atomicCount = new AtomicInteger();
public Integer commonCount = 0;public void atomicInc(){
try {
Thread.sleep(1); //遅延1ミリ秒
)catch(InterruptedException e){
e.printStackTrace();
}
atomicCount.getAndIncrement();
}
public void commonInc(){
try {
Thread .sleep(1); //同時実行の問題を明確に確認するために、1ミリ秒の遅延
} catch(InterruptedException e){
e.printStackTrace();
}
commonCount = commonCount + 1;
}public static void main(String [] args)throws InterruptedException {
OptimisticLockDemo demo = new OptimisticLockDemo(); //有锁case final CountDownLatch latch1 = new CountDownLatch(100); for(int i=0;i<100;i++){ new Thread(() -> { demo.atomicInc(); latch1.countDown(); }).start(); } latch1.await(); System.out.println("atomicCount运行结果(确定为100):"+demo.atomicCount); //无锁case final CountDownLatch latch2 = new CountDownLatch(100); for(int i=0;i<100;i++){ new Thread(() -> { demo.commonInc(); latch2.countDown(); }).start(); } latch2.await(); System.out.println("commonCount运行结果(不确定):"+demo.commonCount);
}
}
=====出力結果======
atomicCount操作結果(100と決定):100
commonCount操作結果(不明):91
## 自旋锁与非自旋锁
### 基本概念
在展开此概念之前,先介绍下线程的几个状态,详见如下图(图摘自:[线程的六种状态及转化](https://baijiahao.baidu.com/s?id=1658121385190352035&wfr=spider&for=pc))。同学们可以看到运行中的线程到就绪再到阻塞,会进行线程上下文切换,比较耗CPU资源。在JVM系统中,往往频繁地线程上下文切换会导致CPU使用率偏高,故我们需要避免,不断优化。
![image.png](https://cdn.nlark.com/yuque/0/2020/png/262173/1600421083542-8e913b64-d0fb-4c42-aa41-dde223dfb07d.png#align=left&display=inline&height=403&margin=%5Bobject%20Object%5D&name=image.png&originHeight=806&originWidth=1228&size=409389&status=done&style=none&width=614)
自旋锁是让当前线程"稍微等一下",但是_**CPU资源仍旧没有放弃**_,避免了线上下文的切换。_**同学们请注意,自旋不代表阻塞**_。自旋短时间等待,效果非常好。反之,如果锁被占用的时间很长,那就白白浪费了CPU资源。所以,建议_**自旋等待的时间必须要有一定的限度**_。
### 使用场景
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间。
### 代码示例
在AtomicInteger中的addAndGet方法实现,内部依赖的就是Unsafe中的getAndAddInt,其内部实现就是通过一个do-while来实现自旋。同时JVM也提供相关参数来设置自旋次数,具体可以参考[JVM参数](https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html)——PreBlockSpin。
```java
=======AtomicInteger部分源码=====
public class AtomicInteger extends Number implements java.io.Serializable {
......
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
......
}
public final class Unsafe {
public 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;
}
}
再入国および非再入国ロック
基本概念
リエントラントロックとは、同期ロックやリエントラントロックリエントラントロックなど、同じスレッドがデッドロックなしで同じロックを複数回取得できることを意味します(リエントラントである理由を分析するための特別なトピックがあります)。再入国ロックとは対照的に、非再入国ロックは、同じロックを複数回取得することはできません。そうしないと、デッドロックが発生します。
使用するシーン
同じスレッドがクリティカルリージョンリソースに複数回繰り返し入る必要がある場合、デッドロックを効果的に回避するためにリエントリーロックが必要です。
サンプルコード
学生の皆さん、JDKのロックReentrantLockがどのように再入国ロックを実装するかを見てみましょう。これは主に州全体の再入国者の数をカウントし、頻繁な保留解除操作を回避して効率を向上させ、デッドロックを回避します。 。
public class ReentrantLock implements Lock, java.io.Serializable {
/**
* The synchronization state.
*/
private volatile int state;
/**
* Returns the current value of synchronization state.
* This operation has memory semantics of a {@code volatile} read.
* @return current state value
*/
protected final int getState() {
return state;
}
......
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
......
}
共有ロックと排他ロック
基本概念
排他的ロック:1つのスレッドのみがそれを占有できます。同期の場合、それは明らかに排他的ロックです。
共有ロック:複数のスレッドを同時に保持できます。JDKの一般的な代表は、効率的なデータ読み取りを保証できるReentrantReadWriteLockの読み取りロックです。
使用するシーン
読み取りスループットを高めるために、一部の重要なセクションリソースでは、複数のスレッドが更新せずに同時に共有読み取りロックを取得できます。名前が示すように、排他ロックは主に重要なセクションリソースが汚れないように保護するために使用されます。
サンプルコード
共有ロックの実装については、JDKでのReentrantReadWriteLockの読み取りロックの実装を見てみましょう。tryAcquireSharedメソッドでは、他のスレッドがすでに書き込みロックを取得している場合、現在のスレッドは読み取りロックの取得に失敗し、待機状態に入ることがわかります。現在のスレッドが書き込みロックを取得するか、書き込みロックが取得されない場合、現在のスレッドは読み取りステータスを増やし、読み取りロックを正常に取得します。特定の実装はまだ同様の状態を経ていますが、これは主にJDKでのAQSの素晴らしい抽象化によるものです(特定のソースコード分析列は後であります)。
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
フェアロックとアンフェアロック
基本概念
フェアロックとは、主に、FIFO(ファーストインファーストアウト)の原則に従って、ロックが適用された順序で複数のスレッドがキューに入ることを意味します。その利点は明らかであり、一部のスレッドを長時間待たせず、「空腹」のシーンが現れるのを効果的に防ぎますが、欠点も明らかです。CPUはスレッドコンテキストの切り替えを大量に消費するため、不公平なロックよりもスループットが低くなります。
不公平なロックとは、主に、一部のスレッドがキューにジャンプしてロックを取得できることを意味し、キューが失敗するとキューが処理されます。利点は、スレッドを呼び出すオーバーヘッドを減らすことができ、スレッドがブロックせずに直接ロックを取得する機会があり、CPUがすべてのスレッドをウェイクアップする必要がないため、全体的なスループット効率が高いことです。欠点は、待機キュー内のスレッドが枯渇したり、長時間待機したりする可能性があることです。ロックを取得します。
使用するシーン
スレッドビジネスの処理時間が待機時間よりもはるかに長い場合、不公平なロックの効率はそれほど高くない可能性がありますが、公平なロックはビジネスに多くの制御性を追加します。
サンプルコード
ReenTrantLockには、2つの組み込みのフェアおよびアンフェア実装メソッドがあります。具体的なサンプルコードは次のとおりです。フェアロックはキューを通過します。詳細については、メソッドhasQueuedPredecessorsを参照してください。
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
同期ロック状態
同期ロックには、バージョン1.6より前のロックまたはヘビーウェイトロックがありませんでした。欠点は非常に明白です。ヘビーウェイトロックではスループットが非常に低くなります。パフォーマンスを向上させるために、1.6以降で最適化が実行されました。さまざまなシナリオに対処するために、2つの追加状態(バイアスロックと軽量ロック)が導入されています。進行の順序は、ロックなし->バイアスロック->軽量ロック->重量ロックです。
ロックなし
リソースロックはありません。たとえば、後で詳しく説明するCASの原則とアプリケーションはすべてロックフリーです。
バイアスロック
スレッドが同期ブロックコードに複数回アクセスすると、スレッドはバイアスされたロックを自動的に取得し、ロックの取得コストを削減してパフォーマンスを向上させます。
軽量ロック
バイアスロックにアクセスする別のスレッドがある場合、バイアスロックは軽量ロックにアップグレードされ、他のスレッドはブロックせずにスピンループを介してロックを取得しようとするため、パフォーマンスが向上します。
ヘビーウェイトロック
他のスレッドがスピンなどの操作でロックを取得できない場合、それらはヘビーウェイトロックに入り、スレッドをブロックし、CPUを使用する権利を返します。
総括する
この記事では、Javaの一般的なロックの概念の基本的な概要を説明し、ソースコードと実際のアプリケーションの観点から説明します。実際、Java自体はロック自体を適切にカプセル化しており、R&Dの学生が日常業務で使用する際の困難さを軽減しています。ただし、インタビューの段階では、学生は自由に応答するためにロックの基本的な原則に精通している必要があります。
次に、Javaでのロックの実装と使用について詳しく説明します。しばらくお待ちください。
パブリックアカウントに注意してください:Tpark技術職人。オリジナルコンテンツは毎週プッシュされます!!!さらに、Aliを中に押し込んで、履歴書を送信することができます:[email protected]