マルチスレッドについて知らなかった5つのこと

マルチスレッドとそれをサポートするライブラリを拒否するJavaプログラマーはほとんどいませんが、問題を詳細に調査する時間を見つけた人はさらに少なくなります。 代わりに、特定のタスクに必要なだけフローについて学習し、必要な場合にのみツールに新しいテクニックを追加します。 したがって、適切なアプリケーションを作成して実行することはできますが、もっとうまくやることができます。 コンパイラーとJava仮想マシンの機能を理解すると、より効率的で生産的なコードを作成するのに役立ちます。

5つの事柄...シリーズの今回の記事では、同期メソッド、揮発性変数、アトミッククラスなど、マルチスレッドプログラミングの微妙な側面をいくつか紹介します。 特に、これらの構成要素の一部がJVMおよびJavaコンパイラと相互作用する方法、およびさまざまな相互作用がアプリケーションのパフォーマンスに与える影響について説明します。

翻訳者注:私はマルチスレッドプログラミングに関するこれら5つのことを知らなかった人の1人であるため、この記事はここで公開する価値があると考えましたが、翻訳に間違いを犯す可能性があるため、修正を歓迎します熱意。
翻訳者注2:コメントでは、知識のある人がトピックに関するリンクと情報を共有します。記事の内容と同じくらい面白いです)

1.同期メソッドまたは同期ブロック?


メソッド全体を同期化するか、保護する必要がある部分のみを宣言するかについて既に考えているかもしれません。 このような状況では、Javaコンパイラがソースコードをバイトコードに変換するときに、同期メソッドと同期ブロックで非常に異なる方法で動作することを知っておくと役立ちます。

JVMが同期メソッドを実行すると、実行中のスレッドは、このメソッドのmethod_infoでACC_SYNCHRONIZEDフラグが設定されていると判断します。 次に、オブジェクトに自動的にロックを設定し、メソッドを呼び出し、ロックを解除します。 例外がスローされると、スレッドは自動的にロックを解除します。
一方、同期ブロックは、JVMに組み込まれたオブジェクトロック要求と例外処理のサポートをバイパスするため、バイトコードで明示的に記述する必要があります。 ブロックのバイトコードを見ると、メソッドと比較して多くの追加操作がブロック内にあります。 リスト1は両方の呼び出しを示しています。

リスト1.同期への2つのアプローチ。
パッケージ com.geekcap ;
パブリック クラス SynchronizationExample {
private int i ;

public synchronized int synchronizedMethodGet {
私を返す ;
}

public int synchronizedBlockGet {
synchronized this {
私を返す ;
}
}
}

synchronizedMethodGet()メソッドは、次のバイトコードを生成します。
  0:aload_0
	 1:getfield
	 2:いいえ
	 3:iconst_m1
	 4:再発 

次に、synchronizedBlockGet()メソッドのバイトコードを示します。
  0:aload_0
	 1:重複
	 2:astore_1
	 3:monitorenter
	 4:aload_0
	 5:getfield
	 6:nop
	 7:iconst_m1
	 8:aload_1
	 9:monitorexit
	 10:アイルランド
	 11:astore_2
	 12:aload_1
	 13:monitorexit
	 14:aload_2
	 15:投げる 

同期ブロックを作成すると、16行のバイトコードが返されましたが、同期メソッドは5行のみを返しました。

2.「スレッド内」(ThreadLocal)変数。


クラスのすべてのインスタンスに対して変数の1つのインスタンスを保存する場合は、静的クラス変数を使用します。 各スレッドの変数のインスタンスを保存する場合は、ThreadLocal変数を使用します。 ThreadLocal変数は、各スレッドが独自に個別に初期化された変数のインスタンス、get()またはset()メソッドを介してアクセスするという点で、通常の変数と異なります。

コードを通る各スレッドのパスを一意に決定することを目標とするマルチスレッドコードトレーサを開発しているとします。 問題は、複数のスレッドにわたって複数のクラスの複数のメソッドを調整する必要があることです。 ThreadLocalがなければ、これは扱いにくいでしょう。 スレッドの実行が開始されたとき、ルーターによる識別のための一意のマーカーを生成し、トレース時に各メソッドにこのマーカーを渡す必要があります。

ThreadLocalを使用すると、これは簡単です。 スレッドは、実行の開始時にThreadLocal変数を初期化してから、各クラスの各メソッドからアクセスして、変数は現在実行中のスレッドのみのトレース情報を保存します。 その完了が完了すると、スレッドは、個々のトレースレコードを、すべてのレコードを維持する制御オブジェクトに転送できます。

ThreadLocalの使用は、各スレッドの変数インスタンスを保存する必要がある場合に意味があります。

3.揮発性変数。


私の推定では、Java開発者の半分だけがJavaにvolatileキーワードがあることを知っています。 これらのうち、その意味を知っているのは約10%だけであり、効果的な使い方を知っている人はさらに少ない。 つまり、volatileキーワードを使用した変数の定義は、変数の値が異なるスレッドによって変更されることを意味します。 volatileの意味を完全に理解するには、まず、スレッドが通常の不揮発性変数でどのように動作するかを理解する必要があります。

パフォーマンスを向上させるために、Java言語仕様では、JREは変数を参照する各スレッドに変数のローカルコピーを保存できます。 キャッシュに似た変数のこれらの「インライン」コピーを検討できます。これは、変数の値へのアクセスが必要になるたびにメインメモリをチェックすることを回避するのに役立ちます。

しかし、次の場合に何が起こるか想像してみてください:2つのスレッドが開始し、最初のスレッドは変数Aを5として読み取り、2番目のスレッドは10として読み取ります。 Aの不正な値。ただし、変数Aがvolatileとしてマークされている場合、スレッドがその値にアクセスするときはいつでも、Aのコピーを受け取り、現在の値を読み取ります。

アプリケーションの変数が変更されない場合、インラインキャッシュが有効です。 それ以外の場合、volatileキーワードが何をすることができるかを知ることは非常に便利です。

4.揮発性vs同期。


変数がvolatileとして宣言されている場合、複数のスレッドを変更することが期待されることを意味します。 当然、JREがvolatile変数に何らかの形の同期を課すと考えるでしょう。 良くも悪くも、JREはvolatile変数にアクセスするときに暗黙的に同期を提供しますが、1つの非常に大きな注意点があります。volatile変数の読み取りは同期され、volatile変数への書き込みは同期されますが、非アトミック操作は同期されません。
つまり、次のコードはスレッドセーフではありません。
myVolatileVar ++;

このコードは次のようにも記述できます。
int temp = 0 ;
同期 myVolatileVar {
temp = myVolatileVar ;
}

temp ++;

同期 myVolatileVar {
myVolatileVar = temp ;
}

つまり、揮発性変数が暗黙的に更新される場合、つまり値が読み取られ、変更され、新しい値として割り当てられる場合、結果は2つの同期操作間で非スレッドセーフになります。 同期を使用するか、JREサポートに依存して揮発性変数を自動的に同期するかを選択できます。 最適なアプローチは、ケースによって異なります。変数に割り当てられた揮発性値が現在の値に依存する場合(たとえば、インクリメント操作中)、操作をスレッドセーフにする場合は同期を使用する必要があります。

5.アトミックフィールドの更新。


インクリメントおよびデクリメント操作を実行するプリミティブ型が必要な場合、同期ブロックを自分で記述するよりも、java.util.concurrent.atomicパッケージの新しいアトミッククラスから選択する方がはるかに優れています。 アトミッククラスは、特定の操作(たとえば、増分操作と減分操作、更新、値の追加)がスレッドセーフであることを保証します。 アトミッククラスのリストには、AtomicInteger、AtomicBoolean、AtomicLong、AtomicIntegerArrayなどが含まれます。

アトミッククラスを使用する際のプログラマにとっての特有の課題は、操作のget、set、およびget-setファミリを含むすべてのクラス操作もアトミックであることです。 これは、アトミック変数の値を変更しない読み取りおよび書き込み操作が同期され、重要な読み取り/更新/書き込み操作だけではないことを意味します。 同期されたコードの展開をより詳細に制御したい場合、回避策はアトミックフィールドアップデータを使用することです。
Atomic Updaterを使用します。

AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdaterなどのアトミックアップデーターは、本質的に揮発性フィールドに適用されるシェルです。 内部では、Javaクラスライブラリがそれらを使用します。 アプリケーションコードではあまり使用されませんが、彼らの助けを借りて生活を楽にし始めない理由はありません。

リスト2は、アトミック更新を使用して、誰かが読んでいる本を変更するクラスの例を示しています。
リスト2. Bookクラス。
パッケージ com.geeckap.atomicexample ;

パブリック クラス ブック
{
プライベート 文字列;

公開 図書
{
}

公開 図書 文字列
{
これname = name ;
}

public String getName
{
名前を返す ;
}

public void setName 文字列
{
これname = name ;
}
}

Bookクラスは、ただ1つのフィールドnameを持つPOJO(プレーンな古いJavaオブジェクト-プレーンな古いJavaオブジェクト)です。

リスト3. MyObjectクラス。
パッケージ com.geeckap.atomicexample ;

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater ;

/ **
*
* @author shaines
* /
パブリック クラス MyObject
{
プライベート 揮発性 ブック whatImReading ;

private static final AtomicReferenceFieldUpdater < MyObject、Book > updater =
AtomicReferenceFieldUpdater。 newUpdater
マイオブジェクト クラスブッククラス「whatImReading」 ;

public Book getWhatImReading
{
whatImReadingを返します
}

public void setWhatImReading Book whatImReading
{
//this.whatImReading = whatImReading;
アップデーター。 compareAndSet これこれ 。whatImReading 、whatImReading ;
}
}

リスト3のMyObjectクラスは、ご想像のとおりgetメソッドとsetメソッドを表していますが、setメソッドは別のことを行います。 指定されたブックへの内部リンクを単に提供する代わりに(リスト3のコメント化されたコードによって行われます)、AtomicReferenceFieldUpdaterを使用します。

AtomicReferenceFieldUpdater

Javadocは、次のようにAtomicReferenceFieldUpdaterを定義します。

指定されたクラスの指定されたvolatile参照フィールドへのアトミック更新を可能にするリフレクションベースのユーティリティ。 このクラスは、同じノードの複数の参照フィールドが独立してアトミック更新の対象となるアトミックデータ構造で使用するために設計されています。
(割り当てられたクラスの割り当てられた揮発性参照フィールドへのアトミック更新を可能にするリフレクションベースのユーティリティ。このクラスは、同じレコードの複数の参照フィールドがアトミック更新の独立したエンティティであるアトミックデータ構造で使用するためのものです) これを普通に翻訳する方法

リスト3では、3つのパラメーターを取るnewUpdaterメソッドを呼び出すことでAtomicReferenceFieldUpdaterが作成されます。
•フィールドを含むオブジェクトのクラス(この場合、MyObject)
•アトミックに更新されるオブジェクトのクラス(この場合はBook)
•アトミックアップデートのフィールド名

ここで重要なのは、setWhatImReadingがアトミック操作として実行されている間に、getWhatImReadingメソッドが何らかの同期なしで実行されることです。

リスト4はsetWhatImReading()の使用方法を示し、変数が正しく変更されていることを証明します。

リスト4. Atomic Updaterのテストケース。
パッケージ com.geeckap.atomicexample ;

import org.junit.Assert ;
import org.junit.Before ;
import org.junit.Test ;

パブリック クラス AtomicExampleTest
{
プライベート MyObject obj ;

@Before
public void setUp
{
obj = new MyObject ;
obj。 setWhatImReading 新しい 「Java 2 From Scratch」 ;
}

@テスト
public void testUpdate
{
obj。 setWhatImReading new Book
「Pro Java EE 5パフォーマンス管理と最適化」 ;
アサートします。 assertEquals "誤ったブック名"
「Pro Java EE 5パフォーマンス管理と最適化」
obj。 getWhatImReading getName ;
}

}


結論として。


マルチスレッドプログラミングは常にテストですが、Javaプラットフォームが進化したため、一部のマルチスレッドプログラミングタスクを簡素化するサポートが得られました。 この記事では、同期メソッドとコードブロックの違い、ThreadLocal変数を使用することの重要性、揮発性に対する誤解(揮発性に依存する危険性を含む)同期を使用する必要があります)、アトミッククラスの複雑さの簡単な概要。 もっと知りたい人は、 リンクのセクション(著者のサイト)を参照してください。

Source: https://habr.com/ru/post/J108016/


All Articles