JAvaセキュリティ符号化ガイドライン:可視性と原子性

8317 ワード

概要


JAvaクラスには多くの変数が定義され、クラス変数やインスタンス変数があり、これらの変数はアクセス中に可視性と原子性の問題に遭遇します.ここでは、これらの問題を回避する方法について詳しく説明します.

可変オブジェクトの可視性


可変オブジェクトは初期化後に変更できないオブジェクトですが、クラスに可変オブジェクトが導入され、すべての可変オブジェクトの変更がすぐにすべてのスレッドに表示されるのではないでしょうか.
実際には,可変オブジェクトはマルチスレッド環境でのみオブジェクトが使用する安全性を保証し,オブジェクトの可視性を保証することはできない.
まず可変性について議論し、次の例を考えてみましょう.
public final class ImmutableObject {
    private final int age;
    public ImmutableObject(int age){
        this.age=age;
    }
}

ImmutableObjectオブジェクトを定義しました.classはfinalで、中の唯一のフィールドもfinalです.このImmutableObjectは初期化後は変更できません.
次に、getとsetというImmutableObjectのクラスを定義します.
public class ObjectWithNothing {
    private ImmutableObject refObject;
    public ImmutableObject getImmutableObject(){
        return refObject;
    }
    public void setImmutableObject(int age){
        this.refObject=new ImmutableObject(age);
    }
}

上記の例では、可変オブジェクトへの参照refObjectを定義し、getメソッドとsetメソッドを定義します.
なお、ImmutableObjectというクラス自体は可変ではありませんが、オブジェクトへの参照refObjectは可変です.これは、setImmutableObjectメソッドを複数回呼び出すことができることを意味します.
可視性についてもう一度議論します.
上記の例では、マルチスレッド環境では、setImmutableObjectのたびにgetImmutableObjectに新しい値が返されるのではないでしょうか.
答えは否定的だ.
ソースコードをコンパイルすると,コンパイラで生成される命令の順序はソースコードの順序と完全に一致しない.プロセッサは、プログラムの最終的な実行結果と厳密なシリアル環境での実行結果が一致する限り、JVMで命令を実行するために乱順または並列に使用することができる.プロセッサにはローカルキャッシュがあり、結果をローカルキャッシュに格納すると、他のスレッドでは結果が表示されません.それ以外にキャッシュがプライマリメモリにコミットされる順序も変化します.
どうやって解決しますか?
最も簡単な可視性を解決する方法はvolatileキーワードを加えることであり、volatileキーワードはjavaメモリモデルのhappens-beforeルールを使用することができ、volatileの変数変更がすべてのスレッドに表示されることを保証することができる.
public class ObjectWithVolatile {
    private volatile ImmutableObject refObject;
    public ImmutableObject getImmutableObject(){
        return refObject;
    }
    public void setImmutableObject(int age){
        this.refObject=new ImmutableObject(age);
    }
}

また、ロック機構を使用しても同様の効果が得られます.
public class ObjectWithSync {
    private  ImmutableObject refObject;
    public synchronized ImmutableObject getImmutableObject(){
        return refObject;
    }
    public synchronized void setImmutableObject(int age){
        this.refObject=new ImmutableObject(age);
    }
}

最後に、同じ効果を達成するために原子類を使用することもできます.
public class ObjectWithAtomic {
    private final AtomicReference refObject= new AtomicReference<>();
    public ImmutableObject getImmutableObject(){
        return refObject.get();
    }
    public void setImmutableObject(int age){
        refObject.set(new ImmutableObject(age));
    }
}

共有変数の複合操作の原子性を保証する


共有オブジェクトであれば,マルチスレッド環境における原子性を考慮する必要がある.共有変数に対する複合操作であれば、例えば:++,--*=,/=,%=,+=,-=,<=,>>=,>>>=,^=など、1つの文のように見えますが、実際には複数の文の集合です.
マルチスレッドの下のセキュリティを考慮する必要があります.
次の例を考えます.
public class CompoundOper1 {
    private int i=0;
    public int increase(){
        i++;
        return i;
    }
}

例ではint iを加算します.しかし、++は実際には3つの操作から構成されています.
  • メモリからiの値を読み出し、CPUレジスタに書き込む.
  • CPUレジスタは、i値+1
  • は、メモリ内のiに値を書き込みます.

  • 単一スレッド環境では問題ありませんが、マルチスレッド環境では原子操作ではないため、問題が発生する可能性があります.
    解決策はいろいろありますが、1つ目はsynchronizedキーワードを使うことです
     public synchronized int increaseSync(){
            i++;
            return i;
        }

    2つ目はlockを使用することです.
     private final ReentrantLock reentrantLock=new ReentrantLock();
    
        public int increaseWithLock(){
            try{
                reentrantLock.lock();
                i++;
                return i;
            }finally {
                reentrantLock.unlock();
            }
        }

    3つ目はAtomic原子クラスを使用します.
     private AtomicInteger atomicInteger=new AtomicInteger(0);
    
        public int increaseWithAtomic(){
            return atomicInteger.incrementAndGet();
        }

    複数のAtomic原子類の動作の原子性を保証する


    1つの方法で複数の原子類の操作を用いると,単一の原子操作は原子的であるが,組み合わせると必ずしもそうではない.
    例を見てみましょう
    public class CompoundAtomic {
        private AtomicInteger atomicInteger1=new AtomicInteger(0);
        private AtomicInteger atomicInteger2=new AtomicInteger(0);
    
        public void update(){
            atomicInteger1.set(20);
            atomicInteger2.set(10);
        }
    
        public int get() {
            return atomicInteger1.get()+atomicInteger2.get();
        }
    }

    上記の例では、2つのAtomicIntegerを定義し、それぞれupdateとget操作で2つのAtomicIntegerを操作します.
    AtomicIntegerは原子性ですが、2つの異なるAtomicIntegerが合併するとそうではありません.マルチスレッド操作中に問題が発生する可能性があります.
    同様に、同期メカニズムまたはロックを使用して、データの一貫性を保証することができます.

    メソッド呼び出しチェーンの原子性を保証する


    オブジェクトのインスタンスを作成する場合、このオブジェクトのインスタンスはチェーンコールによって作成されます.では、チェーン呼び出しの原子性を保証する必要があります.
    次の例を考えてみましょう.
    public class ChainedMethod {
        private int age=0;
        private String name="";
        private String adress="";
    
        public ChainedMethod setAdress(String adress) {
            this.adress = adress;
            return this;
        }
    
        public ChainedMethod setAge(int age) {
            this.age = age;
            return this;
        }
    
        public ChainedMethod setName(String name) {
            this.name = name;
            return this;
        }
    }

    簡単なオブジェクトです.3つのプロパティを定義し、setのたびにthisへの参照を返します.
    マルチスレッド環境でどのように呼び出すかを見てみましょう.
     ChainedMethod chainedMethod= new ChainedMethod();
            Thread t1 = new Thread(() -> chainedMethod.setAge(1).setAdress("www.flydean.com1").setName("name1"));
            t1.start();
    
            Thread t2 = new Thread(() -> chainedMethod.setAge(2).setAdress("www.flydean.com2").setName("name2"));
            t2.start();

    マルチスレッド環境では,上記のsetメソッドが混乱する可能性があるためである.
    どうやって解決しますか?ローカルアクセスのためスレッドが安全で、最後に新しく作成したインスタンスオブジェクトにコピーするローカルコピーを作成できます.
    主なコードは次のとおりです.
    public class ChainedMethodWithBuilder {
        private int age=0;
        private String name="";
        private String adress="";
    
        public ChainedMethodWithBuilder(Builder builder){
            this.adress=builder.adress;
            this.age=builder.age;
            this.name=builder.name;
        }
    
        public static class Builder{
            private int age=0;
            private String name="";
            private String adress="";
    
            public static Builder newInstance(){
                return new Builder();
            }
            private Builder() {}
    
            public Builder setName(String name) {
                this.name = name;
                return this;
            }
    
            public Builder setAge(int age) {
                this.age = age;
                return this;
            }
    
            public Builder setAdress(String adress) {
                this.adress = adress;
                return this;
            }
    
            public ChainedMethodWithBuilder build(){
                return new ChainedMethodWithBuilder(this);
            }
        }

    呼び出し方法を見てみましょう.
     final ChainedMethodWithBuilder[] builder = new ChainedMethodWithBuilder[1];
            Thread t1 = new Thread(() -> {
                builder[0] =ChainedMethodWithBuilder.Builder.newInstance()
                    .setAge(1).setAdress("www.flydean.com1").setName("name1")
                    .build();});
            t1.start();
    
            Thread t2 = new Thread(() ->{
                builder[0] =ChainedMethodWithBuilder.Builder.newInstance()
                    .setAge(1).setAdress("www.flydean.com1").setName("name1")
                    .build();});
            t2.start();

    Lambda式で使用される変数はfinalまたはfinalで等価でなければならないため、finalの配列を構築する必要があります.

    64 bitsの値の読み書き


    Javaでは、64 bitsのlongとdoubleは2つの32 bitsとして扱われています.
    従って、1つの64 bitsの動作は、2つの32 bitsの動作に分けられる.原子的な問題を引き起こした.
    次のコードを考慮します.
    public class LongUsage {
        private long i =0;
    
        public void setLong(long i){
            this.i=i;
        }
        public void printLong(){
            System.out.println("i="+i);
        }
    }

    longの読み書きは2つの部分に分かれて行われるため,マルチスレッド環境でsetLongとprintLongのメソッドを複数回呼び出すと問題が発生する可能性がある.
    解決策本は簡単で、longまたはdouble変数をvolatileとして定義すればよい.
    private volatile long i = 0;

    [原文リンク](https://developer.aliyun.com/...,許可なく転載してはならない.