Javaスレッドセーフな顔の質問、あなたは本当に知っているのですか?

かかわらず、複数のスレッドがクラスにアクセスする方法の、および呼び出し元のコードでの同期を必要としない、それが正しい動作を表示することができます。

いくつかのスレッドセーフな実装があります。

不変

不変(不変)オブジェクトはスレッドセーフでなければならない、あなたはどのようなセキュリティ対策のスレッドを実行する必要はありません。限り不変オブジェクトが正しく出て構成された、あなたは複数のスレッド間で一貫性のない状態でそれを見ることはありません。マルチスレッド環境で、オブジェクトは不変で、スレッドセーフを満たすために作ってみる必要があります。

あなたはタイプを変更することはできません。

  • 基本データ型のキーワード最終変更

  • 列挙型

  • Double値とロング、BigIntegerのBigDecimalのおよび他の大規模なデータ・タイプをパッケージの他のタイプの品番サブクラス、。しかし、クラスの同一の原子とのAtomicInteger AtomicLong数は可変です。

コレクション型の場合、あなたは不変のセットを取得するためにCollections.unmodifiableXXX()メソッドを使用することができます。

パブリッククラスImmutableExample { 
    パブリック静的無効メイン(文字列[] args){ 
        地図<文字列、整数>マップ=新しいHashMapの<>(); 
        地図<文字列、整数> unmodifiableMap = Collections.unmodifiableMap(マップ); 
        unmodifiableMap.put( "A"、1); 
    } 
} 
スレッドの例外"メイン" java.lang.UnsupportedOperationExceptionが
    java.util.Collections $ UnmodifiableMap.put(Collections.java:1457)で
    ImmutableExample.mainで(ImmutableExample.java:9)

Collections.unmodifiableXXX()、元のコレクションの最初のコピーは、収集方法は、直接スローされる変更する必要があります。

パブリックVプット(Kキー、V値){ 
    )(新しいUnsupportedOperationExceptionが投げ。
}

ミューテックス同期

同期和のReentrantLockの。

ノンブロッキング同期

主な問題は、相互に排他的な同期スレッドがブロックされるとウェイクアップは、パフォーマンスの問題によって引き起こされるので、これはまた、同期ブロッキング同期として知られています。

ミューテックス同期悲観的同時方式に属し、彼らは常に、それは確かに問題になり、右同期対策を行うにはないと思います。かどうかは、実際に競争力のあるデータの共有があるだろう、それはロックされなければならない(ここで説明する概念モデルが不要なロックを離れて最適化する仮想機会の大部分、実際には、ある)、ユーザーモードカーネルモードの変換、およびロックカウンタのメンテナンスブロックされたスレッドのチェックはして他の操作をウェイクアップする必要があります。

1. CASE

共有データには、他のスレッドの競合が存在しない場合に行うには、まず、操作が成功したことを、そうでない場合は補償措置をとる(それが成功するまで再試行を続け:ハードウェアの命令セットの開発により、我々は、衝突検出に基づく楽観的同時方式を使用することができます今のところ)。多くは、この楽観的同時方式は、スレッドがブロックされている必要はありません実現するため、この操作は、同期非ブロック同期と呼ばれています。

オプティミスティック・ロックと衝突検出は、これら二つの手順では、ハードウェアのみに頼ることができ、同期を確保するためにミューテックスを使用することはできません原子性を、持って操作する必要があります。アトミック操作のためのハードウェアサポートは、最も典型的なものである:コンペア・アンド・スワップ(比較交換、CAS)。CAS命令は3つのオペランド、すなわちメモリアドレスV、古いものと新しい値BがAの期待値が必要です 動作はVの値が等しい場合にのみ実行されると、それはV Bの値を更新します

2.のAtomicInteger

原子の整数内部メソッドJUCパッケージはCASベースのAtomicInteger安全でない操作クラスと呼ばれています。

次のコードは、操作が行われたのAtomicInteger増分を使用します。

民間のAtomicIntegerのCNT =新しいのAtomicInteger(); 

公共ボイド追加(){ 
    cnt.incrementAndGet()。
}

次のコードは、それがgetAndAddIntの安全でない()を呼び出し、incrementAndGet()ソースコードです。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。

可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。

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;
}

3. ABA

如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。

J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。

无同步方案

要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。

1. 栈封闭

多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。

public class StackClosedExample {
    public void add100() {
        int cnt = 0;
        for (int i = 0; i < 100; i++) {
            cnt++;
        }
        System.out.println(cnt);
    }
}
public static void main(String[] args) {
    StackClosedExample example = new StackClosedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> example.add100());
    executorService.execute(() -> example.add100());
    executorService.shutdown();
}
100
100

2. 线程本地存储(Thread Local Storage)

如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。

符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。

可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。

对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。

public class ThreadLocalExample {
    public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal();
        Thread thread1 = new Thread(() -> {
            threadLocal.set(1);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadLocal.get());
            threadLocal.remove();
        });
        Thread thread2 = new Thread(() -> {
            threadLocal.set(2);
            threadLocal.remove();
        });
        thread1.start();
        thread2.start();
    }
}
1

为了理解 ThreadLocal,先看以下代码:

public class ThreadLocalExample1 {
    public static void main(String[] args) {
        ThreadLocal threadLocal1 = new ThreadLocal();
        ThreadLocal threadLocal2 = new ThreadLocal();
        Thread thread1 = new Thread(() -> {
            threadLocal1.set(1);
            threadLocal2.set(1);
        });
        Thread thread2 = new Thread(() -> {
            threadLocal1.set(2);
            threadLocal2.set(2);
        });
        thread1.start();
        thread2.start();
    }
}

它所对应的底层结构图为:


v2-1e966a8d7506ac3431245252257b1f81_hd.jpg



每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

get() 方法类似。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。

在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。

3. 可重入代码(Reentrant Code)

这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。

可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。


免费Java高级资料需要自己领取,涵盖了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高并发分布式等教程,一共30G。
传送门: mp.weixin.qq.com/s/Jzdd


おすすめ

転載: blog.51cto.com/13672983/2402655