0
点赞
收藏
分享

微信扫一扫

【设计模式_青春版】创建型|单例模式

以前干嘛去了 2022-03-11 阅读 39

文章目录

单例模式(创建型)

饿汉式

饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程而不会出现问题。

该模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在了。

package com.zbz.设计模式;

public class Hungry {

    /*
     * 缺点:直接创建一个静态的对象,可能造成内容消耗过大
     * */
    private byte[] b1=new byte[1024*1024];
    private byte[] b2=new byte[1024*1024];
    private byte[] b3=new byte[1024*1024];
    private byte[] b4=new byte[1024*1024];
    
    //私有构造类,阻止实例的生成
    private Hungry(){

    }
    public static final Hungry hungry=new Hungry();
    public static Hungry getInstance(){
        return hungry;
    }
}

解析:

私有构造器

所谓私有构造器,就是用private关键字声明的构造器。

其访问权限是private,它只能被类自身所访问,而无法在类的外部调用,故而可以阻止对象的生成。

所以,如果一个类只有一个私有构造器,而没有任何公有构造器,是无法生成任何对象的。

那么无法生成对象的带有私有构造器的类究竟有什么作用呢?

这样的类在实际应用中最常用的是作为工具类,如字符串的验证、枚举转换之类的,通常只做成静态接口被外部调用即可。


静态方法是用static关键字声明的方法,可以用类来直接调用而无需用从类中实例化出来的具体对象来调用,因此这样的方法也被称为类方法。static方法只能访问类中的static字段和其他的static方法,这是因为非static成员必须通过对象的引用来访问。


1、我们要知道静态属性是在初始化对象(new Hungry())第一时间初始化的:

在这里插入图片描述

在这里插入图片描述

2、对象的内存布局

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7muGZ3hv-1646406923378)(java_notes.assets/image-20211025003747709-16350934691083.png)]

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XvowKDIN-1646406923378)(java_notes.assets/image-20211025004843352-16350941251166.png)]

3、new Hungry()背后操作系统做的事

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ITehYgLO-1646406923379)(java_notes.assets/592743-20160319235423381-1926278401.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gnPWu1Z0-1646406923380)(java_notes.assets/image-20211025005059724-16350942609899.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-imc88MFs-1646406923381)(java_notes.assets/image-20211025005027379-16350942288918.png)]

指令1:分配内存空间

指令2:执行构造方法,初始化对象

指令3:把这个对象指向这个空间

但是cpu允许 132这样的执行顺序,所以多线程情况下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fiQupBQD-1646406923381)(java_notes.assets/image-20211025005341842-163509442297310.png)]

​ A线程执行了132 B线程来的时候对象正在初始化中,此时对象是不为空的,所以就会被拿去用。

并发编程volatile

Java内存结构

Java的内存结构就是之前在学习Java虚拟机时的内存区域的划分。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gx0eWCy1-1646406923381)(java_notes.assets/img1.png)]


Java内存结构在这里就不展开了,在JVM那里在详细说,这里提起是要说明哪些区域是线程私有的,哪些是线程共享的,还有就是和JMM区分开来。

JMM

JMM 是 JVM 中定义的一种并发编程的底层模型机制。

JMM,全称为Java Memory Model,Java内存模型。不要和Java内存结构搞混了。

Java内存模型是一组规范或者说是规则,每个线程执行都要遵循这个规范,是用来解决在线程的通信问题的。

JMM是一种规范,是一个抽象的概念,并不真实存在,内存结构才是真实存在的

在讲解JMM之前先要理解两个概念,主内存和工作内存。

主内存

主内存是Java运行时在计算机存储数据的地方,是所有线程共享的,同时多个线程对这个主内存进行修改,就会出现很多的问题,这就是并发操作的问题,需要我们去解决。

工作内存

每个线程都有一个存储数据的地方,用来存储线程需要操作的数据,为什么要这样呢?

因为线程是不能直接对主内存中的数据进行修改的,只能修改线程工作内存中的数据,所以线程修改主内存中的数据时就会将主内存中的数据保存在自己的工作内存,然后在进行操作。

这样就会存在一个问题,每个线程都会对自己的工作内存进行操作,所以每一个线程都无法得知其他线程工作内存中的数据是怎么样的,这就是一个可见性的问题。

JMM的抽象结构

JMM 的规定:
- 所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。

  • 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
  • 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
  • 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7ir2ADVz-1646406923382)(java_notes.assets/v2-3d312429710bd6a11eca171858f67751_720w.jpg)]

以下是JMM对主内存数据操作时会执行的八个操作。(按顺序)

JMM的三个特性

Java内存模型就是为了解决对共享数据中的可见性,原子性和有序性问题的一组规则。

即JMM的存在就是为了 保证这三个特性,现在具体来看看这三个特性。

可见性

可见性刚刚也讲工作内存的时候也是有提到的,这个其实很好理解,每个线程中的工作内存经过修改写回主内存之后,其他线程都可以看见主内存中的值发生变化,从而解决一些缓存不一致的情况。

原子性

原子性表示一个操作在执行中是不可以被中断的,有点类似事务的原子性,要么成功完成,要么直接失败。

有序性

有序性表示JMM会保证操作是有序执行的。或许有人会感到疑惑,难道程序不都是有序执行的吗?

这就要说到处理器的指令重排了,这涉及到了一些汇编的知识,所以不怎么展开了,大概了解一下。

为了提高CPU的使用率,在程序编译执行的时候,处理器会将指令进行重排优化,一般分为以下三个。

指令重排使得语句不一定是按从上到下执行的,可能会是乱序执行的,有些语句是存在数据依赖性的才会保持前后顺序

为什么单线程的时候没有感觉呢?

这是因为指令重排不会干扰到单线程执行的结果的,但是在多线程中乱序执行就会出现一些问题,导致得到的结果不一样。

多线程下的变量不可见现象

内存可见性

package com.zbz.设计模式;

public class visibility {
    public static void main(String[] args) {
            MyThread th=new MyThread();
            th.start();

            while (true){
                if(th.isFlag()){//th.start子线程启动1s后才设置flag=true
                    //主线程一直处于while死循环,但是并不能进入if
                    System.out.println("进入while---");
                }
            }
    }
}

class MyThread extends Thread{
    private boolean flag=false;

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        //模拟在flag=true执行前执行其他代码花费的时间
        try {
            Thread.sleep(1000);//1s
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag=true;
        System.out.println("flag="+flag);
    }
}

可见性问题的解决方案

我们如何保证多线程下共享变量的可见性呢?也就是当一个线程修改了某个值后,对其他线程是可见的。

这里有两种方案:加锁使用 volatile 关键字

加锁synchronized:
package com.zbz.设计模式;

public class visibility {
    public static void main(String[] args) {
            MyThread th=new MyThread();
            th.start();

            while (true){
                //**这里大家应该有个疑问是,为什么加锁后就保证了变量的内存可见性了?** 
                synchronized(th) {//加锁
                    if (th.isFlag()) {//th.start子线程启动1s后才设置flag=true
                        System.out.println("进入while---");
                    }
                }
            }
    }
}

class MyThread extends Thread{
    private boolean flag=false;

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        //模拟在flag=true执行前执行其他代码花费的时间
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag=true;
        System.out.println("flag="+flag);
    }
}

这里大家应该有个疑问是,为什么加锁后就保证了变量的内存可见性了?

因为当一个线程进入 synchronized 代码块

这里除了 synchronized外,其它锁也能保证变量的内存可见性。

使用 volatile 关键字

使用 volatile 修饰共享变量后,每个线程要操作变量时

会从主内存中将变量拷贝到本地内存作为副本,当线程操作变量副本并写回主内存后,会通过 CPU 总线嗅探机制告知其他线程该变量副本已经失效,需要重新从主内存中读取。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FeuZ76QK-1646406923382)(java_notes.assets/image-20211025093915695-163512595692714.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-txy8MU39-1646406923383)(java_notes.assets/image-20211025094003087-163512600447915.png)]

package com.zbz.设计模式;
public class visibility {
    public static void main(String[] args) {
            MyThread th=new MyThread();
            th.start();

            while (true){
                    if (th.isFlag()) {//th.start子线程启动1s后才设置flag=true
                        //主线程一直处于while死循环,但是并不能进入if
                        System.out.println("进入while---");
                    }
            }
    }
}
class MyThread extends Thread{
    private volatile boolean flag=false; //volatile关键字

    public boolean isFlag() {
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        //模拟在flag=true执行前执行其他代码花费的时间
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag=true;
        System.out.println("flag="+flag);
    }
}

总线嗅探机制

为了提高处理速度,CPU 不直接和内存进行通信,而是在 CPU 与内存之间加入很多寄存器,多级缓存,它们比内存的存取速度高得多,这样就解决了 CPU 运算速度和内存读取速度不一致问题。

由于 CPU 与内存之间加入了缓存,在进行数据操作时,先将数据从内存拷贝到缓存中,CPU 直接操作的是缓存中的数据。但在多处理器下,将可能导致各自的缓存数据不一致(这也是可见性问题的由来),为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,而嗅探是实现缓存一致性的常见机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sAZ6NHDB-1646406923383)(java_notes.assets/v2-77acfee471d8827a2121157c68f2485d_720w.jpg)]

处理器内存模型

嗅探机制工作原理:每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。

可见性问题小结

上面的例子中,我们看到,使用 volatile 和 synchronized 锁都可以保证共享变量的可见性。相比 synchronized 而言,volatile 可以看作是一个轻量级锁,所以使用 volatile 的成本更低,因为它不会引起线程上下文的切换和调度。但 volatile 无法像 synchronized 一样保证操作的原子性。

volatile原子性问题

所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。

在多线程环境下,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的

要解决线程不安全的这个问题,我们可以使用锁机制,或者使用原子类(如 AtomicInteger)。

特别说明:

对任意单个使用 volatile 修饰的变量的读 / 写是具有原子性,但类似于 flag = !flag 这种复合操作不具有原子性。简单地说就是,单纯的赋值操作是原子性的


package com.zbz.设计模式;

//创建100个线程,每个线程将共享数据count累加10000次  count最终结果应该为100000
public class Atomicity {
    public static void main(String[] args) {
        Runnable target=new MyThread0();
        for (int i=1;i<=100;++i){
            new Thread(target,"第"+i+"个线程").start();
        }
    }
}
class MyThread0 implements Runnable{
    private int count=0; //共享变量
    @Override
    public void run() {
        for(int i=1;i<=1000;i++){
            ++count;
            System.out.println(Thread.currentThread().getName()+"--->count: "+count);
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kN9jGa9Z-1646406923384)(java_notes.assets/image-20211025102239367-163512856041917.png)]

结果是并不能够加到100000

浅析:线程A和线程B都拿到了count,A拿到count时的值100,B同时也拿到count的值也为100,并“同时修改”count的值为101,并同时覆盖进主内存

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4J16nKMI-1646406923384)(java_notes.assets/image-20211025102631502-163512879251818.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p1NdAj4o-1646406923385)(java_notes.assets/image-20211025102705904-163512882708219.png)]

加上volatile
解决办法

要解决线程不安全的这个问题,我们可以使用锁机制,或者使用原子类(如 AtomicInteger)。

package com.zbz.设计模式;

//创建100个线程,每个线程将共享数据count累加10000次  count最终结果应该为1000000次
public class Atomicity {
    public static void main(String[] args) {
        Runnable target=new MyThread0();
        for (int i=1;i<=100;++i){
            new Thread(target,"第"+i+"个线程").start();
        }
    }
}
class MyThread0 implements Runnable{
    private int count=0;
    @Override
    public void run() {
        synchronized (MyThread0.class) { //加锁
            for (int i = 1; i <= 1000; i++) {
                ++count;
                System.out.println(Thread.currentThread().getName() + "--->count: " + count);
            }
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qlVYBMLo-1646406923386)(java_notes.assets/image-20211025103228571-163512914970620.png)]

volatile禁止指令重排序

指令重排序

为了提高性能,在遵守 as-if-serial 语义(即不管怎么重排序,单线程下程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守。)的情况下,编译器和处理器常常会对指令做重排序。

一般重排序可以分为如下三种类型:

从 Java 源代码到最终执行的指令序列,会分别经历下面三种重排序:

img 重排序顺序

为了更好地理解重排序,请看下面的部分示例代码:

int a = 0;
// 线程 A
a = 1;           // 1
flag = true;     // 2

// 线程 B
if (flag) { // 3
  int i = a; // 4
}

单看上面的程序好像没有问题,最后 i 的值是 1。

为了提高性能,编译器和处理器常常会在不改变数据依赖的情况下对指令做重排序

假设线程 A 在执行时被重排序成:先执行代码 2,再执行代码 1;

而线程 B :在线程 A 执行完代码 2 后,读取了 flag 变量。

由于条件判断为真,线程 B 将读取变量 a。此时,变量 a 还根本没有被线程 A 写入,那么 i 最后的值是 0,导致执行结果不正确。

**那么如何程序执行结果正确呢?**这里仍然可以使用 volatile 关键字。

这个例子中, 使用 volatile 不仅保证了变量的内存可见性,还禁止了指令的重排序,即保证了 volatile 修饰的变量编译后的顺序与程序的执行顺序一样。那么使用 volatile 修饰 flag 变量后,在线程 A 中,保证了代码 1 的执行顺序一定在代码 2 之前。

volatile 是如何禁止指令重排序的呢?

内存屏障指令

内存屏障是一组处理器指令,它的作用是禁止指令重排序和解决内存可见性的问题。

JMM 把内存屏障指令分为下列四类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o2qxj0At-1646406923387)(java_notes.assets/v2-a992d2e50fc353edb9027683bf62ee2b_720w.jpg)]

下面我们来看看 volatile 读 / 写时是如何插入内存屏障的,见下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8WkU2otH-1646406923387)(java_notes.assets/v2-fdead3feef1123c1c225826559def686_720w.jpg)]

从上图,我们可以知道 volatile 读 / 写插入内存屏障规则:

  • 在每个 volatile 读操作的后面插入 LoadLoad 屏障和 LoadStore 屏障。
  • 在每个 volatile 写操作的前后分别插入一个 StoreStore 屏障和一个 StoreLoad 屏障。
happens-before

什么是happens-before

happens-before不会影响指令重排,是在排好序的前提下保证可见性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sIfTr2C2-1646406923387)(java_notes.assets/image-20211025111835899-163513191707123.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rHhzCb6O-1646406923388)(java_notes.assets/image-20211025112049604-163513205125524.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nzNTkpiJ-1646406923388)(java_notes.assets/b999a9014c086e064cb6e3063c88dcf30bd1cbed.jpeg)]

happens-before规则

**程序次序规则:**在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变。

**管程锁定规则:**就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)

**volatile变量规则:**就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iABw2PoQ-1646406923388)(java_notes.assets/image-20211025130116377-163513807839329.png)]

**线程启动规则:**在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。(注意:但是线程A之后对共享变量的修改结果对线程B不一定可见)

**线程终止规则:**在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。也称线程join()规则。

线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。

**传递性规则:**这个简单的,就是happens-before原则具有传递性,即hb(A, B) , hb(B, C),那么hb(A, C)。

**对象终结规则:**这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。

volatile写读建立的happens-before

问题:

package com.zbz.设计模式;
public class HappensBefore {
    private int a=1;
    private int b=2;

    public void write(){
        a=3;
        b=a;
    }
    public void read(){
        System.out.println("b="+b+",a="+a);
    }

    public static void main(String[] args) {
        HappensBefore hb=new HappensBefore();
        new Thread(new Runnable() {//线程1
            @Override
            public void run() {
                hb.write();//写
            }
        }).start();

        new Thread(new Runnable() {//线程2
            @Override
            public void run() {
                hb.read();//读
            }
        }).start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-okwLV3PN-1646406923389)(java_notes.assets/image-20211025131911538-163513915268531.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vj2ngSgw-1646406923389)(java_notes.assets/image-20211025131930492-163513917174632.png)]

package com.zbz.设计模式;
public class HappensBefore {
    private int a=1;
    private volatile int b=2; //加上 volatile关键字

    public void write(){
        a=3;
        b=a;
    }
    public void read(){
         System.out.println("b="+b+",a="+a);
    }

    public static void main(String[] args) {
        HappensBefore hb=new HappensBefore();
        while(true){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    hb.write();
                }
            }).start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    hb.read();
                }
            }).start();
        }

    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qGpZw6Q6-1646406923390)(java_notes.assets/image-20211025133003226-163513980416234.png)]

b被写入a=3,b之前的操作全部可见:a=3是可见的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7UxzM5YU-1646406923390)(java_notes.assets/image-20211025132441657-163513948310333.png)]


在这里插入图片描述


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PFlbsaqB-1646406923390)(java_notes.assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1hKMDkyNw==,size_16,color_FFFFFF,t_70#pic_center-163513218329226)]

懒汉式

package com.zbz.设计模式;
//懒汉式单例设计模式
public class LazyMan {

    //private 避免类在外部被实例化
    private LazyMan(){
    }

    private static LazyMan lazyMan;
    private static LazyMan getInstance(){
        if(lazyMan==null){
            lazyMan=new LazyMan();
        }
        return lazyMan;
    }

    public static void main(String[] args) {
        LazyMan instance =LazyMan.getInstance();
        LazyMan instance0=LazyMan.getInstance();
        System.out.println(instance);
        System.out.println(instance0);
    }
}

结果:

com.zbz.设计模式.LazyMan@10f87f48
com.zbz.设计模式.LazyMan@10f87f48

反射破坏单例式

单例模式的终结者:
declaredConstructor.setAccessible(true);//设置在使用构造器的时候不执行权限检查
package com.zbz.设计模式;

import java.lang.reflect.Constructor;

//懒汉式单例设计模式
public class LazyMan {

    //private 避免类在外部被实例化
    private LazyMan(){
    }
	 //懒汉式单例,只有在调用getInstance时才会实例化一个单例对象
    //但是啊但是,反射很强大!!!
    private static volatile LazyMan lazyMan;
    private static LazyMan getInstance(){
        if(lazyMan==null){
            lazyMan=new LazyMan();
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        LazyMan instance =LazyMan.getInstance();
        //LazyMan instance0=LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor=LazyMan.class.getDeclaredConstructor(null);//无参构造
        declaredConstructor.setAccessible(true);//设置在使用构造器的时候不执行权限检查
        LazyMan instance1=declaredConstructor.newInstance();//用无参构造来创建一个对象实例
        //由于没有了权限检查,所以在Lazyman类外面也可以创建对象了,然后执行方法
		//观察控制台,私有构造器又被调用了一次,单例模式被攻陷了,执行方法成功
        System.out.println(instance);
        System.out.println(instance1);
    }
}

结果为:

com.zbz.设计模式.LazyMan@10f87f48
com.zbz.设计模式.LazyMan@b4c966a

哈希值不一样,证明是两个不同的实例,所以单例模式被破坏

属性lazyman加volatile关键字的原因:

上述代码中lazyMan=new LazyMan();并不是原子操作 ,JVM会分解成以下几个命令执行:

也可以使用锁 synchronzied:

volatile可以理解为是轻量级锁

 public static LazyMan getInstance(){
       if(lazyMan==null){
           synchronized (LazyMan.class){
               if(lazyMan==null){
                   lazyMan = new LazyMan();
               }
           }
       }
       return lazyMan;
   }
复习Java构造器

构造器特性:

==作用:==构建创造一个对象。同时可以给我们的属性做一个初始化操作。

下面主要学习构造器和方法的区别:

1、功能和作用的不同
构造器是为了创建一个类的实例。用来创建一个对象,同时可以给属性做初始化。这个过程也可以在创建一个对象的时候用到:Platypus p1 = new Platypus();

相反,方法的作用是仅仅是功能函数,为了执行java代码。

2、修饰符,返回值和命名的不同

构造器和方法在下面三个方便的区别:修饰符,返回值,命名。

和方法一样,构造器可以有任何访问的修饰: 
public, protected, private或者没有修饰(通常被package 和 friendly调用). 
不同于方法的是,构造器不能有以下非访问性质的修饰: abstract, final, native, static, 或者 synchronized。

3、返回类型

方法必须要有返回值,能返回任何类型的值或者无返回值(void),构造器没有返回值,也不需要void。

4、命名

构造器使用和类相同的名字,而方法则不同。按照习惯,方法通常用小写字母开始,而构造器通常用大写字母开始。

构造器通常是一个名词,因为它和类名相同;而方法通常更接近动词,因为它说明一个操作。

5、调用:

构造方法:只有在对象创建的时候才会去调用,而且只会调用一次。

一般方法:在对象创建之后才可以调用,并且可以调用多次。

6、"this"的用法

   构造器和方法使用关键字this有很大的区别。方法引用this指向正在执行方法的类的实例。静态方法不能使用this关键字,因为静态方法不属于类的实例,所以this也就没有什么东西去指向。构造器的this指向同一个类中,不同参数列表的另外一个构造器
单例模式被破坏解决方法1:
package com.zbz.设计模式;

import java.lang.reflect.Constructor;

//懒汉式单例设计模式
public class LazyMan {

    private static int count=0; //通过static count变量控制
    //private 避免类在外部被实例化
    private LazyMan(){
        System.out.println("构造器被调用了"+(++count)+"次");
        if(count>1){
            throw new RuntimeException("单例构造器不能重复使用");
        }
    }
    private static volatile LazyMan lazyMan;
    private static LazyMan getInstance(){
        if(lazyMan==null){
            lazyMan=new LazyMan();
        }
        return lazyMan;
    }
    public static void main(String[] args) throws Exception {
        LazyMan instance =LazyMan.getInstance();
        //LazyMan instance0=LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor=LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyMan instance1=declaredConstructor.newInstance();
        System.out.println(instance);
        System.out.println(instance1);
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tA20CJSp-1646406923391)(java_notes.assets/image-20211025223519745-163517252073735.png)]

但是还是可以通过反射进行破坏:

package com.zbz.设计模式;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

//懒汉式单例设计模式
public class LazyMan {

    private static int count=0;
    //private 避免类在外部被实例化
    private LazyMan(){
        System.out.println("构造器被调用了"+(++count)+"次");
        if(count>1){
            throw new RuntimeException("单例构造器不能重复使用");
        }
    }
    private static volatile LazyMan lazyMan;
    private static LazyMan getInstance(){
        if(lazyMan==null){
            lazyMan=new LazyMan();
        }
        return lazyMan;
    }
    public static void main(String[] args) throws Exception {
        //LazyMan instance =LazyMan.getInstance();
        //LazyMan instance0=LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor=LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
		//反射:构造器实例化instance2
        LazyMan instance2=declaredConstructor.newInstance();
        //获取字段
        Field count=LazyMan.class.getDeclaredField("count");
        //破坏 变量count的private属性
        count.setAccessible(true);
        //实例化instance2后给instance2的static count重新复制为0
        count.set(instance2,0);
        //通过反射构造器实例化对象,此时count已经被重置为1
        LazyMan instance1=declaredConstructor.newInstance();

        System.out.println(instance2);
        System.out.println(instance1);
    }
}

java中,类刚被加载时,所有类的信息都放在方法区,包括static

静态属性和非静态属性的区别:

> [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xGNQEN3e-1646406923391)(java_notes.assets/image-20211025225316241-163517359730736.png)]

静态内部类实现

代码

package com.zbz.设计模式;
//静态内部类实现单例模式
public class Holder {
    //单例模式:构造器私有
    private Holder(){
    }
    //外部类的静态成员不可以直接使用非静态内部类
    public static Holder getInstance(){
        return InnerClass.HOLDER;
    }
    //静态内部类
    public static class InnerClass{
        private static final Holder HOLDER=new Holder();
    }
}

但还是可以被反射破坏

枚举实现

package com.zbz.设计模式;
//枚举实现单例模式可以不被反射破坏
//枚举本质上也是一个类
public enum EnumSingle {
    INSTANCE;//实例
    public static EnumSingle getInstance(){
        return INSTANCE;
    }
    public static void main(String[] args) {
        EnumSingle ins1=EnumSingle.INSTANCE;
        EnumSingle ins2=EnumSingle.getInstance();
        System.out.println(ins1);
        System.out.println(ins2);
    }
}

输出:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5QuSnvXd-1646406923392)(java_notes.assets/image-20211028195823028-16354223041631.png)]

1、我们试图通过反射破坏:

package com.zbz.设计模式;

import java.lang.reflect.Constructor;

//枚举实现单例模式可以不被反射破坏
//枚举本质上也是类
public enum EnumSingle {
    INSTANCE;//实例
    public static EnumSingle getInstance(){
        return INSTANCE;
    }
    public static void main(String[] args) throws Exception {
        EnumSingle ins1=EnumSingle.INSTANCE;
        //通过反射 实例化对象
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor();//无参构造
        declaredConstructor.setAccessible(true);
        EnumSingle ins2 = declaredConstructor.newInstance();
        System.out.println(ins1);
        System.out.println(ins2);
    }
}

2、我们通过jdk自带的反编译发现存在无参构造:

在idea中,[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-idYFwwpm-1646406923392)(java_notes.assets/image-20211028200731257-16354228526484.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UUilqI8d-1646406923392)(java_notes.assets/image-20211028200640237-16354228013563.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sHHI3zUm-1646406923393)(java_notes.assets/image-20211028200317847-16354225994712.png)]

3、于是我们产生了疑问?不能通过无参构造实例化对象呢?

于是我们用专业一点的反编译工具 jad.exe:

我们要把jad.exe文件放在.class文件同级目录下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dyVQkRfp-1646406923393)(java_notes.assets/image-20211028201134837-16354230958935.png)]

4、于是我们发现源码中只有有参构造:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uOnrkqXg-1646406923394)(java_notes.assets/image-20211028201241036-16354231626296.png)]

于是我们使用反射再次破坏:

package com.zbz.设计模式;

import java.lang.reflect.Constructor;

//枚举实现单例模式可以不被反射破坏
//枚举本质上也是类
public enum EnumSingle {
    INSTANCE;//实例
    public static EnumSingle getInstance(){
        return INSTANCE;
    }
    public static void main(String[] args) throws Exception {
        EnumSingle ins1=EnumSingle.INSTANCE;
        //通过反射 实例化对象
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);//有参构造
        declaredConstructor.setAccessible(true);
        EnumSingle ins2 = declaredConstructor.newInstance();
        System.out.println(ins1);
        System.out.println(ins2);
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-89GuNBJn-1646406923394)(java_notes.assets/image-20211028201346212-16354232274297.png)]

5、证明反射不能破坏枚举实现单例模式

最后:总结及其应用场景

优点:

缺点:

使用注意事项

适用场景

举报

相关推荐

0 条评论