Java 单例模式

概述

单例,为什么要使用单例?主要考虑以下两种情况

  • 实例对象比较占用资源,例如内存等,所以保持系统中只有一个实例对象,能有效的避免内存浪费,前提是你得业务逻辑允许你使用同一个对象实例。实际案例,JDBC 中数据库连接池。
  • 实例对象使用场景比较频繁,而且可以使用同一对象。如果你每次都去 New 一个出来,浪费内存是一个问题,同时也浪费了代码的执行时间。

今天拿出 Java 中常用的单例模式进行讲解,因为这个也是面试(校招类面试)经常叫你写的。

饿汉式单例

 public class HungrySingle {
private static HungrySingle single =new HungrySingle() ;

private HungrySingle(){
    System.out.println("HungrySingle.HungrySingle ");
}
public static HungrySingle getInstance(){
    System.out.println("HungrySingle.getInstance");
    return single;
}

public static void main(String[] args) {
    HungrySingle hungrySingle = getInstance();
}

 }

这种单例利用静态变量初始化的特点,会在第一次调用这个类的时候,自动进行初始化。

打印结果

HungrySingle.HungrySingle 
HungrySingle.getInstance

由于使用的是静态初始化机制,所以不存在线程安全问题,但是这个单例叫做饿汉单例,也就是说,可能存在这种情况,单例被实例化了但是却没有被调用,会造成浪费。同时另一个问题就是,构造方法里面如果执行的初始化操作较多,或者代码操作比较耗时,这样的话,单例实例化执行的速度会受到影响。

懒汉式单例

懒汉式单例可以避免饿汉式单例的问题,能够保证在第一次使用的时候初始化对象

public class LazySingle {
private static LazySingle single;
private LazySingle(){

}
public synchronized LazySingle getInstance(){
    if (single == null){
        single=  new LazySingle();
    }
    return single;
}
}

这种懒汉式不仅能避免浪费的问题,同时使用了 synchronized 关键字,所以是线程安全的。但是线程安全的牺牲代价就是,代码执行效率不高,因为每次只允许一个线程执行获取单例对象的代码,所以对多线程高效访问的场景基本不适用。
tip: synchroized 修饰的是 getInstance() 方法,在一个线程访问这个方法的时候,其他线程不能访问LazySingle 类的这个 getInstance() 方法,但是可以访问其他非 synchronized 修饰的方法。

double - check 的单例模式

还是从线程安全考虑,不过我们将 synchronized 的代码粒度变小,因为 synchronized 可以修饰方法,也可以修饰代码块,分别称为同步方法和同步代码块,同步代码块能够让我们更加有目的的对代码进行线程安全控制。下面的单例模式就是这种用法,称为 double - check 的单例模式。

public class SingleTon {
private volatile static SingleTon singleTon;

private SingleTon() {
}

public static SingleTon getInstance(){
    if(singleTon == null)
        synchronized (SingleTon.class)
        {
            if(singleTon == null){
                singleTon = new SingleTon();
            }
        }
    return singleTon;
}
}

这个代码是线程安全的,而且它的线程同步机制,仅仅是在单例对象没有被创建的时候有效,所以基本不会牺牲代码的执行性能。
这里我还使用了 volatile 关键字,这里 volatile 关键字是一种轻量级的 synchronized ,它在多处理器开发中保证了共享变量的可见性,也即是说,当一个线程修改了 volatile 共享变量之后,另一个线程读到的是修改后的值。

static 机制的单例

这种单例模式,例如了 static 机制,和内部类的特点

public class StaticSingle {

private StaticSingle(){

}

public static StaticSingle getInstance(){
    return SingleHolder.staticSingle;
}

private static class SingleHolder {
    private static StaticSingle staticSingle = new StaticSingle();

}
}

这种单例是线程安全的,同时代码的执行性能基本是不受影响的,也能保证使用的时候,才会进行初始化。这种实现机制的意思是,参看下面的代码

 public class StaticSingle {
static {
new MyInnerClass();
 }

private StaticSingle(){
    System.out.println("StaticSingle.StaticSingle");
}

public static StaticSingle getInstance(){
    System.out.println("StaticSingle.getInstance");
    return SingleHolder.staticSingle;
}

private static class SingleHolder {
    private static StaticSingle staticSingle = new StaticSingle();
    private SingleHolder(){
        System.out.println("SingleHolder.SingleHolder");
    }
}
public static class MyInnerClass{

    public MyInnerClass(){
        System.out.println("MyInnerClass.MyInnerClass");
    }
}

public void foo(){
    System.out.println("StaticSingle.foo");
}
public static void fooS(){
    System.out.println("StaticSingle.fooS");
}

public static void main(String[] args) {
    //fooS();
    new StaticSingle().foo();
}
}

执行 main 方法中 的 foos() 方法打印如下

MyInnerClass.MyInnerClass
StaticSingle.fooS

执行 main 方法中 new StaticSingle().foo() 打印如下

MyInnerClass.MyInnerClass
StaticSingle.StaticSingle
StaticSingle.foo

所以这个类第一次创建的时候,内部静态类并不会被创建,之前饿汉式的单例是,只要这个类被第一次访问了,就一定会执行单例对象的实例化操作。所以很好的避免了浪费问题。

关于单例对象被回收

单例对象基本是静态变量,在 Java 中对静态变量的回收是很谨慎的,所以基本可以认为不会主动被 GC 回收掉。

关于更深层次的线程安全问题

参考这篇文章

关于对象初始化延迟的问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值