一 、单例模式的应用场景
单例模式(Singleton Pattern )是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。隐藏其所有的构造方法。一般用getInstance()方法名创建单例实例。
单例模式是创建型模式。
应用非常广泛,例如:J2EE中的ServletContext、ServletContextConfig等。Spring中的ApplicationContext、数据库的连接池等都是单例模式。
二、 单例模式的常见写法
饿汉式单例;
懒汉式单例;
注册式单例;
ThreadLocal单例;
2.1 饿汉式单例的2种写法与优缺点
HungrySingleton
public class HungrySingleton {
private HungrySingleton(){}
private static final HungrySingleton INSTANCE = new HungrySingleton();
public static HungrySingleton getInstance(){
return INSTANCE;
}
}
HungryStaticSingleton
public class HungryStaticSingleton {
private HungryStaticSingleton(){}
private static final HungryStaticSingleton instance;
static {
instance = new HungryStaticSingleton();
}
public static HungryStaticSingleton getInstance(){
return instance;
}
}
/*两种饿汉单例模式写法不同,实际上一模一样,第二种只是用来提升逼格*/
**优点:**执行效率高,没有任何的锁。
**缺点:**在某些情况下会造成内存浪费,项目中需要单例的Bean过多的时候,造成内存浪费。
2.2 懒汉式单例之简单写法与优缺点
LazySimpleSingleton
public class LazySimpleSingleton {
private LazySimpleSingleton(){}
private static LazySimpleSingleton instance;
public static LazySimpleSingleton getInstance(){
if (instance == null){
instance = new LazySimpleSingleton();
}
return instance;
}
}
被外部类使用时才创建实例
**优点:**节省内存
**缺点:**非线程安全
验证
// 创建线程类
public class MyThread extends Thread {
@Override
public void run() {
LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
System.out.println(instance);
}
}
// 测试类
public class Test {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.start();
thread2.start();
System.out.println("END");
}
}
// 控制台打印
END
com.gy.lazy.simpleLazy.LazySimpleSingleton@32e3276d
com.gy.lazy.simpleLazy.LazySimpleSingleton@1d296048
**解决办法:**给方法上枷锁
public class LazySimpleSingleton2 {
private LazySimpleSingleton2(){}
private static LazySimpleSingleton2 instance;
public synchronized static LazySimpleSingleton2 getInstance(){
if (instance == null){
instance = new LazySimpleSingleton2();
}
return instance;
}
}
**缺点:**如果加锁synchronized,实现线程安全,则会造成性能低。
2.3 懒汉式单例之双重检查锁写法与优缺点
被外部类使用时才创建实例
LazyDoubleCheckSingleton
针对性能低的问题,解决办法,先判断是否存在实例,再给创建对象加锁,再锁里面在判断一次是否存在实例。
public class LazyDoubleCheckSingleton {
private LazyDoubleCheckSingleton(){}
private volatile static LazyDoubleCheckSingleton instance;
public static LazyDoubleCheckSingleton getInstance(){
// 判断是否加锁
if(instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
// 判断是否新建实例
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
// 指令重排序的问题,导致线程紊乱。用volatile关键字解决。
}
}
}
return instance;
}
}
**优点:**提高性能,并且线程安全
**缺点:**程序可读性差
2.4 懒汉式单例之静态内部类写法与优缺点
被外部类使用时才创建实例
LazyStaticInnerClassSingleton,内部类 LazyHolder
public class LazyStaticInnerClassSingleton {
// 内部类创建实例
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
private LazyStaticInnerClassSingleton(){}
public LazyStaticInnerClassSingleton getInstance(){
return LazyHolder.INSTANCE;
}
}
**优点:**代码优雅,利用了java本身的语法特点,性能高,避免内存浪费
**缺点:**以上几种单例模式的通病,能够被反射破坏
**解决方法:**通过在构造器中判断内部类不为空则抛出异常,以达到不被反射破坏的目的
三、 反射是如何破坏单例的?
通过反射创建实例:
public class Test {
public static void main(String[] args) {
// 拿到反射
Class<?> clazz = LazyStaticInnerClassSingleton.class;
try {
// 获取构造方法
Constructor<?> c = clazz.getDeclaredConstructor();
// 设置构造方法访问权限为public
c.setAccessible(true);
Object obj1 = c.newInstance();
Object obj2 = c.newInstance();
System.out.println(obj1);
System.out.println(obj2);
System.out.println(obj1 == obj2);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
// 控制台打印
com.gy.lazy.staticInnerClass.LazyStaticInnerClassSingleton@7382f612
com.gy.lazy.staticInnerClass.LazyStaticInnerClassSingleton@1055e4af
false
解决办法:
public class LazyStaticInnerClassSingleton2 {
// 内部类创建实例
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton2 INSTANCE = new LazyStaticInnerClassSingleton2();
}
private LazyStaticInnerClassSingleton2(){
if (LazyHolder.INSTANCE != null){
throw new RuntimeException("非法访问");
}
}
public LazyStaticInnerClassSingleton2 getInstance(){
return LazyHolder.INSTANCE;
}
}
// 控制台打印 抛出异常
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.gy.lazy.staticInnerClass.Test.main(Test.java:23)
Caused by: java.lang.RuntimeException: 非法访问
at com.gy.lazy.staticInnerClass.LazyStaticInnerClassSingleton2.<init>(LazyStaticInnerClassSingleton2.java:17)
... 5 more
四、 注册式单例
**定义:**将每一个实例都缓存到统一的容器中,使用唯一标识符获取单例。
4.1 枚举式单例写法与优缺点
优点:代码优雅,线程安全
缺点:造成内存浪费
等同于饿汉模式,但是不能通过反射破坏单例,因为枚举类不能通过反射创建实例。
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;
}
}
4.2 容器式单例写法与优缺点
ContainerSingleton
public class ContainerSingleton {
private ContainerSingleton(){}
// 容器
private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
// 反射创建实例,放入容器中
public static Object getInstance(String className){
Object instance = null;
synchronized (ContainerSingleton.class) {
if (!ioc.containsKey(className)) {
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
} catch (Exception e) {
e.printStackTrace();
}
return instance;
} else {
return ioc.get(className);
}
}
}
}
优点:线程安全,效率快
缺点:序列化会破坏单例
五、 序列化是如何破坏单例的?
public class SeriableSingleton {
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton(){}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
}
// 测试类
public class Test {
public static void main(String[] args) {
// Student instance1 = (Student)ContainerSingleton.getInstance("com.gy.register.Student");
// Student instance2 = (Student)ContainerSingleton.getInstance("com.gy.register.Student");
// System.out.println(instance1 == instance2);
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
s1 = (SeriableSingleton)ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 结果
com.gy.register.SeriableSingleton@7cca494b
com.gy.register.SeriableSingleton@7f31245a
false
解决方法:防止序列化破坏单例模式,加入readResolve()方法,方法名与返回值为固定写法。
public class SeriableSingleton implements Serializable {
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton(){}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
private Object readResolve(){ return INSTANCE;}
}
// 结果
com.gy.register.SeriableSingleton@7f31245a
com.gy.register.SeriableSingleton@7f31245a
true
六、 ThreadLocal单例介绍
ThreadLocalSinglton
public class ThreadLocalSingleton {
private ThreadLocalSingleton(){}
private static final ThreadLocal<ThreadLocalSingleton> INSTANCE =
new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
public static ThreadLocalSingleton getInstance(){
return INSTANCE.get();
}
}
线程类
public class MyThread extends Thread {
@Override
public void run() {
ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
System.out.println(instance);
}
}
测试类
public class Test{
public static void main(String[] args) {
ThreadLocalSingleton t1 = ThreadLocalSingleton.getInstance();
ThreadLocalSingleton t2 = ThreadLocalSingleton.getInstance();
System.out.println(t1 == t2);
System.out.println("--------------------------------------------------");
MyThread thread1 = new MyThread();
thread1.start();
MyThread thread2 = new MyThread();
thread2.start();
}
}
// 结果
true
--------------------------------------------------
com.gy.threadLocal.ThreadLocalSingleton@7c2aa267
com.gy.threadLocal.ThreadLocalSingleton@731f7d35
**结论:**不是真正的全局单例,只是保证线程内部的全局唯一,但是天生线程安全。
七、单例模式在源码中的应用
spring中的:AbstractFactoryBean类getObject()方法
mybatis中的:ErrorContext类应用到了ThreadLocal单例
结合源码实例更好理解设计模式。
八、总结
1、优点
在内存中只有一个实例,减少了内存开销;
可以避免对资源的多重占用;
设置全局访问点,严格控制访问。
2、缺点
没有接口,很难扩展;
如果要扩展单例对象,只能修改代码,没有其他途径。
3、知识点
1、私有化构造器;
2、保证线程安全;
3、延迟加载;
4、防止序列化与反序列化;
5、防止反射破坏单例。