单例模式

单例模式第一版:

public class Singleton1 {
	
	private Singleton1(){}//私有化构造函数
	
	private static Singleton1 instance = null;//单例对象
	
	//静态工厂方法
	public static Singleton1 getInstance(){
		if(instance == null){
			instance = new Singleton1();
		}
		return instance;
	}
}

 如上单例模式却不是线程安全的。两个线程同时满足 if(instance == null){}就会new两次

改进:单例模式第二版:

  

public class Singleton1 {
	
	private Singleton1(){}//私有化构造函数
	
	private static Singleton1 instance = null;//单例对象
	
	//静态工厂方法
	public static Singleton1 getInstance(){
		if(instance == null){//双重检测机制
			synchronized (Singleton1.class) {
				if(instance == null){//双重检测机制,防止A线程进来new了对象后释放锁,B线程获得所进来直接又new对象
					instance = new Singleton1();
				}
			}
		}
		return instance;
	}
}

  但是上述代码,也会存在线程安全问题。原因:可能会出现JVM指令重排

  一般来说,对于instance = new Singleton1(),一般有三步,1.分配内存对象 2. 初始化对象 3. 将instance指向分配的内存地址

  但是这些指令顺序不是一成不变的,有可能会经过JVM和CPU的优化,指令重排城下面的顺序。

  1.分配内存对象 2. 将instance指向分配的内存地址  3. 初始化对象

  当A执行完2时,被B抢占线程执行权时,B会判断 if(instance == null)发现不为null,则会返回未初始化的对象。

  优化方案单例模式第三版:

public class Singleton3 {
    
private Singleton3(){}//私有化构造函数
    
    private volatile static Singleton3 instance = null;//单例对象
    
    //静态工厂方法
    public static Singleton3 getInstance(){
        if(instance == null){//双重检测机制
            synchronized (Singleton1.class) {
                if(instance == null){//双重检测机制
                    instance = new Singleton3();
                }
            }
        }
        return instance;
    }
}

  加volatile关键字,volatile会禁用编译优化,并强制刷新缓存。对于instance = new Singleton1(),一定会是,1.分配内存对象 2. 初始化对象 3. 将instance指向分配的内存地址

  加final关键字,禁用编译优化重排序

  用静态内部类实现单例:

  

/**
 * 
 * @author FL
 *	内部类只有当外部类被调用的时候才会加载
 *	兼顾了饿汉式的内存浪费也兼顾了懒汉式1、2的问题
 */
public class LazyThree {

	//private  boolean intialized = false;//怎么防止反射暴力修改

	// 默认使用的时候会初始化内部类,在线程之前被初始化,巧妙的避免了线程安全的问题
	// 如果没使用的时候内部类是不加载的
	private LazyThree() {
		//防止反射暴力访问
		synchronized (LazyThree.class) {
			if (Sign.intialized == false) {
				Sign.intialized = true;
			} else {
				throw new RuntimeException("单例已经被侵犯");
			}
		}
	}
	//static 使其可以被共享
	//final 保证其不会被重写、重载
	public static final LazyThree getInstance(){
		return LazyHold.lazy;
	}
	
	public static class LazyHold{
		
		private static final LazyThree lazy = new LazyThree();
	}
	public static class Sign{
		
		private  static boolean intialized = false;
	}
}

  简化版:利用反射可以打破单例

public class Singleton4 {
	
    private Singleton4(){}//私有化构造函数

    private static class LazyHolder{
    	private static final Singleton4 instance = new Singleton4();
    }
	//静态工厂方法
	public static Singleton4 getInstance(){
		return LazyHolder.instance;
	}
}

  反射代码:

                        Class<?> clazz=LazyThree.class;
			Constructor<?> c = clazz.getDeclaredConstructor();//获取构造器
			//反射暴力访问
			c.setAccessible(true);
			Object O1 = c.newInstance();
			Object O2 = c.newInstance();
			System.out.println(O1);
			System.out.println(O2);
			/**
			 * 通过反射已经拿到对象了,在通过类本身调静态方法获取对象时,
			 * 就会抛出异常,单例被侵犯
			 */
			Object O3=clazz.newInstance();
			System.out.println(O3);

  

1.从外部无法访问静态内部类,Instance初始化的时机不是在LazyThree 被加载的时候,而是在调用LazyThree .getInstance()的时候LazyHold被加载的时候。这种实现方式利用了classLoader的加载机制来实现懒加载,并保证单里的线程安全。

  用枚举实现单例模式:

  

public enum SingletonEnun {
	
	INSTANCE;
}
SingletonEnun类被加载的时候就初始化了,属于饿汉式。Enum语法糖,JVM会阻止反射获取枚举的私有方法。 

使用枚举实现的单例模式,不但可以防止反射强行构建单例对象,而且可以在枚举对象被反序列化时,保证反序列化的结果是同一对象。对于其他实现的单例模式,要想可序列化、反序列化为同一对象,则必须实现readResolve方法。

枚举解析:

  

定义枚举时使用enum和class一样,是Java中的一个关键字。就像class对应用一个Class类一样,enum也对应有一个Enum类。

通过将定义好的枚举反编译,我们就能发现,其实枚举在经过javac的编译之后,会被转换成形如public final class T extends Enum的定义。

而且,枚举中的各个枚举项同事通过static来定义的。如:

public enum T {
    SPRING,SUMMER,AUTUMN,WINTER;
}
反编译后代码为:

public final class T extends Enum
{
    //省略部分内容
    public static final T SPRING;
    public static final T SUMMER;
    public static final T AUTUMN;
    public static final T WINTER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}
了解JVM的类加载机制应该对这部分比较清楚。static类型的属性会在类被加载之后被初始化,我们在深度分析Java的ClassLoader机制(源码级别)中介绍过,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。

也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。

所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。

  

原文地址:https://www.cnblogs.com/flgb/p/10639467.html