单例模式是 Java 中最简单的设计模式之一。
特点:
(一)只能有一个实例
(二)必须自己创建自己的唯一实例
(三)需要给其他对象使用提供这一实例
创建方式:
(1)饿汉式:类加载时就会创建该单例对象。(不使用也会一直占用内存)
(2)懒汉式:类加载不会创建对象,首次使用该对象时才会创建。(需注意控制线程安全问题)
具体实现单例模式代码:
(1-1)饿汉式:静态成员变量
public class Singleton {
//构造方法私有化
private Singleton() {
}
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
(1-2)饿汉式:静态代码块
public class Singleton {
private Singleton(){}
private static Singleton instance;
static {
instance = new Singleton();
}
public static Singleton getInstance() {
return instance;
}
}
(1-3)饿汉式:枚举
public enum Singleton {
INSTANCE;
}
饿汉式三种创建方法总结:
1、静态成员变量、静态代码块两种方式没有太大区别,在类加载时会执行创建对象方法。
2、枚举是推荐的实现方式,枚举类型是线程安全的,只会装载一次,枚举是所有单例实现中唯一一种不会被破坏的单例实现模式。(下面会介绍破坏单例的方式和解决方法)
(2-1)懒汉式:(线程不安全)
public class Singleton {
private Singleton() {
}
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
//线程1执行到此处开始等待
// 线程2获取到cpu执行权,也会执行到此处
//多个线程执行到此处,进而会创建多个对象
instance = new Singleton();
}
return instance;
}
}
(2-2)懒汉式:(同步方法锁,线程安全)
public class Singleton {
private Singleton() {
}
private static Singleton instance;
//单例绝大部分都是读操作,读操作是线程安全的
//没必要每个线程必须持有锁才能调用该方法,性能低下
//调整锁的时机,所以有了新的实现模式,双重检查锁(2-3)方式
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
(2-3)懒汉式:(双重检查锁,线程安全)
public class Singleton {
private Singleton() {
}
//多线程情况下,可能会出现空指针问题
//原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
//在变量属性前用volatile关键字修饰,可以保证可见性和有序性
private static volatile Singleton instance;
public static Singleton getInstance() {
//第一次判断 如果!=null 不需要抢占锁 直接返回对象 进而提升效率
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
(2-4)懒汉式:(静态内部类,线程安全)
/**
*由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化静态属性
*/
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
//final修饰 防止外部类对它进行修改
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
懒汉式三种创建方法总结:
1、当不涉及多线程调用时,可以使用(2-1)不加同步锁的方式进行创建
2、当多线程操作时,可以使用(2-2)、(2-3)的实现方式。推荐使用(2-3)双重检查锁的方式,更加高效。
3、更加推荐的是(2-4)静态内部类的实现方式,它是一种优秀的单例模式, 是开源项目中比较常用的单例模式。不需要使用任何锁,也保证了多线程安全,并且不影响性能,也不浪费空间。
破坏单例模式
可以理解为通过一定的方式,使单例类可以创建多个对象,(枚举方式除外)。有两种方式,分别是反射和序列化。
1、反射
首先我们看一下通过反射创建实例的代码
//获取字节码对象
Class clazz = Singleton.class;
//获取无参的构造方法
Constructor cons = clazz.getDeclaredConstructor();
//取消访问检查
cons.setAccessible(true);
//创建Singleton对象
Singleton instance1 = (Singleton) cons.newInstance();
Singleton instance2 = (Singleton) cons.newInstance();
通过打印instance1和instance2地址可以得知这是两个不同的对象。那我可以在无参私有的构造方法中进行处理,在类中添加一个静态变量进行标记是否是第一调用构造方法,在第二次调用时我们抛异常处理。如下代码:(双重检查锁举例)
public class Singleton {
private static boolean flag;
private Singleton() {
if (flag) {
throw new RuntimeException("不允许创建多个对象");
}
flag = true;
}
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
2、序列化
这里我们以静态内部类的单例模式进行举例
public class Singleton implements Serializable {
private Singleton(){}
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
//当进行反序列化时,会自动调用该方法,将该方法的返回值直接返回,解决序列化反序列化单例被破坏问题
// public Object readResolve(){
// return SingletonHolder.INSTANCE;
// }
}
public static void writeObjToFile() throws Exception {
//将对象序列化之后 写入到文件中
Singleton singleton = Singleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("***/a.txt"));
oos.writeObject(singleton);
oos.close();
}
public static void readObjFromFile() throws Exception{
//从文件中读取对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("***/a.txt"));
Singleton singleton = (Singleton) ois.readObject();
System.out.println(singleton);
ois.close();
}
首先我们的单例需实现Serializable接口,然后再调用writeObjToFile方法将单例Singleton对象写入到文件中。然后再多次调用readObjFromFile读取对象,经过打印对象地址发现每次取出的对象不是同一个,所以该单例被破坏。
原因在ObjectInputStream 的readObject()方法,这里jdk源码以截图形式说明:(由于截图不全,大家可以根据代码行数自行查看)
从这几个主要方法中可以得知,我们需要在Singleton类的readResolve方法中,将我们实例返回即可,也就是我上面代码中注释掉的readResolve方法。这时我们再进行读写操作时,就会得到同一个实例。