ホームページ  >  記事  >  Java  >  Javaでのロックの実装方法は何ですか?

Javaでのロックの実装方法は何ですか?

WBOY
WBOY転載
2023-05-12 08:37:051563ブラウズ

1. 悲観的ロック

その名前が示すように、データ変更に対する保守的な態度と、他の人もデータを変更するだろうという信念を指します。そのため、データを操作した場合、操作が完了するまでデータはロックされます。ほとんどの場合、ペシミスティック ロックはデータベースのロック メカニズムに依存して、操作の排他性を最大限に確保します。ロック時間が長すぎると、他のユーザーが長時間アクセスできなくなり、プログラムの同時アクセスに影響を与えるだけでなく、データベースのパフォーマンスのオーバーヘッドにも大きな影響を与えます。トランザクションが長い場合、このようなオーバーヘッドは耐えられないことがよくあります。

スタンドアロン システムの場合は、JAVA 独自の synchronized キーワードをメソッドまたは synchronized ブロックに追加してリソースをロックできます。分散システムの場合は、データベース独自のロック メカニズムを使用できます。これを達成するために。

select * from 表名 where id= #{id} for update

悲観的ロックを使用するときは、ロック レベルに注意する必要があります。MySQL innodb がロックするとき、行ロックは主キーまたは (インデックス フィールド) が明示的に指定されている場合にのみ使用されます。それ以外の場合は、テーブル ロックが使用されます。テーブル全体をロックするとパフォーマンスが低下します。悲観的ロックを使用する場合、mysql はデフォルトで自動コミット モードを使用するため、MySQL データベースの自動コミット属性をオフにする必要があります。悲観的ロックは、書き込みが多く、同時実行パフォーマンス要件が高くないシナリオに適しています。

2. 楽観的ロック

楽観的ロックは、文字通りの意味からおおよその意味が推測できますが、他の人が同時にデータを変更しないと考えて、データを操作する際に非常に楽観的です。ロックは、更新が送信されたときにデータが競合するかどうかを正式に検出するだけです。競合が見つかった場合は、エラー メッセージが返され、ユーザーが何をすべきかを決定できる、フェイルファスト メカニズムです。それ以外の場合は、この操作を実行します。

データの読み取り、書き込み検証、データの書き込みの 3 つの段階に分かれています。

スタンドアロン システムの場合は、JAVA の CAS に基づいて実装できます。CAS は、ハードウェアの比較と交換を利用して実装されるアトミックな操作です。

分散システムの場合は、バージョンなどのバージョン番号フィールドをデータベース テーブルに追加できます。

update 表 
set ... , version = version +1 
where id= #{id} and version = #{version}

操作前にレコードのバージョン番号を読み取り、更新する場合は SQL ステートメントでバージョン番号を比較し、一致しているかどうかを確認します。一貫性がある場合は、データを更新します。それ以外の場合は、バージョンが再度読み取られ、上記の操作が再試行されます。

3. JAVA の分散ロック

synchronized、ReentrantLock などはすべて、単一アプリケーションの単一マシン展開におけるリソースの相互排他問題を解決します。ビジネスの急速な発展に伴い、単一のアプリケーションが分散クラスターに進化すると、マルチスレッドとマルチプロセスが異なるマシンに分散され、元の単一マシンの同時実行制御ロック戦略が無効になります。分散ロックを導入する必要があるときは、マシン間の相互排他メカニズムを解決して、共有リソースへのアクセスを制御する必要があります。

分散ロックに必要な条件:

    ロックの基礎となるスタンドアロンシステムと同じリソース相互排他機能
  • 高パフォーマンスのロックの取得と解放
  • ##高可用性

  • ##再入可能

  • #デッドロックを防ぐためのロック失敗メカニズムがあります
  • ##ノンブロッキング、ロックが取得されたかどうかに関係なく、すぐに復帰できる必要があります
  • これを実装するには、データベース、Redis、Zookeeper などに基づいたさまざまな方法が多数あります。主流の Redis ベースの実装方法は次のとおりです。
  • Locking

    SET key unique_value  [EX seconds] [PX milliseconds] [NX|XX]

    アトミック コマンドにより、実行が成功して 1 が返された場合、ロックの追加が成功したことを意味します。注意: unique_value は、異なるクライアントからのロック操作を区別するためにクライアントによって生成される一意の識別子です。ロック解除には特に注意してください。最初に unique_value がロックされたクライアントかどうかを確認してください。ロックされているクライアントであれば、ロック解除と削除が許可されます。結局のところ、他のクライアントによって追加されたロックを削除することはできません。
ロック解除: ロック解除には 2 つのコマンド操作があり、アトミック性を確保するために Lua スクリプトを使用する必要があります。

// 先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

Redis の高いパフォーマンスにより、Redis は現在の主流の実装方法でもある分散ロックを実装します。ただし、すべてには長所と短所があり、ロックされたサーバーがダウンし、スレーブ ノードがデータをバックアップする時間がなかった場合、他のクライアントもロックを取得する可能性があります。

この問題を解決するために、Redis は分散ロック Redlock を正式に設計しました。

基本的な考え方: クライアントが複数の独立した Redis ノードで並行してロックを要求できるようにします。ロック操作が半分以上のノードで正常に完了できた場合、クライアントはディストリビューションを正常に取得したと見なされます。ロックしないとロックが失敗します。

4. リエントラント ロック

リエントラント ロックは、再帰的ロックとも呼ばれ、同じスレッドが外側のメソッドを呼び出してロックを取得し、その後内側のメソッドに入ると、ロックが自動的に取得されることを意味します。ロック。

オブジェクト ロックまたはクラス ロック内にはカウンタがあり、スレッドがロックを取得するたびにカウンタは 1 になり、ロックが解除されるとカウンタは -1 になります。

ロックの数はロック解除の数に対応しており、ロックとロック解除はペアで表示されます。

ReentrantLock と Java の synchronized はどちらも再入可能なロックです。リエントラント ロックの利点の 1 つは、デッドロックをある程度回避できることです。

5、自旋锁

自旋锁是采用让当前线程不停地在循环体内执行,当循环的条件被其他线程改变时才能进入临界区。自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不断增加时,性能下降明显,因为每个线程都需要执行,会占用CPU时间片。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

自旋锁缺点:

  • 可能引发死锁。

  • 可能占用 CPU 的时间过长。

我们可以设置一个 循环时间 或 循环次数,超出阈值时,让线程进入阻塞状态,防止线程长时间占用 CPU 资源。JUC 并发包中的 CAS 就是采用自旋锁,compareAndSet 是CAS操作的核心,底层利用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;
}

如果内存中 var1 对象的var2字段值等于预期的 var5,则将该位置更新为新值(var5 + var4),否则不进行任何操作,一直重试,直到操作成功为止。

CAS 包含了Compare和Swap 两个操作,如何保证原子性呢?CAS 是由 CPU 支持的原子操作,其原子性是在硬件层面进行控制。

特别注意,CAS 可能导致 ABA 问题,我们可以引入递增版本号来解决。

6、独享锁

独享锁,也有人叫它排他锁。无论读操作还是写操作,只能有一个线程获得锁,其他线程处于阻塞状态。

缺点:读操作并不会修改数据,而且大部分的系统都是 读多写少,如果读读之间互斥,大大降低系统的性能。下面的 共享锁 会解决这个问题。

像Java中的 ReentrantLock 和 synchronized 都是独享锁。

7、共享锁

共享锁是指允许多个线程同时持有锁,一般用在读锁上。读锁的共享锁可保证并发读是非常高效的。读写,写读 ,写写的则是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。

8、读锁/写锁

如果对某个资源是读操作,那多个线程之间并不会相互影响,可以通过添加读锁实现共享。如果有修改动作,为了保证数据的并发安全,此时只能有一个线程获得锁,我们称之为 写锁。读读是共享的;而 读写、写读 、写写 则是互斥的。

像 Java中的 ReentrantReadWriteLock 就是一种 读写锁。

9、公平锁/非公平锁

公平锁:多个线程按照申请锁的顺序去获得锁,所有线程都在队列里排队,先来先获取的公平性原则。

优点:所有的线程都能得到资源,不会饿死在队列中。

缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,CPU 唤醒下一个阻塞线程有系统开销。

Javaでのロックの実装方法は何ですか?

非公平锁:多个线程不按照申请锁的顺序去获得锁,而是同时以插队方式直接尝试获取锁,获取不到(插队失败),会进入队列等待(失败则乖乖排队),如果能获取到(插队成功),就直接获取到锁。

优点:可以减少 CPU 唤醒线程的开销,整体的吞吐效率会高点。

缺点:可能导致队列中排队的线程一直获取不到锁或者长时间获取不到锁,活活饿死。

Java 多线程并发操作,我们操作锁大多时候都是基于 Sync 本身去实现的,而 Sync 本身却是 ReentrantLock 的一个内部类,Sync 继承 AbstractQueuedSynchronizer。

像 ReentrantLock 默认是非公平锁,我们可以在构造函数中传入 true,来创建公平锁。

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

10、可中断锁/不可中断锁

可中断锁:指一个线程因为没有获得锁在阻塞等待过程中,可以中断自己阻塞的状态。不可中断锁:恰恰相反,如果锁被其他线程获取后,当前线程只能阻塞等待。如果持有锁的线程一直不释放锁,那其他想获取锁的线程就会一直阻塞。

内置锁 synchronized 是不可中断锁,而 ReentrantLock 是可中断锁。

ReentrantLock获取锁定有三种方式:

  • lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于阻塞状态,直到该线程获取锁。

  • tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false。

  • tryLock(long timeout,TimeUnit Unit), ロックが取得された場合は、すぐに true を返します。別のスレッドがロックを保持している場合は、パラメーターで指定された時間待機します。待機中のプロセス (if) ロックが取得された場合は true を返し、待機がタイムアウトした場合は false を返します。

  • lockInterruptibly() は、ロックが取得されるとすぐに戻ります。ロックが取得されない場合は、ロックが取得されるか、スレッドが別のスレッドによって中断されるまで、スレッドはブロックされます。

11. セグメント ロック

セグメント ロックは、実際にはロック設計の一種であり、その目的は、特定の種類のロックではなく、ロックの粒度を調整することです。 ConcurrentHashMap の場合、その同時実行性の実装は、セグメント化されたロックの形式で効率的な同時操作を実現することです。

ConcurrentHashMap のセグメント ロックはセグメントと呼ばれ、HashMap (JDK7 の HashMap の実装) に似た構造です。つまり、内部に Entry 配列があり、配列内の各要素はリンクされています。リスト; 同時に別の ReentrantLock (セグメントは ReentrantLock を継承します)。

要素を配置する必要がある場合、HashMap 全体をロックするのではなく、まずハッシュコードを通じてどのセグメントに要素を配置するかを知ってから、このセグメントをロックするため、マルチスレッドの put では並列挿入が行われます。同じセグメントに配置されない限りサポートされます。

12. ロックのアップグレード (ロックなし | バイアスされたロック | 軽量ロック | 重量ロック)

JDK 1.6 より前では、synchronized はまだ効率が比較的低い重量ロックでした。しかし、JDK 1.6 以降、ロックの取得と解放の効率を向上させるために JVM が同期を最適化し、バイアス ロックと軽量ロックが導入され、ロックなし、バイアス ロック、軽量レベル ロックの 4 つのロック状態が存在します。 、重量級ロック。これら 4 つの州は競争によって段階的にアップグレードされ、ダウングレードすることはできません。

Javaでのロックの実装方法は何ですか?

ロックフリー

ロックフリーではリソースはロックされません。すべてのスレッドが同じリソースにアクセスして変更できますが、アクセスできるのは 1 つのスレッドだけです。正常に変更されました。これは、私たちがよく楽観的ロックと呼ぶものです。

バイアスされたロック

は、ロックにアクセスする最初のスレッドにバイアスされます。同期されたコード ブロックが初めて実行されるとき、オブジェクト ヘッダーのロック フラグは CAS を通じて変更され、ロック オブジェクトはバイアスされたロックになります。

スレッドが同期されたコード ブロックにアクセスしてロックを取得すると、ロック バイアスのスレッド ID が Mark Word に保存されます。スレッドが同期ブロックに出入りするとき、CAS 操作を通じてロックおよびロック解除を行うのではなく、Mark Word が現在のスレッドを指すバイアス ロックを保存しているかどうかを検出します。軽量ロックの取得と解放は複数の CAS アトミック命令に依存しますが、バイアスされたロックは ThreadID を置き換えるときに 1 つの CAS アトミック命令にのみ依存する必要があります。

同期コード ブロックの実行後、スレッドはバイアス ロックを積極的に解放しません。スレッドが同期されたコード ブロックを 2 回目に実行するとき、スレッドは、その時点でロックを保持しているスレッドがそれ自体であるかどうかを判断します (ロックを保持しているスレッドの ID はオブジェクト ヘッダーにもあります)。通常どおり実行を続行します。以前にロックが解除されていないため、ここで再度ロックする必要がなく、バイアスされたロックには追加のオーバーヘッドがほとんどなく、非常に高いパフォーマンスが得られます。

バイアス ロック 他のスレッドがバイアス ロックをめぐって競合しようとした場合にのみ、バイアス ロックを保持しているスレッドがロックを解放します。スレッドが積極的にバイアス ロックを解放することはありません。バイアスされたロックの取り消しに関しては、グローバル セーフティ ポイントを待つ必要があります。つまり、特定の時点でバイトコードが実行されていない場合、まずバイアスされたロックを所有するスレッドを一時停止し、次に、そのロックが解除されているかどうかを判断します。ロックオブジェクトはロックされています。スレッドがアクティブでない場合は、オブジェクト ヘッダーをロック フリー状態に設定し、バイアスされたロックをキャンセルして、ロック フリー (フラグ ビットが 01) または軽量ロック (フラグ ビットが 00) 状態に戻ります。

バイアスされたロックとは、同期コードの一部が同じスレッドによってアクセスされたとき、つまり、複数のスレッド間で競合がないとき、スレッドが後続のアクセスで自動的にロックを取得し、それによって負荷が軽減されることを意味します。ロック取得の必要性、消費が引き起こされます。

軽量ロック

現在のロックはバイアス ロックです。複数のスレッドが同時にロックを競合すると、バイアス ロックは軽量ロックにアップグレードされます。軽量ロックでは、競争は存在するものの、理想的には競争の度合いは非常に低く、ロックはスピンによって獲得されると考えられています。

軽量ロックが取得される状況は 2 つあります。

  • バイアス ロック機能がオフになっている場合。

  • 複数のスレッドがバイアス ロックをめぐって競合すると、バイアス ロックが軽量ロックにアップグレードされます。 2 番目のスレッドがロック競合に参加すると、バイアスされたロックは軽量ロック (スピン ロック) にアップグレードされます。

軽量ロック状態でロック競合を続行します。ロックを取得していないスレッドはスピンして継続的にループし、ロックが正常に取得できるかどうかを判断します。ロックを取得する操作は、実際には、CAS を介してオブジェクト ヘッダー内のロック フラグを変更することです。まず、現在のロックフラグが「解放」されているかどうかを比較し、解放されている場合は「ロック」に設定するというアトミックな処理です。ロックが取得されると、スレッドは現在のロック所有者の情報をそれ自体に変更します。

重量级锁

如果线程的竞争很激励,线程的自旋超过了一定次数(默认循环10次,可以通过虚拟机参数更改),将轻量级锁升级为重量级锁(依然是 CAS  修改锁标志位,但不修改持有锁的线程ID),当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。

重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资。

13、锁优化技术(锁粗化、锁消除)

锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

举个例子:有个循环体,内部。

for(int i=0;i<size;i++){
    synchronized(lock){
        ...业务处理,省略
    }
}

经过锁粗化的代码如下:

synchronized(lock){
    for(int i=0;i<size;i++){
        ...业务处理,省略
    }
}

锁消除指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。

锁消除的依据是逃逸分析的数据支持,如 StringBuffer 的 append() 方法,或 Vector 的 add() 方法,在很多情况下是可以进行锁消除的,比如以下这段代码:

public String method() {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < 10; i++) {
        sb.append("i:" + i);
    }
    return sb.toString();
}

以上代码经过编译之后的字节码如下:

Javaでのロックの実装方法は何ですか?

从上述结果可以看出,之前我们写的线程安全的加锁的 StringBuffer 对象,在生成字节码之后就被替换成了不加锁不安全的 StringBuilder 对象了,原因是 StringBuffer 的变量属于一个局部变量,并且不会从该方法中逃逸出去,所以我们可以使用锁消除(不加锁)来加速程序的运行。

以上がJavaでのロックの実装方法は何ですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。