单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例类的特殊类。
通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。
如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
两种单例模式
单例模式大致可以分成饿汉式和懒汉式。
饿汉式是单例类被加载的时候就去创建一个单例实例,而懒汉式是在真正需要的时候才去创建单例实例。所以说,饿汉式和懒汉式是根据单例实例的创建方式而言的。
接下来,我们一起来看一下饿汉式和懒汉式的单例模式,并给出一些JDK源码中使用单例模式的代码示例。
饿汉式
饿汉式大致的图如下:
图来自 https://i-blog.csdnimg.cn/blog_migrate/b2e280789841d2f8b62dbe190d86dad9.png
根据上述类图结构,可以编写类似如下的代码:
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {
}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
代码特点:
- 在Singleton类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用。这个时候,单例类的唯一实例就被创建出来了。
- 单例类最重要的特点是类的构造函数是私有的,从而外界不能创建出多个实例出来。
- 在类中创建一个静态方法,始终返回类中的静态实例。这样每次调用静态方法获取实例的时候,始终获取的是同一个实例。
懒汉式
懒汉式单例和饿汉式单例的区别在于,懒汉式不是马上创建一个实例,而是在第一次被引用时才去创建单例实例。
懒汉式的类图结构如下:
图来自http://image.lxway.com/upload/9/0a/90a263a5d99d21923490a364841d1388_thumb.png
根据上述类图结构,可以编写类似如下的代码:
public class LazySingleton {
private static LazySingleton INSTANCE = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}
从上图可以看出:
当Singleton类被加载的时候,instance并不实例化,当静态方法getInstance()调用的时候才去判断要不要实例化,这个过程是延迟的,直到使用时才实例化,所以叫懒汉式。
上述代码存在一个问题:
如果多个线程同时调用getInstance()方法,线程A调用的时候instance为null,而线程B调用方的时候instance也是null。这种就可能导致线程A和线程B各自创建了一个Singleton实例,会导致单例失败。
为了解决这种情况,一种解决方法是引入同步锁 (synchronized ),也即在对getInstance()方法加锁:
public class LazySingleton {
private static LazySingleton INSTANCE = null;
private LazySingleton() {
}
public synchronized static LazySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}
在上述程序中,对于方法getInstance(),一个线程必须等待另外一个线程创建完成后才能使用这个方法,这就保证了单例的唯一性。保证多线程情况下,单例正常工作。
JDK源码中的单例
饿汉式
JDK源码中,饿汉式的典型示例,我们可以参考java.lang.Runtime的getRuntime方法:
- java.lang.Runtime#getRuntime()
package java.lang;
import java.io.*;
import java.util.StringTokenizer;
import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;
/**
* Every Java application has a single instance of class
* <code>Runtime</code> that allows the application to interface with
* the environment in which the application is running. The current
* runtime can be obtained from the <code>getRuntime</code> method.
* <p>
* An application cannot create its own instance of this class.
*
* @author unascribed
* @see java.lang.Runtime#getRuntime()
* @since JDK1.0
*/
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
// 省略其它
}
懒汉式
JDK源码中,懒汉式的典型例子,我们看以参考java.awt.Desktop的getDesktop方法:
- java.awt.Desktop#getDesktop()
package java.awt;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URI;
import java.net.URL;
import java.net.MalformedURLException;
import java.awt.AWTPermission;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.peer.DesktopPeer;
import sun.awt.SunToolkit;
import sun.awt.HeadlessToolkit;
import java.io.FilePermission;
import sun.security.util.SecurityConstants;
/**
* The {@code Desktop} class allows a Java application to launch
* associated applications registered on the native desktop to handle
* a {@link java.net.URI} or a file.
*
* <p> Supported operations include:
* <ul>
* <li>launching the user-default browser to show a specified
* URI;</li>
* <li>launching the user-default mail client with an optional
* {@code mailto} URI;</li>
* <li>launching a registered application to open, edit or print a
* specified file.</li>
* </ul>
*
* <p> This class provides methods corresponding to these
* operations. The methods look for the associated application
* registered on the current platform, and launch it to handle a URI
* or file. If there is no associated application or the associated
* application fails to be launched, an exception is thrown.
*
* <p> An application is registered to a URI or file type; for
* example, the {@code "sxi"} file extension is typically registered
* to StarOffice. The mechanism of registering, accessing, and
* launching the associated application is platform-dependent.
*
* <p> Each operation is an action type represented by the {@link
* Desktop.Action} class.
*
* <p> Note: when some action is invoked and the associated
* application is executed, it will be executed on the same system as
* the one on which the Java application was launched.
*
* @since 1.6
* @author Armin Chen
* @author George Zhang
*/
public class Desktop {
private DesktopPeer peer;
/**
* Suppresses default constructor for noninstantiability.
*/
private Desktop() {
peer = Toolkit.getDefaultToolkit().createDesktopPeer(this);
}
/**
* Returns the <code>Desktop</code> instance of the current
* browser context. On some platforms the Desktop API may not be
* supported; use the {@link #isDesktopSupported} method to
* determine if the current desktop is supported.
* @return the Desktop instance of the current browser context
* @throws HeadlessException if {@link
* GraphicsEnvironment#isHeadless()} returns {@code true}
* @throws UnsupportedOperationException if this class is not
* supported on the current platform
* @see #isDesktopSupported()
* @see java.awt.GraphicsEnvironment#isHeadless
*/
public static synchronized Desktop getDesktop(){
if (GraphicsEnvironment.isHeadless()) throw new HeadlessException();
if (!Desktop.isDesktopSupported()) {
throw new UnsupportedOperationException("Desktop API is not " +
"supported on the current platform");
}
sun.awt.AppContext context = sun.awt.AppContext.getAppContext();
Desktop desktop = (Desktop)context.get(Desktop.class);
if (desktop == null) {
desktop = new Desktop();
context.put(Desktop.class, desktop);
}
return desktop;
}
//省略其它
}
单例模式的其它实现
使用内部类
public class Singleton {
private static class InstanceHolder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return InstanceHolder.INSTANCE;
}
private Singleton() {}
}
上述示例,单例模式的实现比较特别,
其在单例类Singleton 中定义了一个私有的静态内部类 ( private static class ), 在这个内部类中包含了static final 的Singleton实例。
当Singleton类被JVM加载时,静态内部类InstanceHolder 不会初始化,直到InstanteHolder必要要执行(即静态方法getInstance的时候)。
这种方式不需要同步(syncronized),加之实现起来也比较容易,正受到很多程序员的喜欢和使用。
使用枚举
枚举类中的实例唯一的,而且其构造函数也是私有的,所以,也是一种可以实现单例模式的方法。
如果枚举类中使用两个相同的实例,则会发生编译错误。
public enum Attendant {
INSTANCE,INSTANCE;
private Attendant() {
}
}
使用枚举Enum的一个单例模式示例代码如下:
public enum Attendant {
INSTANCE;
private Attendant() {
// perform some initialization routine
}
public void sayHello() {
System.out.println("Hello!");
}
}
public class Main {
public static void main(String[] args) {
Attendant.INSTANCE.sayHello();// instantiated at this point
}
}
小结
本文主要对单例模式的实现给出了几种不同的方式,并且给出了JDK源码中的几个单例模式示例。
大家如果有其它实现单例模式的好方法,也请分享。
【参考】
[1] https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom
[3] http://stackoverflow.com/documentation/java/155/enums#t=20170111114039435872