Java的单例模式

一.定义:

核心:单例模式可以保证一个类仅创建一个实例,并提供一个访问它的全局访问点。

该模式有三个基本要点:一是这个类只能有一个实例;

二是它必须自行创建这个实例;

三是它必须自行向整个系统提供这个实例。

在一个系统中,一个类经常会被使用在不同的地方,通过单例模式,我们可以避免多次创建多个实例,从而节约系统资源。

二.代码示例

1.饿汉式

//饿汉模式
public final class Singleton {
    private static Singleton instance=new Singleton();//自行创建实例
    private Singleton(){}//构造函数
    public static Singleton getInstance(){//通过该函数向整个系统提供实例
        return instance;
    }
}

        我们可以发现,以上第一种实现单例的代码中,使用了 static 修饰了成员变量 instance,所以该变量会在类初始化的过程中 被收集进类构造器即<clinit> 方法中。 在多线程场景下,JVM 会保证只有一个线程能执行该类的<clinit> 方法,其它线程将会被阻塞等待。 等到唯一的一次<clinit> 方法执行完成,其它线程将不会再执行 <clinit>方法,转而执行自己的代码。 也就是说,static 修饰了成员变量 instance,在多线程的情况下能保证只实例化一次。这种方式实现的单例模式, 在类初始化阶段就已经在堆内存中开辟了一块内存,用于存放实例化对象,所以也称为饿汉模式。 饿汉模式实现的单例的优点是,可以保证多线程情况下实例的唯一性,而且 getInstance 直接返回唯一实例,性能非常高。 然而,在类成员变量比较多,或变量比较大的情况下,这种模式可能会在没有使用类对象的情况下,一直占用堆内存。 试想下,如果一个第三方开源框架中的类都是基于饿汉模式实现的单例,这将会初始化所有单例类,无疑是灾难性的。

2.懒汉式

//懒汉模式
public final class Singleton {
    private static Singleton instance= null;//不实例化
    private Singleton(){}//构造函数
    public static Singleton getInstance(){//通过该函数向整个系统提供实例
        if(null == instance){//当instance为null时,则实例化对象,否则直接返回对象
            instance = new Singleton();//实例化对象
        }
        return instance;//返回已存在的对象
    }
}

        以上代码在单线程下运行是没有问题的,但要运行在多线程下,就会出现实例化多个类对象的情况。这是怎么回事呢?当线程 A 进入到 if 判断条件后,开始实例化对象,此时 instance 依然为 null;又有线程 B 进入到 if 判断条件中,之后也会通过条件判断,进入到方法里面创建一个实例对象。所以我们需要对该方法进行加锁,保证多线程情况下仅创建一个实例。这里我们使用 Synchronized 同步锁来修饰 getInstance 方法

//懒汉模式 + synchronized同步锁
public final class Singleton {
    private static Singleton instance= null;//不实例化
    private Singleton(){}//构造函数
    public static synchronized Singleton getInstance(){//加同步锁,通过该函数向整个系统提供实例
        if(null == instance){//当instance为null时,则实例化对象,否则直接返回对象
            instance = new Singleton();//实例化对象
        }
        return instance;//返回已存在的对象
    }
}

        但我们前面讲过,同步锁会增加锁竞争,带来系统性能开销,从而导致系统性能下降,因此这种方式也会降低单例模式的性能。还有,每次请求获取类对象时,都会通过 getInstance() 方法获取,除了第一次为 null,其它每次请求基本都是不为 null 的。在没有加同步锁之前,是因为 if 判断条件为 null 时,才导致创建了多个实例。基于以上两点,我们可以考虑将同步锁放在 if 条件里面,这样就可以减少同步锁资源竞争。

//懒汉模式 + synchronized同步锁
public final class Singleton {
    private static Singleton instance= null;//不实例化
    private Singleton(){}//构造函数
    public static Singleton getInstance(){//加同步锁,通过该函数向整个系统提供实例
        if(null == instance){//当instance为null时,则实例化对象,否则直接返回对象
          synchronized (Singleton.class){
              instance = new Singleton();//实例化对象
          } 
        }
        return instance;//返回已存在的对象
    }
}

        看到这里,你是不是觉得这样就可以了呢?答案是依然会创建多个实例。这是因为当多个线程进入到 if 判断条件里,虽然有同步锁,但是进入到判断条件里面的线程依然会依次获取到锁创建对象,然后再释放同步锁。所以我们还需要在同步锁里面再加一个判断条件:

//懒汉模式 + synchronized同步锁 + double-check
public final class Singleton {
    private static Singleton instance= null;//不实例化
    private Singleton(){}//构造函数
    public static Singleton getInstance(){//加同步锁,通过该函数向整个系统提供实例
        if(null == instance){//第一次判断,当instance为null时,则实例化对象,否则直接返回对象
          synchronized (Singleton.class){//同步锁
             if(null == instance){//第二次判断
                instance = new Singleton();//实例化对象
             }
          } 
        }
        return instance;//返回已存在的对象
    }
}

        在 JMM 中,重排序是十分重要的一环,特别是在并发编程中。如果 JVM 可以对它们进行任意排序以提高程序性能,也可能会给并发编程带来一系列的问题。例如,我上面讲到的 Double-Check 的单例问题,假设类中有其它的属性也需要实例化,这个时候,除了要实例化单例类本身,还需要对其它属性也进行实例化:

//懒汉模式 + synchronized同步锁 + double-check
public final class Singleton {
    private static Singleton instance= null;//不实例化
    public List<String> list = null;//list属性
    private Singleton(){
      list = new ArrayList<String>();
    }//构造函数
    public static Singleton getInstance(){//加同步锁,通过该函数向整个系统提供实例
        if(null == instance){//第一次判断,当instance为null时,则实例化对象,否则直接返回对象
          synchronized (Singleton.class){//同步锁
             if(null == instance){//第二次判断
                instance = new Singleton();//实例化对象
             }
          } 
        }
        return instance;//返回已存在的对象
    }
}

        我们知道 volatile 关键字可以保证线程间变量的可见性,简单地说就是当线程 A 对变量 X 进行修改后,在线程 A 后面执行的其它线程就能看到变量 X 的变动。除此之外,volatile 在 JDK1.5 之后还有一个作用就是阻止局部重排序的发生,也就是说,volatile 变量的操作指令都不会被重排序。所以使用 volatile 修饰 instance 之后,Double-Check 懒汉单例模式就万无一失了。

//懒汉模式 + synchronized同步锁 + double-check
public final class Singleton {
    private volatile static Singleton instance= null;//不实例化
    public List<String> list = null;//list属性
    private Singleton(){
      list = new ArrayList<String>();
    }//构造函数
    public static Singleton getInstance(){//加同步锁,通过该函数向整个系统提供实例
        if(null == instance){//第一次判断,当instance为null时,则实例化对象,否则直接返回对象
          synchronized (Singleton.class){//同步锁
             if(null == instance){//第二次判断
                instance = new Singleton();//实例化对象
             }
          } 
        }
        return instance;//返回已存在的对象
    }
}

3.通过内部类实现

        以上这种同步锁 +Double-Check 的实现方式相对来说,复杂且加了同步锁,那有没有稍微简单一点儿的可以实现线程安全的懒加载方式呢?我们知道,在饿汉模式中,我们使用了 static 修饰了成员变量 instance,所以该变量会在类初始化的过程中被收集进类构造器即 方法中。在多线程场景下,JVM 会保证只有一个线程能执行该类的 方法,其它线程将会被阻塞等待。这种方式可以保证内存的可见性、顺序性以及原子性。如果我们在 Singleton 类中创建一个内部类来实现成员变量的初始化,则可以避免多线程下重复创建对象的情况发生。这种方式,只有在第一次调用 getInstance() 方法时,才会加载 InnerSingleton 类,而只有在加载 InnerSingleton 类之后,才会实例化创建对象。具体实现如下:

//懒汉模式 内部类实现
public final class Singleton {
  public List<String> list = null;// list属性

  private Singleton() {//构造函数
    list = new ArrayList<String>();
  }

  // 内部类实现
  public static class InnerSingleton {
    private static Singleton instance=new Singleton();//自行创建实例
  }

  public static Singleton getInstance() {
    return InnerSingleton.instance;// 返回内部类中的静态变量
  }
}

4.枚举

public class SinletonExample5 {
    private static SinletonExample5 instance = null;

    // 私有构造函数
    private SinletonExample5(){
    }

    public static SinletonExample5 getInstance(){
        return Sinleton.SINLETON.getInstance();
    }

    private enum Sinleton{
        SINLETON;

        private SinletonExample5 singleton;

        // JVM保证这个方法只调用一次
        Sinleton(){
            singleton = new SinletonExample5();
        }

        public SinletonExample5 getInstance(){
            return singleton;
        }
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lima1995

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值