饿汉式
单例模式的通用写法,一般均指饿汉式。该写法在类加载的时候会立即初始化,并且创建单例对象。之所以说它线程安全,是因为在线程还未run起来之前就实例化了,不存在访问安全问题。
//饿汉式静态代码块单例模式
public class HungryStaticSingleton {
private static final HungryStaticSingleton instance = new HungryStaticSingleton();
private HungryStaticSingleton(){}
public static HungryStaticSingleton getInstance(){
return instance;
}
}
饿汉式静态代码块写法:
//饿汉式静态代码块单例模式
public class HungryStaticSingleton {
private static final HungryStaticSingleton instance;
static {
instance = new HungryStaticSingleton();
}
private HungryStaticSingleton(){}
public static HungryStaticSingleton getInstance(){
return instance;
}
}
这种写法使用了静态代码块机制。饿汉式适用于单例对象比较少的情况,可以保证绝对的线程安全,执行效率比较高。
其缺点也很明显,所有对象类在加载时候就会实例化。如果系统中有大批量的单例对象存在,而且单例对象的数量不确定的情况下,当系统初始化时会有大量的内存浪费。无论对象会不会使用到,都占用了一定的内存空间,造成系统资源的浪费。
懒汉式
懒汉式是为了解决饿汉式会带来的内存浪费问题。这种写法在对象被使用时才会被初始化。
//懒汉式单例模式在外部需要使用的时候才进行实例化
public class LazySimpleSingletion {
//静态块,公共内存区域
private static LazySimpleSingletion instance;
private LazySimpleSingletion() {
}
public static LazySimpleSingletion getInstance() {
// 判断instance是否为空
if (instance == null) {
instance = new LazySimpleSingletion();
}
return instance;
}
}
这种写法在多线程环境中会产生线程安全的问题吗?
可以看出,会存在一定的概率出现两种不同的结果,有可能两个线程获取到的对象是一致的,也有可能是不一致的。
因此,上面的单例会存在线程安全问题。
假设两个线程在同一时间同时进入getInstance()
方法,那么就会同时满足 if (null == instance)
条件,创建两个对象。
如果两个线程都继续往下执行,有可能后执行的线程的结果会覆盖先执行的线程的结果。
解决以上问题,仅需在getInstance()
方法前加把同步锁 synchronize
,使这个方法变为同步方法。
//懒汉式单例模式在外部需要使用的时候才进行实例化
public class LazySimpleSingletion {
//静态块,公共内存区域
private static LazySimpleSingletion instance;
private LazySimpleSingletion() {
}
// 加上synchronize关键字,变为同步方法
public synchronized static LazySimpleSingletion getInstance() {
// 判断instance是否为空
if (instance == null) {
instance = new LazySimpleSingletion();
}
return instance;
}
}
如果线程数量骤增,懒汉式是否还适用?为什么?
当线程数量在短时间内剧增,使用synchronize加锁会导致线程阻塞,使程序性能下降。
举例:
如图所示,餐厅有5个分餐口,但是入口只有1条通道,这样的话会造成大量的堵塞,降低了用户体验。
是否有比上面更好的方案呢?
双重检查锁单例
如上图所示,将人群分开排队,进入餐厅后仍保持分流,如此一来效率会提升许多。
对 LazySimpleSingletion
进行改造,得到 LazyDoubleCheckSingleton1
,代码如下:
public class LazyDoubleCheckSingleton1 {
private volatile static LazyDoubleCheckSingleton1 instance;
private LazyDoubleCheckSingleton1() {
}
// 参照 LazySimpleSingletion
public static LazyDoubleCheckSingleton1 getInstance() {
synchronized (LazyDoubleCheckSingleton1.class) {
if (instance == null) {
instance = new LazyDoubleCheckSingleton1();
}
}
return instance;
}
}
那么这种写法和没有加锁的 LazySimpleSingletion
并无差异,因此将 if 判断向上提一级,得到LazyDoubleCheckSingleton2
:
public class LazyDoubleCheckSingleton2 {
private volatile static LazyDoubleCheckSingleton2 instance;
private LazyDoubleCheckSingleton2() {
}
// 参照 LazySimpleSingletion
public static LazyDoubleCheckSingleton2 getInstance() {
if (instance == null) {
synchronized (LazyDoubleCheckSingleton2.class) {
instance = new LazyDoubleCheckSingleton2();
}
}
return instance;
}
}
对 LazyDoubleCheckSingleton2
进行调试发现,仍存在线程不安全问题
造成这种情况的原因在于,如果两个线程在同一时间都满足 if(null == instance)
条件,那么两个线程还是会执行 synchronize 中的代码,继续优化:
public class LazyDoubleCheckSingleton3 {
private volatile static LazyDoubleCheckSingleton3 instance;
private LazyDoubleCheckSingleton3() {
}
// 参照 LazySimpleSingletion
public static LazyDoubleCheckSingleton3 getInstance() {
if (null == instance) {
// 检查是否要阻塞
synchronized (LazyDoubleCheckSingleton3.class) {
// 检查是否要重新创建实例
if (null == instance) {
instance = new LazyDoubleCheckSingleton3();
}
}
}
return instance;
}
}
调试如下:
虽然双重检查锁单例解决了线程的安全与性能问题。当使用到 synchronize 关键字时还需上锁,对程序的性能还是存在影响。
静态内部类单例
public class LazyStaticInnerClassSingleton1 {
//使用 LazyStaticInnerClassSingleton1 的时候,默认会先初始化内部类
//如果没使用,则内部类是不加载的
private LazyStaticInnerClassSingleton1() {
}
// static是为了使单例的空间共享,保证这个方法不会被重写、重载
private static LazyStaticInnerClassSingleton1 getInstance() {
//在返回结果之前,一定会先加载内部类
return LazyHolder.INSTANCE;
}
// 利用Java内部类的语法特点,默认不加载内部类
private static class LazyHolder {
private static final LazyStaticInnerClassSingleton1 INSTANCE = new LazyStaticInnerClassSingleton1();
}
}
该方式即解决了饿汉式的内存资源浪费及 synchronize 加锁的性能问题,内部类一定是在方法调用之前初始化,避免了线程安全问题。
那么,是否可以使用反射来调用构造方法,再调用 getInstance()
方法实现上面的单例模式?
public static void main(String[] args) {
try {
// 使用反射进行破坏
Class<?> clazz = LazyStaticInnerClassSingleton1.class;
// 通过反射获取私有构造方法
Constructor<?> c = clazz.getDeclaredConstructor(null);
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
调用结果如下:
很显然,这种方式在内存中创建了两个不同的实例
public class LazyStaticInnerClassSingleton2 {
//使用LazyStaticInnerClassSingleton2的时候,默认会先初始化内部类
//如果没使用,则内部类是不加载的
private LazyStaticInnerClassSingleton2() {
if (LazyHolder.INSTANCE != null) {
throw new RuntimeException("不允许创建多个实例");
}
}
// static是为了使单例的空间共享,保证这个方法不会被重写、重载
private static LazyStaticInnerClassSingleton2 getInstance() {
//在返回结果之前,一定会先加载内部类
return LazyHolder.INSTANCE;
}
// 利用Java内部类的语法特点,默认不加载内部类
private static class LazyHolder {
private static final LazyStaticInnerClassSingleton2 INSTANCE = new LazyStaticInnerClassSingleton2();
}
}
彩蛋:ThreadLocal单例模式
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalSingleton =
ThreadLocal.withInitial(ThreadLocalSingleton::new);
// 等价于上面的写法
/*private static final ThreadLocal<ThreadLocalSingleton> threadLocalSingleton =
new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};*/
private ThreadLocalSingleton() {
}
public static ThreadLocalSingleton getInstance() {
return threadLocalSingleton.get();
}
public static void main(String[] args) {
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
Thread t1 = new Thread(new ExectorThread());
Thread t2 = new Thread(new ExectorThread());
t1.start();
t2.start();
System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~");
}
}
调用结果:
可以看到,无论在主线程中调用多少次,获取到的实例都是同一个,但在两个子线程中获取到了不同的实例。
其实,ThreadLocal是将所有的对象全部放在ThreadLocalMap中,为每一个线程提供一个对象。