天天看点

Java单例设计模式

1.单例模式的介绍

单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

2.单例模式的两种基本实现

  1. 饿汉式: 饿汉式单例模式在类初始化时就加载了单例对象,本身就是线程安全的。但是当加载类的时间很长,但是后面又没有用这个示例时,就造成了资源浪费。饿汉式示例代码如下。
package singleton;

/**
 * 测试饿汉式单例模式
 * 加载类时创建对象线程安全 调用效率高
 * @author XinghaiLiao
 */
public class SingletonDemo01 {
    //类初始化时,立即加载这个对象 后面可能根本没有用 造成资源浪费
    private static SingletonDemo01 instance = new SingletonDemo01();

    private SingletonDemo01() {}

    //方法不用考虑线程同步问题 调用效率高
    public static SingletonDemo01 getInstance() {
        return instance;
    }
}
           
  1. 懒汉式: 真正用到单例对象时才加载,但是当多个线程去调用同一个对象的getInstance时会有并发问题。比如线程A调用getInstance的时候发现instance是null,然后进入挂起状态;之后线程B调用getInstance方法new了一个对象,这时线程A接着运行时就又建立了一个新的对象,从而违背类单例模式的初衷。所以懒汉式的单例模式在getInstance方法前面加上synchronized关键字来解决线程同步问题,带来的问题也就是调用效率降低了。懒汉式的示例代码如下。
package singleton;

/**
 *@author XinghaiLiao
 *懒汉式 延时加载
 */
public class SingletonDemo02 {
    private static SingletonDemo02 instance;

    private SingletonDemo02() {

    }

    /**
     * 真正用的时候才加载 但是当多个线程去调用的时候会有并发问题 比如A调用的时候发现instance是null
     * 然后进入了挂起状态 B调用的时候new了一个新对象 这时候就会有两个单例对象
     * @return
     */
    public static synchronized SingletonDemo02 getInstance() {
        if(instance == null) {
            instance = new SingletonDemo02();
        }
        return instance;
    }
}
           

3.其他方法实现单例模式

  1. 双重检测锁来实现。由于编译器优化原因和JVM底层内部模型原因,偶尔会出现问题。不建议使用。 但是也给大家介绍一下。这种模式将同步内容放到if内部,只有当instance为空时不同线程访问时才需要同步,当已经有单例实例时则直接获取单例实例。demo代码如下。
package singleton;

/**
 1. @author XinghaiLiao
 */
public class SingletonDemo03 {
    private static SingletonDemo03 instance;

    private SingletonDemo03() {}

    public static SingletonDemo03 getInstance() {
        if(instance == null) {
            SingletonDemo03 s;
            synchronized (SingletonDemo03.class) {
                s = instance;
                if(s == null) {
                    synchronized (SingletonDemo03.class) {
                        if(s == null) {
                            s = new SingletonDemo03();
                        }
                    }
                    instance = s;
                }
            }
        }
        return instance;
    }
}
           
  1. 静态内部类来实现优化。这种方法兼备了线程安全、调用效率高和延迟加载等优点。这种方法添加了一个私有静态内部类,由于初始化这个类时并不会初始化静态内部类,只有调用内部类时才会初始化内部类,从而实现延时加载,而且类加载时线程是安全的。demo代码如下。
package singleton;

/**
 1. 静态内部类的实现
 2. @author XinghaiLiao
 */
public class SingletonDemo04 {
    private static class SingletonClassInstance {
        private static final SingletonDemo04 instance = new SingletonDemo04();
    }

    private SingletonDemo04() {}

    public static SingletonDemo04 getInstance() {
        return SingletonClassInstance.instance;
    }
}
           
  1. 使用枚举实现单例模式。优点是实现简单,枚举类本身就是单例模式,可以避免通过反射和反序列化来创建对象。demo代码如下。这种方法调用效率也比较高,遗憾的是不能实现延时加载。demo代码如下。
package singleton;

/**
 1. 枚举实现单例模式
 2. @author XinghaiLiao
 */
public enum SingletonDemo05 {
    //这个枚举元素本身就是单例对象
    INSTANCE;

    /**
     * 添加自己的方法
     */
    public void singletonOperation() {

    }
}
           

4.总结

  1. 这篇是本人学习笔记,有错误的地方希望大家指正(^_−)☆。
  2. 大家可以写个简单的测试程序测试一下单例模式,比如测试demo。
package singleton;

public class Demo {
    public static void main(String[] args) {
        SingletonDemo01 s1 = SingletonDemo01.getInstance();
        SingletonDemo01 s2 = SingletonDemo01.getInstance();
        System.out.println(s1 == s2);
    }
}
           
  1. 关于暴力反射破解单例模式的测试代码如下,可以发现暴力反射创建的对象和原来的不同。
package reflection;

import singleton.SingletonDemo01;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 1. 暴力反射获取实例
 2. @author XinghaiLiao
 */
public class Demo {
    public static void main(String[] args) {
        SingletonDemo01 s1 = SingletonDemo01.getInstance();
        SingletonDemo01 s2 = SingletonDemo01.getInstance();
        System.out.println(s1);
        System.out.println(s2);

        try {
            Class<SingletonDemo01> singletonDemo01Class = (Class<SingletonDemo01>)Class.forName("singleton.SingletonDemo01");
            Constructor<SingletonDemo01> constructor = singletonDemo01Class.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            SingletonDemo01 s3 =  constructor.newInstance();
            System.out.println(s3);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}
           
  1. 如果想要防止用暴力反射的方式来创建对象,可以在构造函数中抛出异常来防止。示例代码如下。这时候如果通过反射获取实例时会直接抛出异常。
package singleton;
/**
 4. @author XinghaiLiao
 */
public class SingletonDemo01 {
    private static SingletonDemo01 instance = new SingletonDemo01();

    private SingletonDemo01() {
        if(instance != null) {
            throw new RuntimeException();
        }
    }
    
    public static SingletonDemo01 getInstance() {
        return instance;
    }
}
           
  1. 如果想要具体比较每种方式的效率,可以通过下面的demo代码来测试(下面是测试饿汉式的效率)。
package reflection;

import singleton.SingletonDemo01;

import java.util.concurrent.CountDownLatch;

/**
 * 测试效率
 * @author XinghaiLiao
 */
public class Demo {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        int threadCnt = 10;
        CountDownLatch countDownLatch = new CountDownLatch(threadCnt);

        for(int i = 0; i < threadCnt; i++) {
            new Thread(() -> {
                for(int j = 0; j < 1000000; j++) {
                    Object o = SingletonDemo01.getInstance();
                }
                countDownLatch.countDown();
            }).start();
        }

        try {
            //main线程阻塞,直到计数器清0
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long end = System.currentTimeMillis();
        System.out.println("总耗时:" + (end-start) + "ms");
    }
}
           

继续阅读