JUC并发编程学习笔记(十七)彻底玩转单例模式
彻底玩转单例模式
单例中最重要的思想------->构造器私有!
恶汉式、懒汉式(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并发编程学习笔记(十七)彻底玩转单例模式的更多相关文章
- JUC并发编程学习笔记
JUC并发编程学习笔记 狂神JUC并发编程 总的来说还可以,学到一些新知识,但很多是学过的了,深入的部分不多. 线程与进程 进程:一个程序,程序的集合,比如一个音乐播发器,QQ程序等.一个进程往往包含 ...
- 并发编程学习笔记(3)----synchronized关键字以及单例模式与线程安全问题
再说synchronized关键字之前,我们首先先小小的了解一个概念-内置锁. 什么是内置锁? 在java中,每个java对象都可以用作synchronized关键字的锁,这些锁就被称为内置锁,每个对 ...
- Java并发编程学习笔记
Java编程思想,并发编程学习笔记. 一.基本的线程机制 1.定义任务:Runnable接口 线程可以驱动任务,因此需要一种描述任务的方式,这可以由Runnable接口来提供.要想定义任务,只需实现R ...
- 并发编程学习笔记(15)----Executor框架的使用
Executor执行已提交的 Runnable 任务的对象.此接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节.调度等)分离开来的方法.通常使用 Executor 而不是显式地创建 ...
- 并发编程学习笔记(14)----ThreadPoolExecutor(线程池)的使用及原理
1. 概述 1.1 什么是线程池 与jdbc连接池类似,在创建线程池或销毁线程时,会消耗大量的系统资源,因此在java中提出了线程池的概念,预先创建好固定数量的线程,当有任务需要线程去执行时,不用再去 ...
- 并发编程学习笔记(13)----ConcurrentLinkedQueue(非阻塞队列)和BlockingQueue(阻塞队列)原理
· 在并发编程中,我们有时候会需要使用到线程安全的队列,而在Java中如果我们需要实现队列可以有两种方式,一种是阻塞式队列.另一种是非阻塞式的队列,阻塞式队列采用锁来实现,而非阻塞式队列则是采用cas ...
- 并发编程学习笔记(11)----FutureTask的使用及实现
1. Future的使用 Future模式解决的问题是.在实际的运用场景中,可能某一个任务执行起来非常耗时,如果我们线程一直等着该任务执行完成再去执行其他的代码,就会损耗很大的性能,而Future接口 ...
- 并发编程学习笔记(12)----Fork/Join框架
1. Fork/Join 的概念 Fork指的是将系统进程分成多个执行分支(线程),Join即是等待,当fork()方法创建了多个线程之后,需要等待这些分支执行完毕之后,才能得到最终的结果,因此joi ...
- 并发编程学习笔记(10)----并发工具类CyclicBarrier、Semaphore和Exchanger类的使用和原理
在jdk中,为并发编程提供了CyclicBarrier(栅栏),CountDownLatch(闭锁),Semaphore(信号量),Exchanger(数据交换)等工具类,我们在前面的学习中已经学习并 ...
- 并发编程学习笔记(9)----AQS的共享模式源码分析及CountDownLatch使用及原理
1. AQS共享模式 前面已经说过了AQS的原理及独享模式的源码分析,今天就来学习共享模式下的AQS的几个接口的源码. 首先还是从顶级接口acquireShared()方法入手: public fin ...
随机推荐
- Redis设计
目录 过期键删除策略 持久化 RDB AOF AOF重写 主从复制 完整重同步和部分重同步 哨兵Sentinel 哨兵对redis服务器集群的监听 执行者选举 故障转移 选择新的主服务器流程 过期键删 ...
- React组件设计之性能优化篇
我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品.我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值. 本文作者:空山 前言 由于笔者最近在开发中遇到了一个重复渲染导致子组 ...
- 零基础入门——从零开始学习PHP反序列化笔记(一)
靶场环境搭建 方法一:PHPstudy搭建 GitHub地址 https://github.com/mcc0624/php_ser_Class 方法二:Docker部署 pull镜像文件 docker ...
- 微软面向企业的Private ChatGPT 参考应用 Chat Copilot
这两天你可能看过这篇文章:微软面向企业的Private ChatGPT 开源!超详细安装流程反馈![1], 这篇文章顶多就是一个Azure OpenAI 服务的简单Demo, 就连插件机制都没有,这也 ...
- win10安装mysql5.7.35教程
前提条件:我下载的是压缩包版本5.7.35,下载地址是 https://downloads.mysql.com/archives/community/ 下载完后解压,并在如下图目录里加入data文件夹 ...
- docker网络 bridge 与overlay 模式
转载请注明出处: 1.bridge网络模式 工作原理: 在Bridge模式中,Docker通过创建一个虚拟网络桥接器(bridge)将容器连接到主机上的物理网络接口.每个容器都会被分配一个IP地址, ...
- Linux字符界面安装更新VMware Tools
注:yeesn为我自己的用户名,实际操作中改用自己的用户名 1.切换到虚拟光驱目录 cd /media/yeesn/VMware Tools 2.复制压缩包到桌面 cp VMwareTools-xxx ...
- 老问题了:idea中使用maven archetype新建项目时卡住
背景 作为一个后端Java打工人,idea就是最重要的打饭工具.创建项目,熟悉吧,但是,这么多年下来,因为idea换了版本,电脑换了等等,我还是时不时遇到根据maven archetype新建mave ...
- Python中字符串截取
# 截取字符串时,如果位数不够,Python不会报错,而是返回空字符串 # 这是因为Python中的字符串是不可变的,所以当我们尝试访问一个不存在的索引时,Python会返回空字符串而不是报错 # 示 ...
- Java Maven POM配置参考
介绍 什么是POM? POM代表"项目对象模型".它是一个名为pom.XML的文件中保存的Maven项目的XML表示. 快速概览 这是一个直接位于POM项目元素下的元素列表.请注意 ...