一、含义
单件模式(又称单例模式)确保一个类只有一个实例,并提供一个全局访问点。
二、单件模式的经典实现
在Java中实现单件模式需要私有的构造器、一个静态方法和一个静态变量
使用Java实现单件模式如下:
package com.pattern.singleton;
public class Singleton
{
private static Singleton uniqueInstance = null;
//其他有用的实例变量
//构造方法是私有的,所以在类外不能new出多个实例
private Singleton()
{
//初始化其他变量
}
public static Singleton getInstance()
{
if (uniqueInstance == null)
{
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
只有在首次使用这个类的实例时才会产生这个“单例”,否则永远不会产生,这就是“延迟实例化(lazy instantiaze)”;
三、处理多线程
可以看出在多线程情况下,上述的getInstance方法可能会返回不同的实例(比如两个线程同时判断出uniqueInstance为null,接下来就会产生两个不同的实例),为了解决这种情况,可以使用以下方法:
1.使用synchronized关键字将getInstance()方法变成同步方法
public static synchronized Singleton getInstance()
{
if (uniqueInstance == null)
{
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
使用synchronized可以迫使每个线程进入该方法之前,要要先等候其他线程离开该方法(synchronized锁定的是该类对应的Class对象),java提供的这种并发控制方法在此处可能会使性能严重降低:只有第一次执行getInstance()方法时才需要同步,之后每次调用这个方法时,同步都会成为累赘(并行执行改为串行执行),若getInstance()需要被频繁执行,则性能会大大降低。
2.使用“急切实例化”,而不用延迟实例化
public class Singleton
{
//在JVM加载该类时已经创建此唯一的单件实例
private static Singleton uniqueInstance = new Singleton();
private Singleton()
{
}
public static Singleton getInstance()
{
return uniqueInstance;
}
}
在静态初始化时创建单件,保证了线程安全。但是如果这个对象非常耗费资源,而在程序的执行过程中并没有使用到它,那就造成资源的浪费了。
3.用“双重检查加锁”,在getInstance()中减少使用同步
使用双重检查加锁(double-checked locking),首先检查实例是否已经创建了,如果尚未创建,才进行同步,这样一来既实现了延迟实例化又避免了多线程同步synchronized所产生的性能降低的问题,是一个不错的解决方法。
public class Singleton
{
private volatile static Singleton uniqueInstance = null;
//其他
private Singleton()
{
//其他
}
public static Singleton getInstance()
{
if (uniqueInstance == null)
{
synchronized(Singleton.class)
{
if(uniqueInstance==null)
{
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
那么就具备了两层语义:
volatile关键词确保:当uniqueInstance变量被初始化成Singleton实例时,多个线程正确地处理uniqueInstance变量。
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
注意:双重检查加锁不适用于1.4及更早的版本,在1.4及更早版本中许多JVM对volatile关键字的实现会导致双重检查加锁的失效。
四、注意事项
- 如果不是采用第五版的Java 2,双重检查加锁实现可能会失效。
- 你可能使用多个类加载器,可能导致单件失效而产生多个实例。
解决办法:自行指定类加载器,并指定同一个类加载器。 - 如果使用JVM 1.2或之前的版本,你必须建立单件注册表,以免垃圾回收器将单件回收。
- 确定性能和资源上的限制,然后小心的选择适当地方案来实现单件,以解决多线程的问题。
- 最好不要继承单件类。
全局变量与单例模式的区别
在java中,全局变量基本上就是对对象的静态(static)引用。在这样的情况下使用全局变量会有一些缺点。如果将对象赋值给一个全局变量,那么就必须在程序一开始时就创建好对象而不是延迟实例化,可能会造成资源的浪费,而单件模式可以实现延迟实例化;全局变量可以提供全局访问,但是不能确保只有一个实例,用许多全局变量指向许多小对象会造成命名空间(namespace)的污染,单件不鼓励这样的现象,但单件仍然可能被滥用。
参考:《Head First设计模式》