JavaEE初阶Day 9:多线程(7)

目录

  • [Day 9:多线程(7)](#Day 9:多线程(7))
    • 多线程代码案例
      • [1. 案例一:单例模式](#1. 案例一:单例模式)
        • [1.1 饿汉模式](#1.1 饿汉模式)
        • [1.2 懒汉模式](#1.2 懒汉模式)
        • [1.3 线程安全问题](#1.3 线程安全问题)
        • [1.4 懒汉模式修改](#1.4 懒汉模式修改)

Day 9:多线程(7)

多线程代码案例

1. 案例一:单例模式

单例模式 是一种经典的++设计模式++,面试中非常常见

设计模式 类似于"棋谱",将编程中各种经典的问题场景进行整理并且提供一些解决方案;设计模式其实有很多种,绝对不止23种,随着时代的变化,新的设计模式不断地诞生,旧的模式也就在消亡

简单来说,单例模式就是单个实例

  • 整个进程中的某个类,有且只有一个对象,并不会new出多个对象,这样的对象,称为单例
  • 但是如何保证这个类只有一个实例呢,靠程序员口头保证肯定是不可行的
  • 需要让编译器来帮我们做一个强制的检查,通过一些编码上的技巧,使编译器可以自动发现代码中是否有多个实例,并且在尝试创建多个实例的时候,直接编译出错

代码中的有些对象,本身就不应该是有多个实例的,从业务角度就应该是单个实例

  • 比如,写的服务器,要从硬盘上加载100G的数据到内存中(加载到若干个哈希表里)

    肯定要写一个类,封装上述加载操作,并且写一些获取/处理数据的业务逻辑

    这样的类,就应该是单例的,一个实例,就管理100G的内存数据,搞多个实例就是N*100G的内存数据,机器吃不消也没必要

  • 再比如,服务器也可能涉及到一些"配置项"(MySQL有配置文件)

    代码中也需要有专门的类,管理配置,需要加载配置数据到内存中供其他代码使用

    这样的类的实例也应该是单例的,如果是多个实例,就存储了多份数据,如果一样还可以接受,如果不一样,以哪一个为准?

根本上保证对象是唯一实例 ,这样的代码,就称为单例模式,单例模式有很多不同的写法,下面主要介绍两种:饿汉模式与懒汉模式

1.1 饿汉模式
java 复制代码
package thread;

class Singleton {
    private static Singleton instance = new Singleton();

    public static Singleton getInstance(){
        return instance;
    }

    private Singleton(){

    }
}

public class Demo27 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1==s2);
    }
}
  • private static Singleton instance = new Singleton();
    • static成员初始化时机是在类加载的时候,此处可以简单地认为,JVM已启动,就立即加载(具体情况可能有变数)
    • static修饰的,其实是**"类属性"**,就是在"类对象"上的,每个类的类对象在JVM中只有一份
    • 此时的Singleton类只存在一个实例instance,初始化的时候只执行一次
  • getInstance():此处后续需要使用这个类的实例,就可以通过getInstance()来获取已经new好的这个实例,而不是重新实例化
  • private Singleton(){ }
    • 这样的private构造方法可以防止其他代码重新实例化这个类
    • 类之外的代码,尝试实例化的时候,就必须调用构造方法,由于构造方法是私有的,无法调用,就会编译出错
1.2 懒汉模式

懒在计算机中往往是一个褒义词,而且是高效率的代表

懒汉模式不是在程序启动的时候创建实例,而是在第一次使用的时候才去创建

如果不使用了,就会把创建实例的代码节省下来了

如果代码中存在多个单例类

  • 使用饿汉模式,就会导致这些实例都是在程序启动的时候扎堆创建的,可能会把程序启动时间拖慢,
  • 使用懒汉模式,什么时候首次调用,调用时机是分散的,化整为零,用户不太容易感知到卡顿
java 复制代码
package thread;


class SingletonLazy {
    private static SingletonLazy instance = null;

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

    private SingletonLazy() {

    }
}
public class Demo28 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }

}

if (instance == null) {instance = new SingletonLazy();}:什么时候调用就什么时候创建,如果不调用,就不创建了

1.3 线程安全问题

上述两种单例模式,是否是线程安全的

考虑有多个线程同时调用getInstance,是否会产生线程安全问题

  • 对于饿汉模式

    • 创建实例的时机是在Java进程启动,比main调用还早的时机
    • 后续代码里创建线程,一定比上述实例创建要更迟
    • 后续执行getInstance的时候,意味着上述实例早都已经有了
    • 每个线程的getInstance只做了一件事,就是读取上述静态变量的值
    • 多个线程读取同一个变量,是线程安全的
  • 对于懒汉模式

    • if (instance == null) {instance = new SingletonLazy();}:这行代码可以理解为,包含了读和写
    • 读:查看一下instance变量的值
    • if条件判定,拿出来instance里面的引用的地址,看一下是否为null
    • 写:修改,赋值就是修改
    • 上述代码在多线程环境下就可能产生问题
1.4 懒汉模式修改

上述的懒汉模式造成的多线程不安全问题,本质是因为,如果执行顺序如下:

  • 线程t1判断了instance为null后
  • 此时,线程t2也进行了判断,同样认为instance为null
  • 接下来线程t1开始创建实例
  • 由于t2之前判断instance为null,于是也创建实例
  • 最后导致第二个对象的地址覆盖了第一个

那么我们通过加锁的方式来进行尝试

java 复制代码
public static SingletonLazy getInstance(){
    if (instance == null){
        synchronized (locker){
            instance = new SingletonLazy();
        }
    }
    return instance;
}

但是上述代码仍然存在问题:

  • 线程t1判断了instance为null后
  • 此时,线程t2也进行了判断,同样认为instance为null
  • 接下来线程t1进行加锁,之后,线程t1开始创建实例,并释放锁
  • t2拿到锁之后,由于t2之前判断instance为null,于是也创建实例
  • 最后同样导致第二个对象的地址覆盖了第一个

于是应该把if和new打包成一个原子操作

java 复制代码
public static SingletonLazy getInstance(){
    synchronized (locker){
        if (instance == null){
            instance = new SingletonLazy();
        }
    }
    return instance;
}

同时,上述代码还有修改的空间,懒汉模式只是在最开始调用getInstance会存在线程安全问题,一旦把实例创建好了,后续再调用,就只是读操作了,就不存在线程安全问题了,针对后续调用,明明没有线程安全问题,还要加锁,就是画蛇添足(加锁本身,也是有开销的,可能会使线程阻塞)

代码如下:

java 复制代码
package thread;


class SingletonLazy {
    private static volatile SingletonLazy instance = null;
    private static Object locker = new Object();

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

    private SingletonLazy() {

    }
}
public class Demo28 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }

}	
  • 第一个if用于判定是否要加锁
    • 实例化之后,线程自然就安全了,无需加锁了
    • 实例化之前,应该要加锁
  • 第二个if用于判断是否要创建对象
  • 同时volatile也是有必要的,避免触发了优化,避免内存可见性与指令重排序问题
相关推荐
原野心存几秒前
java基础进阶——继承、多态、异常捕获(2)
java·java基础知识·java代码审计
进阶的架构师5 分钟前
互联网Java工程师面试题及答案整理(2024年最新版)
java·开发语言
黄俊懿5 分钟前
【深入理解SpringCloud微服务】手写实现各种限流算法——固定时间窗、滑动时间窗、令牌桶算法、漏桶算法
java·后端·算法·spring cloud·微服务·架构
木子020414 分钟前
java高并发场景RabbitMQ的使用
java·开发语言
夜雨翦春韭25 分钟前
【代码随想录Day29】贪心算法Part03
java·数据结构·算法·leetcode·贪心算法
大霞上仙1 小时前
jmeter学习(1)线程组与发送请求
java·学习·jmeter
笃励1 小时前
Java面试题二
java·开发语言·python
易雪寒1 小时前
IDEA在git提交时添加忽略文件
java·git·intellij-idea
打码人的日常分享2 小时前
企业人力资源管理,人事档案管理,绩效考核,五险一金,招聘培训,薪酬管理一体化管理系统(源码)
java·数据库·python·需求分析·规格说明书
27669582922 小时前
京东e卡滑块 分析
java·javascript·python·node.js·go·滑块·京东