在Singleton中使用挥发性物质(Bill Pughs Solution)

时间:2018-03-17 16:35:41

标签: java multithreading singleton volatile

以下是使用Bill Pugh单例解决方案的java类。

public class Singleton {

    int nonVolatileVariable;

    private static class SingletonHelper {
        private static Singleton INSTANCE = new Singleton();
    }

    private Singleton() { }

    public static Singleton getInstance() {
        return SingletonHelper.INSTANCE;
    }

    public int getNonVolatileVariable() {
        return nonVolatileVariable;
    }

    public void setNonVolatileVariable(int nonVolatileVariable) {
        this.nonVolatileVariable= nonVolatileVariable;
    }
}

我在很多地方都读过这种方法是线程安全的。因此,如果我理解正确,那么单例实例只创建一次,并且访问getInstance方法的所有线程将接收类Singleton的相同实例。但是我想知道线程是否可以在本地缓存获得的单例对象。他们可以吗?如果是,则不会意味着每个线程都可以将实例字段nonVolatileVariable更改为可能会产生问题的不同值。

我知道还有其他单例创建方法,例如enum singleton,但我对这个单例创建方法特别感兴趣。

所以我的问题是,是否需要使用volatile关键字 int volatile nonVolatileVariable;以确保使用此方法的单例是否真的是线程安全的?或者它是否真的是线程安全的?如果是这样的话?

2 个答案:

答案 0 :(得分:2)

  

所以我的问题是,是否需要使用volatile关键字   比如int volatile nonVolatileVariable;确保   单身使用这种方法真的是线程安全吗?或者它已经存在了   真的线程安全吗?如果是这样的话?

单例模式确保创建类的单个实例。它并不能确保字段和方法是线程安全的,并且易失性也不能确保它。

  

但是我想知道线程是否可以在本地缓存获得的   单身对象?

根据Java中的内存模型,是的,他们可以。

  

如果是,那么这并不意味着每个线程都可以改变   实例字段nonVolatileVariable为可能的不同值   制造问题。

确实但你仍然存在与volatile变量一致的问题,因为volatile处理内存可见性问题,但它没有处理线程之间的同步。

尝试使用以下代码,其中多个线程会增加易变int 100次 您将看到每次都无法获得100作为结果。

import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Singleton {

    volatile int  volatileInt;

    private static class SingletonHelper {
        private static Singleton INSTANCE = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonHelper.INSTANCE;
    }

    public int getVolatileInt() {
        return volatileInt;
    }

    public void setVolatileInt(int volatileInt ) {
        this.volatileInt = volatileInt ;
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        List<Callable<Void>> callables = IntStream.range(0, 100)
                                                  .mapToObj(i -> {
                                                      Callable<Void> callable = () -> {
                                                          Singleton.getInstance().setVolatileInt(Singleton.getInstance().getVolatileInt()+1);
                                                          return null;
                                                      };
                                                      return callable;
                                                  })
                                                  .collect(Collectors.toList());

        executorService.invokeAll(callables);

        System.out.println(Singleton.getInstance().getVolatileInt());
    }

}

为了确保每个线程都考虑其他调用,您必须使用外部同步,在这种情况下,不需要变量volatile。

例如:

synchronized (Singleton.getInstance()) {
  Singleton.getInstance()
           .setVolatileInt(Singleton.getInstance().getVolatileInt() + 1);
}

在这种情况下,不再需要volatile。

答案 1 :(得分:1)

这种单身人士的具体保证基本上是这样的:

  1. 每个类都有一个用于类初始化的唯一锁。
  2. 任何可能导致类初始化的操作(例如访问静态方法)都需要先获取此锁,检查是否需要初始化类,如果是,则初始化它。
  3. 如果JVM可以确定该类已经初始化并且当前线程可以看到该效果,那么它可以跳过第2步,包括跳过获取锁。
  4. (这在§12.4.2中有记录。)

    换句话说,这里保证的是所有线程必须至少看到private static Singleton INSTANCE = new Singleton();中赋值的影响,以及SingletonHelper类的静态初始化期间执行的任何其他操作。

    虽然语言规范不是根据缓存编写的,但您对非volatile变量的并发读取和写入在线程之间可能不一致的分析是正确的。编写语言规范的方式是读取和写入可能无序出现。例如,假设按时间顺序列出以下事件序列:

    nonVolatileVariable is 0
    ThreadA sets nonVolatileVariable to 1
    ThreadB reads nonVolatileVariable (what value should it see?)
    

    语言规范允许ThreadB在读取nonVolatileVariable时看到值0,就好像事件按以下顺序发生一样:

    nonVolatileVariable is 0
    ThreadB reads nonVolatileVariable (and sees 0)
    ThreadA sets nonVolatileVariable to 1
    

    实际上,这是由于缓存,但语言规范没有说明可能会缓存什么内容(herehere除外,简要提及),它只指定事件的顺序。

    关于线程安全的一个额外注意事项:某些操作始终被视为原子操作,例如对象引用的读取和写入(§17.7),因此有一些的情况下使用非volatile变量可以被认为是线程安全的,但它取决于您具体使用它做什么。仍然可能存在内存不一致,但并发读取和写入无法以某种方式交错,因此您不能以例如某种程度上无效的指针值。因此,有时可以安全地使用非volatile变量。懒惰初始化字段如果初始化过程可能不止一次发生无关紧要。我知道JDK中至少有一个地方使用了这个地方,在java.lang.reflect.Field中(也见this comment in the file),但这不是常态。