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();
    }
}
相关推荐
CHANG_THE_WORLD1 天前
并发编程指南 同步操作与强制排序
开发语言·c++·算法
仰泳之鹅1 天前
【C语言】深入理解指针(5)
c语言·开发语言
无为之士1 天前
君正交叉编译链工具mips-gcc540-glibc222-64bit-r3.3.0.smaller.bz2编译st-device-sdk-c
c语言·开发语言
源力祁老师1 天前
深入分析 json2(新)与标准的 jsonrpc的区别
开发语言
小wanga1 天前
C++知识
java·开发语言·c++
学渣676561 天前
文件传输工具rsync|rust开发环境安装|Ascend实验相关命令
开发语言·后端·rust
木心爱编程1 天前
C++容器内存布局与性能优化指南
开发语言·c++·性能优化
我是渣哥1 天前
Java String vs StringBuilder vs StringBuffer:一个性能优化的探险故事
java·开发语言·jvm·后端·算法·职场和发展·性能优化
工一木子1 天前
深入Java并发:锁机制原理剖析与性能优化实战
java·性能优化·并发·