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方法重载与重写之同名方法的双面魔法(最新整理)

《Java方法重载与重写之同名方法的双面魔法(最新整理)》文章介绍了Java中的方法重载Overloading和方法重写Overriding的区别联系,方法重载是指在同一个类中,允许存在多个方法名相同... 目录Java方法重载与重写:同名方法的双面魔法方法重载(Overloading):同门师兄弟的不同绝

Spring配置扩展之JavaConfig的使用小结

《Spring配置扩展之JavaConfig的使用小结》JavaConfig是Spring框架中基于纯Java代码的配置方式,用于替代传统的XML配置,通过注解(如@Bean)定义Spring容器的组... 目录JavaConfig 的概念什么是JavaConfig?为什么使用 JavaConfig?Jav

Java数组动态扩容的实现示例

《Java数组动态扩容的实现示例》本文主要介绍了Java数组动态扩容的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1 问题2 方法3 结语1 问题实现动态的给数组添加元素效果,实现对数组扩容,原始数组使用静态分配

Java中ArrayList与顺序表示例详解

《Java中ArrayList与顺序表示例详解》顺序表是在计算机内存中以数组的形式保存的线性表,是指用一组地址连续的存储单元依次存储数据元素的线性结构,:本文主要介绍Java中ArrayList与... 目录前言一、Java集合框架核心接口与分类ArrayList二、顺序表数据结构中的顺序表三、常用代码手动

JAVA项目swing转javafx语法规则以及示例代码

《JAVA项目swing转javafx语法规则以及示例代码》:本文主要介绍JAVA项目swing转javafx语法规则以及示例代码的相关资料,文中详细讲解了主类继承、窗口创建、布局管理、控件替换、... 目录最常用的“一行换一行”速查表(直接全局替换)实际转换示例(JFramejs → JavaFX)迁移建

Spring Boot Interceptor的原理、配置、顺序控制及与Filter的关键区别对比分析

《SpringBootInterceptor的原理、配置、顺序控制及与Filter的关键区别对比分析》本文主要介绍了SpringBoot中的拦截器(Interceptor)及其与过滤器(Filt... 目录前言一、核心功能二、拦截器的实现2.1 定义自定义拦截器2.2 注册拦截器三、多拦截器的执行顺序四、过

JAVA线程的周期及调度机制详解

《JAVA线程的周期及调度机制详解》Java线程的生命周期包括NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING和TERMINATED,线程调度依赖操作系统,采用抢占... 目录Java线程的生命周期线程状态转换示例代码JAVA线程调度机制优先级设置示例注意事项JAVA线程

JavaWeb项目创建、部署、连接数据库保姆级教程(tomcat)

《JavaWeb项目创建、部署、连接数据库保姆级教程(tomcat)》:本文主要介绍如何在IntelliJIDEA2020.1中创建和部署一个JavaWeb项目,包括创建项目、配置Tomcat服务... 目录简介:一、创建项目二、tomcat部署1、将tomcat解压在一个自己找得到路径2、在idea中添加

Springboot3统一返回类设计全过程(从问题到实现)

《Springboot3统一返回类设计全过程(从问题到实现)》文章介绍了如何在SpringBoot3中设计一个统一返回类,以实现前后端接口返回格式的一致性,该类包含状态码、描述信息、业务数据和时间戳,... 目录Spring Boot 3 统一返回类设计:从问题到实现一、核心需求:统一返回类要解决什么问题?

Java使用Spire.Doc for Java实现Word自动化插入图片

《Java使用Spire.DocforJava实现Word自动化插入图片》在日常工作中,Word文档是不可或缺的工具,而图片作为信息传达的重要载体,其在文档中的插入与布局显得尤为关键,下面我们就来... 目录1. Spire.Doc for Java库介绍与安装2. 使用特定的环绕方式插入图片3. 在指定位