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


812

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


1
“在Java中实现单例模式的有效方法是什么?” 请定义有效的。
玛丽安(MarianPaździoch)'16年

medium.com/@kevalpatel2106/…。这是有关如何在单例模式中实现线程,反射和序列化安全性的完整文章。这是了解单例类的优点和局限性的好资料。
Keval Patel

正如Joshua Bloch在“有效Java”中指出的那样,枚举单例是最好的方法。在这里,我将各种实现方式归类为“懒惰/渴望”等
。– isaolmez

Answers:


782

使用一个枚举:

public enum Foo {
    INSTANCE;
}

约书亚·布洛赫(Joshua Bloch)在Google I / O 2008上的“ 有效的Java重新加载”演讲中解释了这种方法:链接到视频。另见幻灯片他的介绍(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));
    }
}

编辑:“有效Java”在线部分说:

“该方法在功能上等同于公共领域方法,除了它更简洁,免费提供序列化机制,甚至在面对复杂的序列化或反射攻击时也提供了针对多重实例化的坚定保证。尚未被广泛采用的单元素枚举类型是实现单例的最佳方法。”


202
我认为人们应该开始将枚举视为具有功能的类。如果您可以在编译时列出类的实例,请使用枚举。
阿米尔·阿拉德

7
我个人并不经常需要直接使用单例模式。有时,我会将spring的依赖项注入与包含其称为单例的应用程序上下文一起使用。我的实用程序类往往只包含静态方法,而我不需要它们的任何实例。
Stephen Denne

3
嗨,有人可以告诉我如何在测试案例中模拟和测试这种单例。我尝试将这种类型的伪单例实例交换掉,但是不能。
Ashish Sharma

29
我想这很有道理,但我仍然不喜欢它。您将如何创建扩展另一个类的单例?如果使用枚举,则不能。
chharvey 2011年

11
@bvdb:如果您想获得很大的灵活性,那么您首先要实现单例已经搞砸了。在需要时创建独立实例的能力本身是无价的。
cHao

233

根据用法,有几个“正确”的答案。

由于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规范确保仅在首次使用该类时才调用构造函数。

如果您有一个很大的对象或繁重的构造代码,并且还需要在需要实例之前使用其他可访问的静态方法或字段,则仅在那时才需要使用惰性初始化。

您可以使用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()将确保将返回唯一的实例,即使该对象在程序的先前运行中已被序列化也是如此。


32
进行反射检查是没有用的。如果其他代码在私有对象上使用反射,则为Game Over。在这种滥用情况下,甚至没有理由尝试正常运行。而且,如果您尝试这样做,那么无论如何它都是不完整的“保护”,只会浪费很多代码。
Wouter Coekaerts 09年

5
>“首先,您希望课程是最终的”。有人可以详细说明吗?
PlagueHammer

2
反序列化保护已被完全破坏(我认为在Java有效第二版中已经提到过)。
Tom Hawtin-大头钉

9
-1这绝对不是最简单的情况,它是人为的并且不必要地复杂。看看乔纳森(Jonathan)的答案,它实际上是最简单的解决方案,足以解决所有案例的99.9%。
Michael Borgwardt 2009年

2
当您的单例需要从超类继承时,这很有用。在这种情况下,您不能使用枚举单例模式,因为枚举不能具有超类(尽管它们可以实现接口)。例如,谷歌番石榴使用静态最终场当枚举单件模式是不是一种选择:code.google.com/p/guava-libraries/source/browse/trunk/guava/src/...
艾蒂安·奈芙

139

免责声明:我刚刚总结了所有很棒的答案,并用我的话写下来。


在实施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

现在我们可以确定邪恶线程,但是残酷的序列化又如何呢?我们必须确保即使反序列化也不会创建新对象

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();
}

而已!不再担心序列化,线程和丑陋的代码。此外枚举单被延迟初始化

这种方法在功能上与公共领域方法等效,除了它更简洁,免费提供序列化机制,甚至针对复杂的序列化或反射攻击,还提供了针对多重实例化的明确保证。尽管此方法尚未广泛采用,但单元素枚举类型是实现单例的最佳方法。

-“有效Java”中的约书亚·布洛赫(Joshua Bloch)

现在您可能已经意识到为什么将ENUMS视为实现Singleton的最佳方法了,并感谢您的耐心:)
在我的博客上进行了更新。


3
只是澄清一下:使用枚举实现的单例被延迟初始化。详情点击这里:stackoverflow.com/questions/16771373/...

2
好答案。最后一件事,重写克隆方法以引发异常。
riship89

2
@xyz很好的解释,我非常喜欢并且很轻松地学习,我希望永远不要忘记这一点
Premraj 2015年

1
我对stackoverflow曾经有过最好的回答之一。谢谢!
Shay Tsadok '17

1
存在使用枚举作为一个单序列化问题:任何构件字段值序列化,因此不会恢复。请参阅Java Object Serialization Specification版本6.0。另一个问题:没有版本控制-所有枚举类型都有一个固定serialVersionUID0L。第三个问题:没有自定义:枚举类型定义的任何特定于类的writeObject,readObject,readObjectNoData,writeReplace和readResolve方法在序列化和反序列化期间都会被忽略。
罗勒·布尔克

124

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

容易忘记易失性陈述,也很难理解为什么这样做是必要的。如果没有volatile,由于经过了双重检查的锁定反模式,该代码将不再是线程安全的。有关更多信息,请参见《Java并发实践》第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;
    }
}

1
很公平!我对volatile很满意,它的使用。哦,为JCiP欢呼三声。
Stu Thompson

1
哦,这显然是FindBugz著名的William Pugh提倡的方法。
Stu Thompson

4
@Stu有效的Java(版权2001)的第一版项目48下详细介绍这一模式
帕斯卡尔Thivent

9
@Bno:那让构造函数私有化呢?
xyz

2
@ AlikElzin-kilaka不完全是。该实例是在BarHolder的类加载阶段中创建的,该阶段被延迟到第一次需要它时。Bar的构造函数可以随您所愿而变,但是要等到第一个开始时才能调用它getBar()。(如果getBar被称为“过早”,那么无论如何实现单例,您都将面临相同的问题。)您可以在此处看到上面代码的延迟类加载: pastebin.com/iq2eayiR
Ti Strga

95

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的答案详细介绍了比尔·普格(FindBugs)建议的方法,并且可以说是更好的方法。去阅读并投票支持他的答案。


1
在哪里可以了解有关volatile修饰符的更多信息?
2009年


2
我认为提到反射攻击很重要。的确,大多数开发人员都不必担心,但似乎这些示例(基于Enum的单例)应该包括防止多重实例攻击的代码,或者只是放置免责声明以表明这种可能性。
luis.espinal,2010年

2
这里不需要挥发性关键字-因为同步既可以互斥又可以提供内存可见性。
Hemant

2
为什么要在Java 5+中为所有这些烦恼呢?我的理解是,枚举方法同时提供了线程安全性和延迟初始化。它也要简单得多...此外,如果您想避免枚举,我仍然会避免使用嵌套的静态类方法……
Alexandros

91

忘了懒惰的初始化,这太成问题了。这是最简单的解决方案:

public class A {    

    private static final A INSTANCE = new A();

    private A() {}

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

21
单例实例变量也可以设为最终值。例如,私有静态final A单例= new A();
jatanp

19
这实际上是懒惰的初始化,因为在加载类之前不会实例化静态单例,并且直到需要使用类时才加载该类(在您首次引用getInstance()方法的时候这是正确的)。
Dan Dyer

7
如果在希望实例化静态对象之前确实加载了类A,则可以将静态对象包装在静态内部类中以解耦类的初始化。
汤姆·哈特芬

3
我同意这个答案是最简单的,而Anirudhan则无需将实例声明为final。静态成员初始化时,没有其他线程可以访问该类。这是由编译器保证的,换句话说,所有静态初始化都是以同步方式完成的-只有一个线程。
2014年

4
这种方法有一个局限性:构造函数不能引发异常。
wangf

47

确保您确实需要它。用谷歌搜索“单反模式”,看看反对它的论点。我想这没有天生的错误,但这只是公开某些全局资源/数据的一种机制,因此请确保这是最好的方法。特别是,我发现依赖注入更有用,特别是在您还使用单元测试的情况下,因为DI允许您将模拟资源用于测试目的。


您也可以使用传统方法注入模拟值,但我想它不是标准的/ srping方式,因此它的额外工作仅是获取遗留代码...
tgkprog

21

我对某些答案感到迷惑不解,这些答案建议使用DI作为使用单例的替代方法。这些是不相关的概念。您可以使用DI注入单例或非单例(例如,每线程)实例。至少如果您使用Spring 2.x,这是正确的,我不能代表其他DI框架。

因此,我对OP的回答将是(除了最琐碎的示例代码之外):

  1. 使用像Spring这样的DI框架,然后
  2. 无论您的依赖项是单例,请求范围,会话范围还是任何其他类型,都将其作为DI配置的一部分。

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


4
也许是因为人们不同意您的看法。我没有拒绝您,但我不同意:我认为DI可以用于解决单例的相同问题。这是基于将“单例”理解为“具有单个实例且可以通过全局名称直接访问的对象”,而不仅仅是“具有单个实例的对象”,这可能有点棘手。
汤姆·安德森

2
为了对此稍作扩展,请考虑一个TicketNumberer需要具有单个全局实例的实例,以及要在其中编写TicketIssuer包含一行代码的类的位置int ticketNumber = ticketNumberer.nextTicketNumber();。在传统的单例思维中,上一行代码必须类似于TicketNumberer ticketNumberer = TicketNumberer.INSTANCE;。在DI思维中,类将具有类似的构造函数public TicketIssuer(TicketNumberer ticketNumberer) { this.ticketNumberer = ticketNumberer; }
汤姆·安德森

2
调用该构造函数成为别人的问题。DI框架可以通过某种全球地图来实现。手工构建的DI架构可以做到这一点,因为该应用程序的main方法(或其其中之一)将创建依赖项,然后调用构造函数。本质上,使用全局变量(或全局方法)只是可怕的服务定位器模式的一种简单形式,并且可以用依赖项注入代替,就像对该模式的任何其他用法一样。
汤姆·安德森

@TomAnderson我对为什么人们“恐惧”服务定位器模式感到非常困惑。我认为在大多数情况下,它是过大的,或至多不需要,但是,似乎有一些有用的情况。使用较少数量的参数时,DI绝对是首选,但可以想象20+。说代码没有结构化不是一个有效的参数,因为有时参数的分组只是没有意义。而且,从单元测试的角度来看,我不在乎测试服务,仅在测试它的业务逻辑,并且如果它的编码正确,那么这将很容易。我只在非常大规模的项目中看到了这种需求。
布兰登·灵

20

真正考虑一下为什么在编写之前需要单身人士。关于使用它们的准宗教辩论,如果您用Java搜索单身人士,就很容易绊倒。

就我个人而言,出于多种原因,我尝试尽可能多地避免单身人士,而大多数原因可以通过对单身人士进行搜索来找到。我觉得很多时候滥用单例是因为每个人都容易理解它们,它们被用作一种将“全局”数据输入到OO设计中的机制,并且它们被使用是因为它很容易规避对象生命周期管理(或者真正在考虑如何从B内部做A)。查看诸如控制反转(IoC)或依赖注入(DI)之类的东西可以找到一个很好的中间立场。

如果您真的需要一个,那么维基百科提供了一个正确实现单例的很好的例子。


同意 它更多地是一个基础类,可启动应用程序的其余部分,如果重复了该应用程序,则最终将导致整个混乱(即对资源的单次访问或强制执行安全性)。在整个应用程序中传递全局数据是一个巨大的危险信号。当您确认确实需要时使用它。
萨尔瓦多·瓦伦西亚

16

以下是3种不同的方法

1)枚举

/**
* 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;
    }
}

13

我使用Spring框架来管理我的单身人士。它不会强制类的“单一性”(如果涉及多个类加载器,您将无法真正做到),但是它提供了一种非常简单的方法来构建和配置用于创建不同类型对象的不同工厂。


11

Wikipedia也有一些单例的示例,也使用Java。Java 5实现看起来很完整,并且是线程安全的(应用了双重检查锁定)。


11

版本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;
    }
}

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


10

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

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方法或为所有Singleton创建一个注册表。


2
第一个版本是最好的。假设该类除了提供单例功能外不做任何其他事情,那么由于懒惰的类加载,它通常会在与第二个版本中的那个点大致相同的位置实例化。
Dan Dyer

1
双重检查对于静态而言毫无意义。为什么要公开受保护的克隆方法?
汤姆·霍顿

1
-1您的双重检查锁定版本已损坏。
assylias 2012年

5
另外,您还需要使单例变量volatile
MyTitle

第一个版本惰性的和线程安全的。
Miha_x64 '19

9

我会说Enum singleton

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

枚举的使用非常容易实现,并且对于可序列化的对象没有任何缺点,而必须以其他方式加以避免。

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

您可以通过进行访问Singleton.INSTANCE,比getInstance()在Singleton上调用方法要容易得多。

1.12枚举常量的序列化

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

通过枚举常数被序列过程中不能被定制:任何类特异性writeObjectreadObjectreadObjectNoDatawriteReplace,和readResolve由枚举类型定义的方法被序列化和反序列化期间被忽略。同样,任何serialPersistentFieldsserialVersionUID字段声明也将被忽略-所有枚举类型都有一个固定serialVersionUID0L。不需要记录枚举类型的可序列化字段和数据,因为发送的数据类型没有变化。

引用Oracle文档

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

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

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


好读

  1. 单例模式
  2. 枚举,单例和反序列化
  3. 双重检查锁定和单例模式

8
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");
    }
}

(1)并不急,由于JVM类的加载机制,它很懒。
Miha_x64,19年

@ Miha_x64我什么时候说过急切加载,我说过急切初始化。如果您认为两者相同,那么急切加载是什么。也许您应该写一本书并纠正以前的作者如约书亚·布洛赫(Joshua Bloch)所犯的错误。
Dheeraj Sachan

有效的Java是一本很棒的书,但绝对需要进行编辑。
Miha_x64

@ Miha_x64什么是热切的加载,您可以举例说明吗
Dheeraj Sachan

“急切地”做某事意味着“尽快”。例如,如果明确要求,Hibernate会积极支持加载关系。
Miha_x64 '19

8

在这方面可能要晚一些,但是实现单例有很多细微差别。持有人模式无法在许多情况下使用。和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;
}

在这里,我们在方法签名中使用了synced关键字来同步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)。有人可能会说这条路径是边缘情况,因为我们仅在方法的第一次调用期间创建实例。也许对创作的性能冲击是可以容忍的。但是,即使是我们的主要用例,也将对易失性成员运行两次。一次检查是否存在,再次返回其值。


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的值,然后对局部变量进行操作。这将减少我们对挥发物进行操作的次数,从而收回我们损失的部分性能。注意,当我们进入同步块时,我们必须再次设置局部变量。这样可以确保它与我们等待锁时发生的任何更改都是最新的。

我最近写了一篇有关此的文章。解构Singleton。您可以在这些示例中找到更多信息以及“ holder”模式的示例。还有一个真实的示例,展示了双重检查的易失性方法。希望这可以帮助。


您能否解释一下为什么不在BearerToken instance您的文章static?那是什么result.hasExpired()
Woland

那怎么办?class MySingleton也许应该是final
Woland

1
@Woland BearerToken实例不是静态的,因为它是-的一部分,该实例是BearerTokenFactory 使用特定的授权服务器配置的。可能有很多BearerTokenFactory对象-每个对象都有自己的“缓存” BearerToken,分发出去直到过期。在hasExpired()对方法BeraerToken是所谓的工厂的get()方法,以确保它不会伸手过期的令牌。如果过期,将向授权服务器请求一个新令牌。代码块后面的段落对此进行了更详细的说明。
迈克尔·安德鲁斯

4

这是实现简单的方法singleton

public class Singleton {
    // It must be static and final to prevent later modification
    private static final Singleton INSTANCE = new Singleton();
    /** The constructor must be private to prevent external instantiation */ 
    private Singleton(){}
    /** The public static method allowing to get the instance */
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

这是正确延迟创建您的方法singleton

public class Singleton {
    // The constructor must be private to prevent external instantiation   
    private Singleton(){}
    /** The public static method allowing to get the instance */
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    /** 
     * The static inner class responsible for creating your instance only on demand,
     * because the static fields of a class are only initialized when the class
     * is explicitly called and a class initialization is synchronized such that only 
     * one thread can perform it, this rule is also applicable to inner static class
     * So here INSTANCE will be created only when SingletonHolder.INSTANCE 
     * will be called
     */
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
}

两者都是懒惰的,假设单例中您唯一需要的就是它的实例。
Miha_x64

@ Miha_x64第一种情况将在JVM初始化类时实例化单例,第二种情况将仅在调用时实例化单例getInstance()。但是确实,如果您的类中没有任何其他静态方法,Singleton而仅调用getInstance()它,则没有真正的区别。
Nicolas Filotto '19年

3

如果需要延迟加载类的实例变量,则需要仔细检查习惯用法。如果需要延迟加载静态变量或单例,则需要按需持有者习惯进行初始化

另外,如果单例需要可序列化,则所有其他字段都必须是瞬态的,并且需要实现readResolve()方法以保持单例对象不变。否则,每次反序列化对象时,都会创建该对象的新实例。readResolve()所做的是用readObject()替换读取的新对象,由于没有变量引用该对象,因此该对象被强制垃圾回收。

public static final INSTANCE == ....
private Object readResolve() {
  return INSTANCE; // original singleton instance.
} 


3

枚举单例

实现线程安全的Singleton的最简单方法是使用Enum

public enum SingletonEnum {
  INSTANCE;
  public void doSomething(){
    System.out.println("This is a singleton");
  }
}

此代码自Java 1.5中引入Enum以来一直有效

双重检查锁定

如果要编写在多线程环境(从Java 1.5开始)中工作的“经典”单例代码,则应使用此代码。

public class Singleton {

  private static volatile Singleton instance = null;

  private Singleton() {
  }

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

在1.5之前,这不是线程安全的,因为volatile关键字的实现不同。

早期加载Singleton(甚至在Java 1.5之前都可以工作)

该实现在加载类时实例化单例并提供线程安全性。

public class Singleton {

  private static final Singleton instance = new Singleton();

  private Singleton() {
  }

  public static Singleton getInstance() {
    return instance;
  }

  public void doSomething(){
    System.out.println("This is a singleton");
  }

}

2

对于JSE 5.0及更高版本,请采用Enum方法,否则请使用静态单例持有人方法((由Bill Pugh描述的一种惰性加载方法)。后者解决方案也是线程安全的,不需要特殊的语言构造(即volatile或sync)。


2

通常反对Singleton的另一个论点是它们的可测试性问题。单例不容易被嘲笑用于测试目的。如果发现这是个问题,我想做一下以下修改:

public class SingletonImpl {

    private static SingletonImpl instance;

    public static SingletonImpl getInstance() {
        if (instance == null) {
            instance = new SingletonImpl();
        }
        return instance;
    }

    public static void setInstance(SingletonImpl impl) {
        instance = impl;
    }

    public void a() {
        System.out.println("Default Method");
    }
}

添加的setInstance方法允许在测试过程中设置单例类的模型实现:

public class SingletonMock extends SingletonImpl {

    @Override
    public void a() {
        System.out.println("Mock Method");
    }

}

这也适用于早期的初始化方法:

public class SingletonImpl {

    private static final SingletonImpl instance = new SingletonImpl();

    private static SingletonImpl alt;

    public static void setInstance(SingletonImpl inst) {
        alt = inst;
    }

    public static SingletonImpl getInstance() {
        if (alt != null) {
            return alt;
        }
        return instance;
    }

    public void a() {
        System.out.println("Default Method");
    }
}

public class SingletonMock extends SingletonImpl {

    @Override
    public void a() {
        System.out.println("Mock Method");
    }

}

这也具有将该功能也暴露给普通应用程序的缺点。可能会尝试使用该代码的其他开发人员使用“ setInstance”方法来更改特定功能,从而更改整个应用程序的行为,因此,此方法应在其Javadoc中至少包含一个良好的警告。

尽管如此,为了进行模型测试(必要时),此代码公开可能是可以接受的价格。



0

我仍然认为在Java 1.5之后,枚举是可用的最佳可用单例实现,因为它还确保即使在多线程环境中也只能创建一个实例。

public enum Singleton{ INSTANCE; }

你完成了!!!


1
几年前其他答案中已经提到了这一点。

0

看看这篇文章。

Java核心库中的GoF设计模式示例

在最佳答案的“单身”部分中,

单例(可通过创建方法识别,每次返回相同实例(通常是其自身))

  • java.lang.Runtime#getRuntime()
  • java.awt.Desktop#getDesktop()
  • java.lang.System#getSecurityManager()

您还可以从Java本机类本身学习Singleton的示例。


0

我见过的最好的单例模式使用Supplier接口。

  • 它是通用且可重用的
  • 它支持延迟初始化
  • 它只有在初始化之前才被同步,然后将阻塞供应商替换为非阻塞供应商。

见下文:

public class Singleton<T> implements Supplier<T> {

    private boolean initialized;
    private Supplier<T> singletonSupplier;

    public Singleton(T singletonValue) {
        this.singletonSupplier = () -> singletonValue;
    }

    public Singleton(Supplier<T> supplier) {
        this.singletonSupplier = () -> {
            // The initial supplier is temporary; it will be replaced after initialization
            synchronized (supplier) {
                if (!initialized) {
                    T singletonValue = supplier.get();
                    // Now that the singleton value has been initialized,
                    // replace the blocking supplier with a non-blocking supplier
                    singletonSupplier = () -> singletonValue;
                    initialized = true;
                }
                return singletonSupplier.get();
            }
        };
    }

    @Override
    public T get() {
        return singletonSupplier.get();
    }
}

-3

有时,简单的“ static Foo foo = new Foo();”是不够的。只需考虑您要执行的一些基本数据插入。

另一方面,您将必须同步任何实例化此类单例变量的方法。同步本身还不错,但是它可能导致性能问题或锁定(在非常罕见的情况下,使用此示例。解决方案是

public class Singleton {

    private static Singleton instance = null;

    static {
          instance = new Singleton();
          // do some of your instantiation stuff here
    }

    private Singleton() {
          if(instance!=null) {
                  throw new ErrorYouWant("Singleton double-instantiation, should never happen!");
          }
    }

    public static getSingleton() {
          return instance;
    }

}

现在会发生什么?该类通过类加载器加载。从字节数组解释完类后,VM会立即执行静态{} -块。这就是全部秘密:在此类加载器加载给定包的给定类(名称)时,只会调用一次static-block。


3
不对。加载类时,静态变量将与静态块一起初始化。无需拆分声明。
Craig P. Motlin,2009年

-5
public class Singleton {

    private static final Singleton INSTANCE = new Singleton();

    private Singleton(){
    if (INSTANCE != null)
        throw new IllegalStateException (“Already instantiated...”);
}

    public synchronized static Singleton getInstance() { 
    return INSTANCE;

    }

}

由于我们在getInstance之前添加了Synchronized关键字,因此避免了在两个线程同时调用getInstance的情况下的竞争情况。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.