JAVA设计模式-单例模式

目录

前言:

怎样设计单例模式?

三条原则:

具体实现:

1. 懒汉式,线程不安全

2. 懒汉式,线程安全

3. 饿汉式

4. 饿汉式

5. 双检锁/双重校验锁(DCL,即 double-checked locking)

6. 静态内部类

7. 枚举

7.1 原型模式中的序列化克隆原型对象

8.JDK中的单例模式使用



前言:

在java面试过程中(当然了,不能只是出于面试的目的去学设计模式,要做一个有理想的程序员),设计模式是一定会问到的一环,而其中的单例模式因为比较简单,而且涉及到的面比较广,线程安全、内存模型、类加载机制、反射等基础知识点,比较好拿出来考察面试者,值得深入理解。

单例模式是什么?为什么需要单例模式?

单例模式是指系统在运行过程中,某个类自始至终只有一个实例对象。

因为类的对象的创建和销毁是需要消耗资源的,有的类频繁的创建和销毁不会消耗多少资源,例如String类,但是如果有的类的对象庞大而复杂(比较重),那么多次创建和销毁,并且这些对象是完全可以复用的,那么就会造成不必要的性能浪费,例如访问数据库时(比如datasource数据源->数据库连接池、session工厂等),需要创建数据库连接对象,这是一个耗资源的操作,并且这个对象是可以复用的,那么就可以将这个对象设计为单例模式,因为如果频繁的创建销毁这个对象对系统性能影响是很大的。

怎样设计单例模式?

三条原则:

虽然实现单例模式有很多种方式,但是要考虑下面的这三条原则,每种具体的实现都要尽量满足下面的三条原则,因为反射是程序员自己设计的,所以可以不考虑,实际就是满足前两条原则。

  • 是不是线程安全
  • 是否懒加载
  • 是不是可以反射破坏: 这个是一定可以被破坏的,因为通过反射可以调用私有构造方法,去new一个新的对象,这就打破了单例,单例的构造方法是私有的。

具体实现:

单例模式的实现有多种方式,分为下面几类

  • 懒汉式
  • 饿汉式
  • 双重校验
  • 静态内部类
  • 枚举

关键代码:构造函数是私有的

1. 懒汉式,线程不安全

是否 Lazy 初始化-懒加载:是

是否线程安全:否

实现难度:易

描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){} //一定是private,不能使用new创建新的instance,那样就不是单例了
  
    public static Singleton getInstance() {  
      if (instance == null) {  
        instance = new Singleton();  
      }  
      return instance;  
    }  
}

2. 懒汉式,线程安全

是否 Lazy 初始化-懒加载:是
是否线程安全:是
实现难度:易
优点:懒加载,第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
这种方式具备很好的 lazy loading,也能够在多线程中很好的工作,但是效率太低了,每个线程想要获取这个单例,执行getInstance()方法的时候都需要同步,而实际上只要这个单例创建出来以后,后面的线程就不需要再进行同步了,判断instance不为null,直接return一个就好了,所以对方法级别进行同步,效率太低

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  //一定是private,不能使用new创建新的instance,那样就不是单例了
    public static synchronized Singleton getInstance() {  
      if (instance == null) {  
        instance = new Singleton();  
      }  
      return instance;  
    }  
}

3. 饿汉式

是否 Lazy 初始化-懒加载:否
是否线程安全:是
实现难度:易
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制,保证初始化 instance 时只有一个线程(类加载过程中的初始化阶段),避免了多线程的同步问题,加载->链接(验证,准备,解析等)->初始化,在类加载子系统的初始化阶段,如果类中有静态变量或者静态代码块,则classloader会自动生成一个叫做的类构造器方法,去为静态变量赋值,并执行静态代码块中的内容。下面的代码中,private static Singleton instance = new Singleton() 会在初始化阶段被执行(线程安全),这样就有了这个类的实例。

init和clinit方法的区别:

init是对象构造器方法,就是在new 一个对象的初始化阶段(初始化对象的非静态成员变量,以及调用该类的 constructor 方法)才会执行init方法。而clinit是类构造器方法,也就是在jvm进行类加载—–链接(验证,准备,解析等)—–初始化 当中的初始化阶段,jvm会调用clinit方法。

不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

public class Singleton {  
    private final static Singleton instance = new Singleton(); //不管是不是加了final,都是在类加载的初始化阶段进行赋值,因为用到了new,需要执行代码,只有在初始化阶段才开始执行类的一些代码
    private Singleton (){}  //一定是private,不能使用new创建新的instance,那样就不是单例了
    public static Singleton getInstance() {  
      return instance;  
    }  
}

4. 饿汉式

与上面的饿汉式几乎一样,只是把初始化代码放到了静态代码块中

public class Singleton {  
    private static Singleton instance; //跟上面相比,去掉了final
    static {
        instance = new Singleton();
    }
    private Singleton (){}  //一定是private,不能使用new创建新的instance,那样就不是单例了
    public static Singleton getInstance() {  
      return instance;  
    }  
}

5. 双检锁/双重校验锁(DCL,即 double-checked locking)

是否 Lazy 初始化-懒加载:是
是否多线程安全:是
实现难度:较复杂
这种方式采用双重校验机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。

Note: 在多线程环境下,为了提高效率,double-check是经常被使用的。

public class Singleton {  
    private volatile static Singleton singleton;  //为什么使用volatile ??? 这里就不进行解释了,自行查询相关知识。 
    private Singleton (){} //一定是private,不能使用new创建新的instance,那样就不是单例了 
    public static Singleton getSingleton() {  
      if (singleton == null) {  //双重校验1
        synchronized (Singleton.class) {  
          if (singleton == null) {  //双重校验2
            singleton = new Singleton();  
          }  
        }  
      }  
      return singleton;  
    }  
}

为什么使用volatile ? 请参考文章 全面理解Java内存模型(JMM)及volatile关键字_zejian_的博客-CSDN博客,以及https://blog.csdn.net/wdquan19851029/article/details/115378751。 说简单点儿就是因为singleton = new Singleton(); 对象的实例化要分为多步,不是原子性操作,正常来说应该是对象初始化完成之后,才将其赋值给singleton引用变量,这样能保证多线程情况下,对象在没有实例化完成的情况下,双重校验1 if (singleton == null)  始终成立。但是由于有指令重排优化的原因,先将分配的内存地址赋值给instance变量,然后再去初始化对象,导致对象没有实例化完成,instance却不为空,多线程情况下,其它线程可能拿到没有实例化完成的对象,并且去使用。这叫做多线程下的有序性问题,可以通过加上volatile关键字 private volatile static Singleton singleton; ,禁止singleton = new Singleton(); 实例化相关的指令重排优化。

6. 静态内部类

是否 Lazy 初始化-懒加载:是
是否线程安全:是
实现难度:一般

静态内部类:在外部的类被加载的时候,静态内部类不会随着被一起加载,只有用到它的时候,才会被装载,而且只会装载一次,所以能保证懒加载,并且是线程安全的。
这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(clinit方法给静态变量赋值,从而创建了一个实例,没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载(例如其它的地方,调用了Singleton.count这个静态属性,则Singleton类需要被加载,但是这个时候不需要实例化,还用不到这个实例),那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。


public class Singleton { 
    public static count = 0; 
    private static class SingletonHolder {   //静态内部类
      private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){} 一定是private,不能使用new创建新的instance,那样就不是单例了  
    public static Singleton getInstance() {  
      return SingletonHolder.INSTANCE;  
    }  
}

7. 枚举

是否 Lazy 初始化-懒加载:否
是否线程安全:是
实现难度:易
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。

public enum Singleton {  
    INSTANCE;  
    public void sayHello() {
        System.out.println("Hello world!");
    }  
}

//测试
public class SingletonTest{
    public static void main(String[] args){
        Singleton instance = Singleton.INSTANCE;
        Singleton instance1 = Singleton.INSTANCE;
        System.out.println(instance==instance1);
        
        System.out.println(instance.hashCode());
        System.out.println(instance1.hashCode());

        instance.sayHello();
        instance1.sayHello();
    }
}

7.1 原型模式中的序列化克隆原型对象

通过单例类只能获取到用一个单例对象,但是在获得单例对象后,我们可以通过序列化的方式,将单例对象克隆一个,这样就变成2个及2个以上,破坏了单例模式。

下面类中的public Object deepClone()方法,给出了使用序列化方式克隆原型对象的例子。

package com.prototype;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class  DeepClone implements Cloneable, Serializable{
	/**
	 * 
	 */
	private static final long serialVersionUID = -8474514933067441515L;
	private String name;
	private String weight;
	private int age;
	private String address;	
	
	public DeepClone(String name, String weight, int age, String address) {
		super();
		this.name = name;
		this.weight = weight;
		this.age = age;
		this.address = address;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getWeight() {
		return weight;
	}
	public void setWeight(String weight) {
		this.weight = weight;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	public String getAddress() {
		return address;
	}
	public void setAddress(String address) {
		this.address = address;
	}
	@Override
	public String toString() {
		return "Sheep [name=" + name + ", weight=" + weight + ", age=" + age
				+ ", address=" + address + "]";
	}
	
	//对象浅拷贝,不拷贝对象中的引用类型,只拷贝基本数据类型和String类型。
	@Override
	protected Object clone(){
		DeepClone deepClone = null;
		try {
			deepClone = (DeepClone) super.clone();
		} catch (CloneNotSupportedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return deepClone;
	}
	
	//对象深拷贝,使用序列化的方式克隆一个原型对象
	public Object deepClone(){
		ObjectOutputStream ooStream;
		ByteArrayOutputStream baOutStream;
		ObjectInputStream oiStream;
	    ByteArrayInputStream baInputStream;
	    
	    try {
	    	baOutStream = new ByteArrayOutputStream();
			ooStream = new ObjectOutputStream(baOutStream);
			ooStream.writeObject(this);
			
			baInputStream = new ByteArrayInputStream(baOutStream.toByteArray());
			oiStream = new ObjectInputStream(baInputStream);
				DeepClone cloneObject;
				try {
					cloneObject = (DeepClone) oiStream.readObject();
					return cloneObject;
				} catch (ClassNotFoundException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}

			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	    return null;
	}

}

8.JDK中的单例模式使用

1.在JDK中,java.lang.Runtime是经典的单例模式,而且是采用了饿汉式单例模式。

2.Spring容器中bean管理,可以选择单例模式或者原型模式,当然了,这种单例模式只是对于一个spring容器来说的,如果有两个或者两个以上的spring容器,那么就不是标准的单例模式了,标准的单例模式是指在JVM中仅有一个instance。

3.java1.8版本中,CAS底层实现类Unsafe,使用饿汉式提供了一个单例,theUnsafe,private static final Unsafe theUnsafe = new Unsafe(); ,在类加载的初始化阶段,这个Unsafe字段就已经被赋值。

        // 通过反射得到theUnsafe对应的Field对象
		// 使用饿汉式提供了一个单例theUnsafe,private static final Unsafe theUnsafe = new Unsafe(); ,在类加载的初始化阶段,这个Unsafe字段就已经被赋值。
		// 通过反射直接得到Unsafe的引用值,就是一个Unsafe实例。
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        // 设置该Field为可访问
        field.setAccessible(true);
        // 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
        Unsafe unsafe = (Unsafe) field.get(null);
        System.out.println(unsafe);

从java11开始,Unsafe类提供了一个静态方法getUnsafe(),通过它可以直接获取到Unsafe单例,不需要再用反射去获取单例。

4.java类加载,每个类被加载到内存中,仅有一个对应的Class对象。

你可能感兴趣的:(Java,设计模式,java,设计模式)