单例模式是设计模式中使用最为普遍的模式之一,它是一种对象创建模式,用于产生一个对象具体事例,可以确保系统中一个类只产生一个实例。在Java中,这样的行为能带来两大好处:①对于频繁使用的对象可以省略new操作花费的时间,这对于那些重量级对象而言是非常可观的一笔系统开销②由于new操作的次数减少,对系统内存的使用频率也会降低,将减轻GC压力,缩短GC停顿时间。
1、饿汉式
public class SingletonHungry {
private static SingletonHungry instance= new SingletonHungry();
private SingletonHungry(){
}
public static SingletonHungry getInstance(){
return instance;
}
}
该方式性能非常好,getInstance()方法只是简单的返回instance,没有任何锁操作,在并行程序中会有良好的表现。
缺点:SingletonHungry实例在什么时候创建不受控制。对于静态成员instance, 它会在类第一次初始化的时候被创建,但是这个时刻并不一定是getInstance()方法第一次被调用的时候。比方说SingletonHungry中如果还包含一个表示状态的静态成员STATUS:public static int STATUS = 1, 此时在任何地方引用这个STATUS都会导致instance实例被创建(任何对SingletonHungry方法或者字段的引用都会导致类初始化,并创建instance实例,但是类初始化只有一次,因此instance实例永远只会被创建一次),比如外部调用:System.out.println(SingletonHungry.STATUS),此时即使没有要求创建单例,new Singleton()也会被调用。当然,如果这个不足在实际开发中并不重要,那么这种单例模式也是一种不错的选择,它容易实现,代码易读且性能优越。
如果想精确控制instance创建时间,这种方式就不太友善了,需要一种延迟加载策略,只会在instance第一次使用时创建对象。
2、懒汉式--对象创建的时候使用Synchoronized关键字加锁
public class SingletonLazySimple {
private static SingletonLazySimple instance;
private SingletonLazySimple(){
}
public static synchronized SingletonLazySimple getInstance(){
if (instance == null) {
instance = new SingletonLazySimple();
}
return instance;
}
}
这种方式的核心思想是我们不需要实例化instance,当getInstance方法第一次被调用时创建单例对象,为了防止对象被多次创建需要使用synchronized 关键字进行方法同步。好处是充分利用了延迟加载,坏处也很明显:并发环境下加锁竞争激烈的场合对性能产生一定影响。
3、双重校验锁实现
public class SingletonLazyDoubleCheck {
private volatile static SingletonLazyDoubleCheck instance = null;
private SingletonLazyDoubleCheck(){
}
public static SingletonLazyDoubleCheck getInstance(){
if (instance == null) {
synchronized (SingletonLazyDoubleCheck.class) {
if (instance == null) {
instance = new SingletonLazyDoubleCheck();
}
}
}
return instance;
}
}
和2相比,我们在getInstance()方法上增加了两次非空判断,并且同步代码放在一次instance空判断之后,这样可以一定程度上减少等待,不至于每一个调用该方法的线程都会产生竞争。我们注意到这个时候我们在instance变量上增加了volatile关键字,这是防止指令重排序导致我们在getInstance拿到的对象可能没有完全初始化引发的错误。
在getInstance()方法的instance = new SingletonLazyDoubleCheck()中,虚拟机实际做了三步工作:
1.给SingletonLazyDoubleCheck实例分配内存;
2.调用构造函数初始化成员字段;
3.将instance 对象指向分配的内存空间(instance 不再为null)。
由于指令重排序,执行顺序可能是123、132。多线程情况下假如A线程的3执行完成,2未执行,此时B线程也调用getInstance()方法,此时判断instance 不为空,直接返回instance 对象,此时的对象由于可能在构造函数中有一些初始化操作未完成,拿到该对象去操作可能就会导致使用出现一些问题,此时双重枷锁就会失效,所以需要给singletonLazyDoubleCheck变量加上volatile关键字防止指令重排序。
这种方式设计复杂、丑陋,不推荐使用。
4、内部类实现(最优)
public class SingletonInnerClass {
private SingletonInnerClass(){
}
public static SingletonInnerClass getInstance(){
return SingletonHolder.instance;
}
private static class SingletonHolder{
private static SingletonInnerClass instance = new SingletonInnerClass();
}
}
这种方式的优点:
1.getInstance()方法中没有锁,这使得在高并发环境下性能优越
2.只有在getInstance()方法第一次被调用时,SingletonInnerClass的实例才会被创建,因为这种方法使用了内部类和类的初始化方式,内部类SingletonHolder被声明为private,使得不可能在外部访问并初始化它,而我们只能在getInstance()方法内部对SingletonHolder类进行初始化,利用虚拟机的类初始化机制创建单例。
5、枚举实现
public enum SingletonEnum {
INSTANCE;
/**
* 单利可以有自己的操作
*/
public void singletonOperation(){
//功能处理
}
public static void main(String[] args) {
SingletonEnum singletonEnum1 = SingletonEnum.INSTANCE;
SingletonEnum singletonEnum2 = SingletonEnum.INSTANCE;
System.out.println(singletonEnum1 == singletonEnum2);
}
}
优点:①实现简单②枚举本身就是单例模式,由JVM从根本上提供保障,避免通过反射(即使构造函数被私有了也可以通过反射调用)和反序列化的漏洞
缺点:无延迟加载
6、单例模式的问题
反射 反射可以破解上面几种(不包含枚举模式)实现方式:
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
}
public static synchronized LazySingleton getInstance(){
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
public static void main(String[] args) throws Exception {
LazySingleton s1 = LazySingleton.getInstance();
LazySingleton s2 = LazySingleton.getInstance();
System.out.println(s1 == s2);
Class<LazySingleton> clazz = (Class<LazySingleton>) Class.forName("com.mobei.lazy.LazySingleton");
Constructor<LazySingleton> declaredConstructor = clazz.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazySingleton lazySingleton1 = declaredConstructor.newInstance();
LazySingleton lazySingleton2 = declaredConstructor.newInstance();
System.out.println(lazySingleton1 == lazySingleton2);
}
解决办法:可以在构造方法中手动抛出异常控制
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
if (instance != null) {
throw new RuntimeException();
}
}
public static synchronized LazySingleton getInstance(){
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
反序列化 反序列化可以破解上面几种(不包含枚举模式)实现方式:
public class LazySingleton implements Serializable {
private static LazySingleton instance;
private LazySingleton() {
if (instance != null) {
throw new RuntimeException();
}
}
public static synchronized LazySingleton getInstance(){
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
public static void main(String[] args) throws Exception {
LazySingleton s1 = LazySingleton.getInstance();
FileOutputStream fos = new FileOutputStream("d:/a.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.close();
fos.close();
FileInputStream fis = new FileInputStream("d:/a.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
LazySingleton s2 = (LazySingleton) ois.readObject();
System.out.println(s2 == s1);
}
解决办法:可以通过定义readResolve()防止获得不同对象:反序列化时如果对象所在类定义了readResolve()(实际是一种回调),返回此方法指定的对象
public class LazySingleton implements Serializable {
private static LazySingleton instance;
private LazySingleton() {
if (instance != null) {
throw new RuntimeException();
}
}
public static synchronized LazySingleton getInstance(){
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
//反序列化时,如果定义了readResolve方法则直接返回此方法指定的对象,而不需要再创建新对象
private Object readResolve() throws ObjectStreamException {
return instance;
}
}
7、效率测试
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
int threadNum = 10;
CountDownLatch latch = new CountDownLatch(threadNum);
for (int i = 0; i < threadNum; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
Object o = LazySingleton.getInstance();
}
latch.countDown();
}
}).start();
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("耗时 : " + (end - start));
}
8、常见应用场景
1、项目中读取配置文件的类,一般不需要每次使用配置文件数据都new一个对象去读取
2、日志应用,由于共享的日志文件一直处于打开状态,因此只能有一个实例去操作,否则不好追加
3、数据库连接池的设计
4、Spring中每个Bean默认是单例的,优点是方便Spring容器管理
5、SpringMVC中控制器对象