单例模式

单例模式

1 饿汉模式

private static Singleton single=new Singleton();

特点: 饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全 ,但是缺点是内存开销( 用与不用都占着空间 )。

2 懒汉模式

public class Singleton1 {
	//1、第一步先将构造方法私有化
	private Singleton1() {}
	//2、然后声明一个静态变量保存单例的引用
	private static Singleton1 single = null;
	//3、通过提供一个静态方法来获得单例的引用
	
	public static Singleton1 getInstance() {
		if (single == null) {
			single = new Singleton1();
		}
		return single;
	}
}

特点:被外部类调用的时候内部类才会加载 ,线程不安全

为什么线程不安全?

显而易见,多线程可能同时进入到single = new Singleton1();

解决线程不安全

getInstance()方法加上synchronized关键字保证某时刻只能有一个线程进入到getInstance()方法。

当第一个线程执行到 synchronized 会上锁 ,第二个钱程试图执行getInstance()方法就会变成 MONITOR 状态,出现阻塞。

synchronized锁住整个方法,存在性能问题
双重校验锁模式

一种更细腻,优化后的的synchronized。

public class Singleton {
 
    //volatile变量  保证可见性、禁止指令重排序   不保证原子性
    private volatile Singleton singleton;
 
    public Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    //三步执行:
                    // 1.为singleton分配内存空间
                    // 2.初始化singleton
                    // 3.将singleton指向分配的内存地址
                    //但是由于JVM具有指令重排的特性,执行的顺序可能会1>3>2,
                    // 指令重排在单线程下不会有线程安全问题,
                    // 但在多线程环境下,会导致一个线程获得还没有初始化的实例
                    //例如线程T1执行1、3,此时线程T2调用getSingleton()方法后发现singleton不为空,
                    //因此返回singleton,但此时singleton 还未被实例化
                    //使用volatile关键字可以禁止指令重排序,保证在多线程环境下能正常运行
                    singleton = new Singleton();
                }
 
            }
        }
        return singleton;
    }
}

3 内部类

  public class LazyInnerClassSingleton{
     private LazyInnerClassSingleton(){}
    public static Singleton1 getInstance() {
		return LazyHolder.INSTANCE;
	}

    //默认不加载
	private static class LazyHolder{
         private static final LazyInnerClassSingleton INSTANCE=new   LazyInnerClassSingleton();
	}
}

内部类的静态变量默认不加载。

这种方式兼顾了饿汉式单例模式的内存浪费问题和 synchronzed 的性能问题。

4 通过反射破坏单例

以上的构造方法除了加上 private 关键字,没有做任何处理 如果我们使用反射来调用其构造方法,再调用 getlnstance()方法,应该有两个不同的实例

        Class<?> clazz = LazyInnerClassSingleton.class;
		Constructor<?> c = clazz.getDeclaredConstructor(null);
		c.setAccessible(true);
		//相当于new了两次
		Object o1 = c.newInstance();
		Object o2 = c.newInstance();
		System.out.priintln(o1==o2); //false

5 通过序列化破坏单例

一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。 如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例

public class Singleton implements Serializable{...}

测试:

        Singleton1 s1=null;
        Singleton1 s2=Singleton1.getInstance();
		FileOutputStream fos=null;
		fos=new FileOutputStream("SeriableSingleton.obj");
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(s2);
		oos.close();

		FileInputStream fis=new FileInputStream("SeriableSingleton.obj");
		ObjectInputStream ois = new ObjectInputStream(fis);
		 s1 = (Singleton1) ois.readObject();
		 ois.close();

		System.out.println(s1==s2);  //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;
    }
}
为什么加上readResolve()可以保证反序列化的对象与源对象相等?

从 ois.readObject()说起,通过jdk源码发现是通过反射获取INSTANCE对象,开销大

readResolveMethod.invoke(obj, (Object[]) null);

注册式单例模式

注册式单例模式分两种:一 种为枚举式单例模式 ,另一种为容器式单例模式

枚举式单例
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;
    }

    public static void main(String[] args) throws Exception {
        EnumSingleton instance1=null;
        EnumSingleton instance2 = EnumSingleton.getInstance();
        instance2.setData(new Object());
        
        FileOutputStream fos=null;
        fos=new FileOutputStream("EnumSingleton.obj");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(instance2);
        oos.close();

        FileInputStream fis=new FileInputStream("EnumSingleton.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        instance1 = (EnumSingleton) ois.readObject();
        ois.close();

        System.out.println(instance1.getData()==instance2.getData()); //true
    }
}

通过jad工具反编译EnumSingleton.class 发现枚举模式类似饿汉模式,在静态代码块给实例进行了赋值

 static 
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }

1 验证序列化能否破坏枚举单例。看源码ois.readObject->readObject0->readEnum发现

Enum<?> en = Enum.valueOf((Class)cl, name);

原来枚举类型是通过Enum.valueOf方法返回

2 验证能否通过反射创建枚举类型

测试代码

        Class clazz= EnumSingleton.class;
        Constructor c = clazz.getDeclaredConstructor();
        c.newInstance();

运行抛出异常java.lang.NoSuchMethodException.

意思是没有找到无参的构造方法,看Enum类发现只有一个protected构造方法

 protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

更换测试代码

        Class clazz= EnumSingleton.class;
        Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
        c.setAccessible(true);
        c.newInstance();

再次报错 java.lang.IllegalArgumentException: Cannot reflectively create enum objects

即不能用反射来创建枚举类型

容器式单例
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();
                           ioc.put(class,obj);
                       }catch(Exception e){
                           e.printStackTrace();
                       }
                       return obj;
                  } else{
                     return ioc.get(className);
                  }
             }
     }
}

容器式单例模式适用于实例非常多的情况,便于管理。但它是非线程安全的。 存在序列化和反射攻击问题

线程单例实现ThreadLocal

ThreadLocal不能保证其创建的对象是全局的,但是能保证在单个线程中是唯一的,天生是线程安全的

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();
     }
}

测试代码

         System.out.println(ThreadLocalSingleton.getInstance());
         System.out.println(ThreadLocalSingleton.getInstance());
         System.out.println(ThreadLocalSingleton.getInstance());     

         Thread t1 = new Thread(new ExecutorThread());
         Thread t2 = new Thread(new ExecutorThread());
         t1.start();
         t2.start();
         System.out.println("End");

结果

com.gupaoedu.singleton.ThreadLocalSingleton@1b6d3586
com.gupaoedu.singleton.ThreadLocalSingleton@1b6d3586
com.gupaoedu.singleton.ThreadLocalSingleton@1b6d3586
End
Thread-0com.gupaoedu.singleton.Singleton1@4f9d1ec9
Thread-1com.gupaoedu.singleton.Singleton1@4f9d1ec9

我们发现,在主线程中无论调用多少次,获取到的实例都是同一个。在两个子线程中分别获取到了不同的实例。那么ThreadLocal是如何实现这样的效果的呢?我们知道,单例模式为了达 到线程安全的目的,会给方法上锁,以时间换空间,ThreadLocal将所有的对象全部放在ThreadLocalMap 中,为 个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值