java的模式

单例模式

单例模式定义

保证一个类仅有一个实例,并提供一个访问它的全局接口。

单例模式的类图

image.png

单例模式示例
饿汉模式
public class Singleton1 {

 private final static Singleton1 singleton = new Singleton1();

 private Singleton1() {
 }

 public static Singleton1 getSingleton() {
  return singleton;
 }
}

饿汉模式是比较简单的一种单例实现,利用类静态属性的特征,在类加载时就实例化单例对象。
优点:利用类静态属性的特征,解决了后继单例对象初始化的逻辑判断,及多线程情况下资源征用的问题。
缺点:类加载的时就会创建对象,如果短时间内单例对象不被使用,单例对象就会一直占用内存。如果单例对象保存的信息特别多的话,过早加载到内存是对内存的浪费。

懒汉模式
public class Singleton2 {

 private static Singleton2 singleton = null;

 private Singleton2() {
 }

 public static synchronized Singleton2 getSingleton() {
  if(null == singleton){
   singleton = new Singleton2();
  }
  return singleton;
 }
}

懒汉模式的单例模式是在首次调用的时候才会创建单例对象, 以后多次调用不会再次创建,内存利用率较高。
优点: 需要的时候才创建单例对象,内存利用率高。
缺点: 为了解决多线程环境下有可能会创建多个实例的问题,必须在接口处声名类同步,这无疑增加了系统调用开销,调用增加了synchronized关键字的方法,要比没有synchronized关键字的方法开销大很多。

双重校验 【错误】
public class Singleton3 {

 private static Singleton3 singleton = null;

 private Singleton3() {
 }

 public static Singleton3 getSingleton() {
  // 首次 Check
  if (null == singleton) {
   // 加锁
   synchronized (Singleton3.class) {
    // 二次 check
    if (null == singleton) {
     singleton = new Singleton3();
    }
   }
  }
  return singleton;
 }
}

单例对象只被创建一次,当单例对象为空时,在首次check处会有多线程并发问题, 因此会有多个线程到达加锁处,在加锁处只会有一个线程成功获得同步锁,其他线程只能在加锁处挂起等待,由于是首次创建对象,首个获取锁的线程成功创建对象返回并释放同步锁。其他在同步锁外等待的线程会陆续成功获取同步锁,由于singleton已经不为null,这些后续线程进入同步块之后,singleton == null的条件已经不成立,就释放了同步锁。
双重检查的方式即实现了单例对象的延迟创建,也没有方法同步synchronized的开销,看起来很完美。但是在 Java1.4之前,这样写是不对,因为 JVM 为了提高程序的执行效率,会在内部对 JVM 指令重排优化。
所谓指令重排优化是 JVM 在不改变程序结果的情况下,通过调整指令的顺序,让程序执行的更快。而这个问题的关键就点是: 指令重排使得初始化 Singleton对象,将Singleton对象地址赋给 Singleton 字段两者的顺序是不确定的,即singleton = new Singleton() 语句的执行并不是原子性的。

在 Java 中创建一个对象分为3个步骤:

  1. 分配对象的内存空间。
  2. 调用构造器方法,执行初始化
  3. 设置 singleton 指向刚分配的地址

在 JVM 中会对上面的执行顺序做优化,指令的执行顺序可能不是1,2,3,有可能是1,3,2。再试想一个场景,有A, B 两个线程,A 先调用getSingleton()函数,A 成功创建了对象,执行了1,3步,B 线程在 A 调用之后的很短时间内,也调用getSingleton()函数,进行singleton == null的判断,发现 singleton不为空,就直接使用singleton对象,但由于第2步还未执行,即对象还没有初始化,所以 B 线程进行调用时,就会引发异常。
上面就是双重检查锁在 Java1.4及之前的问题。

上面的问题在Java1.5及之后使用volatile关键字修饰singleton字段即可解决。

volatile 关键字有两个作用:

  • 通过volatile修饰的变量,保证对所有线程的可见性。
  • volatile 禁止指令重排优化,所以其他线程不会访问一个没有初始化完成的对象。

正确的代码:

public class Singleton3 {

 private static volatile Singleton3 singleton = null;

 private Singleton3() {
 }

 public static Singleton3 getSingleton() {
  // 首次 Check
  if (null == singleton) {
   // 加锁
   synchronized (Singleton3.class) {
    // 二次 check
    if (null == singleton) {
     singleton = new Singleton3();
    }
   }
  }
  return singleton;
 }
}
静态内部类的方式
public class Singleton4 {

 private static final class Singleton4Holder{
  private static final Singleton4 singleton4 = new Singleton4();
 }

 private static Singleton4 singleton = null;

 private Singleton4() {
 }

 public static Singleton4 getSingleton() {
  return Singleton4Holder.singleton4;
 }
}

静态内部类的实现方式也是利用了Java 类的加载机制来保证只创建一个对象。这种方式看起来和饿汉模式有点类似,但还是有不一样的地方。Java 中类是延迟加载的,只有在首次被使用时才会加载。而静态部类模式正是使用了这个特性。当有在线程访问getSingle()方法时,JVM会判断Singleton4Holder类是否已经被加载,没有加载就执行类加载所有操作,并初始化类中的静态资源。静态内部类的方式巧妙的实现了懒汉模式的延迟加载,又实现了饿汉模式的资源征用的开销。

通过枚举方式
public class Singleton5 {

 private Singleton5() {
 }

 private enum Singleton5Enum {
  SINGLETON_5_ENUM;
  private Singleton5 singleton;

  Singleton5Enum() {
   singleton = new Singleton5();
  }

  public Singleton5 getSingletong5() {
   return singleton;
  }
 }

 public static Singleton5 getSingleton() {
  return Singleton5Enum.SINGLETON_5_ENUM.singleton;
 }
}

Java 的枚举类型是功能齐全的类,可以有自己的属性和方法。
Java 的枚举类型的每个实例都是 public static final 类型的,类的每个实例只会被实例化一次。
相比于上4种单例模式,枚举模式最大的优势主要有2点:

  • 序例化机制:JVM 层为枚举类的序例化提供了根本的保障,避免了被实例化多次。
  • 强行调用私有构造函数: 利用 Java 强大的反射机制,可以设置构造函数属性cons.setAccessible(true)来强行调用构造函数构造对象。枚举类则没有这个问题。80%的Java程序员不知道反射强行调用私有构造器这事儿

总得来说枚举类很好的解决了两个问题,使用枚举除了线程安全,防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值