彻底玩转单例模式

单例中最重要的思想------->构造器私有!

恶汉式、懒汉式(DCL懒汉式!)

恶汉式

package single;
//饿汉式单例(问题:因为一上来就把对象加载了,所以可能会导致浪费内存)
public class Hungry {
/*
* 如果其中有大量的需要开辟的空间,如new byte[1024*1024]这些,那么一开始就会加载,而不是需要时才加载,所以非常浪费空间
*
* */
private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
private byte[] data3 = new byte[1024*1024];
private byte[] data4 = new byte[1024*1024];
private Hungry() {
}
private final static Hungry HUNGRY = new Hungry(); public static Hungry getInstance(){
return HUNGRY;
}
}

懒汉式

DCL懒汉式

完整的双重检测锁模式的单例、懒汉式、DCL懒汉式

package single;

public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread() + "ok");
} private volatile static LazyMan lazyMan; // 单线程下确实ok
public static LazyMan getInstance() {
// 加锁、锁整个类
// 双重检测锁模式的单例、懒汉式、DCL懒汉式
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan == null) {
lazyMan = new LazyMan();//不是原子性操作 }
}
}
return lazyMan;
}
/*
* 1、分配内存空间
* 2、执行构造方法,初始化对象
* 3、把这个对象指向这个空间
*
* 期望的结果:1、2、3
* 但是由于指令重排可能导致结果为1、3、2,这在cpu中是没问题的
* 线程A:1、3、2
* 线程B如果在线程A执行到3时开始执行判断是否为null,由于已经占用空间了,所以会被判断为不为空,但实际还未初始化对象,实际结果还是为null
*
*
* */ // 多线程并发测试
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazyMan.getInstance();
}).start();
}
} }

但是有反射!只要有反射,任何的代码都不安全,任何的私有关键字都是摆设

正常的单例模式:

/*
* 正常的单例模式创建的都为同一个对象,并且该对象全局唯一
* 只执行一次创建,并且对象都是同一个
* Thread[main,5,main]ok
* true
* */
LazyMan instance1 = LazyMan.getInstance();
LazyMan instance2 = LazyMan.getInstance();
System.out.println(instance2==instance1);

反射破坏单例:

/*
* 通过反射破坏单例
* 执行两个创建,两个不同的对象
* Thread[main,5,main]ok
Thread[main,5,main]ok
false
* */
LazyMan instance1 = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance2 == instance1);

怎么去解决这种破坏呢?

首先反射走了无参构造器,我们可以在构造器中进行加锁判断是否已经存在了对象。

private LazyMan() {
//通过构造器来加锁判断防止反射破坏
synchronized (LazyMan.class){
if (lazyMan!=null){
throw new RuntimeException("不要试图使用反射破坏单例模式");
}
} }

通过反射破坏单例模式

道高一尺,魔高一丈

1、通过普通的反射来破坏单例模式

Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = LazyMan.getInstance();
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);

解决方法:通过构造器加锁解决

private LazyMan() {
//通过构造器来加锁判断防止反射破坏
synchronized (LazyMan.class){
if (lazyMan == null){ }else {
throw new RuntimeException("不要试图使用反射破坏单例模式");
} }
}

2、通过反射创建两个类来破坏单例模式

Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = declaredConstructor.newInstance();
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);

解决方法:设置一个外部私有变量,在构造方法中通过外部私有变量来操作

//创建一个外部的标,用于防止通过newInstance破坏单例模式
private static boolean flg = true;
private LazyMan() {
//通过构造器来加锁判断防止反射破坏
synchronized (LazyMan.class){
if (flg){
flg = false;
}else {
throw new RuntimeException("不要试图使用反射破坏单例模式");
}
}
}

3、通过反射字段来将外部私有变量修改。

Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
//通过反射修改内部私有变量
Field flg1 = LazyMan.class.getDeclaredField("flg");
flg1.setAccessible(true); //通过反射的newInstance创建的两个对象依旧破坏了单例模式
LazyMan instance1 = declaredConstructor.newInstance();
//通过反射字段对单例模式进行破坏
flg1.set(instance1,true);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance2 == instance1);

解决方法,通过枚举类型!枚举类型自带单例模式,禁止反射破坏

package single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException; //枚举类
public enum EnumDemo {
INSTANCE;
public EnumDemo getInstance(){
return INSTANCE;
}
}
class EnumDemoTest{
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<EnumDemo> declaredConstructor = EnumDemo.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumDemo enumDemo1 = declaredConstructor.newInstance();
EnumDemo enumDemo2 = declaredConstructor.newInstance();
System.out.println(enumDemo1);
System.out.println(enumDemo2);
}
}

发现抱错,没有对应的无参构造

但是idea编译的源码中是由无参构造的

idea欺骗了我们,那么编译好的类到底有没有无参构造,通过javap -p反编译源码查看所以方法

可以看到,也有空参的构造方法,也就意味了反编译源码也欺骗了你,所以我们通过更专业的工具来查看,使用jad查看。

查看当前目录新生成的java文件可以发现,通过jad反编译的源码的构造函数时个有参构造函数


// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumDemo.java package single; public final class EnumDemo extends Enum
{
public static EnumDemo[] values()
{
return (EnumDemo[])$VALUES.clone();
} public static EnumDemo valueOf(String name)
{
return (EnumDemo)Enum.valueOf(single/EnumDemo, name);
} private EnumDemo(String s, int i)
{
super(s, i);
} public EnumDemo getInstance()
{
return INSTANCE;
} public static final EnumDemo INSTANCE;
private static final EnumDemo $VALUES[]; static
{
INSTANCE = new EnumDemo("INSTANCE", 0);
$VALUES = (new EnumDemo[] {
INSTANCE
});
}
}

我们尝试在反射中加入这两个参数类

Constructor<EnumDemo> declaredConstructor = EnumDemo.class.getDeclaredConstructor(String.class,int.class);

可以发现,它根据我们预想的结果抛出一个异常

在newInstance方法中如果时枚举类就会抛出这个异常,这是从反射层面限制了对枚举类单例模式的破坏!!

JUC并发编程学习笔记(十七)彻底玩转单例模式的更多相关文章

  1. JUC并发编程学习笔记

    JUC并发编程学习笔记 狂神JUC并发编程 总的来说还可以,学到一些新知识,但很多是学过的了,深入的部分不多. 线程与进程 进程:一个程序,程序的集合,比如一个音乐播发器,QQ程序等.一个进程往往包含 ...

  2. 并发编程学习笔记(3)----synchronized关键字以及单例模式与线程安全问题

    再说synchronized关键字之前,我们首先先小小的了解一个概念-内置锁. 什么是内置锁? 在java中,每个java对象都可以用作synchronized关键字的锁,这些锁就被称为内置锁,每个对 ...

  3. Java并发编程学习笔记

    Java编程思想,并发编程学习笔记. 一.基本的线程机制 1.定义任务:Runnable接口 线程可以驱动任务,因此需要一种描述任务的方式,这可以由Runnable接口来提供.要想定义任务,只需实现R ...

  4. 并发编程学习笔记(15)----Executor框架的使用

    Executor执行已提交的 Runnable 任务的对象.此接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节.调度等)分离开来的方法.通常使用 Executor 而不是显式地创建 ...

  5. 并发编程学习笔记(14)----ThreadPoolExecutor(线程池)的使用及原理

    1. 概述 1.1 什么是线程池 与jdbc连接池类似,在创建线程池或销毁线程时,会消耗大量的系统资源,因此在java中提出了线程池的概念,预先创建好固定数量的线程,当有任务需要线程去执行时,不用再去 ...

  6. 并发编程学习笔记(13)----ConcurrentLinkedQueue(非阻塞队列)和BlockingQueue(阻塞队列)原理

    · 在并发编程中,我们有时候会需要使用到线程安全的队列,而在Java中如果我们需要实现队列可以有两种方式,一种是阻塞式队列.另一种是非阻塞式的队列,阻塞式队列采用锁来实现,而非阻塞式队列则是采用cas ...

  7. 并发编程学习笔记(11)----FutureTask的使用及实现

    1. Future的使用 Future模式解决的问题是.在实际的运用场景中,可能某一个任务执行起来非常耗时,如果我们线程一直等着该任务执行完成再去执行其他的代码,就会损耗很大的性能,而Future接口 ...

  8. 并发编程学习笔记(12)----Fork/Join框架

    1. Fork/Join 的概念 Fork指的是将系统进程分成多个执行分支(线程),Join即是等待,当fork()方法创建了多个线程之后,需要等待这些分支执行完毕之后,才能得到最终的结果,因此joi ...

  9. 并发编程学习笔记(10)----并发工具类CyclicBarrier、Semaphore和Exchanger类的使用和原理

    在jdk中,为并发编程提供了CyclicBarrier(栅栏),CountDownLatch(闭锁),Semaphore(信号量),Exchanger(数据交换)等工具类,我们在前面的学习中已经学习并 ...

  10. 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理

    1. AQS共享模式 前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码. 首先还是从顶级接口acquireShared()方法入手: public fin ...

随机推荐

  1. hdfs小文件合并

    HDFS small file merge 1.hive Settings There are 3 settings that should be configured before archivin ...

  2. 学习LVM:archive 和 backup目录

    推荐文档:Logical Volume Manager Administration 在掌握了基本的commands操作后,你是否对操作背后的一些东西产生好奇呢? 程序的configure一般会提供这 ...

  3. 用 Rust 生成 Ant-Design Table Columns

    经常开发表格,是不是已经被手写Ant-Design Table的Columns整烦了? 尤其是ToB项目,表格经常动不动就几十列.每次照着后端给的接口文档一个个配置,太头疼了,主要是有时还会粘错就尴尬 ...

  4. 2022-02-08 IValueConverter和StringFormat

    主页 后台 stringFormat

  5. 【实践篇】推荐算法PaaS化探索与实践

    作者:京东零售 崔宁 1. 背景说明 目前,推荐算法部支持了主站.企业业务.全渠道等20+业务线的900+推荐场景,通过梳理大促运营.各垂直业务线推荐场景的共性需求,对现有推荐算法能力进行沉淀和积累, ...

  6. 规范代码编写风格就用 eslint 和 prettier

    eslint 可以用于规范我们的编码,使得项目中的代码风格一致,更利于阅读和维护,而 prettier 可以在当我们代码不符合 eslint 规范是进行部分自动修复. eslint 通过 npm in ...

  7. DELPHI应用EXCEL(1)

    在介绍使用delphi控制excel之前前,我们首先需要了解关于EXCEL的几个基本概念:EXCEL应用程序.工作薄(book).工作表(sheet)以及单元格(CELLS): 首先,我们是打开exc ...

  8. springboot项目图片不显示的问题

    首先确认你的图片路径是对的,maven重新clean,install了,web服务也重启了 那么大概率就是浏览器缓存的原因,因为页面直接用的是缓存的旧数据,所以显示不出来. 在不修改浏览器设置的情况下 ...

  9. CVE-2023-2825-GitLab目录穿越poc

    Gitlab CVE-2023-2825 目录穿越漏洞 前言 昨天 GitLab 出了一个版本目录穿越漏洞(CVE-2023-2825),可以任意读取文件.当时我进行了黑盒测试并复现了该漏洞. &qu ...

  10. Java日志系列:日志门面JCL、SLF4J

    目录 一.日志门面说明 二.JCL 使用 JCL 入门 JCL 原理 三.SLF4J 使用 配合自身简单日志实现(slf4j-simple) 配置logback日志实现 配置Log4J日志实现(需适配 ...