• 单例模式


    单例模式第一版:

    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类型是线程安全的。
    
    也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。
    
    所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。
    

      

  • 相关阅读:
    [SQL SERVER] The CHECK_POLICY and CHECK_EXPIRATION options cannot be turned OFF when MUST_CHANGE is ON. (Microsoft SQL Server, Error: 15128)
    CENTOS7 SYSTEMD SERVICE 将自己的程序放入自动启动的系统服务
    CentOS7 关闭selinux
    面试总结TODO
    很好用的 UI 调试技巧
    点满 webpack 技能点,让你的打包速度提效 90%
    前端缓存最佳实践
    Fiddler抓包工具总结
    按钮粒子效果
    如何优雅的在 vue 中添加权限控制
  • 原文地址:https://www.cnblogs.com/flgb/p/10639467.html
Copyright © 2020-2023  润新知