当前位置: 首页 > news >正文

Java EE——线程安全和单例模式

wait和sleep对比

这两个关键字都是使线程进入阻塞等待的状态。区别就是sleep使时间到了就自己唤醒了,而wait是需要别的线程调用notify才能唤醒。
wait还有一种重载版本,可以传时间参数,代表最大的等待时间

wait 和 notify demo

我们可以通过wait和notify来管理线程执行的顺序,进而影响输出。下面这个demo就是通过三个线程互相调用wait和notify,相互唤醒,以达到循环输出10次abc的目的

public class homework2 {
    private static Object locker1 = new Object();
    private static Object locker2 = new Object();
    private static Object locker3 = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                synchronized (locker1){
                    try {
                        locker1.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("A");
                    synchronized (locker2){
                        locker2.notify();
                    }
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                synchronized (locker2){
                    try {
                        locker2.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("B");
                    synchronized (locker3){
                        locker3.notify();
                    }
                }
            }
        });
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                synchronized (locker3){
                    try {
                        locker3.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("C");
                    synchronized (locker1){
                        locker1.notify();
                    }
                }
            }
        });

        t1.start();
        t2.start();
        t3.start();

        synchronized (locker1){
            locker1.notify();
        }
    }
}

单例模式

单例模式是一种常见的设计模式。

由于并不是所有的程序员都能够保证自己的代码不会出现一些常见的错误,因此有些优秀的程序员设计了一些设计模式,通过按照设计模式编写代码,可以避免一些错误。

所谓单例模式,就是限制一个类值只能有一个实例。例如之前的jdbc代码中的DataSource,我们只需要一个实例。

我们的单例模式就是为了避免一时马虎,从而犯下的bug。其原理就是通过java本身的语法特性,限制了我们的代码写法

而java中我们的static修饰符就是可以让我们只拥有一个实例,事实上,其本质相当于是类对象的属性,而类对象是通过jvm加在.class文件得到的,而.class文件只能加载一次,因此就只有一个

static关键字介绍

我们的static关键字在c语言中代表将变量放到静态内存中,随后由于静态变量的消失,static演变成下面几个用途

  1. 修饰局部变量,改变了变量的生命周期,让静态局部变量出了作用域依然存在,程序结束,生命周期才结束。
  2. 修饰全局变量,改变了变量的作用域,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。
  3. 修饰函数,改变了其作用域,使得这个函数只能在本源文件内使用,不能在其他源文件内使用。

而在c++中,static用来控制变量的存储方式和可见性

实现单例模式

public class SingletonDemo {
    private static SingletonDemo instance = new SingletonDemo();
    
    public static SingletonDemo getInstance(){
        return instance;
    }
}

我们在代码类创建过程中就把对象创建出来,这样其他人就创建不了了,但是还有一个问题,别人可以通过构造方法来创建新的实例
因此我们把构造方法也变成private就可以了

饿汉模式

public class SingletonDemo {
    private static SingletonDemo instance = new SingletonDemo();

    public static SingletonDemo getInstance(){
        return instance;
    }

    private SingletonDemo(){
        
    }

    public static void main(String[] args) {
        SingletonDemo instance = SingletonDemo.getInstance();
    }
}

事实上,上面我们的单例模式是饿汉模式,也就是类加载阶段就把实例创造好了,相对应的还有懒汉模式,也就是需要创建的时候再创建,这样的话可以提升效率,节省资源

懒汉模式

public class SingletonLazy {
    private static SingletonLazy instance = null;
    public static SingletonLazy getInstance(){
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }
    private SingletonLazy(){
        
    }
}

当我们调用getInstance方法时,如果已经有实例了,就返回实例,否则就创建实例

线程安全问题

通过代码分析,我们可以发现饿汉模式是线程安全的,而懒汉模式是线程不安全的。
因为懒汉模式中的if相当于“读”操作,而new相当于“写”操作,如果一个线程刚判断这个instance是null的,还没来得及创建实例,另一个线程这个时候也读取了instance的值,然后也创建了一个实例,这个时候我们就有两个实例了,就不安全了

我们可以用synchronized,让我们代码变得满足原子性,从而使懒汉模式的单例模式线程安全

public class SingletonLazy {
    private static SingletonLazy instance = null;
    public static SingletonLazy getInstance(){
        synchronized (SingletonLazy.class){
            if(instance == null){
                instance = new SingletonLazy();
            }
        }
        return instance;
        
    }
    private SingletonLazy(){

    }
}

我们的synchronized中的对象就传入SingletonLazy的类对象

然而,虽然解决了线程不安全问题,但是我们的代码在单线程情况下就会变得效率低下,这是因为synchronized这个锁本身调用也是有开销的,这也是为什么vector虽然线程安全,但是并不推荐使用,就是因为里面synchronized关键字的滥用导致效率低下

因此我们希望在实例没有创建之前,不加synchronized,在实例创建之后,再加synchronized

解决效率问题

public class SingletonLazy {
    private static SingletonLazy instance = null;
    public static SingletonLazy getInstance(){
        if(instance == null){
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy(){

    }
}

事实上,我们的代码还有一个问题
当我们两个线程同时想要获取实例时,如果第一个线程进入synchronized开始new对象
由于new对象在cpu上存在多个指令

  1. 申请内存,得到首地址
  2. 调用构造方法,初始化实例
  3. 将内存首地址赋予实例引用
    如果我们的cpu为了提升效率,进行了指令重排序,将2和3调换了顺序,这时如果在我们的线程1刚执行完3,还没执行到2时,那么线程1中的instance虽然有内存的地址,但是地址上并没有相应的数据
    这时线程2调用了这个getInstance方法,并且对这个instance进行了解引用操作,那么就会出现问题

解决指令重排序问题

事实上,解决指令重排序问题和解决内存可见性问题的关键字都是volatile
因此我们这要把instance这个变量用volatile修饰就好了

public class SingletonLazy {
    private volatile static SingletonLazy instance = null;
    public static SingletonLazy getInstance(){
        if(instance == null){
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy(){

    }
}

相关文章:

  • [SUCTF 2019]EasyWeb
  • 人脸识别及检测
  • Apache Doris 系列: 基础篇-Routine Load
  • 机器学习笔记 - CRAFT(文本检测的字符区域感知)论文解读
  • 【云原生-Docker】Docker 安装 Python
  • ESP8266-Arduino编程实例-TLV493D磁传感器驱动
  • Hue在大数据生态圈的集成
  • AtCoder Beginner Contest 267 (A~D)
  • 羊了个羊游戏源码搭建开发过程
  • 基于人工蜂群算法的新型概率密度模型无人机路径规划(Matlab代码实现)
  • File Inclusion 全级别
  • 微信小程序——云开发|计费方式调整大家怎么看?
  • Github 最新霸榜,号称架构师修炼之路的“葵花宝典”限时开源
  • RFSoC应用笔记 - RF数据转换器 -07- RFSoC关键配置之RF-DAC内部解析(一)
  • 【老生谈算法】matlab实现霍夫变换算法源码——霍夫变换算法
  • 网络传输文件的问题
  • 《Java8实战》-第四章读书笔记(引入流Stream)
  • 345-反转字符串中的元音字母
  • axios请求、和返回数据拦截,统一请求报错提示_012
  • CentOS 7 修改主机名
  • ES6核心特性
  • javascript 总结(常用工具类的封装)
  • JS专题之继承
  • MySQL用户中的%到底包不包括localhost?
  • Python3爬取英雄联盟英雄皮肤大图
  • Python十分钟制作属于你自己的个性logo
  • SpiderData 2019年2月13日 DApp数据排行榜
  • windows-nginx-https-本地配置
  • 百度小程序遇到的问题
  • 第十八天-企业应用架构模式-基本模式
  • 工作中总结前端开发流程--vue项目
  • 诡异!React stopPropagation失灵
  • 看完九篇字体系列的文章,你还觉得我是在说字体?
  • 前言-如何学习区块链
  • 吐槽Javascript系列二:数组中的splice和slice方法
  • puppet连载22:define用法
  • ​二进制运算符:(与运算)、|(或运算)、~(取反运算)、^(异或运算)、位移运算符​
  • ​中南建设2022年半年报“韧”字当头,经营性现金流持续为正​
  • #gStore-weekly | gStore最新版本1.0之三角形计数函数的使用
  • #HarmonyOS:Web组件的使用
  • #Spring-boot高级
  • #每日一题合集#牛客JZ23-JZ33
  • $(function(){})与(function($){....})(jQuery)的区别
  • (51单片机)第五章-A/D和D/A工作原理-A/D
  • (java版)排序算法----【冒泡,选择,插入,希尔,快速排序,归并排序,基数排序】超详细~~
  • (Redis使用系列) SpirngBoot中关于Redis的值的各种方式的存储与取出 三
  • (zt)最盛行的警世狂言(爆笑)
  • (八)光盘的挂载与解挂、挂载CentOS镜像、rpm安装软件详细学习笔记
  • (二)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (附源码)apringboot计算机专业大学生就业指南 毕业设计061355
  • (牛客腾讯思维编程题)编码编码分组打印下标(java 版本+ C版本)
  • (状压dp)uva 10817 Headmaster's Headache
  • *Algs4-1.5.25随机网格的倍率测试-(未读懂题)
  • 、写入Shellcode到注册表上线
  • .bat批处理(十一):替换字符串中包含百分号%的子串