顾名思义,单例就是程序内部只有一个实例。java 单例包含 2 个要点:
- 构造方法私有。只能自己创建实例
- 公开静态访问方法。既然别人不能创建实例了,那么要获取实例,就得提供一个公开的静态方法了
常用写法
1 饿汉式
public class SingletonA {
private static SingletonA instance = new SingletonA();
private SingletonA() {
}
public static SingletonA getInstance() {
return instance;
}
}
这种方式在类装载的时候就初始化了实例,所以是线程安全的。
2 懒汉式
public class SingletonB {
private static SingletonB instance = null;
private SingletonB() {
}
public static SingletonB getInstance() {
if (instance == null) {
instance = new SingletonB();
}
return instance;
}
}
这种方式在第一次访问 getInstance() 方法时才初始化实例,所以可以达到懒加载的效果,但是在高并发的情况下,多个线程有可能同时判断到 instance == null
为 true ,导致实例被初始化多次。为了实现线程安全,我们很容易想到给程序加锁,于是有个下面两种写法:
方法上加锁
public class SingletonC { private static volatile SingletonC instance = null; private SingletonC() { } public static synchronized SingletonC getInstance() { if (instance == null) { instance = new SingletonC(); } return instance; } }
代码块加锁
public class SingletonD { private static volatile SingletonD instance = null; private static final Object lockObj = new Object(); private SingletonD() { } public static SingletonD getInstance() { synchronized (lockObj) { if (instance == null) { instance = new SingletonD(); } } return instance; } }
方法上加锁和代码块加锁的方式都能实现线程安全,但每次访问都需要上锁,高并发情况下势必会影响程序性能,因此我们有了下面的改良版的加锁方式,即双重检查锁的实现:
public class SingletonE {
private static volatile SingletonE instance = null;
private static final Object lockObj = new Object();
private SingletonE() {
}
public static SingletonE getInstance() {
if (instance == null) {
synchronized (lockObj) {
if (instance == null) {
instance = new SingletonE();
}
}
}
return instance;
}
}
这种方式在初始化代码上加了 synchronized 同步锁,保证了高并发情况下只有一个线程执行了初始化操作,实现了线程安全,另外在初始化完成后,外层的 instance == null
将返回 false ,程序不会再次进入同步代码块,解决了访问性能问题。
3 静态内部类
public class SingletonF {
private SingletonF() {
}
private static class SingletonHolder {
static final SingletonF instance = new SingletonF();
}
public static SingletonF getInstance() {
return SingletonHolder.instance;
}
}
这种方式由 jvm 保证了单例的线程安全,在 getInstance() 第一次被调用时,内部类 SingletonHolder 被装载,装载过程中会初始化它的静态域,从而 SingletonF 被实例化,这种写法利用了 jvm 及内部类的特性实现了一个线程安全的单例模式,不需要加锁,同时也保证了性能,懒加载的情况个人推荐这种写法。
4 枚举
public enum SingletonG {
instance;
public static SingletonG getInstance() {
return instance;
}
}
通过 getInstance() 或者直接使用枚举 SingletonG.instance 来获取实例,枚举同样是在类装载时就初始化了,所以不是懒加载,也不存在线程安全问题。
5 线程内单例
public class SingletonH {
private static ThreadLocal<SingletonH> instanceLocal = new ThreadLocal<SingletonH>();
private int status;
private SingletonH() {
status = (int) (Math.random() * 100);
}
public static SingletonH getInstance() {
SingletonH instance = instanceLocal.get();
if (instance == null) {
instance = new SingletonH();
instanceLocal.set(instance);
}
return instance;
}
public int getStatus() {
return status;
}
}
这种方式可以保证 SingletonH 在每个线程内部是单例的,下面我们写段测试代码来验证下:
@Test
public void testSingleton() {
ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 100; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
long id = Thread.currentThread().getId();
String name = Thread.currentThread().getName();
SingletonH instance = SingletonH.getInstance();
int status = instance.getStatus();
System.out.println(String.format("thread[id=%s,name=%s],instance.status=%s", id, name, status));
}
});
if (i % 5 == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
在测试代码中创建一个5个线程数的线程池并向线程池提交 100 个任务,在任务内部打印线程号和 SingletonH 实例,通过控制台输出我们可以看出 instance.ststus 在每个线程内都是相等的。
总结
单例的写法有很多种,但大致都有上面几种方法衍变而来,应该使用哪种方式,要看具体的应用场景,就个人喜好而言,不需要懒加载的情况使用 SingletonA 饿汉式的写法,需要懒加载的情况使用 SingletonF 静态内部类的写法。