单例模式
什么是单例模式
Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”
定义中有三个要点:
- 我们设定的某个类必须有且有一个实例
- 这个类必须自己去实例化自己
- 这个类需要向整个系统提供调用它的实例的接口
我们可以结合上面的三个要点来推测该设计模式的实现方式:
它需要自己实例化自己,那么就不允许别的类来实例化,所以它的构造函数必定是private 来声明的私有构造;而且它只允许有一个实例,我们必须使用static来声明该类的私有对象,并控制它不会重复的被new出来;该类不允许在别的地方实例化,并且需要向整个整个系统提供获取该类实例的接口,那么我们只能通过static 方法来提供该接口,这样就可以通过接口向整个系统提供该类的实例了。
在日常生活中使用的打印机就可以类比出一个单例模式,我们的电脑可以连接若干个打印机,但是每个打印机只能同时打印一份文件,而无法同时执行两个打印作业。
在 Java 语言中,这样的行为能带来两大好处:
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
- 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。
单例模式分类
单例模式一般分为两类:饿汉式和懒汉式。
饿汉式:不管单例类的实例是否要使用,只要类初始化时,就将静态实例创建出来。
public class HungrySingleton {
//私有的静态实例
private final static HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
//私有构造,避免其它的类实例化
private HungrySingleton() {}
//对外提供的实例接口
public static final HungrySingleton getInstance(){
return HUNGRY_SINGLETON;
}
//该实例可以向外提供的方法
public void print(String msg){
System.out.printf(msg);
}
}
那么我可以在外部这样调用
public class MainClass {
public static void main(String[] args){
//不能声明,因为该类使用了私有的构造
// HungrySingleton hungrySingleton = new HungrySingleton();
HungrySingleton.getInstance().print("饿汉模式,外部调用饿汉的打印方法");
}
}
饿汉模式比较简单,也很好理解,而且它是线程安全的,它基于Classloader机制避免的线程安全问题,但是它在类初始化的时候就实例化了,对于性能有一定的影响。最好的情况是,当我们需要使用它的时候在实例化,而不是在类初始化的时候就实例化。这就需要下面的懒汉式了。
懒汉式:相对上面的饿汉时,懒汉式主要在于类实例化的延迟加载。
public class LazySingleton {
//私有的静态实例
private static LazySingleton sLazySingleton = new LazySingleton();
//私有构造,避免其它的类实例化
private LazySingleton() {
}
//对外提供的实例接口
public static final LazySingleton getInstance() {
//如果类没有实例化,才去实例化
if (sLazySingleton == null) {
sLazySingleton = new LazySingleton();
}
return sLazySingleton;
}
//该实例可以向外提供的方法
public void print(String msg) {
System.out.printf(msg);
}
}
上面的代码简单实现了延迟加载的懒汉式单例模式。只有当我们需要实例化的时候才去实例化。上面的单例实现方式很简单,但是它存在多线程问题。存在问题的地方主要在对外提供实例的接口方法中;对于这个地方我们来简单看看。对于下面的方法:
//对外提供的实例接口
public static final LazySingleton getInstance() {
//如果类没有实例化,才去实例化
if (sLazySingleton == null) {
sLazySingleton = new LazySingleton();
}
return sLazySingleton;
}
如果现在存在着线程A和B,线程A执行到了 if(sLazySingleton == null)
,线程B执行到了 sLazySingleton = new LazySingleton()
;线程B虽然实例化了一个sLazySingleton
,但是对于线程A来说判断sLazySingleton
还是木有初始化的,所以线程A还会对sLazySingleton
进行初始化。这样就会有两个实例。就违背了单例的原则。那么怎么解决呢?很多人肯定都知道,涉及到多个线程同时访问同一个方法,那么必然涉及到 锁的问题,肯定会用到 synchronized
关键字,这样我们就给当前方法加上了锁,两个线程不能同时访问该同步方法。那么就成了下面的这样:
public class LazySingleton {
//私有的静态实例
private static LazySingleton sLazySingleton = new LazySingleton();
//私有构造,避免其它的类实例化
private LazySingleton() {
}
//对外提供的实例接口
public static final synchronized LazySingleton getInstance() {
//如果类没有实例化,才去实例化
if (sLazySingleton == null) {
sLazySingleton = new LazySingleton();
}
return sLazySingleton;
}
//该实例可以向外提供的方法
public void print(String msg) {
System.out.printf(msg);
}
}
synchronized
修饰静态方法,使用的该类的class文件对象,它的作用范围是整个静态方法。虽然锁住的整个静态并不复杂,但是还有没有更好一些的方式,锁住的内容更少一些呢?答案是肯定synchronized
不光可以修饰方法,还可以修饰代码块,通过上面我们知道线程安全问题主要在不同线程实例化的过程中。那么我们就可以在这个地方放做文章了。我们可以将实例化使用代码块包括起来,给它上锁,如下:
public class LazySingleton {
//私有的静态实例
private static LazySingleton sLazySingleton = new LazySingleton();
//私有构造,避免其它的类实例化
private LazySingleton() {
}
//对外提供的实例接口
public static final LazySingleton getInstance() {
//如果类没有实例化,才去实例化
if (sLazySingleton == null) {
//给实例化过程加锁
synchronized (LazySingleton.class) {
sLazySingleton = new LazySingleton();
}
}
return sLazySingleton;
}
//该实例可以向外提供的方法
public void print(String msg) {
System.out.printf(msg);
}
}
通过上面我们看到我们在给实例化加了锁,这样就可以避免多线程的问题了吗?是否还有其它问题呢?当两个线程A和B,同时执行到加锁的代码块,A先进去实例化后,A出来,然后B进去,又实例了一次,所有还是问题的,怎么解决呢?解决方案很简单,当实例不为空时就不在去实例化了。我们进行双重校验。如下:
public class LazySingleton {
//私有的静态实例
private static LazySingleton sLazySingleton = new LazySingleton();
//私有构造,避免其它的类实例化
private LazySingleton() {
}
//对外提供的实例接口
public static final LazySingleton getInstance() {
//如果类没有实例化,才去实例化
if (sLazySingleton == null) {
//给实例化加锁,并添加双重验证
synchronized (LazySingleton.class) {
if (sLazySingleton == null) {
sLazySingleton = new LazySingleton();
}
}
}
return sLazySingleton;
}
//该实例可以向外提供的方法
public void print(String msg) {
System.out.printf(msg);
}
}
这样就解决了延迟加载而且线程安全的问题。
其实延迟加载在真正的使用过程中是很少的。一般都是类初始化的时候,便需要实例化,如果很明确的知道不需要延迟实例化,那么我们仅仅使用饿汉式单例模式就可以了。
在程序中如果不涉及到多线程操作,但涉及到延迟加载,我们就可以不给实例化加锁。这样也可以节省一部分性能。
懒汉式的单例其它写法:
一般在开发中都会注意延迟加载和线程安全问题,就算再小的几率不去使用,也需要注意,那么懒汉式就成了重重之重了,所以研究的人多了,写法也就多了。下面就一一介绍。
懒汉变种:
public class VariantSingleton {
private static VariantSingleton sInstance = null;
static {
sInstance = new VariantSingleton();
}
private VariantSingleton() {
}
public static VariantSingleton getInstance() {
System.out.printf(sInstance.toString());
return sInstance;
}
//该实例可以向外提供的方法
public void print(String msg) {
System.out.println(msg);
}
}
内部类式:
这个是我常用的一种,因为它写法简单,又兼顾延迟加载和多线程问题。下面看代码:
public class InnerClassSingleton {
//私有构造,避免其它的类实例化
private InnerClassSingleton() {}
private static final class InnerSingletonHolder {
//基于Classloader机制避免的线程安全问题
private static final InnerClassSingleton INNER_CLASS_SINGLETON = new InnerClassSingleton();
}
//对外提供的实例接口
public static InnerClassSingleton getInstance() {
return InnerSingletonHolder.INNER_CLASS_SINGLETON;
}
//该实例可以向外提供的方法
public void print(String msg){
System.out.println(msg);
}
}
我经常使用这种方式的原因是:这种方式能达到双检锁方式一样的功效,但实现更简单。
枚举方式:
public enum EnumSingleton {
SINGLETON;
public void print(String msg) {
System.out.println(msg);
}
}
如果看过Effective Java 的同学,应该知道这是作者Josh Bloch 比较提倡的方式;它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,使用的时候需要稍微注意。但是 enum 实际上也是一个Java 类,只不过 Java 编译器帮我们做了语法的解析和编译而已。如果我们使用enum,性能上会有影响,如果是做Android开发的同学,需要注意一下。
为什么要使用单例模式?
结合上面我们可以知道,单例在整个系统中只会存留一个实例,在开发当中,我们可以使用单例来保存一些应用配置,也方便资源之间的互相通信;不会频繁的实例化对象,相应的也会节省很多内存。
使用单例模式有时可以避免过多的使用静态方法。那么问题来了,为什么不能使用静态方法来替代单例模式呢?
虽然使用静态方法也能实现相同的目的。但是他们一个是基于对象的,一个是面向对象的;单例是面向对象的,面相对象的代码提供一个更好的编程思想。
如果一个方法和它所在的类的实例无关,那么这个方法就应该是静态的,比如我们的工具类,提供了各种工具方法。反之那这个方法就是非静态的。如果我们确实应该使用非静态方法,但是创建类时只需要维护一个实例,那么这就是单例模式。
比如说我们在系统运行时候,就需要加载一些配置和属性,这些配置和属性是一定存在了,又是公共的,同时需要在整个生命周期中都存在,所以只需要一份就行,这个时候如果需要我再需要的时候new一个,再给他分配值,显然是浪费内存并且再赋值没什么意义,所以这个时候我们就需要单例模式或静态方法去维持一份且仅这一份拷贝,但此时这些配置和属性又是通过面向对象的编码方式得到的,我们就应该使用单例模式,或者不是面向对象的,但他本身的属性应该是面对对象的,我们使用静态方法虽然能同样解决问题,但是最好的解决方案也应该是使用单例模式。(引自为什么要用单例模式?)
扩展,控制多个实例
如果我们想控制多个实例?怎么办呢?我们可以使用Map来控制;我们使用Map来缓存创建过的对象。
public class MultiSingleton {
//缓存的key的前缀
private final static String KEY_PRE = "key_pre";
//使用Map作为容器缓存实例对象
private static Map<String, MultiSingleton> map = new HashMap<>();
//初始化时实例的index
private static int sIndex = 1;
//允许控制的实例的最大个数
private final static int NUM_MAX = 3;
private MultiSingleton() {
}
public static synchronized MultiSingleton getInstance() {
//使用前缀+当前实例的index
String key = KEY_PRE + sIndex;
MultiSingleton singleton = map.get(key);
if (singleton == null) {
singleton = new MultiSingleton();
map.put(key, singleton);
}
//每调用一次,实例创建一个,实例的index+1
sIndex++;
if (sIndex > NUM_MAX) {
sIndex = 1;
}
return singleton;
}
下面是调用:
public static void main(String args[]) {
MultiSingleton t1 = MultiSingleton.getInstance();
MultiSingleton t2 = MultiSingleton.getInstance();
MultiSingleton t3 = MultiSingleton.getInstance();
MultiSingleton t4 = MultiSingleton.getInstance();
MultiSingleton t5 = MultiSingleton.getInstance();
MultiSingleton t6 = MultiSingleton.getInstance();
System.out.println(t1.toString());
System.out.println(t2.toString());
System.out.println(t3.toString());
System.out.println(t4.toString());
System.out.println(t5.toString());
System.out.println(t6.toString());
}
下面为输出:
me.liteng.example.singleton.MultiSingleton@14ae5a5
me.liteng.example.singleton.MultiSingleton@7f31245a
me.liteng.example.singleton.MultiSingleton@6d6f6e28
me.liteng.example.singleton.MultiSingleton@14ae5a5
me.liteng.example.singleton.MultiSingleton@7f31245a
me.liteng.example.singleton.MultiSingleton@6d6f6e28
我们可以看到只有三个实例,在我们控制的实例范围内。