单例模式,顾名思义就是只有一个实例,并且它自己负责创建自己的对象,这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。下面我们来看下有哪几种实现方式吧。
单例模式分饿汉式和懒汉式,下面介绍下两种的具体写法:
饿汉式
饿汉式就是在类加载的时候就已经创建实例,不管你用没用到,都创建。
好处:线程安全
坏处:浪费内存空间
1 2 3 4 5 6 7 8 9 10 11 12
| public class Hungry { private Hungry() { } private static Hungry hungry = new Hungry();
public static Hungry getInstance(){ return hungry; } }
|
我们可以简单的测试下,它在多线程环境下是否会产生线程安全问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class TestHungry { public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(8); for (int i = 0; i < 1000; i++) { pool.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+Hungry.getInstance()); } }); } pool.shutdown(); } }
|
pool-1-thread-8design.singleton.Hungry@fa49b1e
pool-1-thread-7design.singleton.Hungry@fa49b1e
pool-1-thread-8design.singleton.Hungry@fa49b1e
pool-1-thread-5design.singleton.Hungry@fa49b1e
pool-1-thread-1design.singleton.Hungry@fa49b1e
由于打印数据过多,我们只截取一部分,发现他们的hashcode都是一样的,说明他们都是同一个实例对象
那为什么这种方法就能实现线程安全呢?
类加载的方式是按需加载,且只加载一次。
因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用。单例就是该类只能返回一个实例。
换句话说,在线程访问单例对象之前就已经创建好了。再加上,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例。
也就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此就说,饿汉式单例天生就是线程安全的。
懒汉式
懒汉式,顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则返回,没有则新建。有线程安全和线程不安全两种写法。
Lazy01
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public class Lazy01 {
private Lazy01(){}
private static Lazy01 instance = null;
public static Lazy01 getInstance(){ if (instance == null){ instance = new Lazy01(); } return instance; } }
|
我们通过测试,发现多线程环境下,拿到实例的hashcode有的是不一样的,就说明它并不能保证线程安全。
Lazy02
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
public class Lazy02 {
private Lazy02(){}
private static Lazy02 instance = null;
public static synchronized Lazy02 getInstance(){ if (instance == null){ instance = new Lazy02(); } return instance; } }
|
可以看到在获取实例时,该类在方法是加入synchronized以保证线程的有序性,但synchronized是重量级锁,效率并不高。
Lazy03
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
public class Lazy03 {
private Lazy03(){}
private static Lazy03 instance = null;
public static Lazy03 getInstance(){ if (instance == null){ synchronized (Lazy03.class){ if (instance == null){ instance = new Lazy03(); } } } return instance; } }
|
注释说的很详细了,就是由于指令的重排,照成代码执行顺序的不一致,可能未初始化取返回给需要实例的类,就会造成安全问题。
指令重排会发生在编译器或指令并行或者操作系统中,虽然这个概率很小,但并不不能说该单例模式是线程安全的。
Java中有Volatile关键字,就可以禁止指令重排,具体原因,是操作系统的知识了(本菜鸟现在也没学),以后再深究。
Lazy04
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
public class Lazy04 {
private Lazy04(){}
private volatile static Lazy05 instance = null;
public static Lazy04 getInstance(){ if (instance == null){ synchronized (Lazy04.class){ if (instance == null){ instance = new Lazy05(); } } } return instance; } }
|
加入valitile,禁止指令重排,实现线程安全!
Lazy05
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
public class Lazy05 {
private Lazy05 (){}
private static Lazy05 instance = null;
static { instance = new Lazy05(); }
public static Lazy05 getInstance(){ return instance; } }
|
静态代码块,在项目运行时只加载一次,可能和饿汉式有点像,都是借助类加载只会加载一次的特点。
Lazy06
以上在懒汉单例模式中实现了线程安全,但都会被万恶之源反射给攻破,所以引入枚举来实现最终的单例模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
public class Lazy06 {
private Lazy06(){}
public static Lazy06 getInstance(){ return Lazy.INSTANCE.getInstance(); }
private enum Lazy{ INSTANCE;
private Lazy06 singleton;
Lazy(){ singleton = new Lazy06(); }
public Lazy06 getInstance(){ return singleton; } }
|
具体为什么能实现避免反射的问题,下面的博文讲的很好:
为什么要用枚举实现单例模式(避免反射、序列化问题)
总结
在写饿汉式单例模式的时候,我建议可以写Lazy04和Lazy06,前者双重检测+volatile,后者是JDK5引入枚举后写单例的方式,都需要掌握。