ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

从头开始学java架构之设计模式2:单例模式详解

2019-06-30 18:53:53  阅读:181  来源: 互联网

标签:线程 java getInstance static 单例 new 设计模式 public


单例模式-是指确保任何一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用。

单例模式是创建型模式,在现实生活中类似于国家主席,公司CEO等只能有一个的模式。在J2EE标准中,ServletContext、ServletContextConfig等;在Spring框架应用中ApplicationContext;数据库连接池也都是单例形式。

主要分为1.饿汉式单例、2.懒汉式单例、3.注册式单例和4.ThreadLocal线程单例。

一.饿汉式单例:

在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题。适用于单例对象较少的情况。

优点:没有加任何锁、执行效率比较高,在用户体验上,比懒汉式要好。

缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费内存。

Spring中IOC容器ApplicationContext本身就是典型的饿汉式单例。

代码案例:

 

public class HungrySingleton {
    //在类刚刚被加载还没有被实例化的时候就实例化一个对象,如果不加final,那么这个对象可能会被别人通过反射机制啥的覆盖掉
    private static final HungrySingleton hungrysingleton = new HungrySingleton();
    
    private HungrySingleton(){}    //构造方法私有化
    
    public static HungrySingleton getInstance(){    //设立一个全局访问点,一般都叫getInstance这个名字。
        return hungrysingleton;
    }

}

 

还有一种利用静态代码块机制的写法—

public class HungryStaticSingleton {
    //在类刚刚被加载还没有被实例化的时候就实例化一个对象,如果不加final,那么这个对象可能会被别人通过反射机制啥的覆盖掉
    private static final HungryStaticSingleton hungrystaticsingleton;
    
    static{
        hungrystaticsingleton = new HungryStaticSingleton();
    }
    
    private HungryStaticSingleton(){};    //构造方法私有化
    
    public static HungryStaticSingleton getInstance(){    //设立一个全局访问点,一般都叫getInstance这个名字。
        return hungrystaticsingleton;
    }

}

用static静态代码块的优点是:静态代码块会随着类的加载而执行,而且只执行一次!这样每次调用这个方法的时候,可以保证只需要实例化一次对象,而不是每次调用都实例化一次。

 

二.懒汉式单例:

被外部类调用的时候才加载,这就是懒汉式单例。

代码案例:

实现LazySimpleSingleton:

如果不加synchronized关键字,那么前面的线程会被后面的线程覆盖掉,线程不安全。

但是,用synchronized加锁,在线程数量比较多的情况下,如果CPU分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降。

 

 

public class LazySimpleSingleton {
    //还是在类加载时就定义一个对象等于null,但是不实例化它,而是在调用getInstance方法的时候才实例化它,这就是懒汉式单例,
    //此时就不需要加final了,因为加了final之后,就无法再在后面去实例化它了。
    private static LazySimpleSingleton lazy = null;
    
    //私有化构造方法
    private LazySimpleSingleton(){}
    
    //设立一个全局访问点
    //public static LazySimpleSingleton getInstance(){
    
    //加上synchronized关键字之后,就不会发生线程不安全的问题了,只能等到一个线程结束后,另一个线程才会执行
    //但是synchronized还是存在性能问题,而且可能导致整个类都被锁住
    public synchronized static LazySimpleSingleton getInstance(){
        if(lazy == null){  //如果不加这个判断,那么每次调用都是重新实例化
            lazy = new LazySimpleSingleton();
        }
        return lazy;
    }

}

 

然后写一个线程类ExectorThread类:

public class ExectorThread implements Runnable{

    @Override
    public void run() {
        // TODO Auto-generated method stub
        LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();
        System.out.println(Thread.currentThread().getName()+ ":" +singleton);
    }

}

客户端测试代码:

public class LazySimpleSingletonTest {
    public static void main(String[] args){
        //LazySimpleSingleton.getInstance();
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        
        t1.start();
        t2.start();
        
        //通过两个线程来调用LazySimpleSingleton的getInstance方法,可以看到是new了两次LazySimpleSingleton对象,
        //不符合单例模式"一个类在任何情况下都绝对只有一个实例"的原则,线程不安全
        
        System.out.println("Exector End");
    }

}

可以看到,这种单例模式对性能是有一定的影响的,那么,有没有一种更好的方式,既兼顾线程安全又提升程序性能呢?那就是——双重检查锁:

它的原理是把synchronized关键字放到方法里,这样就不会锁住整个类了。在synchronized里面再加个if判断,这就是双重检查。

要注意,synchronized外层最好也加个if判断,否则每次调用都要重新实例化,还有个指令重排序问题,最好加个volatile关键字解决。

public class LazyDoubleCheckSingleton {
    //还是在类加载时就定义一个对象等于null,但是不实例化它,而是在调用getInstance方法的时候才实例化它,这就是懒汉式单例,
        //此时就不需要加final了,因为加了final之后,就无法再在后面去实例化它了。
        //volatile关键字是解决下面的指令重排序的问题的
        private volatile static LazyDoubleCheckSingleton lazy = null;
        
        //私有化构造方法
        private LazyDoubleCheckSingleton(){}
        
        //
        public static LazyDoubleCheckSingleton getInstance(){
            if(lazy == null){  //如果不加这个判断,那么每次调用都是重新实例化;外层if不能被去掉
                //把synchronized关键字放到方法里面,就不会锁住整个类了
                //双重锁指的是synchronized和它里面那个if
                synchronized(LazyDoubleCheckSingleton.class){
                    if(lazy == null){
                        lazy = new LazyDoubleCheckSingleton();
                        //CPU执行的时候会转换成JVM指令执行
                        //1、分配内存给这个对象
                        //2、初始化对象
                        //3、将初始化好的对象和内存地址建立关联(即赋值)
                        //4、用户初次访问
                        //其中,第2步和第3步执行顺序可能会不一样,所以可能会发生指令重排序的问题,可以使用volatile关键字解决
                    }
                }
            }
            return lazy;
        }

}

但是,使用synchronized关键字,总归是要影响性能的,我们可以使用静态内部类的方式来更好的实现单例:

它的原理是内部类要在方法调用前初始化,只有被调用才会执行,所以巧妙的避免了线程安全问题。

public class LazyInnerClassSingleton {
    //私有化构造方法
    //虽然构造方法私有了,但是仍然会被反射攻击
    private LazyInnerClassSingleton(){
        //这个if是为了防止反射破坏单例,如果对象已经被创建了,就直接抛出一个异常
        if(LazyHolder.LAZY != null){
            throw new RuntimeException("不允许创建多个实例");
        }
    }
    
    //因为内部类先加载,所以用户调用getInstance的时候,就会执行内部类中的逻辑,所以可以理解为懒汉式单例
    //LazyHolder里的逻辑要等到外部方法调用时才执行,巧妙的利用了内部类的特性
    //JVM底层执行逻辑,完美的避免了线程安全问题
    public static final LazyInnerClassSingleton getInstance(){
        return LazyHolder.LAZY;
    }
    
    //定义一个内部类LazyHolder
    //这里看起来是饿汉式单例,但是由于它的逻辑只有在被调用时才执行,所以是懒汉式
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }

}

反射破坏单例可以通过上面的方法来防止,但是仍然可以通过序列化和反序列化来破坏单例——

序列化是把内存中的状态持久化到磁盘或者网络IO等中,从而使对象的内存状态永久保留下来,反序列化就是反其道而行之。反序列化后的对象会重新分配内存,即重新创建,如果序列化的目标的对象为单例对象,就违反了单例模式的初衷,相当于破坏了单例。

解决方法就是重写readResolve()方法。至于为什么,就要看JDK源码了。总之这样写就好。

public class SeriableSingleton implements Serializable{
    /*
     * 序列化就是把内存中的状态通过转换成字节码的形式,
     * 从而转换一个IO流,写入到其他地方(可以是磁盘,网络IO),
     * 从而使内存中的状态永久保留下来
     */
    
    /*
     * 反序列化
     * 将已经持久化的字节码内容,转换为IO流
     * 通过IO流的读取,进而将读取的内容转换为Java对象
     * 在转换过程中会重新创建对象new
     */
    
    public final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}
    
    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }
    
    //这段代码就是防止序列化破坏单例的,通过重写readResolve方法,就可以防止单例被破坏
    //重写readResolve方法,只不过是覆盖了反序列化出来的对象
    //在JVM层面,对象还是被创建了两次
    //之前反序列化出来的对象会被GC回收
    private Object readResolve(){
        return INSTANCE;
    }

}
public class SeriableSingletonTest {
    public static void main(String[] args){
        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);
            
            //强转为SeriableSingleton类型的对象
            s1 = (SeriableSingleton)ois.readObject();
            ois.close();
            
            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);
        }catch(Exception e){
            e.printStackTrace();
        }
    }

}

但是,虽然重写readResolve()方法,让它返回实例可以解决单例被破坏的问题,但是实际上还是实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率增大,就意味着内存分配开销也随之增大。那么,如何从根本上解决问题呢?

 

三.注册式单例:

注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。

注册式单例有两种写法,一种为容器缓存,一种是枚举登记。先看枚举登记。

代码案例:

创建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;
    }

}

 

创建测试代码:

经过测试,发现两个对象是相等的,也就是说它俩是同一个对象。

通过查看JDK源码,可以发现枚举类型其实是通过类名和class对象类找到一个唯一的枚举对象,因此,枚举对象不可能被类加载器加载多次。

而且,源码里也做了强制性的判断,如果修饰符是Modifier.ENUM枚举型,直接抛出异常,说明反射也无法破坏枚举式单例。

所以,枚举式单例是比较推荐的一种单例实现写法

public class EnumSingletonTest {
    public static void main(String[] args) {
        try{
            EnumSingleton instance1 = null;
            
            EnumSingleton instance2 = EnumSingleton.getinstance();
            instance2.setData(new Object());
            
            FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(instance2);
            oos.flush();
            oos.close();
            
            FileInputStream fis = new FileInputStream("EnumSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            instance1 = (EnumSingleton)ois.readObject();
            ois.close();
            
            System.out.println(instance1.getData());
            System.out.println(instance2.getData());
            System.out.println(instance1.getData() == instance2.getData());
        }catch(Exception e){
            e.printStackTrace();
        }
    }

}

 注册式单例还有一种容器缓存的写法,适用于创建实例非常多的情况,便于管理。但是,它是非线程安全的。

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(className, obj);
                }catch(Exception e){
                    e.printStackTrace();
                }
                return obj;
            }else{
                return ioc.get(className);
            }
        }
    }

}

 

四.ThreadLocal线程单例:

ThreadLocal将所有的map都放入ThreadLocalMap中,为每个线程都提供一个对象,它不能保证其创建的对象是全局唯一,但能保证在单个线程中是唯一的,一时间换空间来实现线程间隔离。

代码案例:

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

}

测试代码:

可以发现,在主线程main中无论调用多少次,获取到的实例都是同一个,都在两个子线程中分别获取到了不同的实例。

public class ThreadLocalSingletonTest {
    public static void main(String[] args){
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
        System.out.println("End");
    }

}

 

标签:线程,java,getInstance,static,单例,new,设计模式,public
来源: https://www.cnblogs.com/yinyj/p/11110110.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有