JavaEE初阶第九期:解锁多线程,从 “单车道” 到 “高速公路” 的编程升级(七)

专栏:JavaEE初阶起飞计划

个人主页:手握风云

目录

一、wait和notify

[1.1. wait和sleep的对比](#1.1. wait和sleep的对比)

二、多线程案例

[2.1. 单例模式](#2.1. 单例模式)


一、wait和notify

1.1. wait和sleep的对比

  • wait的设计是提前唤醒,超时的时间属于是"留后手",而sleep是到时间才唤醒,虽然也可以通过Interrupt()提前唤醒,但会出现异常。
  • wait需要搭配锁来使用,wait执行时需要先释放锁,当sleep放在synchronized内部时,不会释放锁。

二、多线程案例

2.1. 单例模式

单例模式是一种设计模式,属于校招中最常考的设计模式之一。设计模式可以看作在下棋中的研究出来的一些固定套路。为了使一些代码能力比较差的人下限能够得到提升,大佬们研究出一些固定套路来应对一些场景的题。

在多线程编程中,单例模式是控制资源访问的核心设计模式之一。它确保一个类仅有一个实例,并提供全局访问点,避免多线程竞争导致的资源重复创建或状态不一致问题。

单例模式实现的方式有很多,最常见的是饿汉模式和懒汉模式。

  • 饿汉模式
java 复制代码
// 饿汉模式
class Singleton {
    // 创建唯一实例
    // 此处的instance属于static成员,创建时机,就是在类加载的时候
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }

    private Singleton() {
    }
}

public class Demo1 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);// s1和s2指向同一对象

        Singleton s3 = new Singleton();
    }
}

如果所有的代码都是一个人写的,那么有没有单例无所谓,就能人为保证只有一个实例,这些规则自己都能遵守,但在实际工作中,多人一起分工协作,某人对某个方法进行封装,别人无法调用,强制去使用某种规则。

  • 懒汉模式
java 复制代码
// 懒汉模式
class SingletonLazy {
    public static SingletonLazy instance = null;

    // 第一次使用实例的时候才会创建
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

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

如同洗碗一样,吃完饭就洗就是饿汉模式,吃完饭之后等下次吃饭之前再洗就是懒汉模式。如果第一次吃饭用了4个碗,等下次吃饭需要2个碗,按照懒汉模式,下次吃饭前只需洗两个碗,所以懒汉模式效率更高。假设有一个几个G的大文件,当我们想打开文件查看内容时,如果把整个文件加载显示出来显示给用户,就会有明显的卡顿,但按照懒汉模式,只加载一小部分就显示给用户,效率就会大大提高。

从线程安全的角度来说,饿汉模式只是针对同一变量进行读取,因此线程是安全的。但懒汉模式下,当多个线程同时检测到instance == null,就可能会有多个线程创建出多个实例。为了解决线程安全的问题,还是利用加锁来解决。

java 复制代码
class SingletonLazy {
    private static SingletonLazy instance = null;
    private static Object locker = new Object();
    public static SingletonLazy getInstance() {
        // 注意要把哪些操作打包成原子
        synchronized (locker) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }
}

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

刚才的单例模式只是在第一次调用的时候,才会涉及到线程安全问题,只要对象创建完毕,后序都是直接返回的操作,就不涉及修改了。但上述代码每次调用都会加锁,线程已经安全了,却还要进行加锁,显然是不合理的,因为加锁就可能触发阻塞。

java 复制代码
class SingletonLazy {
    private static 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;
    }
}

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

看到上面的代码,可能会打破某些人的认知。以前都是一个线程,代码执行下来,一次判定和二次判定逻辑是一样的。但在多线程编程中,一次判定和二次判定的结论可能不同。

代码写到这里,还是会有一些问题,就是之前提到的指令重排序引起的线程安全问题。指令重排序是编译器的一种优化手段,确保逻辑一致的前提下,通过调整代码的顺序,从而提升效率。上面创建对象的过程可以简化成3个步骤:1.申请内存空间;2.在内存空间上通过构造方法进行初始化;3.将内存地址保存到引用变量中。编译器就可能优化成"132"的顺序,就可能出现问题。

当先执行步骤3时,instance确实非空了,t2就会提前拿到一个未初始化的对象,当去调用s的属性和方法时,就会讲错就错。此时我们就可以通过volatile关键字避免重排序。

java 复制代码
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;
    }
}

public class Demo3 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
    }
}
相关推荐
凤凰院凶涛QAQ5 小时前
《Java版数据结构 & 集合类剖析》Map & Set & 反射:“从哈希碰撞到红黑树转换,反射帮你扒开 HashMap 的桶数组看个究竟”
java·数据结构·算法
H.莓飛5 小时前
【数据结构】排序_OJ题(一):计数、桶与排序后的处理
java·开发语言·数据结构
wangchunyu1146 小时前
JAVA AQS与Lock实现原理(含CAS)
java·开发语言·word
爱喝水的鱼丶7 小时前
SAP-ABAP:ECC→S/4HANA ABAP 迁移升级指南——程序改造、语法兼容与性能优化 导语
运维·开发语言·性能优化·sap·abap·经验交流·开发交流
w_zero_one7 小时前
算法--模拟
java·数据结构·算法
小灰灰搞电子7 小时前
Rust+Slint 实现FTP客户端调试助手源码分享
开发语言·后端·rust
外收内放7 小时前
面向对象高级(实战案例)
开发语言·python·学习
Javatutouhouduan7 小时前
阿里1658页Java面试题总结(含答案)
java·高并发·java面试·java面试题·后端开发·java程序员·java八股文
秋天的怀念_David7 小时前
算法一千零一夜 · 第十三夜 幻方,武侠世界里的数学迷宫
java·程序人生·算法
专业程序开发源8 小时前
springboot欧缇蔓月子中心管理系统82697-计算机课程设计、毕业设计
java·spring boot·后端·python·spring·django·课程设计