HeadFast设计模式-单例模式

什么是单例模式:

众所周知,java中的对象是new出来的,如果没有对象,就new 出来一个,每一个new出来的对象都是这个java类的实例,如果不加以限制,每一个java类都可以new出无数个对象
这些new出来的对象并不是平白无故就产生的,在创建的过程中,需要占用内存,占用资源等等。
对于普通对象而言,多一个少一个并没有区别。但是对于一些占用有限资源的对象,如线程池,缓存等,如果new出来的相关的对象多了,很快就把资源分配完,如果再新的请求new对应对象,由于已经没有资源,会导致系统异常。
为了有效的利用有限的资源,单例模式出现了。顾名思义,在系统中,每个java类只会创建一个实例对象,实例化的过程可以放在系统启动的时候,也可以放在第一次调用的时候。当实例化完成后,无论调用多少次,返回的都是同一个对象。

单例模式的作用:

单例模式主要用到全局只需要一个实例的对象,比如项目的配置对象,如线程池,数据库连接池,缓存配置等等。
现在的spring默认使用的就是单例,每一个配置的bean在spring容器中只有一个。

单例模式的要点:

  • 要点一: 构造私有化
    构造私有化,可以保证实例对象无法通过new的方式创建。
  • 要点二: 提供一个静态方法获取对象
    构造私有化之后,无法通过new的方式创建对象,但是这样我们也没有办法获得到对象了,这时就需要提供一个 对外暴露的静态方法,获取对象只能通过调用这个静态方法。
  • 要点三 :私有化静态变量用来保存实例对象

如果实现单例模式:

好的,根据上面的描述(构造私有话,提供对外暴露获取对象的方法),我们可以创建一个最简单的单例对象

饿汉式
public class SingleTestDemo{
    //1. 私有化静态全局变量
    private static SingleTestDemo singleTestDemo = new SingleTestDemo();
    // 2. 构造私有化
    private SingleTestDemo(){
    }
    // 3. 提供对外暴露获取实例变量的静态方法
    public static SingleTestDemo getInstance(){
        return singleTestDemo;
    }
}

如上所示,确实我们实现了一个满足条件的单例,满足了私有化全局变量,私有化构造,提供唯一的静态方法返回实例。
但是上面的方式有个特点,单例是在项目启动的时候就创建了(饿汉式),如果这个对象很耗费资源和时间,但是创建好后又很久不使用,就造成了浪费。基于这个想法,让我们改进一下,如下:

懒汉式
public class SingleTestDemo{
    // 1. 私有化静态全局变量
    private static SingleTestDemo singleTestDemo = null;
    // 2. 构造私有化
    private SingleTestDemo(){
    }
    // 3. 提供对外暴露获取实例变量的静态方法
    public static SingleTestDemo getInstance(){
        if (null == singleTestDemo) {
            singleTestDemo = new SingleTestDemo();
        }
        return singleTestDemo;
    }
}

相比较饿汉式的方式,在项目启动的时候,不会创建对象,而是在第一次调用静态方法的时候才会创建,这样就解决了在项目启动的时候就创建对象可能造成的浪费的问题。
如果我们的项目一直是单线程的话,这样做当然没有问题,但是实际上我们项目大都是多线程的情况,这就可能出现问题,当第一个线程运行到 if (null == singleTestDemo)这一步后,但是又没有调用 singleTestDemo = new SingleTestDemo();,另一个线程也执行到 if (null == singleTestDemo)这一步,会发现此时单例对象还没有创建,然后进入if块内,调用 singleTestDemo = new SingleTestDemo();,这样就创建了两个实例对象了。
提到了多线程安全问题,我们第一个想到的方式是什么?
Synchronized同步关键字
对,我们可以使用同步关键字保证线程的安全性。
于是,我们把Synchronized同步关键字放到对外暴露的静态方法上,如下

同步方法版懒汉式单例模式
public class SingleTestDemo{
    // 1. 私有化静态全局变量
    private static SingleTestDemo singleTestDemo = null;
    // 2. 构造私有化
    private SingleTestDemo(){
    }
    // 3. 提供对外暴露获取实例变量的静态方法
    synchronized public static  SingleTestDemo getInstance(){
        if (null == singleTestDemo) {
            singleTestDemo = new SingleTestDemo();
        }
        return singleTestDemo;
    }
}

好了,我们把Synchronized放到方法上,解决了多线程下的线程安全问题。
但是同时我们也要意识掉一个问题,方法上使用同步关键字,相当于对整个方法加锁了,必须等上一个线程执行完方法中所有的代码后,才会允许下一个线程调用这个方法,如果上一个线程没有执行完,那么下一个线程就必须无限期的等待了。这样的话效率就变低了。
有没有办法可以解决呢 ?
还真有,现在然我们回忆一下,我们为什么使用同步关键字?
为了防止new的时候出现多个对象,只为了防止这一行代码出错,就给整个方法添加同步关键字,有点得不偿失了,所以,可以使用 同步代码块 的方式对new 对象加锁,如下:

同步代码块版懒汉式单例模式
public class SingleTestDemo{
    // 1. 私有化静态全局变量
    private static SingleTestDemo singleTestDemo = null;
    // 2. 构造私有化
    private SingleTestDemo(){
    }
    // 3. 提供对外暴露获取实例变量的静态方法
     public static  SingleTestDemo getInstance(){
        synchronized (SingleTestDemo.class){
            if (null == singleTestDemo) {
                singleTestDemo = new SingleTestDemo();
            }
        }
        return singleTestDemo;
    }
}

相对于使用同步方法,使用同步代码块通过对关键部位加锁而达到和同步方法相同的效果,又可以解决同步方法效率过低的问题。
事情到了这一不,好像已经解决了同步方法效率低的问题了。
然而事情并没有结束,虽然使用同步代码块比同步方法效率高点,但是相对于不使用Synchronized,效率还是低了不少。
程序员是很懒的,总想使用更少的代码更高效的完成代码。那么有办法提高效率吗?
仔细想一下,我们为什么使用同步代码块?
上面已经说了,是为了防止多线程情况下new出来多个对象,但是实际上单例模式中的懒汉式只有第一次调用这个方法的时候,才会执行new对象这一步,其他的时候都是直接返回对象。
好像发现了什么?
对,上面的代码中,使用Synchronized包裹的是整个判断加new对象这一步,如果把判断这一步放到关键字外面怎么样? 如下:

同步代码块版懒汉式单例模式2
public class SingleTestDemo {
    // 1. 私有化静态全局变量
    private static SingleTestDemo singleTestDemo = null;
    // 2. 构造私有化
    private SingleTestDemo() {
    }
    // 3. 提供对外暴露获取实例变量的静态方法
    public static SingleTestDemo getInstance() {
        if (null == singleTestDemo) {
            synchronized (SingleTestDemo.class) {
                singleTestDemo = new SingleTestDemo();
            }
        }
        return singleTestDemo;
    }
}

上面的代码先判断单例对象是不是为空,如果为空,则执行同步关键字里面的代码,创建一个对象,如果不为空,直接返回单例对象。
完美,好像保证了只有第一次的调用的时候会执行同步代码块里面的new代码!
但是别高兴的太早,别忘记了,我们是在多线程的环境下,让我们放慢线程的执行:
线程一执行到if (null == singleTestDemo) 这一步,发现实例对象是空的,然后正准备执行synchronized (SingleTestDemo.class)这部分,这时,线程二也来了,执行到if (null == singleTestDemo) 这一块,也发现实例对象是空的,也准备执行synchronized (SingleTestDemo.class)这一步,这时,线程一腿脚比较快,先一步获取到了锁,执行了同步代码块中的代码,线程二没有获取到锁,只能等线程一执行完,等线程一执行完,已经创建好实例对象了,然后线程二开始执行同步代码块中的代码,由于没有任何判断,线程二也创建了一个实例对象。问题就出现在这,由于同步代码块中没有判断,会出现线程安全问题,怎么解决呢? 那就在同步代码块中添加一个判断就好了,所以,一个新的单例代码如下:

双重否定模式版懒汉式单例模式
public class SingleTestDemo {
    // 1. 私有化静态全局变量
    private volatile static SingleTestDemo singleTestDemo = null;
    // 2. 构造私有化
    private SingleTestDemo() {
    }
    // 3. 提供对外暴露获取实例变量的静态方法
    public static SingleTestDemo getInstance() {
        if (null == singleTestDemo) {
            synchronized (SingleTestDemo.class) {
                if (null == singleTestDemo) {
                    singleTestDemo = new SingleTestDemo();
                }
            }
        }
        return singleTestDemo;
    }
}

经过上面的场景还原,可以明白为啥使用双重否定单例模式了吧。使用双重否定单例模式,即可以保证线程的安全性,也可以实现只有第一次创建new的时候才会执行到同步代码块中的代码,提高了效率。

注意:
关于使用Synchronized关键字的说明:
懒汉式通过使用同步关键字可以达到线程安全问题,但是因为使用了Synchronized,效率总是会变小的,这就引出一个问题:
什么情况下使用Synchronized
这就需要看我们的需求,不是所有的单例都一定要使用Synchronized来处理的

例如 :
如果我们对性能不是要求很严,完全可以使用同步关键字来处理单例模式多线程安全问题。但是我们必须要知道,使用Synchronized关键字可能会造成效率下降一百倍。

如果程序总是创建单例并使用单例或者,在项目在启动或者运行的时候创建单例的负担在可接受的范围,可以使用饿汉式,在项目启动的时候就创建好单例对象,这样,肯定不会出现线程安全问题。

上面的几种实现是比较正常的实现,有没有非正常的呢?

使用静态内置类实现单例模式(饿汉式)
public class SingleTestDemo {
    // 私有静态内置类
   private static class InnerSingleDemo{
       private static final SingleTestDemo singleTestDemo = new SingleTestDemo();
   }
   // 构造私有化
   private SingleTestDemo(){
   }
   // 唯一静态方法暴露,用于获取对象
   public static SingleTestDemo getInstance(){
       return InnerSingleDemo.singleTestDemo;
   }
}

使用静态内置类可以实现单例模式,原理是静态内置类可以调用外部类的私有构造防范,并且在项目启动的时候实例化对象,饿汉式,内置静态类可以实现线程安全问题,当调用唯一暴露的方法的时候,单例的实例已经创建了,这样就可以避免线程安全问题了。
但是,使用上面的静态内置类方法,遇到序列化问题时,也会产生多例,解决的方式是实现序列化,如下:
序列化静态内置类版单例模式

public class SingleTestDemo implements Serializable {
    private static final long serialVersionUID = 7231734676127745287L;
    // 私有静态内置类
    private static class InnerSingleDemo{
        private static final SingleTestDemo singleTestDemo = new SingleTestDemo();
    }
    // 构造私有化
    private SingleTestDemo(){
        System.out.println("SingleTestDemo 构造");
    }
    // 唯一静态方法暴露,用于获取对象
    public static SingleTestDemo getInstance(){
        return InnerSingleDemo.singleTestDemo;
    }
    // 如果是使用序列化和反序列化方式,需要调用下面这个方法,如果不调用,会导致序列化和反序列化不同的对象
    protected Object readResolve() {
        System.out.println("调用了 readResolve 方法");
        return InnerSingleDemo.singleTestDemo;
    }
}

通过实现 Serializable 来达到在序列化和反序列化对象的过程中不会出现多个实例。

这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:避免了线程不安全,延迟加载,效率高。

静态代码块实现单例
public class SingleTestDemo {
    // 私有化变量
    private static SingleTestDemo singleTestDemo = null;
    // 构造私有化
    private SingleTestDemo(){
    }
    // 静态代码块
    static {
        singleTestDemo = new SingleTestDemo();
    }
    // 提供暴露的方法
    public static SingleTestDemo getInstance(){
        return singleTestDemo;
    }
}

原理和上面静态内置类相似,在调用getInstance获取实例对象的时候,执行静态代码块中的代码,实例化对象。
第二次调用的时候,已经创建好实例对象了,所以保证了实例的单一

上面所有的方法都有一个特点,构造私有化,但是我们不要忘记,java中有个方式叫反射,可以通过反射,绕过私有化的构造,从而创建实例。
那有没有办法避免呢 ?
可以使用枚举类型,枚举可以防止反射和序列化出现多例的情况;

https://www.cnblogs.com/chiclee/p/9097772.html

简化版枚举单例模式
public class SingleTestDemo {
  public EnumSingleDemo{
       singleDemo;
       private EnumSingleDemo(){
       }
       private EnumSingleDemo getSingleDemo(){
           return singleDemo;
       }
   }
   public static EnumSingleDemo doSomething(){
     return EnumSingleDemo.singleDemo.getSingleDemo();
   }
}
枚举类实现单例模式
// 单例模式10 使用枚举的方式实现单例模式   缺点 暴漏了 枚举类
enum SingleDemo10 {
    connectionFactory;
    private Connection connection;
    private SingleDemo10() {
        try {
            System.out.println("调用了SingleDemo10构造!");
            String username = "root";
            String password = "root";
            String url = "jdbc:mysql://192.168.2.224:3306/mytest2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
            String driverName = "com.mysql.cj.jdbc.Driver";
            Class.forName(driverName);
            connection = DriverManager.getConnection(url, username, password);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
    public Connection getConnection(){
        return connection;
    }
}
枚举类实现单例模式2-避免暴露枚举类
// 单例模式11 避免暴漏 枚举类
class SingleDemo11{
   public enum SingleDemo12 {
        connectionFactory;
        private Connection connection;
        private SingleDemo12() {
            try {
                System.out.println("调用了SingleDemo10构造!");
                String username = "root";
                String password = "root";
                String url = "jdbc:mysql://192.168.2.224:3306/mytest2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
                String driverName = "com.mysql.cj.jdbc.Driver";
                Class.forName(driverName);
                connection = DriverManager.getConnection(url, username, password);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
        public Connection getConnection(){
            return connection;
        }
    }

    public static Connection getConnection(){
       return SingleDemo12.connectionFactory.connection;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值