概述
单例,为什么要使用单例?主要考虑以下两种情况
- 实例对象比较占用资源,例如内存等,所以保持系统中只有一个实例对象,能有效的避免内存浪费,前提是你得业务逻辑允许你使用同一个对象实例。实际案例,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 回收掉。
关于更深层次的线程安全问题
参考这篇文章