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();
    }
}
相关推荐
间彧3 分钟前
ReentrantLock与ReadWriteLock在性能和使用场景上有什么区别?
java
Lbwnb丶5 分钟前
p6spy 打印完整sql
java·数据库·sql
间彧6 分钟前
公平锁与非公平锁的选择策略与场景分析
java
渣哥7 分钟前
锁升级到底能不能“退烧”?synchronized 释放后状态解析
java
间彧12 分钟前
Java ReentrantLock详解与应用实战
java
间彧22 分钟前
volatile与Atomic类的性能对比与适用场景分析
java
间彧25 分钟前
Java Atomic类详解与实战应用
java
间彧32 分钟前
Java 中volatile详解与应用
java
多多*35 分钟前
2025最新centos7安装mysql8 相关 服务器配置 纯命令行操作 保姆级教程
java·运维·服务器·mysql·spring·adb
MediaTea37 分钟前
Python 编辑器:IDLE
开发语言·python·编辑器