特点

  1. 单例的类在整个JVM中只有一个实例
  2. 需要提供一个全局访问点(1.公开的静态变量,2.公开的静态方法)
  3. 对象在内存中只有一个,减少了内存的开销
  4. 类构造器私有
  5. 可以控制对象的创建时刻

    创建单例的八种方式

    饿汉式,懒汉式,枚举

    饿汉式

  • 类加载到内存后就实例化一个单例,JVM保证线程安全
  • 提供一个全局的访问点
  • 缺点

    1. 不管用到与否,类装载时就完成实例化

      懒汉式

  • 在需要创建实例的时候才调用方法创建实例对象

    1. 饿汉式(1)

  • 能保证单例

    1. public class Mgr01 { // 能保证单例
    2. private static final Mgr01 INSTANCE = new Mgr01();
    3. private Mgr01() {
    4. }
    5. public static Mgr01 getInstance() {
    6. return INSTANCE;
    7. }
    8. }

    2. 饿汉式(2)

  • 能保证单例

    1. public class Mgr02 { // 能保证单例
    2. /**
    3. * final修饰的变量必须立即初始化,或者“代码块”初始化
    4. * 如果是static修饰的用static代码块初始化
    5. * 如果不是static修饰的必须用普通代码块初始化
    6. */
    7. private static final Mgr02 INSTANCE;
    8. static {
    9. INSTANCE = new Mgr02();
    10. }
    11. private Mgr02() {
    12. }
    13. public static Mgr02 getInstance() {
    14. return INSTANCE;
    15. }
    16. }

    3. 懒汉式(1)

  • 不能保证单例,线程不安全

    1. public class Mgr03 { // 不能保证单例
    2. private static Mgr03 INSTANCE;
    3. private Mgr03() {
    4. }
    5. public static Mgr03 getInstance() {
    6. if (INSTANCE == null) { // 多线程不安全
    7. try {
    8. // 模拟线程阻塞
    9. Thread.sleep(1);
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. }
    13. INSTANCE = new Mgr03();
    14. }
    15. return INSTANCE;
    16. }
    17. }

    4. 懒汉式(2)

  • 能保证单例

    1. public class Mgr04 { // 能保证单例
    2. private static Mgr04 INSTANCE;
    3. private Mgr04() {
    4. }
    5. /**
    6. * 静态方法加锁是:对该类的class对象加锁。Mgr04.class (√)
    7. * 加锁会影响性能
    8. */
    9. public static synchronized Mgr04 getInstance() {
    10. if (INSTANCE == null) {
    11. try {
    12. // 模拟线程阻塞
    13. Thread.sleep(1);
    14. } catch (InterruptedException e) {
    15. e.printStackTrace();
    16. }
    17. INSTANCE = new Mgr04();
    18. }
    19. return INSTANCE;
    20. }
    21. }

    5. 懒汉式(3)

  • 不能保证单例,线程不安全

    1. public class Mgr05 { // 不能保证单例
    2. private static Mgr05 INSTANCE;
    3. private Mgr05() {
    4. }
    5. public static Mgr05 getInstance() {
    6. if (INSTANCE == null) { // 多线程不安全
    7. try {
    8. // 模拟线程阻塞
    9. Thread.sleep(10);
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. }
    13. synchronized (Mgr05.class) {
    14. INSTANCE = new Mgr05();
    15. }
    16. }
    17. return INSTANCE;
    18. }
    19. }

    6. 懒汉式(4)

  • 能保证单例

  • 双重校验方式

    1. public class Mgr06 { // 能保证单例
    2. /**
    3. * 需要加volatile修饰,禁止指令重排
    4. */
    5. private static volatile Mgr06 INSTANCE;
    6. private Mgr06() {
    7. }
    8. public static Mgr06 getInstance() {
    9. if (INSTANCE == null) { // 第一次检测,减小上锁的概率
    10. try {
    11. // 模拟线程阻塞
    12. Thread.sleep(10);
    13. } catch (InterruptedException e) {
    14. e.printStackTrace();
    15. }
    16. synchronized (Mgr06.class) {
    17. if (INSTANCE == null) { // 第二次检测,确保实例一定为空
    18. INSTANCE = new Mgr06();
    19. }
    20. }
    21. }
    22. return INSTANCE;
    23. }
    24. }

    为什么要加volatile呢?

    创建一个对象可以分为3步!

    1. memory = allocate(); //1.分配对象的内存空间
    2. ctorInstance(memory); //2.初始化对象
    3. instance = memory; //3.设置instance指向刚分配的内存地址
    4. 其中:
    5. 23之间,可能会被重排序
    6. memory = allocate(); //1.分配对象的内存空间
    7. instance = memory; //2.设置instance指向刚分配的内存地址
    8. ctorInstance(memory); //3.初始化对象

    虽然重排序不会影响单线程的执行结果,但是由于判断的条件是instance == null,当分配了内存以后,其他线程来到判断的地方,instance不为空,所以直接将引用指向实例对象。但是实例对象还没有初始化,就会出现问题,可能会引发错误(对象还未初始化就已被使用)。
    所以需要加volatile防止重排序。

    7. 懒汉式(5)

  • 能保证单例

  • 静态内部类方式
  • 通过类加载过程的线程安全来保证创建单例实例的线程安全

    1. public class Mgr07 { // 能保证单例
    2. private Mgr07() {
    3. }
    4. public static class InnerMgr07 {
    5. private static final Mgr07 INSTANCE = new Mgr07();
    6. }
    7. public static Mgr07 getInstance() {
    8. return InnerMgr07.INSTANCE;
    9. }
    10. }

    8. 枚举方式

  • 唯一安全的单例创建方式(不能够被反序列化) ```java public enum Mgr08 { INSTANCE; }

  1. <a name="T4VTN"></a>
  2. #### 枚举在底层是怎么保证线程安全的?
  3. > **解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题**
  4. - 定义枚举时使用enum和class一样,时Java中的关键字。
  5. - 枚举经过javac编译后,会被转成 public final class T extends Enum 的定义。
  6. - 枚举中的各个枚举项都是通过static修饰的。
  7. ```java
  8. public enum T {
  9. SPRING,SUMMER,AUTUMN,WINTER;
  10. }

反编译后:

  1. public final class T extends Enum
  2. {
  3. //省略部分内容
  4. public static final T SPRING;
  5. public static final T SUMMER;
  6. public static final T AUTUMN;
  7. public static final T WINTER;
  8. private static final T ENUM$VALUES[];
  9. static
  10. {
  11. SPRING = new T("SPRING", 0);
  12. SUMMER = new T("SUMMER", 1);
  13. AUTUMN = new T("AUTUMN", 2);
  14. WINTER = new T("WINTER", 3);
  15. ENUM$VALUES = (new T[] {
  16. SPRING, SUMMER, AUTUMN, WINTER
  17. });
  18. }
  19. }
  1. static 类型的属性会在类被加载之后被初始化
  2. 当一个Java类第一次被真正使用到的时候静态资源被初始化,Java类的加载和初始化过程都是线程安全的
    1. 因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全
  3. 所以,创建一个enum类型是线程安全的
    枚举在底层是怎么避免反序列化的?
    在序列化的时候,Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.EnumvalueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObjectreadObject等方法。

普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。
但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题

单例与序列化的爱恨情仇

序列化

  1. Java序列化可以保存实例属性的状态,但是不会保存方法,因为方法没有状态。
  2. Java序列化不会保存static变量,因为static变量是类的状态,序列化保存的是对象,而不是类。
  3. transient关键字修饰的变量也不会被序列化保存,反序列化时,该变量会被设置为默认值。
  4. 父类实现了Serializable接口,其子类会继承,从而子类不需要显示实现Serializable接口。
  5. 子类实现了Serializable接口,其父类如未实现该接口,子类序列化不会保存父类属性的状态。
  6. 为了保证序列化和反序列化成功,对象必须有相同的序列化ID。

    单例与序列化

    1. public class Singleton implements Serializable{
    2. private static volatile Singleton singleton = null;
    3. private String s;
    4. private Singleton(){}
    5. public static Singleton getInstance(){
    6. if (singleton == null){
    7. synchronized (Singleton.class){
    8. if (singleton == null)
    9. singleton = new Singleton();
    10. }
    11. }
    12. return singleton;
    13. }
    14. public String getS() {
    15. return s;
    16. }
    17. public void setS(String s) {
    18. this.s = s;
    19. }
    20. }

    以上代码是一个单例模式的实现,Singleton实现了Serializable接口,并且保证了线程安全。有一个私有属性s,通过set和get方法进行赋值取值操作。下面来看序列化与反序列化:

    1. public class serialzableTest {
    2. public static void serialzableSingleton(String file) {
    3. // 1.序列化
    4. ObjectOutputStream objectOutputStream = null;
    5. Singleton instance1 = null;
    6. try {
    7. objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
    8. instance1 = Singleton.getInstance();
    9. instance1.setS("序列化测试!");
    10. objectOutputStream.writeObject(instance1);
    11. } catch (IOException e) {
    12. e.printStackTrace();
    13. } finally {
    14. if (objectOutputStream != null) {
    15. try {
    16. objectOutputStream.close();
    17. } catch (IOException e) {
    18. e.printStackTrace();
    19. }
    20. }
    21. }
    22. // 反序列化
    23. ObjectInputStream objectInputStream = null;
    24. Singleton instance2 = null;
    25. try {
    26. objectInputStream = new ObjectInputStream(new FileInputStream(file));
    27. instance2 = (Singleton) objectInputStream.readObject();
    28. System.out.println(instance2.getS());
    29. System.out.println(instance2 == instance1);
    30. } catch (Exception e) {
    31. e.printStackTrace();
    32. } finally {
    33. if (objectInputStream != null) {
    34. try {
    35. objectInputStream.close();
    36. } catch (IOException e) {
    37. e.printStackTrace();
    38. }
    39. }
    40. }
    41. }
    42. }

    为了代码的规范,因此内容稍微有点多,但主要内容其实就是try里面的两段,最重要的就是注释1和2的两句。测试的结果为:This is a serializable test!和false。这说明序列化确实保存了属性的状态,并且反序列化后的对象singleton2和序列化之前的singleton1不是同一个对象,即单例模式被破坏。那么对ObjectInputStream的readObject方法进行跟踪,发现了以下代码:

    1. private Object readOrdinaryObject(boolean unshared)
    2. throws IOException
    3. {
    4. if (bin.readByte() != TC_OBJECT) {
    5. throw new InternalError();
    6. }
    7. ObjectStreamClass desc = readClassDesc(false);
    8. desc.checkDeserialize();
    9. Class<?> cl = desc.forClass();
    10. if (cl == String.class || cl == Class.class
    11. || cl == ObjectStreamClass.class) {
    12. throw new InvalidClassException("invalid class descriptor");
    13. }
    14. Object obj;
    15. try {
    16. obj = desc.isInstantiable() ? desc.newInstance() : null; //1.对象是否可以进行实例化
    17. } catch (Exception ex) {
    18. throw (IOException) new InvalidClassException(
    19. desc.forClass().getName(),
    20. "unable to create instance").initCause(ex);
    21. }
    22. passHandle = handles.assign(unshared ? unsharedMarker : obj);
    23. ClassNotFoundException resolveEx = desc.getResolveException();
    24. if (resolveEx != null) {
    25. handles.markException(passHandle, resolveEx);
    26. }
    27. if (desc.isExternalizable()) {
    28. readExternalData((Externalizable) obj, desc);
    29. } else {
    30. readSerialData(obj, desc);
    31. }
    32. handles.finish(passHandle);
    33. if (obj != null &&
    34. handles.lookupException(passHandle) == null &&
    35. desc.hasReadResolveMethod())
    36. {
    37. Object rep = desc.invokeReadResolve(obj); //2. 执行readResolve方法
    38. if (unshared && rep.getClass().isArray()) {
    39. rep = cloneArray(rep);
    40. }
    41. if (rep != obj) {
    42. handles.setObject(passHandle, obj = rep); //3. 将需要返回的obj指向rep
    43. }
    44. }
    45. return obj;
    46. }

    注释1表示如果对象可以在运行时被实例化,则使用反射新建一个实例,否则返回null。因此,序列化也是使用反射新建了一个实例,破坏了单例模式。那么注释2就是避免序列化破坏单例的一种方式。我们只需要在单例的类中实现 readResolve 方法,利用它返回其实例,那么序列化得到的对象就是那个唯一的实例。

    1. private Object readResolve(){
    2. return singleton;
    3. }