Java 设计模式之单例

一、概述

单例模式又名单子模式。是一种非常常见的设计模式。我们在运用这模式的时候,基本是为了确保整个系统中只有一个实例。

二、单例的好处

 我们从单例模式的定义和实现,可以知道单例模式具有以下几个优点:

  • 在内存中只有一个对象,节省内存空间;

  • 避免频繁的创建销毁对象,可以提高性能;

  • 避免对共享资源的多重占用,简化访问;

  • 为整个系统提供一个全局访问点。

三、单线程下单例(Singleton)两种经典实现

1、饿汗式

/**
 *  饿汉式单例
 */
public class Singleton1 {

    //私有的指向自己的静态引用---主动创建
    private static Singleton1 singleton1 = new Singleton1();
    //私有的构造方法
    private Singleton1() {
    }
    //获取单例
    public static Singleton1 getInstance(){
        return  singleton1;
    }

}

2、懒汉式

public class Singleton2 {

    //私有的指向自己的静态引用
    private static Singleton2 singleton2 ;
    //私有的构造方法
    private Singleton2() {
    }
    //获取单例
    public static Singleton2 getInstance(){
        //被动创建,需要的时候创建
        if(singleton2==null){
            singleton2=new Singleton2();
        }
        return  singleton2;
    }
}

总结:

    1、饿汗式单例在这个类被加载的时候就会去new 一个静态实例。因为我们知道static修饰的变量 整个生命周期只创建一次 。所以也就确保了整个系统中只有它一个实例。

    2、懒汉式单例是当我们需要的时候去创建实例。

从速度和反应时间上来说,饿汗式更好。但从资源利用效率上来说,懒汉式更好。   


四、多线程下的单例

我们先看如下代码:

public class Test {

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            MyThread thread = new MyThread();
            thread.start();
        }
    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            try{
                sleep(3000);
                Singleton1 singleton1 = Singleton1.getInstance();
                System.out.println("hashcode-----" + singleton1.hashCode());
            }catch(InterruptedException e){
                e.printStackTrace();
            }

        }
    }
}

输出结果:

hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979
hashcode-----2136701979

证明:饿汗式单例 在多线程下也是安全的

我们再看看懒汉式单例在多线程下的表现

hashcode-----100758745
hashcode-----2075012821
hashcode-----2075012821
hashcode-----2075012821
hashcode-----2075012821
hashcode-----2075012821
hashcode-----2075012821
hashcode-----2075012821
hashcode-----2075012821
hashcode-----2075012821

证明: 懒汉式单例在多线程下是不安全的:

总结:其实道理很简单。饿汗式单例 因为在类加载的时候,就已经创建了为一得实例,所以多线程下肯定是线程安全的。

          而懒汉式是在调用的时候去创建的。当我们代码走到if(singleton2 == null)判断语句的时候,多线程的环境下。就有可能同时走到这块,并且同时创建多个实例。


五、懒汉式单例在多线程环境下正确写法

1、第一种写法:

public class Singleton2 {

    //私有的指向自己的静态引用
    private static Singleton2 singleton2 ;
    //私有的构造方法
    private Singleton2() {
    }
    //获取单例
    public  static synchronized Singleton2 getInstance(){
        //被动创建,需要的时候创建
        if(singleton2==null){
            singleton2=new Singleton2();
        }
        return  singleton2;
    }
}

这种写法和上面的懒汉式写法的唯一区别 使用了synchronzied修饰了整个方法,这样写保证了临界资源的互斥访问,从而保证了线程安全。

从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率会很低,因为同步块的作用域有点大,而且锁的粒度有点粗。同步方法效率低,那我们考虑使用同步代码块来实现。

2、第二种写法:

public class Singleton2 {

    //私有的指向自己的静态引用
    private static Singleton2 singleton2 ;
    //私有的构造方法
    private Singleton2() {
    }
    //获取单例
    public  static  Singleton2 getInstance(){
        //被动创建,需要的时候创建
        synchronized (Singleton2.class){
            if(singleton2==null){
                singleton2=new Singleton2();
            }
            return  singleton2;
        }
    }
}

这种写法相对于上面写法,效率仍很差。

3、第三种写法

public class Singleton2 {

    //私有的构造方法
    private Singleton2() {
    }
    private static class   Singleton2Holder{
        //私有的指向自己的静态引用
      private static Singleton2 singleton2 = new Singleton2();
    }
    //获取单例
    public   static  Singleton2 getInstance(){
        return Singleton2Holder.singleton2;
    }
}

如上述代码所示,我们可以使用内部类实现线程安全的懒汉式单例,这种方式也是一种效率比较高的做法,它与饿汉式单例的区别就是:这种方式不但是线程安全的,还是延迟加载的,真正做到了用时才初始化。

  当客户端调用getInstance()方法时,会触发Singleton2Holder类的初始化。由于Singleton2是Singleton2Holder类成员变量,因此在JVM调用Singleton2Holder类的类构造器对其进行初始化时,虚拟机会保证一个类的类构造器在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器,其他线程都需要阻塞等待,直到活动线程执行方法完毕。在这种情形下,其他线程虽然会被阻塞,但如果执行类构造器方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行类构造器,因为 在同一个类加载器下,一个类型只会被初始化一次,因此就保证了单例。


五、单例模式的双重检查写法

public class Singleton2 {

    //这里使用volatile原因是,new Singleton2()不是一个原子操作,会出现指令重排的问题
    private static volatile Singleton2 singleton2 ;
    //私有的构造方法
    private Singleton2() {
    }
    //获取单例
    public  static  Singleton2 getInstance(){
        if(singleton2==null) {
            //被动创建,需要的时候创建
            synchronized (Singleton2.class) {
                if (singleton2 == null) {
                    singleton2 = new Singleton2();
                }
            }
        }
            return  singleton2;
    }
}

  如上述代码所示,为了在保证单例的前提下提高运行效率,我们需要对 singleton2 进行第二次检查,目的是避开过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同步获取锁了)。这种做法无疑是优秀的,但是我们必须注意一点:必须使用volatile关键字修饰单例引用(原因已经在代码注释了)。

需要注意的是为什么我们再这需要使用volatile去修饰Singleton2呢?

首先我们需要弄清楚 new Singleton2()这个操作是一个非原子操作,其次我们要知道JVM到底做了哪些操作

1、 在内存中分配一块地址

2、初始化对象

3、使sinleton2 指向内存中的地址

编译器有可能会出现指令重排的问题,有可能先后顺序变成了1,3,2。当线程1去调getInstance方法获取单例的时候,走到3时,这是线程2也来获取单例,这时候进入判断singleton2==null 判断的时候,其实singleton2只是不为null ,但并未初始化完成。

那线程2获取的实例是一个有问题的实例,会是应用程序奔溃。所以造成这种现象的原因是指令重排。我们又知道volatile是能完美解决这一方案的。所以需要添加修饰。




  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值