引用何时需要是原子的?

时间:2015-06-25 06:23:10

标签: java multithreading

在Java中以原子方式分配引用意味着什么?

  • 我明白这对于long和double意味着什么,即:一个线程可以看到部分构造的数字,
  • 但对于一个我不明白的对象,因为赋值并不意味着复制只是指向内存中的地址

那么,如果引用赋值在Java中不是原子的,那会是什么错误?

3 个答案:

答案 0 :(得分:13)

这意味着你永远不会得到损坏的引用。假设您有以下类:

class MyClass {
    Object obj = null;
}

在内存obj中是一个空指针,通常它是一个整数,如0x00000000。然后假设在一个线程中你有一个任务:

this.obj = new Object();

假设new Object()在内存中分配,并且指针如0x12345678。参考原子性确保当您从另一个线程检查obj时,您将具有空指针(0x00000000)或指向新对象(0x12345678)的指针。但在任何情况下,您都无法获得部分分配的引用(如0x12340000),这些引用无处可寻。

这可能看起来很明显,但这种问题可能出现在C等低级语言中,具体取决于CPU架构和内存对齐方式。例如,如果指针未对齐并穿过缓存行,则可能无法同步更新。为了避免这种情况,Java虚拟机总是对齐指针,因此它们永远不会跨越缓存行。

Java引用是非原子的,当解除引用从另一个线程写入的引用时,你有可能得到的不是在赋值之前或之后引用的对象,而是随机内存位置(可能导致分段)错误,损坏的堆或任何其他灾难)。

答案 1 :(得分:9)

让我们考虑经典的双重检查锁定示例来理解为什么引用需要是原子的:

class Foo {
    private Helper result;
    public static Helper getHelper() {
        if (result == null) {//1
            synchronized(Foo.class) {//2
               if (result == null) {//3
                    result = new Helper();//4
                }
            }
        }
        return result//5;
    }

    // other functions and members...
}

让我们考虑两个将调用getHelper方法的线程:

  1. Thread-1执行第1行,并查找resultnull
  2. Thread-1获取第2行的类级别锁定
  3. 第1行,第1行,result发现nullHelper
  4. Thread-1开始实例化新的Helper
  5. 当Thread-1仍在第4行实例化新的result时,Thread-2执行第1行。
  6. 步骤4和5是可能出现不一致的地方。有可能在步骤4,对象未完全实例化,但Helper变量已经在其中标记了部分创建的Helper对象的地址。如果Step-5在result对象完全初始化之前执行甚至纳秒,则Thread-2将看到null引用不是result并且可能返回对部分创建的对象的引用。

    解决问题的方法是将volatile标记为AtomicReference或使用Singleton。话虽如此,上述场景在现实世界中极不可能发生,并且有更好的方法来实现AtomicReference而不是使用双重检查锁定。

    Here's使用private static AtomicReference instance = new AtomicReference(); public static AtomicReferenceSingleton getDefault() { AtomicReferenceSingleton ars = instance.get(); if (ars == null) { instance.compareAndSet(null,new AtomicReferenceSingleton()); ars = instance.get(); } return ars; } 实现双重检查锁定的示例:

    <android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_my_toolbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:minHeight="?attr/actionBarSize"
    android:background="?attr/colorPrimary"
    app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
    

    如果您有兴趣知道为什么第5步会导致内存不一致,请查看this回答(在评论中pwes建议)

答案 2 :(得分:5)

我假设你在询问AtomicReference<V>

这个想法是,如果两个或多个线程读取或更新引用类型变量的值,您可能会得到意外的结果。例如,假设每个线程检查某个引用类型变量是否为null,如果它为null,则创建该类型的实例并更新该引用变量。

如果两个线程同时看到变量为null,则可能会导致创建两个实例。如果您的代码依赖于使用该变量引用的同一实例的所有线程,那么您将遇到麻烦。

现在,如果您使用AtomicReference<V>,则可以使用compareAndSet(V expect, V update)方法解决此问题。因此,只有当其他一些线程没有将它击败时,线程才会更新变量。

例如:

static AtomicReference<MyClass> ref = new AtomicReference<> ();

... 
// code of some thread
MyClass obj = ref.get();
if (obj == null) {
    obj = new MyClass();
    if (!ref.compareAndSet (null, obj)) // try to set the atomic reference to a new value
                                        // only if it's still null
        obj = ref.get(); // if some other thread managed to set it before the current thread,
                         // get the instance created by that other thread
}