フィールドを宣言した場合は、フィールドを変更しようとするとコンパイル時エラーになるか、初期化されないままにするよりも多くのことがあります。
クラスA
のインスタンスをデータレース(すなわち、同期のない状態で、つまり静的フィールドなどのグローバルに利用可能な場所に格納する)で共有する場合、マルチスレッドコードでは、いくつかのスレッドはgetA()
の値を変更します!
Final
フィールドがあっても、同期せずに、コンストラクタが終了した後、すべてのスレッドから見えるの値を持つ(JVM specsによって)保証されています。
これら2つのクラスを考えてみましょう:
final class A {
private final int x;
A(int x) { this.x = x; }
public getX() { return x; }
}
final class B {
private int x;
B(int x) { this.x = x; }
public getX() { return x; }
}
A
とB
どちらも、あなたは(の反射を忘れてみましょう)初期化後にフィールドx
の値を変更することができないという意味では、不変です。唯一の違いは、フィールドx
にはAにfinal
というマークが付けられていることです。この小さな違いの大きな影響をすぐに実感できます。
は今、次のコードを考えてみます。
class Main {
static A a = null;
static B b = null;
public static void main(String[] args) {
new Thread(new Runnable() { void run() { try {
while (a == null) Thread.sleep(50);
System.out.println(a.getX()); } catch (Throwable t) {}
}}).start()
new Thread(new Runnable() { void run() { try {
while (b == null) Thread.sleep(50);
System.out.println(b.getX()); } catch (Throwable t) {}
}}).start()
a = new A(1); b = new B(1);
}
}
は、両方のスレッドは、メインスレッドが、この仮定は些細に見えるかもしれませんが、彼らは(なお、設定した後、彼らが見ているフィールドがnullではないことを確認するために起こると仮定しますJVMでは保証されていません)。そう、コンストラクタが終了した後に、それはすべてのスレッドのオブジェクトを参照してくださいということが保証されている - この場合
、我々はa
を監視スレッドがそのx
フィールドがfinalであるため、値1
を印刷することを確認することができますx
の正しい値が表示されます。
しかし、他のスレッドが何をするかはわかりません。仕様では、0
または1
のいずれかしか印刷されないことが保証されています。フィールドはfinal
ではないため、同期化(またはvolatile
)を使用していないため、スレッドは初期化されていないフィールドを参照して0を出力する可能性があります。もう1つの可能性は、実際にフィールドが初期化されていることを確認し、1を印刷することです。他の値は印刷できません。
また、何が起こるかもしれませんが、あなたがb
のgetX()
の値を読み取り、印刷し続けるならば、それは0
を印刷するしばらく1
の印刷を開始することができ、ということです!この場合、不変オブジェクトのフィールドがfinal
でなければならない理由は明らかです:セッターを提供しないことで不変であると思われる場合でも、第2スレッドの観点からはb
が変更されました!
2番目のスレッドがフィールドfinal
せずにx
の正しい値を確認することを保証したい場合は、揮発性B
のインスタンスを保持するフィールド宣言することができます:
class Main {
// ...
volatile static B b;
// ...
}
を他の可能性があります
final class B {
private int x;
private synchronized setX(int x) { this.x = x; }
public synchronized getX() { return x; }
B(int x) { setX(x); }
}
又は、メインのコードを変更するときFに同期を追加することによって:、いずれかのクラスBを変更することによって設定し、フィールドを読み取るときに同期させる場合b
が読み込まれ、書かれているときは、両方の操作が同じオブジェクト上で同期しなければならないことに注意してください!
ご覧のとおり、最もエレガントで信頼性が高く、実行可能なソリューションは、フィールドx
を最終的にすることです。最後の注意、として
それは最終すべてのフィールドを持っている不変、スレッド・セーフ・クラスのために絶対に必要というわけではありません。ただし、これらのクラス(スレッドセーフ、不変、非最終フィールドを含む)は、細心の注意を払って設計する必要があり、専門家のために残す必要があります。
この例は、クラスjava.lang.Stringです。それは最終的ではありませんprivate int hash;
フィールドを、持っている、とhashCode()のためにキャッシュとして使用されます。
private int hash;
public int hashCode() {
int h = hash;
int len = count;
if (h == 0 && len > 0) {
int off = offset;
char val[] = value;
for (int i = 0; i < len; i++)
h = 31*h + val[off++];
hash = h;
}
return h;
}
あなたが見ることができるように、のhashCode()メソッドは、最初の(非最終)を読み込むフィールドhash
。初期化されていない場合(つまり、0の場合)、値を再計算して設定します。ハッシュコードを計算してフィールドに書き込んだスレッドの場合、その値は永久に保持されます。
しかし、他のスレッドは、スレッドがそれを他の何かに設定した後でも、そのフィールドにはまだ0が表示されることがあります。この場合、これらの他のスレッドは、ハッシュを再計算し、正確に同じ値を取得してから設定します。
ここで、クラスの不変性とスレッドセーフティを正当化する理由は、最終的なフィールドにキャッシュされていなくても、すべてのスレッドがhashCode()に対して正確に同じ値を取得することです。まったく同じ値が得られます。 この理由は非常に微妙なので、は、すべてのフィールドが不変のスレッドセーフクラスでfinalとマークされていることをお勧めします。
権限には疑問がありますが、ここではJoshua Blochについて話しています。 – duffymo
不変クラスとはどういう意味ですか?そのインスタンスが不変でなければならないか、このタイプにキャストできるオブジェクト(サブクラスからインスタンス化されたオブジェクトなど)ですか? 2番目のケースでは、サブクラスBを作成するだけで十分です。サブクラスBには、設定可能な新しいプロパティbがあります。その後、Bのインスタンスは変更可能です。 –
Brian Goetzの* Java Concurrency in Practice *をお勧めします。 Chapter 3(または4?)を読んだ後に、不変のスレッドセーフなオブジェクトのフィールドが最終的であるべき理由が明確になります。プレビューとして、以下の私の答えを見てください。 –