当前位置:  开发笔记 > 编程语言 > 正文

在Java中实现单例模式的有效方法是什么?

如何解决《在Java中实现单例模式的有效方法是什么?》经验,为你挑选了17个好方法。

在Java中实现单例模式的有效方法是什么?



1> Stephen Denn..:

使用枚举:

public enum Foo {
    INSTANCE;
}

Joshua Bloch 在Google I/O 2008的Effective Java Reloaded演讲中解释了这种方法:链接到视频.另见他演示文稿的幻灯片30-32(effective_java_reloaded.pdf):

实现可序列化单例的正确方法

public enum Elvis {
    INSTANCE;
    private final String[] favoriteSongs =
        { "Hound Dog", "Heartbreak Hotel" };
    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }
}

编辑:"Effective Java"的在线部分说:

"这种方法在功能上等同于公共领域方法,除了它更简洁,免费提供序列化机制,并提供防止多个实例化的铁定保证,即使面对复杂的序列化或反射攻击.虽然这种方法有尚未被广泛采用,单元素枚举类型是实现单例的最佳方式."


我认为人们应该开始将枚举视为一个具有功能的类.如果您可以在编译时列出类的实例,请使用枚举.
我想这很有意义,但我仍然不喜欢它.你会如何创建一个扩展另一个类的单例?如果使用枚举,则不能.
@bvdb:如果你想要很多灵活性,你已经搞砸了首先实现单例.在需要时创建独立实例的能力本身就相当无价.
我个人并不经常发现需要直接使用单例模式.我有时使用spring的依赖注入和应用程序上下文,其中包含它所指的单例.我的实用程序类往往只包含静态方法,我不需要它们的任何实例.
嗨,任何人都可以告诉我如何在测试用例中模拟和测试这种类型的单例.我试图为这种类型交换假单身实例但不能.
我不得不在这里不同意JB,因为他自己*不同意他自己.例如,他说*"不使用接口声明常量"*,并给出*"常量使用不是接口的原因,它产生语义不匹配和噪音,并获得语法糖(不需要使用`静态最终`等)不值得"*.他在EJ2中几次使用类似的论点......通过类比,人们会说*"不使用枚举来声明单例类;单例不是"枚举" - 它会产生语义不匹配和噪音,以及语法糖获得的不值得"*
@Eric您可以创建构造函数,也可以使用初始化块.
从集合论的角度来看,单例的枚举也是正确的。类是无限可变的运行时定义的对象集。枚举是一组有限的,不可变的,编译时定义的对象。单例是一组有限的,不可变的,编译时定义的对象。因此,对单例使用一个枚举。另外,对实用程序类使用枚举。这是@AmirArad所说的更正式的解释。

2> Roel Spilker..:

根据用途,有几个"正确"的答案.

从java5开始,最好的方法是使用枚举:

public enum Foo {
   INSTANCE;
}

前java5,最简单的情况是:

public final class Foo {

    private static final Foo INSTANCE = new Foo();

    private Foo() {
        if (INSTANCE != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    public static Foo getInstance() {
        return INSTANCE;
    }

    public Object clone() throws CloneNotSupportedException{
        throw new CloneNotSupportedException("Cannot clone instance of this class");
    }
}

我们来看看代码吧.首先,你希望课程是最终的.在这种情况下,我使用了final关键字让用户知道它是最终的.然后,您需要将构造函数设置为私有,以防止用户创建自己的Foo.从构造函数中抛出异常会阻止用户使用反射来创建第二个Foo.然后创建一个private static final Foo字段来保存唯一的实例,并创建一个public static Foo getInstance()返回它的方法.Java规范确保仅在首次使用类时调用构造函数.

当你有一个非常大的对象或繁重的构造代码并且还有其他可访问的静态方法或字段可能在需要实例之前使用时,那么你只需要使用延迟初始化.

您可以使用a private static class来加载实例.然后代码看起来像:

public final class Foo {

    private static class FooLoader {
        private static final Foo INSTANCE = new Foo();
    }

    private Foo() {
        if (FooLoader.INSTANCE != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    public static Foo getInstance() {
        return FooLoader.INSTANCE;
    }
}

由于该行private static final Foo INSTANCE = new Foo();仅在实际使用类FooLoader时执行,因此它负责延迟实例化,并且保证是线程安全的.

当您还希望能够序列化对象时,需要确保反序列化不会创建副本.

public final class Foo implements Serializable {

    private static final long serialVersionUID = 1L;

    private static class FooLoader {
        private static final Foo INSTANCE = new Foo();
    }

    private Foo() {
        if (FooLoader.INSTANCE != null) {
            throw new IllegalStateException("Already instantiated");
        }
    }

    public static Foo getInstance() {
        return FooLoader.INSTANCE;
    }

    @SuppressWarnings("unused")
    private Foo readResolve() {
        return FooLoader.INSTANCE;
    }
}

该方法readResolve()将确保将返回唯一的实例,即使该对象在上一次运行的程序中被序列化也是如此.


反射的检查是没用的.如果其他代码使用私有的反射,那就是Game Over.没有理由在这种滥用情况下尝试正常运行.如果你尝试,它将是一个不完整的"保护"反正,只是浪费了很多代码.
-1这绝对不是*最简单的情况,它的设计和不必要的复杂.看看Jonathan对99.9%所有案例中实际上最简单的解决方案的答案.
>"首先,你希望课程成为最终的".有人可以详细说明吗?
反序列化保护完全被破坏(我认为这在Effective Java 2nd Ed中提到).
当你的单例需要从超类继承时,这很有用.在这种情况下,您不能使用枚举单例模式,因为枚举不能有超类(但它们可以实现接口).例如,当枚举单例模式不是一个选项时,Google Guava会使用静态最终字段:http://code.google.com/p/guava-libraries/source/browse/trunk/guava/src/com/google/通用/收集/ EmptyImmutableList.java?规格= svn486&R = 438

3> xyz..:

免责声明:我刚刚总结了所有令人敬畏的答案,并用我的话写下来.


在实施Singleton时,我们有2个选项
1.延迟加载
2.早期加载

延迟加载会增加一些开销(很多是诚实的),所以只有当你有一个非常大的对象或繁重的构造代码并且还有其他可访问的静态方法或字段可能在需要实例之前使用时才使用它,然后才会你需要使用延迟初始化.否则选择早期加载是一个不错的选择.

实现Singleton最简单的方法是

public class Foo {

    // It will be our sole hero
    private static final Foo INSTANCE = new Foo();

    private Foo() {
        if (INSTANCE != null) {
            // SHOUT
            throw new IllegalStateException("Already instantiated");
        }
    }

    public static Foo getInstance() {
        return INSTANCE;
    }
}

除了早期装载的单身人士外,一切都很好.让我们尝试延迟加载单身

class Foo {

    // Our now_null_but_going_to_be sole hero 
    private static Foo INSTANCE = null;

    private Foo() {
        if (INSTANCE != null) {
            // SHOUT  
            throw new IllegalStateException("Already instantiated");
        }
    }

    public static Foo getInstance() {
        // Creating only  when required.
        if (INSTANCE == null) {
            INSTANCE = new Foo();
        }
        return INSTANCE;
    }
}

到目前为止一切都那么好但我们的英雄无法生存,而单独与多个邪恶的线程进行战斗,他们需要我们英雄的许多实例.所以我们要保护它免受邪恶的多线程攻击

class Foo {

    private static Foo INSTANCE = null;

    // TODO Add private shouting constructor

    public static Foo getInstance() {
        // No more tension of threads
        synchronized (Foo.class) {
            if (INSTANCE == null) {
                INSTANCE = new Foo();
            }
        }
        return INSTANCE;
    }
}

但它不足以保护英雄,真的!这是我们能够/应该做的最好的帮助我们的英雄

class Foo {

    // Pay attention to volatile
    private static volatile Foo INSTANCE = null;

    // TODO Add private shouting constructor

    public static Foo getInstance() {
        if (INSTANCE == null) { // Check 1
            synchronized (Foo.class) {
                if (INSTANCE == null) { // Check 2
                    INSTANCE = new Foo();
                }
            }
        }
        return INSTANCE;
    }
}

这被称为"双重锁定成语".很容易忘记易变的陈述,很难理解为什么有必要.
有关详细信息:http: //www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

现在我们确定邪恶的线索,但残酷的序列化怎么样?我们必须确保即使在de-serialiaztion中也没有创建新对象

class Foo implements Serializable {

    private static final long serialVersionUID = 1L;

    private static volatile Foo INSTANCE = null;

    // Rest of the things are same as above

    // No more fear of serialization
    @SuppressWarnings("unused")
    private Object readResolve() {
        return INSTANCE;
    }
}

该方法readResolve()将确保将返回唯一的实例,即使该对象在我们的程序的上一次运行中被序列化也是如此.

最后,我们为线程和序列化添加了足够的保护,但我们的代码看起来很庞大和丑陋.让我们的英雄弥补

public final class Foo implements Serializable {

    private static final long serialVersionUID = 1L;

    // Wrapped in a inner static class so that loaded only when required
    private static class FooLoader {

        // And no more fear of threads
        private static final Foo INSTANCE = new Foo();
    }

    // TODO add private shouting construcor

    public static Foo getInstance() {
        return FooLoader.INSTANCE;
    }

    // Damn you serialization
    @SuppressWarnings("unused")
    private Foo readResolve() {
        return FooLoader.INSTANCE;
    }
}

是的,这是我们非常相同的英雄:)
因为该行private static final Foo INSTANCE = new Foo();仅在FooLoader实际使用该类时执行,所以这将负责惰性实例化,

并保证是线程安全的.

我们到目前为止,这是实现我们所做的一切最好的方式

 public enum Foo {
       INSTANCE;
   }

哪个内部将被视为

public class Foo {

    // It will be our sole hero
    private static final Foo INSTANCE = new Foo();
}

这就是不再担心序列化,线程和丑陋的代码.此外枚举单被延迟初始化.

这种方法在功能上等同于公共字段方法,除了它更简洁,免费提供序列化机制,并提供防止多实例化的铁定保证,即使面对复杂的序列化或反射攻击.虽然这种方法尚未被广泛采用,但单元素枚举类型是实现单例的最佳方法.

-Joshua Bloch in"Effective Java"

现在您可能已经意识到为什么ENUMS被认为是实施Singleton的最佳方式,感谢您的耐心:)
在我的博客上更新了它.


只是澄清一下:使用枚举实现的单例是懒惰地初始化.详情请访问:http://stackoverflow.com/questions/16771373/singleton-via-enum-way-is-lazy-initialized
很好的答案.最后一件事,重写克隆方法抛出异常.
@xyz很好的解释,我非常喜欢和学习很容易,我希望永远不会忘记这一点

4> Benno Richte..:

Stu Thompson发布的解决方案在Java5.0及更高版本中有效.但我宁愿不使用它因为我认为它容易出错.

很容易忘记易变的陈述,很难理解为什么有必要.没有volatile,由于双重检查锁定反模式,此代码将不再是线程安全的.请参阅Java Concurrency in Practice第16.2.4节中的更多相关内容.简而言之:这种模式(在Java5.0之前或没有volatile语句之前)可以返回对(仍然)处于错误状态的Bar对象的引用.

这种模式是为了性能优化而发明的.但这真的不再是一个真正的问题了.以下延迟初始化代码快速且更重要 - 更易于阅读.

class Bar {
    private static class BarHolder {
        public static Bar bar = new Bar();
    }

    public static Bar getBar() {
        return BarHolder.bar;
    }
}


@Bno:如何将构造函数设为私有?
@Stu第一版Effective Java(版权2001)在第48项下详述了这种模式.
@ AlikElzin-kilaka不完全是。该实例在BarHolder *的类加载阶段创建,该阶段被延迟到第一次需要它时。Bar的构造函数可以随您所需而变,但是要等到第一个`getBar()`才会被调用。(如果将getBar称为“为时过早”,那么无论如何实现单例都会遇到相同的问题。)您可以在此处看到上面代码的惰性类加载:http://pastebin.com/iq2eayiR

5> Stu Thompson..:

Java 5+中的线程安全:

class Foo {
    private static volatile Bar bar = null;
    public static Bar getBar() {
        if (bar == null) {
            synchronized(Foo.class) {
                if (bar == null)
                    bar = new Bar(); 
            }
        }
        return bar;
    }
}

编辑:注意volatile这里的修饰符.:)这很重要,因为没有它,JMM(Java内存模型)无法保证其他线程看到其值的更改.同步不会解决这个问题 - 它只序列化对该代码块的访问.

编辑2:@Bno的回答详细介绍了Bill Pugh(FindBugs)推荐的方法,并且可以说是更好的.去阅读并投票他的答案.


我认为提及反射攻击很重要.确实,大多数开发人员都不需要担心,但似乎像这样的例子(基于Enum的单例)应该包括防止多实例化攻击的代码,或者只是放置一个表明这种可能性的免责声明.
这里不需要易失性关键字 - 因为同步提供了互斥和内存可见性.
为什么要在Java 5+中为所有这些烦恼呢?我的理解是,枚举方法同时提供了线程安全性和延迟初始化。它也要简单得多。此外,如果您想避免枚举,我仍然会阻止嵌套的静态类方法。

6> Jonathan..:

忘记延迟初始化,这太有问题了.这是最简单的解决方案:

public class A {    

    private static final A INSTANCE = new A();

    private A() {}

    public static A getInstance() {
        return INSTANCE;
    }
}


单例实例变量也可以是最终的.例如,private static final A singleton = new A();
这实际上是惰性初始化,因为在加载类之前不会实例化静态单例,并且在需要之前不会加载类(这将是您第一次引用getInstance()方法的时候).
如果在希望实例化静态之前,类A确实被加载了,则可以将静态包装在静态内部类中以解除类初始化.
这种方法有一个限制:构造函数不能抛出异常.
我同意这个答案是最简单的,Anirudhan,没有必要声明实例最终.在静态成员初始化时,没有其他线程可以访问该类.这是由编译器保证的,换句话说,所有静态初始化都是以同步方式完成的 - 只有一个线程.

7> 小智..:

确保你真的需要它.做谷歌的"单身反模式"看一些反对它的论点.我想它没有什么本质上的错误,但它只是一种暴露一些全球资源/数据的机制,所以要确保这是最好的方法.特别是我发现依赖注入更有用,特别是如果你也使用单元测试,因为DI允许你使用模拟资源进行测试.



8> Andrew Swan..:

我对一些答案感到困惑,这些答案表明DI可以替代使用单身人士; 这些是不相关的概念.您可以使用DI注入单例或非单例(例如每线程)实例.如果你使用Spring 2.x,至少会这样,我不能代表其他DI框架.

所以我对OP的回答是(除了最简单的示例代码之外的所有代码):

    然后使用像Spring这样的DI框架

    使其成为DI配置的一部分,无论您的依赖项是单例,请求作用域,会话作用域还是其他.

这种方法为您提供了一个很好的解耦(因此也是灵活且可测试的)架构,其中是否使用单例是一个易于反转的实现细节(假设您使用的任何单例都是线程安全的).


也许是因为人们不同意你的观点.我没有投票给你,但我不同意:我认为DI可以用来解决单身人士的同样问题.这是基于将"singleton"理解为"具有单个实例的对象,该对象直接由全局名称访问",而不仅仅是"具有单个实例的对象",这可能有点棘手.
要稍微扩展一下,可以考虑一个需要有一个全局实例的`TicketNumberer`,以及你想写一个类`TicketIssuer`的类,它包含一行代码`int ticketNumber = ticketNumberer.nextTicketNumber();`.在传统的单例思维中,前一行代码必须是"TicketNumberer ticketNumberer = TicketNumberer.INSTANCE;".在DI思考中,类将有一个构造函数,如`public TicketIssuer(TicketNumberer ticketNumberer){this.ticketNumberer = ticketNumberer; }`.
调用该构造函数会成为别人的问题.DI框架可以使用某种全球地图来实现; 一个手工构建的DI架构会这样做,因为app的`main`方法(或其中一个minions)会创建依赖关系,然后调用构造函数.从本质上讲,使用全局变量(或全局方法)只是可怕的[服务定位器模式]的简单形式(http://java.sun.com/blueprints/corej2eepatterns/Patterns/ServiceLocator.html),并且可以用依赖注入替换,就像使用该模式一样.

9> 小智..:

在写之前真的要考虑为什么你需要一个单身人士.关于使用它们存在准宗教的争论,如果你使用Java中的google singletons,你很容易就会发现它们.

我个人试图尽可能多地避免单身,原因很多,其中大部分都可以通过谷歌搜索单身人士来找到.我觉得单身人士经常被滥用,因为他们很容易被所有人理解,他们被用作将"全局"数据转化为OO设计的机制,因为它很容易绕过对象生命周期管理(或者真的在考虑如何从B内部做一个A).查看控制反转(IoC)或依赖注入(DI)等内容,以获得良好的中间地带.

如果你真的需要一个,那么维基百科有一个很好的例子来正确实现单例.



10> Abhijit Gaik..:

以下是3种不同的方法

1)Enum

/**
* Singleton pattern example using Java Enumj
*/
public enum EasySingleton{
    INSTANCE;
}

2)双重检查锁定/延迟加载

/**
* Singleton pattern example with Double checked Locking
*/
public class DoubleCheckedLockingSingleton{
     private static volatile DoubleCheckedLockingSingleton INSTANCE;

     private DoubleCheckedLockingSingleton(){}

     public static DoubleCheckedLockingSingleton getInstance(){
         if(INSTANCE == null){
            synchronized(DoubleCheckedLockingSingleton.class){
                //double checking Singleton instance
                if(INSTANCE == null){
                    INSTANCE = new DoubleCheckedLockingSingleton();
                }
            }
         }
         return INSTANCE;
     }
}

3)静态工厂方法

/**
* Singleton pattern example with static factory method
*/

public class Singleton{
    //initailzed during class loading
    private static final Singleton INSTANCE = new Singleton();

    //to prevent creating another instance of Singleton
    private Singleton(){}

    public static Singleton getSingleton(){
        return INSTANCE;
    }
}



11> Matt..:

我使用Spring Framework来管理我的单身人士.它不强制实现类的"单例"(如果涉及多个类加载器,则无论如何都无法实现),但它提供了一种非常简单的方法来构建和配置不同的工厂来创建不同类型的对象.



12> coderz..:

版本1:

public class MySingleton {
    private static MySingleton instance = null;
    private MySingleton() {}
    public static synchronized MySingleton getInstance() {
        if(instance == null) {
            instance = new MySingleton();
        }
        return instance;
    }
}

延迟加载,线程安全,阻塞,低性能因为synchronized.

版本2:

public class MySingleton {
    private MySingleton() {}
    private static class MySingletonHolder {
        public final static MySingleton instance = new MySingleton();
    }
    public static MySingleton getInstance() {
        return MySingletonHolder.instance;
    }
}

延迟加载,线程安全,无阻塞,高性能.



13> macbirdie..:

维基百科有一些单身人士的例子,也有Java.Java 5实现看起来非常完整,并且是线程安全的(应用了双重检查锁定).



14> Aleksi Yrtti..:

如果您不需要延迟加载,那么只需尝试

public class Singleton {
    private final static Singleton INSTANCE = new Singleton();

    private Singleton() {}

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

    protected Object clone() {
        throw new CloneNotSupportedException();
    }
}

如果您想要延迟加载并且希望Singleton是线程安全的,请尝试使用双重检查模式

public class Singleton {
        private static Singleton instance = null;

        private Singleton() {}

        public static Singleton getInstance() { 
              if(null == instance) {
                  synchronized(Singleton.class) {
                      if(null == instance) {
                          instance = new Singleton();
                      }
                  }
               }
               return instance;
        }

        protected Object clone() {
            throw new CloneNotSupportedException();
        }
}

由于双重检查模式不能保证工作(由于编译器的一些问题,我不知道更多.),您还可以尝试同步整个getInstance方法或为您的所有单身人士创建一个注册表.


你还需要使你的单例变量`volatile`
第一个版本是最好的.假设类除了提供单例之外什么都不做,那么由于延迟类加载,它通常将在与第二个版本中的点相同的点处实例化.

15> NullPoiиteя..:

我会说Enum singleton

在Java中使用枚举的单例通常是声明枚举单例的方法.枚举单例可以包含实例变量和实例方法.为简单起见,还要注意,如果您使用任何实例方法而不是您需要确保该方法的线程安全性,如果它完全影响对象的状态.

枚举的使用非常容易实现,并且对于可序列化对象没有缺点,这些对象必须以其他方式规避.

/**
* Singleton pattern example using Java Enum
*/
public enum Singleton {
        INSTANCE;
        public void execute (String arg) {
                //perform operation here
        }
}

您可以通过Singleton.INSTANCEgetInstance()在Singleton上调用方法更容易访问它.

1.12枚举常量的序列化

枚举常量的序列化与普通的可序列化或可外部化的对象不同.枚举常量的序列化形式仅由其名称组成; 常量的字段值不存在于表单中.要序列化枚举常量,请ObjectOutputStream写入枚举常量名称方法返回的值.要反序列化枚举常量,请ObjectInputStream从流中读取常量名称; 然后通过调用java.lang.Enum.valueOf方法获得反序列化常量,将常量的枚举类型与接收到的常量名称一起作为参数传递.与其他可序列化或可外部化的对象一样,枚举常量可以作为随后出现在序列化流中的反向引用的目标.

通过枚举常数被序列不能被定制的方法:任何类特定的writeObject,readObject,readObjectNoData,writeReplace,和readResolve由枚举类型定义的方法的序列化和反序列化期间被忽略.类似地,任何serialPersistentFieldsserialVersionUID字段声明也被忽略 - 所有枚举类型都有固定serialVersionUID0L.记录枚举类型的可序列化字段和数据是不必要的,因为发送的数据类型没有变化.

引自Oracle文档

传统单例的另一个问题是,一旦实现了Serializable接口,它们就不再是Singleton,因为readObject()方法总是在Java中返回一个像构造函数这样的新实例.这可以通过使用readResolve()如下替换单例来使用和丢弃新创建的实例来避免

 // readResolve to prevent another instance of Singleton
 private Object readResolve(){
     return INSTANCE;
 }

如果您的Singleton类维护状态,这可能变得更加复杂,因为您需要使它们成为瞬态,但在Enum Singleton中,JVM保证了序列化.


好读

    单身模式

    枚举,单身和反序列化

    双重检查锁定和Singleton模式



16> Michael Andr..:

在这个问题上可能会有点晚了,但是在实施单身人士方面存在很多细微差别.在许多情况下不能使用支架图案.和IMO在使用volatile时 - 你也应该使用局部变量.让我们从头开始并迭代问题.你会明白我的意思.


第一次尝试可能看起来像这样:

public class MySingleton {

     private static MySingleton INSTANCE;

     public static MySingleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new MySingleton();
        }

        return INSTANCE;
    }
    ...
}

这里我们有MySingleton类,它有一个名为INSTANCE的私有静态成员,以及一个名为getInstance()的公共静态方法.第一次调用getInstance()时,INSTANCE成员为null.然后,流将进入创建条件并创建MySingleton类的新实例.对getInstance()的后续调用将发现已经设置了INSTANCE变量,因此不会创建另一个MySingleton实例.这确保了只有一个MySingleton实例在getInstance()的所有调用者之间共享.

但是这个实现有一个问题.多线程应用程序将在创建单个实例时具有竞争条件.如果多个执行线程同时(或大约)同时命中getInstance()方法,它们将各自看到INSTANCE成员为null.这将导致每个线程创建一个新的MySingleton实例,然后设置INSTANCE成员.


private static MySingleton INSTANCE;

public static synchronized MySingleton getInstance() {
    if (INSTANCE == null) {
        INSTANCE = new MySingleton();
    }

    return INSTANCE;
}

这里我们使用方法签名中的synchronized关键字来同步getInstance()方法.这肯定会解决我们的竞争状况.线程现在将阻止并一次输入一个方法.但它也会产生性能问题.此实现不仅同步单个实例的创建,还将所有调用同步到getInstance(),包括读取.读取不需要同步,因为它们只返回INSTANCE的值.由于读取将构成我们调用的大部分(请记住,实例化仅在第一次调用时发生),我们将通过同步整个方法而导致不必要的性能损失.


private static MySingleton INSTANCE;

public static MySingleton getInstance() {
    if (INSTANCE == null) {
        synchronize(MySingleton.class) {
            INSTANCE = new MySingleton();
        }
    }

    return INSTANCE;
}

在这里,我们将同步从方法签名移动到包装MySingleton实例创建的同步块.但这能解决我们的问题吗?好吧,我们不再阻止阅读,但我们也向前退了一步.多个线程将同时或大约同时命中getInstance()方法,并且它们都将INSTANCE成员视为null.然后,他们将点击同步块,其中一个将获得锁并创建实例.当该线程退出该块时,其他线程将争用该锁,并且每个线程将逐个通过该块并创建该类的新实例.所以我们回到了我们开始的地方.


private static MySingleton INSTANCE;

public static MySingleton getInstance() {
    if (INSTANCE == null) {
        synchronized(MySingleton.class) {
            if (INSTANCE == null) {
                INSTANCE = createInstance();
            }
        }
    }

    return INSTANCE;
}

在这里,我们从INSIDE块发出另一张支票.如果已经设置了INSTANCE成员,我们将跳过初始化.这称为双重检查锁定.

这解决了我们多实例化的问题.但是,我们的解决方案又一次提出了另一项挑战.其他线程可能不会"看到"INSTANCE成员已更新.这是因为Java优化了内存操作.线程将变量的原始值从主存储器复制到CPU的缓存中.然后,将对值的更改写入该缓存并从中读取.这是Java的一项功能,旨在优化性能.但这给我们的单例实现带来了问题.第二个线程 - 由不同的CPU或核心使用不同的缓存处理 - 将不会看到第一个线程所做的更改.这将导致第二个线程将INSTANCE成员视为null,从而强制创建我们的单例的新实例.


private static volatile MySingleton INSTANCE;

public static MySingleton getInstance() {
    if (INSTANCE == null) {
        synchronized(MySingleton.class) {
            if (INSTANCE == null) {
                INSTANCE = createInstance();
            }
        }
    }

    return INSTANCE;
}

我们通过在INSTANCE成员的声明中使用volatile关键字来解决这个问题.这将告诉编译器始终读取和写入主内存,而不是CPU缓存.

但这种简单的改变需要付出代价.因为我们绕过CPU缓存,所以每次操作易失性INSTANCE成员时我们都会受到性能影响 - 我们会这样做4次.我们仔细检查存在(1和2),设置值(3),然后返回值(4).有人可能认为这条路径是边缘情况,因为我们只在第一次调用方法时创建实例.也许创作的表现可以容忍.但即使是我们的主要用例read也会对volatile组件进行两次操作.一旦检查存在,再次返回其值.


private static volatile MySingleton INSTANCE;

public static MySingleton getInstance() {
    MySingleton result = INSTANCE;
    if (result == null) {
        synchronized(MySingleton.class) {
            result = INSTANCE;
            if (result == null) {
                INSTANCE = result = createInstance();
            }
        }
    }

    return result;
}

由于性能影响是由于直接在volatile成员上运行,因此让我们将局部变量设置为volatile的值,而不是对局部变量进行操作.这将减少我们对易失性操作的次数,从而回收我们失去的一些性能.请注意,当我们进入synchronized块时,我们必须再次设置本地变量.这可确保它在我们等待锁定时发生的任何更改都是最新的.

我最近写了一篇关于此的文章.解构单身人士.您可以在这些示例中找到更多信息,并在那里找到"持有者"模式的示例.还有一个真实的例子展示了双重检查的volatile方法.希望这可以帮助.



17> Dheeraj Sach..:
There are 4 ways to create a singleton in java.

1- eager initialization singleton

    public class Test{
        private static final Test test = new Test();
        private Test(){}
        public static Test getTest(){
            return test;
        }
    ?

2- lazy initialization singleton (thread safe)

    public class Test {
         private static volatile Test test;
         private Test(){}
         public static Test getTest() {
            if(test == null) {
                synchronized(Test.class) {
                    if(test == null){test = new Test();
                }
            }
         }

        return test;
    ?


3- Bill Pugh Singleton with Holder Pattern (Preferably the best one)

    public class Test {

        private Test(){}

        private static class TestHolder{
            private static final Test test = new Test();
        }

        public static Test getInstance(){
            return TestHolder.test;
        }
    }

4- enum singleton
      public enum MySingleton {
        INSTANCE;
    private MySingleton() {
        System.out.println("Here");
    }
}

推荐阅读
个性2402852463
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有