java实现23种设计模式之单例模式
单例模式就是全局对象只有一个,比如开发中spring中常常使用到的bean;
跟静态类有点类似,但是静态类局限很多,不能被继承等。
单例模式又分为饿汉模式和懒汉模式。
饿汉模式是在加载类的时候就创建了实例,不管这个实例能不能用的到;
懒汉模式则是延时加载,用到的时候再创建实例。但是线程不安全。
饿汉模式:
package com.ceshi; public class Singleton { //1.将构造方法私有化,不允许外部直接创建对象 private Singleton(){ } //2.创建类的唯一实例,使用private static修饰 private static Singleton instance=new Singleton(); //3.提供一个用于获取实例的方法,使用public static修饰 public static Singleton getInstance(){ return instance; } public Integer getNum() { return num; } public void setNum(Integer num) { this.num = num; } private Integer num=0; }
package com.ceshi; public class Test { public static void main(String[] args) { //饿汉模式 Singleton s1=Singleton.getInstance(); s1.setNum(2); Singleton s2=Singleton.getInstance(); System.out.println(s2.getNum()); if(s1==s2){ System.out.println("s1和s2是同一个实例"); }else{ System.out.println("s1和s2不是同一个实例"); } } }
输出的是同一个实例,且num为2,所有属性被共用。
懒汉模式:
package com.ceshi; /* * 懒汉模式 * 区别:饿汉模式的特点是加载类时比较慢,但运行时获取对象的速度比较快,线程安全 * 懒汉模式的特点是加载类时比较快,但运行时获取对象的速度比较慢,线程不安全 */ public class Singleton2 { //1.将构造方式私有化,不允许外边直接创建对象 private Singleton2(){ } //2.声明类的唯一实例,使用private static修饰 private static Singleton2 instance; //3.提供一个用于获取实例的方法,使用public static修饰 public static Singleton2 getInstance(){ if(instance==null){ instance=new Singleton2(); } return instance; } }
package com.ceshi; public class Test { public static void main(String[] args) { //懒汉模式 Singleton2 s3=Singleton2.getInstance(); Singleton2 s4=Singleton2.getInstance(); if(s3==s4){ System.out.println("s3和s4是同一个实例"); }else{ System.out.println("S3和s4不是同一个实例"); } } }
结果是同一个实例。
测试线程问题,防止电脑性能好,加一点东西
package com.ceshi; /* * 懒汉模式 * 区别:饿汉模式的特点是加载类时比较慢,但运行时获取对象的速度比较快,线程安全 * 懒汉模式的特点是加载类时比较快,但运行时获取对象的速度比较慢,线程不安全 */ public class Singleton2 { //1.将构造方式私有化,不允许外边直接创建对象 private Singleton2(){ } //2.声明类的唯一实例,使用private static修饰 private static Singleton2 instance; //3.提供一个用于获取实例的方法,使用public static修饰 public static Singleton2 getInstance(){ if(instance==null){ try { // 模拟在创建对象之前做一些准备工作 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } instance=new Singleton2(); } return instance; } }
package com.ceshi; public class Test { public static void main(String[] args) { //懒汉模式 Thread2[] threads = new Thread2[10]; /*for (Thread2 thread2 : threads) { thread2 = new Thread2(); thread2.start(); }*/ for (int i = 0; i < threads.length; i++) { threads[i] = new Thread2(); threads[i].start(); } } } //测试线程 class Thread2 extends Thread { @Override public void run() { System.out.println(Singleton2.getInstance().hashCode()); } }
测试结果显示
完全不一样
如何解决?首先会想到对getInstance方法加synchronized关键字
public static synchronized Singleton2 getInstance(){ if(instance==null){ try { // 模拟在创建对象之前做一些准备工作 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } instance=new Singleton2(); } return instance; }
结果又变得一样了。
这样的用法,在性能上会有所下降,因为每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,需要进行改进。
package com.ceshi; /* * 懒汉模式 * 区别:饿汉模式的特点是加载类时比较慢,但运行时获取对象的速度比较快,线程安全 * 懒汉模式的特点是加载类时比较快,但运行时获取对象的速度比较慢,线程不安全 */ public class Singleton2 { //1.将构造方式私有化,不允许外边直接创建对象 private Singleton2(){ } //2.声明类的唯一实例,使用private static修饰 private static Singleton2 instance; private static synchronized void syncInit() { if (instance == null) { instance = new Singleton2(); } } public static Singleton2 getInstance() { if (instance == null) { syncInit(); } return instance; } }
这样就只创建了一次实例
除非使用枚举类型的单例模式,否则其他模式都可以通过反射机制调用私有的构造方法。
package com.ceshi; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; /** * 单例模式被java反射攻击 * @author Administrator * */ public class SingletonReflectAttack { public static void attack() throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, SecurityException, NoSuchMethodException { Class<?> classType = Singleton2.class; Constructor<?> constructor = classType.getDeclaredConstructor(null); constructor.setAccessible(true); Singleton2 singleton = (Singleton2) constructor.newInstance(); Singleton2 singleton2 = Singleton2.getInstance(); System.out.println(singleton == singleton2); //false } }
package com.ceshi; import java.lang.reflect.InvocationTargetException; public class SingletonReflectAttackMain { public static void main(String[] args) throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, SecurityException, NoSuchMethodException { System.out.println("-------------单例模式被java反射攻击测试--------------"); SingletonReflectAttack.attack(); System.out.println("--------------------------------------------------"); } }
结果:
返回结果为false,说明创建了两个不同的实例。通过反射获取构造函数,然后调用setAccessible(true)就可以调用私有的构造函数;所以创建出来的两个实例时不同的对象。
如果要抵御这种攻击,就要修改构造器,让他在被要求创建第二个实例的时候抛出异常。
package com.ceshi; /* * 懒汉模式 * 区别:饿汉模式的特点是加载类时比较慢,但运行时获取对象的速度比较快,线程安全 * 懒汉模式的特点是加载类时比较快,但运行时获取对象的速度比较慢,线程不安全 */ public class Singleton2 { //1.将构造方式私有化,不允许外边直接创建对象 private Singleton2(){ synchronized (Singleton2.class) { if(false == flag) { flag = !flag; } else { throw new RuntimeException("单例模式正在被攻击"); } } } //2.声明类的唯一实例,使用private static修饰 private static Singleton2 instance; private static boolean flag = false; private static synchronized void syncInit() { if (instance == null) { instance = new Singleton2(); } } public static Singleton2 getInstance() { if (instance == null) { // syncInit(); synchronized (Singleton2.class) { if (instance == null) { //双重校验 instance = new Singleton2(); } } } return instance; } }
在私有的构造方法中再设置同步锁:
抛出异常,如果需要防止此类事件发生,可以通过捕获异常解决问题。