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 ...
随机推荐
- AMH安装Nextcloud出现Access denied
AMH部署了LAMP或者LNMP,然后常规安装Nextcloud,安装完成后跳转到首页就出现Access denied.Nextcloud的文件夹权限已经修改,可读写.http://127.0.0.1 ...
- Pandas: title函数的作用
代码演示 将首字母大写,其余小写 效果演示 参考链接 https://www.w3resource.com/pandas/series/series-str-title.php
- TypeScript: 類型 'HTMLElement | null' 不可指派給類型 'HTMLElement'。 類型 'null' 不可指派給類型 'HTMLElement'
报错截图 解决方案 在结尾添加叹号 // 获取页面中food的元素并将其赋值给element this.element = document.getElementById('food')!;
- 微信小程序+web数据库的开发实践
前言 生活中使用微信小程序的场景越来越多,它实现了用户对于应用"触手可及.用完即走"的理想需求.微信小程序的开发难度也低于APP的开发制作,使用它会更便利.低成本.高经济效益. 但 ...
- 你们眼睛干涩,胀痛吗?C# WPF 久坐提醒桌面小程序
目录 说明 设置提醒时间,及休息时间 久坐提醒倒计时 休息提醒倒计时 休息到计时 代码说明 主窗体设置 工作到计时 休息倒计时 源码 久坐提醒桌面小程序: 干这行职业病比较多,之前用爱丽(即:玻璃酸钠 ...
- 【升职加薪秘籍】我在服务监控方面的实践(7)-业务维度的redis监控
大家好,我是蓝胖子,关于性能分析的视频和文章我也大大小小出了有一二十篇了,算是已经有了一个系列,之前的代码已经上传到github.com/HobbyBear/performance-analyze,接 ...
- 第2章 Git安装
兄弟,恭喜你,刷到这篇超详细安装GIt教程,就让Codeyang带你一步一步的安装Git!~~ Git官网地址: https://git-scm.com/ 查看 GNU 协议,可以直接点击下一步. 选 ...
- Django-rest-framework框架——路由组件、认证权限频率
@ 目录 一 路由Routers 1.1 使用方法 1.2 代码演示 1.2 视图集中附加action的声明 1.3 路由router形成URL的方式 认证权限频率 一 认证Authenticatio ...
- FreeRTOS 中的调度算法
FreeRTOS 中的调度算法 01 调度算法概述 调度算法的作用: 实时系统的调度需求 相应时间要求 任务优先级 资源利用率 FreeRTOS 调度算法的目标 提供可预测的任务调度 实现任务的优先级 ...
- 使用Java统计gitlab代码行数
一.背景: 需要对当前公司所有的项目进行代码行数的统计 二. 可实现方式 1.脚本:通过git脚本将所有的项目拉下来并然后通过进行代码行数的统计 样例: echo 创建项目对应的文件夹 mkdir 项 ...