很多时候为了节约系统资源,需要确保系统中某个类只有一个唯一的实例,当这个唯一实例创建了之后,无法再创建一个同类型的其他对象,所有的操作只能基于这一个唯一实例。这是单例模式的动机所在。
比如Windows的任务管理器,可以按Ctrl+Shift+Esc
启动,而且启动一个,不能启动多个。
单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
单例模式是一种对象创建型模式。
单例模式只有一个角色:
Singleton
(单例角色):在单例类的内部只生成一个实例,同时它提供一个类似名叫getInstance
的静态方法获取实例,同时为了防止外部生成新的实例化对象,构造方法可见性为private
,在单例类内部定义了一个Singleton
的静态对象,作为供外部访问的唯一实例new
等方式创建对象getInstance()
的公有静态方法来获取实例单例角色通常实现如下:
class Singleton
{
//饿汉式实现
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance()
{
return instance;
}
}
客户端直接通过该类获取实例即可:
Singleton singleton = Singleton.getInstance();
某个软件需要使用一个全局唯一的负载均衡器,使用单例模式对其进行设计。
代码如下:
public class LoadBalancer
{
private static LoadBalancer instance = null;
private LoadBalancer(){}
public static LoadBalancer getInstance()
{
return instance == null ? instance = new LoadBalancer() : instance;
}
public static void main(String[] args) {
LoadBalancer balancer1 = LoadBalancer.getInstance();
LoadBalancer balancer2 = LoadBalancer.getInstance();
System.out.println(balancer1 == balancer2);
}
}
这是最简单的单例类的设计,获取实例时仅仅判断是否为null
,没有考虑到线程问题。也就是说,多个线程同时获取实例时,还是有可能会产生多个实例,一般来说,常见的解决方式如下:
饿汉式单例就是在普通的单例类基础上,在定义静态变量时就直接实例化,因此在类加载的时候就已经创建了单例对象,而且在获取实例时不需要进行判空操作直接返回实例即可:
public class LoadBalancer
{
private static LoadBalancer instance = new LoadBalancer();
private LoadBalancer(){}
public static LoadBalancer getInstance()
{
return instance;
}
}
当类被加载时,静态变量instance
被初始化,类的私有构造方法将被调用,单例类的唯一实例将被创建。
懒汉式单例在类加载时不进行初始化,在需要的时候再初始化,加载实例,同时为了避免多个线程同时调用getInstance()
,可以加上synchronized
:
public class LoadBalancer
{
private static LoadBalancer instance = null;
private LoadBalancer(){}
synchronized public static LoadBalancer getInstance()
{
return instance == null ? instance = new LoadBalancer() : instance;
}
}
这种技术又叫延迟加载技术,尽管解决了多个线程同时访问的问题,但是每次调用时都需要进行线程锁定判断,这样会降低效率。
事实上,单例的核心在于instance = new LoadBalancer()
,因此只需要锁定这行代码,优化如下:
public static LoadBalancer getInstance()
{
if(instance == null)
{
synchronized (LoadBalancer.class)
{
instance = new LoadBalancer();
}
}
return instance;
}
但是实际情况中还是有可能出现多个实例,因为如果A和B两个线程同时调用getInstance()
,都通过了if(instance == null)
的判断,假设线程A先获得锁,创建实例后,A释放锁,接着B获取锁,再次创建了一个实例,这样还是导致产生多个单例对象。
因此,通常采用一种叫“双重检查锁定”的方式来确保不会产生多个实例,一个线程获取锁后再进行一次判空操作:
private volatile static LoadBalancer instance = null;
public static LoadBalancer getInstance()
{
if(instance == null)
{
synchronized (LoadBalancer.class)
{
if(instance == null)
{
instance = new LoadBalancer();
}
}
}
return instance;
}
需要注意的是要使用volatile
修饰变量,volatile
可以保证可见性以及有序性。
为了克服饿汉式不能延迟加载以及懒汉式的线程安全控制繁琐问题,可以使用一种叫Initialization on Demand Holder(IoDH)
的技术。实现IoDH时,需在单例类增加一个静态内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()
方法返回给外部使用,代码如下:
public class LoadBalancer
{
private LoadBalancer(){}
private static class HolderClass
{
private static final LoadBalancer instance = new LoadBalancer();
}
public static LoadBalancer getInstance()
{
return HolderClass.instance;
}
}
由于单例对象没有作为LoadBalancer
的成员变量直接实例化,因此类加载时不会实例化instance
。首次调用getInstance()
时,会初始化instance
,由JVM保证线程安全性,确保只能被初始化一次。另外相比起懒汉式单例,getInstance()
没有线程锁定,因此性能不会有任何影响。
通过IoDH既可以实现延迟加载,又可以保证线程安全,不影响系统性能,但是缺点是与编程语言本身的特性相关,很多面向对象语言不支持IoDH。另外,还可能引发NoClassDefFoundError
(当初始化失败时),例子可以戳这里。
其中,无论是饿汉式,还是懒汉式,还是IoDH,都有或多或少的问题,并且还可以通过反射以及序列化/反序列化方式去“强制”生成多个单例,有没有更优雅的解决方案呢?
有!答案就是枚举。
代码如下:
public class Test
{
public static void main(String[] args) {
LoadBalancer balancer1 = LoadBalancer.INSTANCE;
LoadBalancer balancer2 = LoadBalancer.INSTANCE;
System.out.println(balancer1 == balancer2);
}
}
enum LoadBalancer{
INSTANCE;
}
使用枚举实现单例优点如下:
如果觉得文章好看,欢迎点赞。
同时欢迎关注微信公众号:氷泠之路。