java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍两种:懒汉式单例、饿汉式单例
单例模式有以下特点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。
一:饿汉式 -1
何为饿?饿者,饥不择食;但凡有食,必急食之。此处同义:在加载类的时候就会创建类的单例,并保存在类中,这样应该很容易理解饿汉式是什么时候创建实例的吧。
类加载的时候就创建了实例
优点:类加载的时候创建一次实例,避免了多线程同步问题
缺点:即使单例没被用到也会创建,浪费内存
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return instance;
}
}
饿汉式 -2
public class Singleton {
private static Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton() { }
public static Singleton getInstance() {
return this.instance;
}
}
二:懒汉 -1(非线程安全)
何为懒?顾名思义,就是不做事,这里也是同义,懒汉式就是不在系统加载时就创建类的单例,而是在第一次使用实例的时候再创建。很懒。不到用的时候一点也不担心实例的创建。
优点:需要时才去创建
缺点:没有考虑线程安全问题,多个线程并发调用getInstance,可能会创建多个实例
public class Singleton {
private static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉 -2(线程安全)
缺点:性能问题,添加了synchronized的函数比一般方法慢得多,若多次调用getInstance,则累积的性能损耗特别大。
public class Singleton {
private static Singleton instance = null;
private Singleton() { }
public static Synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
三:双重校验锁
大部分情况下,同步代码块都不会执行到,提高了程序的性能。
有一种情况,两个线程ThreadA,ThreadB,如果threadA执行到了第一个if条件判断,instance = null;ThreadB也执行到了if条件判断instance = null,所以A和B会依次执行同步代码块里的代码。为了避免创建两个实例,因此又在同步代码块里添加了if条件进行二重检验。
public class Singleton {
private static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
隐患:
1、此处涉及Java的指令重排优化。指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行地更快。
2、JVM中没有规定编译器优化的相关内容,也即JVM可以自由地进行指令重排序的优化。
3、此问题的关键在于由于指令重排序优化的存在,导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。
4、在某个线程创建单例对象时,在构造函数被调用前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没初始化。若紧接着另一个线程来调用getInstance,获取到的就是状态不正确的对象,程序出错。