单例模式(Singleton).
定义:.
指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。
在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。
单例模式有 3 个特点:.
- 单例类只有一个实例对象;(私有构造器)
- 该单例对象必须由单例类自行创建;(内部new)
- 单例类对外提供一个访问该单例的全局访问点;(getInstance静态方法)
单例的实现.
实现方式
- 饿汉式(类加载时初始化,线程安全,如果未获取实例,会造成资源浪费)
- 懒汉式(类加载时不初始化,获取实例时初始化,线程不安全,不会造成资源浪费)
- 懒汉式(线程安全版)
synchronized
getInstance()(synchronized
是非常消耗性能,多次调用该方法会造成不必要的性能消耗)- 双重校验锁(DCL)(解决了多次调用getInstance对性能消耗的影响,但是存在指令重排问题)
- 双重校验锁(DCL)+ volatile (解决指令重排问题,但是反射可以破解单例模式)
- 三重校验锁(在构造器中再加一个验证,仍然有缺陷,只适用于先产生单例对象,再使用反射获取对象 的情况)
- 红绿灯(可以简单解决放射问题,但是仍然有问题)
- 静态内部类(类加载时不初始化)
- 枚举(默认单例,不能通过反射破解)
除了枚举所有的实现方式,在反射机制面前,都是不安全
1.饿汉式.
饿汉式
线程安全
public class Hungry {
//私有构造器
private Hungry(){
System.out.println(Thread.currentThread().getName());
}
//内部 new对象 private final static
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
/*
饿汉式 由于在内部已经new 实力化
会造成资源浪费
*/
//多线程并发
public static void main(String[] args) {
for (int i = 0; i < 10 ; i++) {
// lambda表达式 jdk8
new Thread(()->{
Hungry.getInstance();
}).start();
}
/*
结果发现 始终只有一个实例产生
(没有考虑到反射情况下)
*/
}
}
结果显示
2.懒汉式(线程不安全).
懒汉式(普通)
存在多线程问题
public class LazyMan {
//私有构造器
private LazyMan(){
System.out.println(Thread.currentThread().getName());
}
//声明域对象 但是不实例化 到要使用时 实例化 private static 相比于饿汉式 没有 final
private static LazyMan LAZYMAN;
//获取实例
public static LazyMan getInstance(){
if (LAZYMAN == null){
LAZYMAN = new LazyMan();
}
return LAZYMAN;
}
/*
虽然解决了 内存浪费问题,在单线程下也是OK的, 对象是唯一的
但是:多线程下,是不安全的,对象可能 不是唯一
看main方法中的测试可以看出
多次的结果产生了多个对象
我们需要的是单例
因此我们需要同步
*/
//多线程并发
public static void main(String[] args) {
for (int i = 0; i < 10 ; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
结果演示
3.懒汉式(线程安全).
懒汉式(
synchronized
getInstance())为了解决多线程问题
public class LazyMan_Synr {
//私有构造器
private LazyMan_Synr(){
System.out.println(Thread.currentThread().getName());
}
//声明域对象 但是不实例化 到要使用时 实例化 private static 相比于饿汉式 没有 final
private static LazyMan_Synr LAZYMAN;
//获取实例 同步操作
public static synchronized LazyMan_Synr getInstance(){
if (LAZYMAN == null){
LAZYMAN = new LazyMan_Synr();
}
return LAZYMAN;
}
/*
虽然进行了同步处理,解决了多线程问题
但是synchronized操作是非常消耗性能的
我们可以使用 DCL 过滤不必要的 同步代码块的调用
*/
//多线程并发
public static void main(String[] args) {
for (int i = 0; i < 10 ; i++) {
new Thread(()->{
LazyMan_Synr.getInstance();
}).start();
}
}
}
结果显示
双重校验锁(DCL)
为了解决多线程问题
DCL单例模式为什么要两次判空.
- getInstance方法中第一个判空条件,逻辑上是可以去除的,去除之后并不影响单例的正确性,但是去除之后效率低。因为去掉之后,不管instance是否已经初始化,都会进行synchronized操作,而synchronized是一个重操作消耗性能。加上之后,如果已经初始化直接返回结果,不会进行synchronized操作。
- getInstance方法中的第二个判空条件是不可以去除,如果去除了,并且刚好有两个线程a和b都通过了第一个判空条件。此时假设a先获得锁,进入synchronized的代码块,初始化instance,a释放锁。接着b获得锁,进入synchronized的代码块,也直接初始化instance,instance被初始化多遍不符合单例模式的要求~。加上第二个判空条件之后,b获得锁进入synchronized的代码块,此时instance不为空,不执行初始化操作。
public class LazyMan_Lock {
//私有构造器
private LazyMan_Lock(){
System.out.println(Thread.currentThread().getName());
}
//声明域对象 但是不实例化 到要使用时 实例化 private static 相比于饿汉式 没有 final
private static LazyMan_Lock LAZYMAN;
//获取实例 双重检查锁模式的 懒汉式单例 (DCL懒汉式)
public static LazyMan_Lock getInstance(){
if (LAZYMAN == null){//外层检测
synchronized (LazyMan_Lock.class){//同步锁
if (LAZYMAN == null){//内层检测
LAZYMAN = new LazyMan_Lock(); // 不是一个原子性操作
/*
new一个对象 正常的3步实现
1.分配内存空间
2.执行构造方法,初始化对象
3.把这个对象指向这个空间
*/
}
}
}
return LAZYMAN;
}
/*
DCL懒汉式
降低了 synchronized 对性能的影响
获得的结果只有个一个实例
但是仍然存在问题
由于上面
LAZYMAN = new LazyMan_Lock(); // 不是一个原子性操作
new 实例的顺序就有可能被打破
假如A获取实例 构造的顺序 是 132
1.分配内存空间
3.把这个对象指向这个空间
2.执行构造方法,初始化对象
此时B又获取实例的话,由于LAZYMAN != null,他就会认为实例是存在的,紧接着返回这个实例,
实际上这个实例指向的是一片虚无,就会报错
*/
//多线程并发
public static void main(String[] args) {
for (int i = 0; i < 10 ; i++) {
new Thread(()->{
LazyMan_Synr.getInstance();
}).start();
}
}
}
展示结果
结果与上一个无区别
存在指令重排问题(目前无法展示出结果,发生概率太小)
.
指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。
指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。
.
懒汉式(双重校验锁(DCL)+ volatile)
为了解决多线程问题 + 原子性
public class LazyMan_Lock_Atom {
//私有构造器
private LazyMan_Lock_Atom(){
System.out.println(Thread.currentThread().getName());
}
//声明域对象 但是不实例化 到要使用时 实例化 private static 相比于饿汉式 没有 final
private volatile static LazyMan_Lock_Atom LAZYMAN;
//获取实例 双重检查锁模式的 懒汉式单例 (DCL懒汉式)
public static LazyMan_Lock_Atom getInstance(){
if (LAZYMAN == null){//外层检测
synchronized (LazyMan_Lock_Atom.class){//同步锁
if (LAZYMAN == null){//内层检测
LAZYMAN = new LazyMan_Lock_Atom(); // 不是一个原子性操作
}
}
}
return LAZYMAN;
}
/*
增加volatile修饰符 保证原子性
private volatile static LazyMan_Lock_Atom LAZYMAN;
当时这些仍然是有问题的
由于放射可以获得 该类的私有构造器,所以可以轻易破解 单例模式 获取多个实例
*/
//使用反射破解 单例模式
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// for (int i = 0; i < 10 ; i++) {
// new Thread(()->{
// LazyMan_Lock_Atom.getInstance();
// }).start();
// }
//单例模式创建的实例
LazyMan_Lock_Atom LazyMan = LazyMan_Lock_Atom.getInstance();
//反射创建的实例
Constructor<LazyMan_Lock_Atom> constructor = LazyMan_Lock_Atom.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazyMan_Lock_Atom LazyMan1 = constructor.newInstance();
System.out.println("单例模式创建的实例 "+ LazyMan);
System.out.println("反射创建的实例 "+ LazyMan1);
/*
运行可以看到 两次产生对象不是同一个
*/
}
}
结果演示
- 结果是一致
- 这里我们考虑反射破解(运行可以看到 两次产生对象不是同一个,单例被破解)
懒汉式(三重校验锁)
为了解决反射破解问题
public class LazyMan_Lock_Atom_PRO {
//私有构造器
private LazyMan_Lock_Atom_PRO(){
System.out.println(Thread.currentThread().getName());
synchronized (LazyMan_Lock_Atom_PRO.class){//第三重检测
if (LAZYMAN != null){//如果对象(单例模式创建的)存在
//抛出异常 由于 反射破解 私有构造器
throw new RuntimeException("不要试图使用反射破解异常");
}
}
}
//声明域对象 但是不实例化 到要使用时 实例化 private static 相比于饿汉式 没有 final
private volatile static LazyMan_Lock_Atom_PRO LAZYMAN;
//获取实例 双重检查锁模式的 懒汉式单例 (DCL懒汉式)
public static LazyMan_Lock_Atom_PRO getInstance(){
if (LAZYMAN == null){//外层检测
synchronized (LazyMan_Lock_Atom_PRO.class){//同步锁
if (LAZYMAN == null){//内层检测
LAZYMAN = new LazyMan_Lock_Atom_PRO(); // 不是一个原子性操作
}
}
}
return LAZYMAN;
}
/*
在私有构造器中添加第三重检测
看上去好像 是解决了反射破解构造器的问题
事实上,仍然有问题
这个只适用于 先产生单例对象,再使用反射获取对象 的情况
如果没有 产生使用单例模式创建对象 而是 使用 反射
*/
//使用反射破解 单例模式
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//反射创建的实例
Constructor<LazyMan_Lock_Atom_PRO> constructor = LazyMan_Lock_Atom_PRO.class.getDeclaredConstructor();
constructor.setAccessible(true);
//************************ 三重检测结果 ***************************//
/*
//单例模式创建的实例
LazyMan_Lock_Atom_PRO LazyMan = LazyMan_Lock_Atom_PRO.getInstance();
//反射实例1
LazyMan_Lock_Atom_PRO LazyMan1 = constructor.newInstance();
System.out.println("单例模式创建的实例 "+ LazyMan);
System.out.println("反射创建的实例1 "+ LazyMan1);
*/
//************* 仍然存在的问题(先注释“三重检测结果”部分)*************//
//反射实例1
LazyMan_Lock_Atom_PRO LazyMan1 = constructor.newInstance();
//反射实例2
LazyMan_Lock_Atom_PRO LazyMan2 = constructor.newInstance();
System.out.println("反射创建的实例1 "+ LazyMan1);
System.out.println("反射创建的实例2 "+ LazyMan2);
}
}
结果展示
三重检测结果
仍然存在的问题(没有使用getInstance获取实例,只使用反射获取实例,仍然可以破解)
懒汉式(红绿灯)
为了解决反射破解问题
public class LazyMan_Lock_Atom_PRO_Signals {
//添加红绿灯
private static boolean qwert = false;
//私有构造器
private LazyMan_Lock_Atom_PRO_Signals(){
System.out.println(Thread.currentThread().getName());
if (qwert == false){//如果实例 不存在
qwert = true;
}else{//如果实例存在
//抛出异常 由于 反射破解 私有构造器
throw new RuntimeException("不要试图使用反射破解异常");
}
}
//声明域对象 但是不实例化 到要使用时 实例化 private static 相比于饿汉式 没有 final
private volatile static LazyMan_Lock_Atom_PRO_Signals LAZYMAN;
//获取实例 双重检查锁模式的 懒汉式单例 (DCL懒汉式)
public static LazyMan_Lock_Atom_PRO_Signals getInstance(){
if (LAZYMAN == null){//外层检测
synchronized (LazyMan_Lock_Atom_PRO_Signals.class){//同步锁
if (LAZYMAN == null){//内层检测
LAZYMAN = new LazyMan_Lock_Atom_PRO_Signals(); // 不是一个原子性操作
}
}
}
return LAZYMAN;
}
/*
使用 红绿灯(一个标志位) 解决 反射破解 构造器 创建对象
私有一个boolean static(保持唯一) 属性 由于这个属性 正常角度是无法通过放射去获取这个 属性的(因为你不知道属性名是啥) 并更改这个属性值的 所以一般来说已经很安全
但是 如果 是在 反编译的情况下 同样可以获取 该属性的名字 然后更改 属性值 破解构造器 创建实例
下面的就不做演示,也就是说 道高一尺,魔高一丈
*/
//使用反射破解 单例模式
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//反射创建的实例
Constructor<LazyMan_Lock_Atom_PRO_Signals> constructor = LazyMan_Lock_Atom_PRO_Signals.class.getDeclaredConstructor();
constructor.setAccessible(true);
//反射实例1
LazyMan_Lock_Atom_PRO_Signals LazyMan1 = constructor.newInstance();
//反射实例2
LazyMan_Lock_Atom_PRO_Signals LazyMan2 = constructor.newInstance();
System.out.println("反射创建的实例1 "+ LazyMan1);
System.out.println("反射创建的实例2 "+ LazyMan2);
}
}
结果展示
4.静态内部类.
public class Outer {
private Outer(){
}
private static class Inner{
private static final Outer instance = new Outer();
}
// 调用该方法,获取实例,并进行初始化
public static Outer getInstance(){
return Inner.instance;
}
}
5.枚举.

//enum 默认就是一个单列的 可以去看 Constructor.class 中 newInstance()
/*
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects"); // 不能反射地创建枚举对象
*/
public enum EnumSingle {
INSTANCE;
private EnumSingle(){
}
public static EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
// EnumSingle instance1 = EnumSingle.getInstance();
// EnumSingle instance2 = EnumSingle.getInstance();
//
// System.out.println(instance1);
// System.out.println(instance2);
Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor();
constructor.setAccessible(true);
EnumSingle enumSingle1 = constructor.newInstance();
EnumSingle enumSingle2 = constructor.newInstance();
System.out.println(enumSingle1);
System.out.println(enumSingle2);
//反编译 1
//javap -p **.class
//反编译 2 专业
//jad.exe 将 class反编译为 java文件
//jad -sjava **.class
}
}