Java中使用volatile的单例模式

15
class MyClass
{
      private static volatile Resource resource;

      public static Resource getInstance()
      {
            if(resource == null)
                  resource = new Resource();
            return resource;
      }
 }

我的疑问是,根据Java并发实践,如果使用volatile,则会发生安全发布(即一旦引用对另一个线程可见,则数据也可用)。那么我可以在这里使用它吗?但如果是正确的,假设线程1现在检查“resource”,并且它为null,因此它开始创建对象。当线程1正在创建对象时,另一个线程即线程2出现并开始检查“resource”的值,并且线程2将其视为null(假设创建“resource”对象需要相当长的时间,并且由于线程1尚未完成创建,因此安全发布尚未发生,因此不可用于线程2),那么它是否也会开始创建对象?如果是,则类不变量破坏了。我理解正确吗?请帮助我特别理解这里使用volatile的含义。


2
在单例中不应该使用volatile。根据定义,单例内部的私有实例不会改变,这意味着没有线程缓存旧值的危险。但是忽略了您当前使用的非线程安全的实现,因为getInstance未同步。在Java中使用enum来创建单例。 - Brian Roach
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html - happybuddha
1
对于几乎所有的用途,你可能会想要使用初始化表达式,因为类加载是懒加载的。如果你正在使用该类进行其他操作,而不使用此实例(听起来很糟糕),那么一个包含静态内容的嵌套类将很好地完成工作。 - Tom Hawtin - tackline
如果您非常需要使用单例,可以考虑使用这个:http://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom - dnault
9个回答

19
你说得对,多个线程可能会尝试创建一个资源对象。Volatile只是保证如果一个线程更新了引用,所有其他线程将看到新的引用,而不是一些缓存的引用。这样做会慢一些,但更安全。
如果你只需要一个懒加载的单一资源,你需要像这样做:
class MyClass
{
      private static volatile Resource resource;
      private static final Object LOCK = new Object();

      public static Resource getInstance()
      {
            if(resource == null) { 
                synchronized(LOCK) { // Add a synch block
                    if(resource == null) { // verify some other synch block didn't
                                           // write a resource yet...
                        resource = new Resource();
                    }
                }
            }
            return resource;
      }
 }

1
@corsiKa 谢谢您的迅速回复。我可以这样表达吗:“由于volatile只通过符合先行发生规则来解决可见性问题,但在这种情况下,我们需要同时满足可见性和原子性,即检查条件并初始化对象(资源)。因此,需要使用synchronized或静态初始化程序。” 如果我说错了,请指正我。 - Trying
@corsiKa 我只是为了记录而添加的。你的解决方案确实是完全有效的。 - ssedano
4
或者您可以使用枚举并避免所有这些麻烦。https://dev59.com/4HVD5IYBdhLWcg3wJIEK - Brian Roach
@Brian 不完全是这样 - 枚举类型无法有效地处理延迟加载的资源。同步问题仍将存在。 - corsiKa
1
@ssedano 如果您不需要支持的方法(如tryLock或多个条件),那么使用内置锁可能更容易,建议使用synchronized。 - John Vint
显示剩余5条评论

16

volatile 解决了一个问题,那就是可见性问题。如果您写入一个被声明为 volatile 的变量,则该值将立即对其他线程可见。正如我们所知,操作系统中有不同级别的缓存 L1、L2、L3,如果我们在一个线程中写入一个变量,则不能保证其对其他线程可见,因此如果我们使用 volatile,则会直接写入内存并对其他线程可见。但是 volatile 无法解决原子性问题,即 int a; a++; 不安全,因为它与三个机器指令相关联。


7

我知道你并没有询问更好的解决方案,但如果你正在寻找一种懒汉式单例解决方案,这绝对是值得考虑的。

使用一个私有静态类来加载单例。该类在调用时才被加载,因此引用也直到该类被加载后才被加载。按照实现,类加载是线程安全的,而且你也几乎不会遇到额外的开销(如果你正在进行重复的volatile加载[这可能仍然很便宜],此解决方案始终在初始构造之后进行常规加载)。

class MyClass {
    public static Resource getInstance() {
        return ResourceLoader.RESOURCE;
    }

    private static final class ResourceLoader {
        private static final Resource RESOURCE = new Resource();
    }
}

1

那将需要对所有读取进行同步。这是非常不可取的。 - corsiKa

0
应用于字段时,Java的volatile关键字保证:
  1. (在所有版本的Java中)读取和写入volatile变量有全局排序。这意味着每个访问volatile字段的线程将在继续执行之前读取它的当前值,而不是(潜在地)使用缓存的值。(但是,volatile读取和写入与常规读取和写入的相对顺序没有保证,因此它通常不是一个有用的线程构造)

  2. (在Java 5或更高版本中)volatile读取和写入建立了 happens-before 关系,就像获取和释放互斥锁一样。

更多信息


2
但是您没有回答我的问题。 :) - Trying

0

0
首先,这种方式创建单例实际上是在创建一个全局对象,这是一种不好的做法。我建议您使用枚举类型代替。

0

volatile 关键字保证对该变量的读写是原子性的。

根据tutorial所述。

Reads and writes are atomic for all variables declared volatile

使用volatile变量可以降低内存一致性错误的风险,因为对volatile变量的任何写入都会与对该变量后续读取建立happens-before关系。这意味着对volatile变量的更改始终可见于其他线程。更重要的是,这也意味着当线程读取volatile变量时,它不仅可以看到对volatile的最新更改,还可以看到导致更改的代码的副作用。

0

这是我建议将volatile和synchronized一起使用的方法。

注意:我们仍然需要进行双重检查。

public class MySingleton {
    private static volatile MySingleton instance;
    private MySingleton() {}

    synchronized private static void newInstance() {
        if(instance == null) {
            instance = new MySingleton();
        }
    }

    public static MySingleton get() {
        if(instance == null) {
            newInstance();
        }
        return instance;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接