Java 线程安全与 volatile与单例模式问题及解决方案

2025-06-30 17:50

本文主要是介绍Java 线程安全与 volatile与单例模式问题及解决方案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Java线程安全与volatile与单例模式问题及解决方案》文章主要讲解线程安全问题的五个成因(调度随机、变量修改、非原子操作、内存可见性、指令重排序)及解决方案,强调使用volatile关键字...

什么是线程安全

在进行多线程编程的时候,当我们编写出来的多线程的代码运行结果不符合我们的预期的时候,这时候就是 bug,这种 bug 是由于多线程的问题而产生出来的 bug 我们称之为 线程安全问题

当我们编写出来的多线程代码运行之后的结果符合我们的预期结果的时候,说明代码没有问题,这时候就是 线程安全

线程安全问题的产生与解决方案

线程安全问题的产生主要有 五个原因

线程的调度是随机的

这个原因是由操作系统产生的,CPU 是多核心的,在进行线程的调度的时候并不是等到线程彻底执行完才轮到下一个线程执行,CPU 使用的是抢占式执行,也就是说,这个线程可能执行到一半,就立马被剥夺了 CPU 资源,开始执行下一个线程,然后执行完一半,又将上一个线程调度回来,这是由随机性的,程序员无法通过代码应用层得知。

这个问题是无法改变的,这也就是为什么会产生线程安全问题的最根本的原因

多个线程对同一个变量进行修改

在之前的文章中就已经设计过这种情况的讨论,如果修改的外部类的成员变量,是会发生线程安全问题的,如果修改的是局部变量,那就会触发 “变量捕获的语法”,这时候是不建议进行修改的。

解决方法也很简单,就是加锁,通过 synchronized 进行加锁。

public class Demo2 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronizChina编程ed(locker) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized(locker) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count =" + count);
    }
}

线程的修改操作不是原子性的

这个问题其实和第二个问题是一样的,为什么修改同一个变量可能会发生线程安全问题,因为我们的修改指令并不是原子性的,也就说,这个操作并不是 CPU 执行一次指令就可以完成 count++ 的,count ++ 实质是由三条指令实现的,首先 load count 这个数值,然后进行 count +1 操作,最后将结果保存到内存里。

为了使修改操作是原子性的,所以我们使用加锁的方式来实现,也就是上面的代码。

内存可见性问题

这个问题是由于 JVM 优化而导致的,

public class Test {
    private static int flag = 1;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(flag == 1) {
            }
            System.out.println("hello t1");
        });
        Thread t2 = new Thread(() -> {
            Scanner scan = new Scanner(System.in);
            System.out.println("请输入falg 的数值");
            flag = scan.nextInt();
        });
        t1.start();
        t2.start();
    }
}

Java 线程安全与 volatile与单例模式问题及解决方案

即使我们修改了 flag 数值,但是程序依旧没有反应,说明在 t1 线程中读取到的 flag 依旧还是 1

一个线程涉及到了读操作,一个线程涉及到了修改操作,这可能会触发线程安全问题,也就是内存可见性问题,读操作没有读到修改过的数值。

原因:JVM / 编译器 其实是带有优化功能的,因为不同的程序员写出来的代码不同,运行效率也是不同,为了提高代码的运行效率,JVM / 编译器 在不改变我们代码的逻辑的情况下,会对我们写的代码进行优化。虽然说对我们代码逻辑不会做出改变,但是在多线程编程下可能会发生误判。

例如上面的代码,t1 线程进行读 flag 操作,也就是寄存器会从内存中读取 flag ,但是这是一个 while 循环,在一秒钟之内就会读取很多次,虽然 t2 线程会对 flag 进行修改,但是 t2 线程在启动之前 flag 这个数值就被 t1 线程读取了 几千万次,所以编译器 / JVM 会认为 flag 是一个不会被修改的数值,即把这个读内存操作优化为 读寄存器操作,也就是把 flag 这个数值拷贝一份到寄存器里,这样 CPU 就直接从寄存器读 flag 数值而不用到 内存中读取了。

www.chinasem.cn

等到了 t2 线程开始运行的时候,我们进行修改 flag 数值,内存中 flag 即使被修改了,但是 t1 线程还是不知道flag 被python修改了,因为此时它是从寄存器读取 flag 数值。

拓展一下,如果我们在 t1 线程 加上 sleep 的话,这个内存可见性问题就消失了。

    private static int flag = 1;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(flag == 1) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("hello t1");
        });
        Thread t2 = new Thread(() -> {
            Scanner scan = new Scanner(System.in);
            System.out.println("请输入falg 的数值");
            flag = scan.nextInt();
        });
        t1.start();
        t2.start();
    }

Java 线程安全与 volatile与单例模式问题及解决方案

即使是 sleep 1 ms 内存可见性问题也没有发生,这是为什么?

因为读内存操作可能就是几 ns 的事情,优化为 读寄存器操作可以再快个几 ns,但是代码存在 sleep 1 ms ,这个 1ms 的存在,编译器/ JVM 即使优化这个读操作也不能让代码的效率有一个质的飞跃,所以干脆就不提升了。所以内存可见性问题也就不存在了。

JVM / 编译器的优化是一个很复杂的事情,具体的细节大家可以参考深入理解Java虚拟机 这本书,在后续文章中也会提到 JVM 的部分内容。

如何解决这个内存可见性问题???
使用 volatile 关键字

Java 线程安全与 volatile与单例模式问题及解决方案

这个关键字的英文翻译的易变的,说明这个变量我是会进行修改的,你不能进行读操作的优化。

注意这个关键字只能修饰变量,不能修饰方法!!!

修改后的代码:

import java.util.Scanner;
public class Test {
    private static volatile int flag = 1;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(flag == 1) {
            }
            System.out.println("hello t1");
        });
        Thread t2 = new Thread(() -> {
            Scanner scan = new Scanner(System.in);
            System.out.pjsrintln("请输入falg 的数值");
            flag = scan.nextInt();
        });
        t1.start();
        t2.start();
    }
}

Java 线程安全与 volatile与单例模式问题及解决方案

指令重排序问题

这个问题在下面的单例模式中的懒汉android模式会提到~~

单例模式

单例模式是一种设计模式,也就是一个规范。

单例模式,顾名思义就是只允许一个对象的创建,也就是一个类只能创建实例化一个对象,不能进行多次实例化。这种设计模式的应用场景还是很多的,例如:我们在进行服务器开发的时候,我们需要一个对象来存放数据,这时候我们就会先写出类,然后再去创建对象,但是如果这个对象包含的数据很大,假如有100G,那么创建多次之后,也就是有几百G 的数据需要放在服务器上,并且这么多重复的数据也就只有一份是有用的,不仅仅是浪费了服务器的内存资源,还可能会导致服务器的崩溃,在这种情况下,我们通常使用单例模式来进行约束,只允许一个对象的创建。

饿汉模式

饿汉模式 是程序已启动,随着类的加载,对象也随之创建出来了,所以称之为 饿汉模式,说明创建的很快。

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

从上面的代码,我们就可以看到是要类一加载,对象instance 也就创建出来了private static Singleton instance = new Singleton();

为什么说我们不能进行多次创建呢?
因为这个类的构造方法被我们用private 修饰了,在外面是不能进行实例化的,这也是单例模式的点睛之笔。

我们来讨论一下,这个饿汉模式 的代码会不会出现线程安全问题?
答案是不会的,线程只是从getInstance() 进行读操作,获取 instance 这个对象,并没有涉及到修改操作,自然没有线程安全问题的存在。

懒汉模式

懒汉模式 顾名思义就是 懒,等我们真正需要这个对象的时候,才会进行实例化对象的操作。我们来看一下代码:

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

当我们真正需要用到这个对象的时候,才进行实例化,这就是懒汉模式。

但是在多线程编程下,是可能会出现线程安全问题,由于代码涉及到写操作,也就是 实例化对象的操作,假设有两个线程同时进行对象的实例化,就会发生线程安全问题,所以要加上锁 synchronized .

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

但是每次进行判断的时候都需要进行加锁,这就导致效率低下,所以我们在外面再加一层 if 判断,减少加锁的次数。

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

即使代码被我们修改成这样,还是会存在一个问题,指令重排序的问题

我们在实例化一个对象有三条指令需要做:第一申请内存空间,第二初始化对象,第三将内存空间的首地址赋值给引用。

在编译器/JVM 下可能会进行优化,将上面的三条指令优化为先执行1,再执行3 ,最后执行 2.

这可能会导致一个线程还没初始化对象,另一个线程就直接拿到这个对象进行使用了,但是这些使用操作,在后面的初始化完之后又被覆盖掉了。这就是第五个引起线程安全问题的原因 —— 指令重排序。

Java 线程安全与 volatile与单例模式问题及解决方案

如何解决这个问题???
使用 volatile 关键字

没错 volatile 关键字不仅仅能解决内存可见性问题,还能解决指令重排序问题。

private static volatile SingletonLazy instance;

懒汉模式最终代码

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

到此这篇关于Java 线程安全 与 volatile 与 单例模式的文章就介绍到这了,更多相关java 线程安全volatile与单例模式内容请搜索China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持China编程(www.chinasem.cn)!

这篇关于Java 线程安全与 volatile与单例模式问题及解决方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1155260

相关文章

Java设计模式---迭代器模式(Iterator)解读

《Java设计模式---迭代器模式(Iterator)解读》:本文主要介绍Java设计模式---迭代器模式(Iterator),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录1、迭代器(Iterator)1.1、结构1.2、常用方法1.3、本质1、解耦集合与遍历逻辑2、统一

Java内存分配与JVM参数详解(推荐)

《Java内存分配与JVM参数详解(推荐)》本文详解JVM内存结构与参数调整,涵盖堆分代、元空间、GC选择及优化策略,帮助开发者提升性能、避免内存泄漏,本文给大家介绍Java内存分配与JVM参数详解,... 目录引言JVM内存结构JVM参数概述堆内存分配年轻代与老年代调整堆内存大小调整年轻代与老年代比例元空

深度解析Java DTO(最新推荐)

《深度解析JavaDTO(最新推荐)》DTO(DataTransferObject)是一种用于在不同层(如Controller层、Service层)之间传输数据的对象设计模式,其核心目的是封装数据,... 目录一、什么是DTO?DTO的核心特点:二、为什么需要DTO?(对比Entity)三、实际应用场景解析

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语

深度解析Java项目中包和包之间的联系

《深度解析Java项目中包和包之间的联系》文章浏览阅读850次,点赞13次,收藏8次。本文详细介绍了Java分层架构中的几个关键包:DTO、Controller、Service和Mapper。_jav... 目录前言一、各大包1.DTO1.1、DTO的核心用途1.2. DTO与实体类(Entity)的区别1

Java中的雪花算法Snowflake解析与实践技巧

《Java中的雪花算法Snowflake解析与实践技巧》本文解析了雪花算法的原理、Java实现及生产实践,涵盖ID结构、位运算技巧、时钟回拨处理、WorkerId分配等关键点,并探讨了百度UidGen... 目录一、雪花算法核心原理1.1 算法起源1.2 ID结构详解1.3 核心特性二、Java实现解析2.

SpringBoot整合liteflow的详细过程

《SpringBoot整合liteflow的详细过程》:本文主要介绍SpringBoot整合liteflow的详细过程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋...  liteflow 是什么? 能做什么?总之一句话:能帮你规范写代码逻辑 ,编排并解耦业务逻辑,代码

JavaSE正则表达式用法总结大全

《JavaSE正则表达式用法总结大全》正则表达式就是由一些特定的字符组成,代表的是一个规则,:本文主要介绍JavaSE正则表达式用法的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录常用的正则表达式匹配符正则表China编程达式常用的类Pattern类Matcher类PatternSynta

Spring Security中用户名和密码的验证完整流程

《SpringSecurity中用户名和密码的验证完整流程》本文给大家介绍SpringSecurity中用户名和密码的验证完整流程,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定... 首先创建了一个UsernamePasswordAuthenticationTChina编程oken对象,这是S

java实现docker镜像上传到harbor仓库的方式

《java实现docker镜像上传到harbor仓库的方式》:本文主要介绍java实现docker镜像上传到harbor仓库的方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录1. 前 言2. 编写工具类2.1 引入依赖包2.2 使用当前服务器的docker环境推送镜像2.2