以下是使用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;
以确保使用此方法的单例是否真的是线程安全的?或者它是否真的是线程安全的?如果是这样的话?
答案 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)
这种单身人士的具体保证基本上是这样的:
(这在§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
实际上,这是由于缓存,但语言规范没有说明可能会缓存什么内容(here和here除外,简要提及),它只指定事件的顺序。
关于线程安全的一个额外注意事项:某些操作始终被视为原子操作,例如对象引用的读取和写入(§17.7),因此有一些的情况下使用非volatile
变量可以被认为是线程安全的,但它取决于您具体使用它做什么。仍然可能存在内存不一致,但并发读取和写入无法以某种方式交错,因此您不能以例如某种程度上无效的指针值。因此,有时可以安全地使用非volatile
变量。懒惰初始化字段如果初始化过程可能不止一次发生无关紧要。我知道JDK中至少有一个地方使用了这个地方,在java.lang.reflect.Field
中(也见this comment in the file),但这不是常态。