ホームページ >Java >&#&チュートリアル >Java 並行プログラミングにおけるオブジェクトの共有実装

Java 並行プログラミングにおけるオブジェクトの共有実装

WBOY
WBOY転載
2023-04-23 17:25:071619ブラウズ

1. 可視性

一般に、各スレッドには独自のキャッシュ メカニズムがあるため、読み取り操作を実行しているスレッドが他のスレッドによって書き込まれた値を参照できるかどうかは保証できません。複数のスレッド間でメモリ書き込み操作の可視性を確保するには、同期メカニズムを使用する必要があります。

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

上記のコードは 42 を出力するように見えますが、実際には、読み取りスレッドは ready の値を決して確認できないため、まったく終了しない可能性があります。読み取りスレッドは 0 を出力する可能性が非常に高いです。準備完了の値が書き込まれているのはわかりますが、その後数値に書き込まれた値は見えません。この現象は「並べ替え」と呼ばれます。同期がない場合、コンパイラー、プロセッサー、ランタイムなどが操作の実行順序に予期しない調整を​​行う可能性があります。

したがって、複数のスレッド間でデータを共有する場合は常に、正しい同期を使用する必要があります。

1.1 無効なデータ

同期を使用しない限り、変数の無効な値が取得される可能性が非常に高くなります。無効な値が同時に現れることはなく、スレッドが 1 つの変数の最新の値と別の変数の無効な値を取得する場合があります。無効なデータは、予期しない例外、データ構造の破損、不正確な計算、無限ループなど、混乱を招く障害を引き起こす可能性もあります。

1.2 非アトミックな 64 ビット操作

不揮発性の long 変数と double 変数の場合、JVM では 64 ビットの読み取りまたは書き込み操作を 2 つの 32 ビット操作に分解できます。したがって、最新の値の上位 32 ビットと無効な値の下位 32 ビットが読み取られる可能性が高く、結果としてランダムな値が読み取られることになります。ただし、キーワード volatile で宣言されているか、ロックで保護されている場合を除きます。

1.3 ロックと可視性

スレッドは、ロックで保護された同期コード ブロックを実行すると、同じ同期コード ブロック内の他のスレッドの以前のすべての操作の結果を確認できます。同期がなければ、上記の保証は達成できません。ロックの意味は相互排他動作に限定されず、可視性も含みます。すべてのスレッドが共有変数の最新の値を確実に参照できるようにするには、読み取りまたは書き込み操作を実行するすべてのスレッドが同じロック上で同期されている必要があります。

1.4 揮発性変数

変数が揮発性型として宣言されている場合、コンパイラもランタイムも、変数に対する操作を他のメモリ操作と一緒に並べ替えることはありません。揮発性変数はレジスタやプロセッサから見えないその他の場所にキャッシュされないため、揮発性変数を読み取ると、常に最後に書き込まれた値が返されます。ロック メカニズムは可視性と原子性の両方を保証できますが、揮発性変数は可視性のみを保証します。

揮発性変数は、次の条件がすべて満たされる場合にのみ使用する必要があります。

  • 変数への書き込み操作は、変数の現在の値に依存しません。変数の値を更新するために単一のスレッドのみを使用することを保証することもできます。

  • この変数は、他の状態変数とともに不変条件には含まれません。

  • 変数にアクセスするときにロックする必要はありません。

2. リリースと漏洩

オブジェクトを公開するということは、そのオブジェクトが現在のスコープ外のコードで使用できることを意味します。オブジェクトの公開方法には、非プライベート変数への参照、メソッド呼び出しによって返される参照、内部クラス オブジェクトの公開および外部クラスへの暗黙的参照などが含まれます。解放されるべきではないオブジェクトが解放されることをリークと呼びます。

public class ThisEscape {
   private int status;
   public ThisEscape(EventSource source) {
      source.registerListener(new EventListener() {
         public void onEvent(Event e) {
            doSomething(e);
         }
      });
      status = 1;
   }

   void doSomething(Event e) {
      status = e.getStatus();
   }

   interface EventSource {
      void registerListener(EventListener e);
   }

   interface EventListener {
      void onEvent(Event e);
   }

   interface Event {
      int getStatus();
   }
}

内部クラスのインスタンスには外部クラスのインスタンスへの暗黙的な参照が含まれているため、ThisEscape が EventListener を解放すると、ThisEscape インスタンス自体も暗黙的に解放されます。ただし、この時点では変数 status が初期化されていないため、この参照がコンストラクター内でリークします。プライベート コンストラクターとパブリック ファクトリ メソッドを使用して、間違った構築プロセスを回避できます:

public class SafeListener {
    private int status;
    private final EventListener listener;
    private SafeListener() {
        listener = new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        };
        status = 1;
    }
    public static SafeListener newInstance(EventSource source) {
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }

    void doSomething(Event e) {
        status = e.getStatus();
    }

    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
        int getStatus();
    }
}

3. スレッドの閉鎖

同期の使用を避ける 1 つの方法は、共有しないことです。データが単一スレッド内でのみアクセスされる場合、同期は必要ありません。これはスレッド クロージャと呼ばれます。スレッドの封じ込めはプログラミング上の考慮事項であり、プログラムに実装する必要があります。 Java は、ローカル変数や ThreadLocal など、スレッドの閉鎖を維持するのに役立ついくつかのメカニズムも提供します。

3.1 アドホック スレッド クロージャ

アドホック スレッド クロージャとは、スレッド クロージャを維持する責任がすべてプログラム実装によって負われることを意味します。 volatile 変数の使用は、アドホック スレッド クロージャを実現する方法です。単一のスレッドのみが共有 volatile 変数に対して書き込み操作を実行することが保証されている限り、これらの変数に対して「読み取り、変更、書き込み」操作を安全に実行できます。 . 、揮発性変数の可視性により、他のスレッドが最新の値を確認できるようになります。

アドホック スレッド クロージャは非常に壊れやすいため、プログラム内ではできるだけ使用しないでください。可能であれば、スタック封じ込めや ThreadLocal などの他のスレッド封じ込め手法を使用してください。

3.2 スタック クロージャ

スタック クロージャでは、オブジェクトにはローカル変数を介してのみアクセスできます。これらは実行スレッドのスタック上に配置され、他のスレッドからアクセスすることはできません。これらのオブジェクトはスレッドセーフではありませんが、依然としてスレッドセーフです。ただし、どのオブジェクトがスタックで囲まれているかを知っているのはコードを作成した人だけであることに注意してください。明確な指示がなければ、後続のメンテナがこれらのオブジェクトを誤って簡単に漏洩してしまう可能性があります。

3.3 ThreadLocal类

使用ThreadLocal是一种更规范的线程封闭方式,它能是线程中的某个值与保存值的对象关联起来。如下代码,通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接:

public class ConnectionDispenser {
    static String DB_URL = "jdbc:mysql://localhost/mydatabase";

    private ThreadLocal<Connection> connectionHolder
        = new ThreadLocal<Connection>() {
            public Connection initialValue() {
                try {
                    return DriverManager.getConnection(DB_URL);
                } catch (SQLException e) {
                    throw new RuntimeException("Unable to acquire Connection, e");
                }
        };
    };

    public Connection getConnection() {
        return connectionHolder.get();
    }
}

从概念上看,你可以将ThreadLocal8742468051c85b06f0a0af9e3e506b5c视为包含了Mapdd13f7ef6939263b34f16ddd764e4ff9对象,其中保存了特定于改线程的值,但ThreadLocal的实现并非如此。这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾被回收。

4. 不变性

如果某个对象在被创建后其状态就不能被修改,那么这个对象就被称为不可变对象。满足同步需求的另一种方法就是使用不可变对象。不可变对象一定是线程安全的。当满足以下条件时,对象才是不可变的:

  • 对象创建以后其状态就不能改变

  • 对象的所有域都是final类型

  • 对象是正确创建的,在对象创建期间,this引用没有泄露

public final class ThreeStooges {
    private final Set<String> stooges = new HashSet<String>();

    public ThreeStooges() {
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
    }

    public boolean isStooge(String name) {
        return stooges.contains(name);
    }
}

上述代码中,尽管stooges对象是可变的,但在它构造完成后无法对其修改。stooges是一个final类型的引用变量,因此所有的对象状态都通过一个final域访问。在构造函数中,this引用不能被除了构造函数之外的代码访问到。

4.1 final域

final类型的域是不能修改的,但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的。final域的对象在构造函数中不会被重排序,所以final域也能保证初始化过程的安全性。和“除非需要更高的可见性,否则应将所有的域都声明为私用域”一样,“除非需要某个域是可变的,否则应将其声明为final域”也是一个良好的编程习惯。

4.2 使用volatile类型来发布不可变对象

因式分解Sevlet将执行两个原子操作:

  • 更新缓存

  • 通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的结果

每当需要一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据:

public class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;

    public OneValueCache(BigInteger i, BigInteger[] factors) {
        lastNumber = i;
        lastFactors = Arrays.copyOf(factors, factors.length);
    }

    public BigInteger[] getFactors(BigInteger i) {
        if (lastNumber == null || !lastNumber.equals(i))
            return null;
        else
            return Arrays.copyOf(lastFactors, lastFactors.length);
    }
}

当线程获取了不可变对象的引用后,不必担心另一个线程会修改对象的状态。如果要更新这些变量,可以创建一个新的容器对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。当一个线程将volatile类型的cache设置为引用一个新的OneValueCache时,其他线程就会立即看到新缓存的数据:

public class VolatileCachedFactorizer implements Servlet {
    private volatile OneValueCache cache = new OneValueCache(null, null);

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = cache.getFactors(i);
        if (factors == null) {
            factors = factor(i);
            cache = new OneValueCache(i, factors);
        }
        encodeIntoResponse(resp, factors);
    }
}

5 安全发布

5.1 不正确的发布

像这样将对象引用保存到公有域中就是不安全的:

public Holder holder;
public void initialize(){
    holder = new Holder(42);
}

由于存在可见性问题,其他线程看到的Holder对象将处于不一致的状态。除了发布对象的线程外,其他线程可以看到Holder域是一个失效值,因此将看到一个空引用或者之前的旧值。

public class Holder {
    private int n;

    public Holder(int n) {
        this.n = n;
    }

    public void assertSanity() {
        if (n != n)
            throw new AssertionError("This statement is false.");
    }
}

上述代码,即使Holder对象被正确的发布,assertSanity也有可能抛出AssertionError。因为线程看到Holder引用的值是最新的,但由于重排序Holder状态的值却是时效的。

5.2 不可变对象与初始化安全性

即使在发布不可变对象的引用时没有使用同步,也仍然可以安全地访问该对象。任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。在没有额外同步的情况下,也可以安全地访问final类型的域。然而,如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。

5.3 安全发布的常用模式

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全发布:

  • 在静态初始化函数里初始化一个对象引用。

  • 将对象的引用保存到volatile类型的域或者AtomicReference对象中。

  • 将对象的引用保存到某个正确构造对象的final类型域中。

  • 将对象的引用保存到一个由锁保护的域中。

线程安全库中的容器类提供了以下的安全发布保证:

  • 通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程。

  • 通过将某个对象放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中,可以将该对象安全地发布到任何从这些容器中访问该对象的线程。

  • オブジェクトを BlockingQueue または ConcurrentLinkedQueue に配置すると、これらのキューからオブジェクトにアクセスする任意のスレッドにオブジェクトを安全に公開できます。

5.4 事実上不変オブジェクト

オブジェクトが技術的には変更可能であるが、公開後にその状態が変わらない場合、このオブジェクトは事実上不変オブジェクトと呼ばれます。安全に公開された事実上不変オブジェクトは、追加の同期なしでどのスレッドでも安全に使用できます。たとえば、各ユーザーの最新のログイン時刻を保存する Map オブジェクトを維持します:

public Map lastLogin =
Collections.synchronizedMap(new HashMapDate オブジェクトの値が Map に配置された後に変更されない場合、synchronizedMap の同期メカニズムで Date 値を安全に公開できるようになり、これらの Date 値にアクセスするときに追加の同期は必要ありません。

5.5 可変オブジェクト

可変オブジェクトの場合、同期はオブジェクトの公開時に必要なだけでなく、後続の変更操作の可視性を確保するためにオブジェクトにアクセスするたびに使用する必要もあります。オブジェクトの公開要件は、その可変性によって異なります。

  • 不変オブジェクトは、どのメカニズムでも公開できます。

  • 実際のところ、不変オブジェクトは安全な方法で公開する必要があります。

  • 可変オブジェクトは安全な方法で解放する必要があり、スレッドセーフであるか、ロックで保護されている必要があります。

#5.6 安全な共有オブジェクト

並行プログラムでオブジェクトを使用および共有する場合、次のようないくつかの実用的な戦略を使用できます。

  • スレッドの閉鎖。スレッドで囲まれたオブジェクトは 1 つのスレッドによってのみ所有でき、オブジェクトはそのスレッドに囲まれ、このスレッドによってのみ変更できます。

  • 読み取り専用共有。追加の同期を行わないと、複数のスレッドが共有読み取り専用オブジェクトに同時にアクセスできますが、どのスレッドもそれを変更できません。共有読み取り専用オブジェクトには、不変オブジェクトと事実上不変オブジェクトが含まれます。

  • スレッドセーフな共有。スレッドセーフ オブジェクトは内部で同期されるため、複数のスレッドがオブジェクトのパブリック インターフェイスを通じて、それ以上の同期を行わずにアクセスできます。

  • 保護されたオブジェクト。保護されたオブジェクトには、特定のロックを保持することによってのみアクセスできます。保護されたオブジェクトには、他のスレッド セーフ オブジェクトにカプセル化されているオブジェクトや、特定のロックによって解放され保護されているオブジェクトが含まれます。

以上がJava 並行プログラミングにおけるオブジェクトの共有実装の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

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