前提
都说单例模式是23种设计模式中最简单的模式,可我觉得,仔细分析下,却并不简单,为什么呢?首先我们先了解下什么是单例模式。
介绍
上图为单例模式的UML图模型,看起来非常简单,关键地方有三点:
- 构造方法一定是私有的
- 有一个该单例类的静态变量
- 有一个获取该静态变量的公开静态方法
该模式的使用场景就是想只维持一个对象,大家都共享使用,比如公共工具类,大家都可以使用,使用单例模式可以减少内存开销,提高性能。
使用方式
饿汉模式
public class Singleton {
private static Singleton smSingleton=new Singleton();
private Singleton()
{
}
public static Singleton getInstance()
{
return smSingleton;
}
public void doSomething()
{
//todo Something
}
}
这种模式也是最普通的模式,直接在单例类加载的时候就创建单例对象,因此单从对象角度来讲是天生安全的,但如果这个单例对象需要改变什么属性的话一定要注意多线程的影响,该加锁的方法里一定要加锁。
懒汉模式
public class Singleton {
private static Singleton smSingleton=null;
private Singleton()
{
}
public static Singleton getInstance()
{
if(null == smSingleton)
{
smSingleton = new Singleton();
}
return smSingleton;
}
public void doSomething()
{
//todo Something
}
}
这种模式是在使用的使用才创建单例对象,比饿汉模式延迟了对象创建时间,由于多线程的影响,该对象有可能会出项多个,因此,在多线程环境中,我们还要做如下修改:
public class Singleton {
private static Singleton smSingleton=null;
private Singleton()
{
}
public static synchronized Singleton getInstance()
{
if(null == smSingleton)
{
smSingleton = new Singleton();
}
return smSingleton;
}
public void doSomething()
{
//todo Something
}
}
在getInstance方法前增加一个synchronized同步关键字可以达到目的。
多例模式
这种模式个人觉得本身就不符合单例模式的定义和使用场景,而且使用地方真心不多,因此我觉得没有必要去讲解
难点
在此知道单例模式的大概定义和使用了,知道这些就认为单例模式简单,那就错了,其实单例模式在多线程和虚拟机两个环境中还是很复杂的。
多线程环境
我们来看看这种写法,会有什么问题:
public class Singleton {
private static Singleton smSingleton=null;
private Singleton()
{
}
public static Singleton getInstance()
{
synchronized (Singleton.class)
{
if(null == smSingleton)
{
smSingleton = new Singleton();
}
}
return smSingleton;
}
public void doSomething()
{
//todo Something
}
}
这种写法确实是安全的,和上面懒汉模式的多线程写法一样,但都会影响效率,因为每次获取单例对象时都要去加锁解锁,而对象的创建时间远远小于使用时间,因此,不建议这样写。
我们再修改试试:
public class Singleton {
private static Singleton smSingleton=null;
private Singleton()
{
}
public static Singleton getInstance()
{
if(null == smSingleton)
{
synchronized (Singleton.class)
{
smSingleton = new Singleton();
}
}
return smSingleton;
}
public void doSomething()
{
//todo Something
}
}
这种写法可以保证在对象成功创建之后再次使用不需要去上锁解锁,提高了效率,可这会在对象创建时带来bug,可能会导致重复创建了多个单例对象,为什么呢?因为在进入同步代码块后,对象不是立马就创建好了,因此smSingleton还是为null,在这时有另外一个线程刚好进入if判断的话,先会阻塞,等待第一个线程创建对象完成之后第二个线程执行代码块中的步骤又创建了一次,因此会导致有两个对象,如果对象不需要改什么属性的话,那倒没什么问题,如果有的话就会把逻辑弄错了,bug就产生了。
我们来验证下:
public class Singleton {
private static Singleton smSingleton=null;
private Singleton()
{
System.out.println("Singleton create!");
}
public static Singleton getInstance()
{
if(null == smSingleton)
{
synchronized (Singleton.class)
{
smSingleton = new Singleton();
}
}
return smSingleton;
}
public void doSomething()
{
System.out.println("Singleton doSomething!"+Thread.currentThread().getId());
}
}
public class Main {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Singleton.getInstance().doSomething();
}
}).start();
}
}
}
结果如下:
Singleton create!
Singleton doSomething!11
Singleton create!
Singleton doSomething!10
Singleton doSomething!13
Singleton doSomething!14
Singleton doSomething!15
Singleton doSomething!16
Singleton doSomething!17
Singleton doSomething!18
Singleton doSomething!19
Singleton create!
Singleton doSomething!12
那我们如何修改呢?
public class Singleton {
private static Singleton smSingleton=null;
private Singleton()
{
}
public static Singleton getInstance()
{
if(null == smSingleton)
{
synchronized (Singleton.class)
{
if(null == smSingleton)
{
smSingleton = new Singleton();
}
}
}
return smSingleton;
}
public void doSomething()
{
//todo Something
}
}
这种写法网上有人叫双重检查锁定,也就是在同步块里又判断了一次对象是否为空,这样是否就可以解决问题呢?同样,我们来验证下:
public class Singleton {
private static Singleton smSingleton=null;
private Singleton()
{
System.out.println("Singleton create!");
}
public static Singleton getInstance()
{
if(null == smSingleton)
{
synchronized (Singleton.class)
{
if(null == smSingleton)
{
smSingleton = new Singleton();
}
}
}
return smSingleton;
}
public void doSomething()
{
System.out.println("Singleton doSomething!"+Thread.currentThread().getId());
}
}
public class Main {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Singleton.getInstance().doSomething();
}
}).start();
}
}
}
为了效果明显,循环次数改为1000,结果如下:
Singleton create!
Singleton doSomething!14
Singleton doSomething!15
Singleton doSomething!11
Singleton doSomething!20
Singleton doSomething!12
Singleton doSomething!13
Singleton doSomething!10
Singleton doSomething!17
Singleton doSomething!16
Singleton doSomething!21
.
.
.
问题确实解决了,只构造了一个单例对象,但网上却有一种声音,说因为java虚拟机有主内存,线程有自己的工作内存,因此在同步块中的smSingleton值不一定就是主内存中的真实值,同样会导致有几率出现重复创建对象,网上的建议是使用 volatile 关键字,可以保证线程工作内存的值和主内存的一致,那么到底有没有这个问题呢?实践是检验真理的唯一标准:
synchronized (Singleton.class)
{
if(null == smSingleton)
{
smSingleton = new Singleton();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
为了效果明显,在单例对象构建后增加了1000ms的延时,并把循环次数设置为10000,发现依旧只创建了一个单例对象,因此,我个人认为至少在hotspot虚拟机,1.8的java版本中网上的问题不存在,当然你为了心安,加上 volatile 关键字也没有什么不妥,那最保险的修改为:
public class Singleton {
private static volatile Singleton smSingleton=null;
private Singleton()
{
}
public static Singleton getInstance()
{
if(null == smSingleton)
{
synchronized (Singleton.class)
{
if(null == smSingleton)
{
smSingleton = new Singleton();
}
}
}
return smSingleton;
}
public void doSomething()
{
//todo Something
}
}
垃圾回收环境
在设计模式之禅这本书中就讲到,单例模式可能会被垃圾回收机制给回收,查略了网上资料发现还真有人死磕这个问题,做了一些实验,证明在Hotspot系列虚拟机中,单例模式是不会被回收的,但在andorid的Dalvik虚拟机中,没有做过测试,其实这个问题的根源在于单例模式只有一个引用,那就是类内部的静态变量,而静态变量保存在虚拟机的方法区中,因此根据不同的虚拟机的垃圾回收算法会得出不同的结论,在此我也不确定哪些垃圾回收策略会回收,因此我觉得最好的建议就是每次使用单例模式的时候都留个心眼,万一遇到bug,反过来看看是不是这里有问题。
另一个问题就是单例模式在不同的场景中使用,很有可能会引发内存泄漏,比如在android开发中,通常一些单例模式需要Context
引用,假如传入的是Activity
引用,当Activity
销毁时,由于单例模式的引用,导致不能被回收,从而引发内存泄漏,因此使用单例模式并不是那么简单的事情。