java设计模式之单例模式
Posted J_Newbie
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java设计模式之单例模式相关的知识,希望对你有一定的参考价值。
定义:
单例模式指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点,属于创建型设计模式
应用场景:
-
需要频繁的创建一些类,使用单例模式可以降低系统的内存压力,减少GC
-
某些类创建实例时占用资源比较多,或实例化耗时较长,且经常使用。
-
频繁访问数据库或者文件的对象
-
对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套
UML类图:
通用写法:
public class Client
public static void main(String[] args)
Singleton.getInstance();
static class Singleton()
private static final Singleton instance = new Singleton();
private Singleton();
public static Singleton getInstance()
return instance;
使用单例模式解决实际问题:
饿汉式单例写法的弊端:
上面的通用型写法就是标准的饿汉式写法。饿汉式单例写法在类加载的时候立即初始化,并且创建单例对象。他绝对线程安全,在线程还没有出现之前就实例化了,不可能存在访问安全问题。饿汉式还有一种写法,代码如下:
//饿汉式静态块单例模式
public class Singleton
private static final Singleton singleton;
static
singleton = new Singleton();
private Singleton();
public static Singleton getInstance()
return singleton;
这种写法的静态块机制,非常简单也容易理解。饿汉式单例写法适用于单例对象比较少的情况,这样写可以保证绝对的线程安全。执行效率比较高。但是它的缺点明显,就是所有对象都会在类加载的时候创建实例化,这样一来,系统中就会有大量的实例化单例对象存在,而且单例对象的数量也不确定,则系统初始化时会造成大量的内存浪费,从而导致系统内存不可控。也就是说,不管对象用不用,都会占用空间,浪费了内存。
懒汉式写法:
//懒汉式单例在外部需要使用的时候才会进行实例化
public class LazySingleton
private LazySingleton()
//静态块,公共内存区域
private static LazySingleton lazySingleton = null;
public static LazySingleton getInstance()
if(lazySingleton == null)
lazySingleton = new LazySingleton();
return lazySingleton;
但这样写又带来一个新的问题,如果在多线程环境下,则会出现线程安全问题。我们来模拟一下,编写线程类ExectorThread
public class ExectorThread implements Runnable
@Override
public void run()
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + lazySingleton);
客户端测试代码:
public class LazySingletonTest
public static void main(String[] args)
Thread t1 = new Thread(new ExectorThread());
Thread t2 = new Thread(new ExectorThread());
t1.start();
t2.start();
System.out.println("End");
运行多次出现的结果:
End
Thread-0:com.design.pattern.singleton.LazySingleton@92734f7
Thread-1:com.design.pattern.singleton.LazySingleton@6a2ff6fb
这意味着上面的单例模式存在着线程安全的隐患。那么结果是怎么产生的。我们分析一下:
如果两个线程在同一时间进入getInstance()方法,则会同时满足if(lazySingleton == null)的条件,创建两个对象。如果两个线程继续执行往下代码,则又可能后执行线程的结果覆盖先执行的线程的结果,如果打印发生在覆盖之前,则最终的结果就是一致的。如果发生在打印发生在覆盖之后,就会得到两个不同的结果。
当然也有可能没有发生并发,正常运行。
如何优化代码,使得懒汉式单例写法在线程环境下是安全的呢?来看下面代码,给getInstance()方法加上synchronized关键字,使这个方法变成线程同步方法。
public class LazySingleton
private LazySingleton()
//静态块,公共内存区域
private static LazySingleton lazySingleton = null;
public synchronized static LazySingleton getInstance()
if(lazySingleton == null)
lazySingleton = new LazySingleton();
return lazySingleton;
这样就解决了线程安全的问题。
双重检查锁单例写法:
上面通过给方法加synchronized的方法解决线程问题,但是在线程数据剧增的情况下,用synchronized加锁,则会导致程序性能的大幅下降。所以引入了双重检查的方式
public class Singleton
private volatile static Singleton instance;
private Singleton();
public static Singleton getInstance()
synchronized(Singleton.class)
if(instance == null)
instance = new Singleton();
return instance;
这样的写法其实与在方法上加synchronized并无差异,还是会造成大规模的阻塞。我们把判断条件往上提一级
public class LazyDoubleCheckSingleton
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton()
public static LazyDoubleCheckSingleton getInstance()
if(instance == null)
synchronized(LazyDoubleCheckSingleton.class)
instance = new LazyDoubleCheckSingleton();
return instance;
运行代码后,还是会存在线程安全问题。这是为什么呐,其实如果两个线程同一时间都满足if(instance == null)条件,则两个线程都会执行 synchronized块中的代码,因此,还是会创建两次,再优化一下代码
public class LazyDoubleCheckSingleton
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton()
public static LazyDoubleCheckSingleton getInstance()
//检查是否要阻塞
if(instance == null)
synchronized(LazyDoubleCheckSingleton.class)
//检查是否要创建实例
if(instance == null)
instance = new LazyDoubleCheckSingleton();
//指令重排序的问题
return instance;
当第一个线程调用getInstance()方法时,第二个线程也可以调用,当第一个线程执行到synchronized时会上锁。第二个线程就会变成MONITOR状态,出现阻塞。此时,阻塞并不是基于整个LazyDoubleCheckSingleton类的阻塞,而是在getInstance()方法内部的阻塞,只要逻辑不太复杂,对于调用者而言感觉不到。
双重检查锁单例写法虽然解决了线程安全的问题和性能问题,但是只要用到synchronized关键字就总是要上锁,对程序的性能还是存在一定的影响。我们从类初始化的角度考虑下面代码,采用静态内部类的方式:
//这种形式兼顾了饿汉式单例写法的内存浪给问题和synchronized的性能问题
//完美的屏蔽了这两个缺点
public class LazyStaticInnerClassSingleton
//使用LazyInnerClassGeneral的时候,默认会初始化内部类
//如果没使用,则内部类是不加载的
private LazyStaticInnerClassSingleton();
//每一个关键字都不是多余的,static是为了使单例模式的空间共享,保证这个写法不会被重写,重载
private static LazyStaticInnerClassSingleton getInstance()
//返回结果前,一定会先加载内部类
return LazyHolder.INSTANCE;
//利用了java本身的语法特点,默认不加载内部类
private static class LazyHolder
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
这种方式兼顾了饿汉式单例写法的内存浪费问题和synchronized的性能问题,内部类一定要在方法调用之前被初始化,巧妙的避免了线程安全问题。
还原反射破坏单例模式的事故现场:
上面的单例模式的构造方法除了加上private关键字,没有坐任何处理,我们如果用反射调用起构造方法,在调用getInstance方法,应该会有两个不同的实例。客户端代码如下:以LazyStaticInnerClassSingleton为例:
public static void main(String[] args)
try
Class<?> clazz = LazyStaticInnerClassSingleton.class;
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Object o1 = c.newInstance();
Object o2 = c.newInstance();
//调用了两次构造方法,相当于new了两次,犯了原则性错误
System.out.println(o1 == o2);
catch(Exception e)
e.printStackTrace();
运行结果
false
我们做一次优化,我们在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常,优化后代码如下:
//这种形式兼顾了饿汉式单例写法的内存浪给问题和synchronized的性能问题
//完美的屏蔽了这两个缺点
public class LazyStaticInnerClassSingleton
//使用LazyInnerClassGeneral的时候,默认会初始化内部类
//如果没使用,则内部类是不加载的
private LazyStaticInnerClassSingleton()
if(LazyHolder.INSTANCE != null)
throw new RuntimeException("不允许创建多个实例");
;
//每一个关键字都不是多余的,static是为了使单例模式的空间共享,保证这个写法不会被重写,重载
private static LazyStaticInnerClassSingleton getInstance()
//返回结果前,一定会先加载内部类
return LazyHolder.INSTANCE;
//利用了java本身的语法特点,默认不加载内部类
private static class LazyHolder
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
测试结果如下:
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.design.pattern.singleton.Test.main(Test.java:11)
Caused by: java.lang.RuntimeException: 不允许创建多个实例
at com.design.pattern.singleton.LazyStaticInnerClassSingleton.<init>(LazyStaticInnerClassSingleton.java:10)
... 5 more
但是会抛出异常,不够优雅。
更加优雅的枚举式单例写法:
枚举式单例的标准写法,创建EnumSingleton类:
public enum EnumSingleton
INSTANCE;
private Object data;
public Object getData()
return data;
public void setData(Object data)
this.data = data;
public static EnumSingleton getInstance()
return INSTANCE;
客户端测试代码:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class TestEnumSingleton
public static void main(String[] args)
try
EnumSingleton instance1 = null;
EnumSingleton instance2 = EnumSingleton.getInstance();
instance2.setData(new Object());
FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("EnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
instance1= (EnumSingleton) ois.readObject();
ois.close();
System.out.println(instance1.getData());
System.out.println(instance2.getData());
System.out.println(instance1.getData() == instance2.getData());
catch(Exception e)
e.printStackTrace();
测试结果:
java.lang.Object@6acbcfc0
java.lang.Object@6acbcfc0
true
还原反序列化破坏单例模式的事故现场:
一个单例对象创建好后,有时候需要讲对象序列化然后写入磁盘,当下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化的对象会重新分配内存,即重新创建。如果序列化的目标为单例对象,则违背了单例模式的初衷,相当于破坏了单例模式:如下代码“
//反序列化破坏了单例模式
public class SeriableSingleton implements Serializable
//序列化就是把内存中的状态通过转换成字节码的形式
//从而转换成一个I/O流,写入其他的地方
//内存中的状态会被永久保存下来
//反序列化就是将已经持久化的字节码内容转换为I/O流
//通过I/O流的读取,进而将读取的内容转换成为Java对象
//在转换过程中会重新创建对象
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton()
public static SeriableSingleton getInstance()
return INSTANCE;
客户端测试代码:
package com.design.pattern.singleton;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SeriableSingletonTest
public static void main(String[] args)
SeriableSingleton s1 = null;
SeriableSingleton s2 = SeriableSingleton.getInstance();
FileOutputStream fos = null;
try
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SeriableSingleton) ois.readObject();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
catch (Exception e)
e.printStackTrace();
测试结果:
com.design.pattern.singleton.SeriableSingleton@7cd84586
com.design.pattern.singleton.SeriableSingleton@5a07e868
false
从运行结果可以知道,反序列化后的对象和手动创建的对象是不一致的,被实例化了两次,违背了单例模式的设计初衷。要想解决,其实增加一个readResolve()方法即可:代码如下:
//反序列化破坏了单例模式
public class SeriableSingleton implements Serializable
//序列化就是把内存中的状态通过转换成字节码的形式
//从而转换成一个I/O流,写入其他的地方
//内存中的状态会被永久保存下来
//反序列化就是将已经持久化的字节码内容转换为I/O流
//通过I/O流的读取,进而将读取的内容转换成为Java对象
//在转换过程中会重新创建对象
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton()
public static SeriableSingleton getInstance()
return INSTANCE;
private Object readResolve()
return INSTANCE;
结果如下:
com.design.pattern.singleton.SeriableSingleton@5a07e868
com.design.pattern.singleton.SeriableSingleton@5a07e868
true
从JDK源码来看,虽然增加readResolve()方法返回实例解决了单例模式被破坏的问题,但实际上单例对象被实例化了两次,只不过新创建的对象没有返回而已。
容器式单例写法:
public class ContainerSingleton
private ContainerSingleton()
private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
public static Object getBean(String className)
synchronized(ioc)
if(!ioc.containsKey(className))
Object obj = null;
try
obj = Class.forName(className).newInstance();
catch(Exception e)
e.printStackTrace();
return obj;
else
return ioc.get(className);
容器式单例写法适用于需要大量创建单例对象的场景,便于管理,但它是非线程安全的。
ThreadLocal单例详解
线程单例实现ThreadLocal,不能保证其创建的对象是全局唯一的,但能保证在单个线程中是唯一的,是线程安全的。看下面代码:
package com.design.pattern.singleton;
public class ThreadLocalSingleton
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
new ThreadLocal<ThreadLocalSingleton>()
@Override
protected ThreadLocalSingleton initialValue()
return new ThreadLocalSingleton();
;
private ThreadLocalSingleton();
public static ThreadLocalSingleton getInstance()
return threadLocalInstance.get();
测试代码:
package com.design.pattern.singleton;
public class ThreadLocalSingletonTest
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("-----------------");
Thread t1 = new Thread(new ExectorThread());
Thread t2 = new Thread(new ExectorThread());
t1.start();
t2.start();
结果如下:
com.design.pattern.singleton.ThreadLocalSingleton@135fbaa4
com.design.pattern.singleton.ThreadLocalSingleton@135fbaa4
com.design.pattern.singleton.ThreadLocalSingleton@135fbaa4
com.design.pattern.singleton.ThreadLocalSingleton@135fbaa4
com.design.pattern.singleton.ThreadLocalSingleton@135fbaa4
-----------------
Thread-1:com.design.pattern.singleton.LazySingleton@33e329ce
Thread-0:com.design.pattern.singleton.LazySingleton@33e329ce
由测试可以知道,在主线程中无论调用多少次,获取的实例都是同一个,都在两个字线程中分别获取了不同的实例。我们知道单例模式为了达到线程安全的目的,会给方法上锁,以时间换空间。ThreadLocal将多有的对象放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。
单例的优点:
-
单例模式可以保证内存里只有一个实例,减少了内存的开销
-
可以避免对资源的多重占用
-
单例模式设置全局访问点,可以优化和共享资源
单例模式缺点:
-
单例模式一般没有接口,扩展困难,如果要扩展,则出了修改原来的代码,没有第二种途径,违背开闭原则
-
在并发测试中,单例模式不利于代码的调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
-
单例模式的功能代码通常写在一个类中,如果功能设计的不合理,则很容易违背单一职责原则
以上是关于java设计模式之单例模式的主要内容,如果未能解决你的问题,请参考以下文章