• java线程安全问题的原因与大致解决思路


    在我们编写多线程程序的时候,经常会因为线程安全问题导致出现各种各样的bug,这里我们总结一些线程安全的原因和大致的解决思路

    目录

    线程安全的问题

    1.操作系统的抢占执行

    2.多个线程修改同一个变量

    3.操作不是原子性的

    4.内存可见性

    5.指令重排序

    线程安全的解决方法

    3.操作不是原子性的

    4.内存可见性

    5.指令重排序问题


    线程安全的问题

    1.操作系统的抢占执行

    这个问题时我们编写多线程代码时,出现线程安全问题的罪魁祸首,当然,这个问题也是操作系统方面的,我们并没有办法去解决这个问题

    2.多个线程修改同一个变量

    在多线程中,我们多个线程修改同一个变量的时候,也很容易触发线程安全问题,我们可以适当调整代码来解决这个问题,但是并不是每次都可以调节代码的,所以这个也不是重点

    3.操作不是原子性

    我们改变不了上面的两个问题,但是我们可以改变这个问题,我们可以吧操作变成原子操作,通过加锁操作来实现,这样我们就可以一定程度上保护线程安全

    4.内存可见性

    所谓的内存可见性,就是指一个线程修改了一个公共的值,另一个线程也能看到

    5.指令重排序

    指令重排序就是指,在单线程情况下,很多代码的执行顺序会影响执行速度,写编译器的大佬就把这代码进行重排序优化

    线程安全的解决方法

    前面的两个我们没有办法改变,我们从第三个开始讲解决思路

    3.操作不是原子性的

    在java中操作不是原子性的很容易出现问题,就比如我们这一段代码

    1. public class Main {
    2. public static int count = 0;
    3. public static void main(String[] args) throws InterruptedException {
    4. Thread t1 = new Thread(() ->{
    5. for (int i = 0; i < 5000; i++) {
    6. count++;
    7. }
    8. });
    9. Thread t2 = new Thread(() ->{
    10. for (int i = 0; i < 5000; i++) {
    11. count++;
    12. }
    13. });
    14. t1.start();
    15. t2.start();
    16. t1.join();
    17. t2.join();
    18. System.out.println(count);
    19. }
    20. }

     

    这段代码中我们使用两个线程对count进行累加操作,但是得到的结果好像并不是我们想要的,这就出现了线程安全问题,因为++操作其实有三个子操作,分别是load,add,save,当着三个子操作不是原子操作的时候,可能就会出现两个线程执行++只进行了一次自增的情况,所以导致结果不是我们想要的,

    那我们应该怎么解决呢?

    加锁!!!

    我们可以通过加锁操作,让操作变成原子性的

    经过改进,我们给两个线程加上锁,使其成为原子操作(这里的代码编写并不是很好,只是为了表达意思)

    1. public class Main {
    2. public static int count = 0;
    3. public static void main(String[] args) throws InterruptedException {
    4. Thread t1 = new Thread(() ->{
    5. synchronized (Main.class){
    6. for (int i = 0; i < 5000; i++) {
    7. count++;
    8. }
    9. }
    10. });
    11. Thread t2 = new Thread(() ->{
    12. synchronized (Main.class){
    13. for (int i = 0; i < 5000; i++) {
    14. count++;
    15. }
    16. }
    17. });
    18. t1.start();
    19. t2.start();
    20. t1.join();
    21. t2.join();
    22. System.out.println(count);
    23. }
    24. }

     可以看到我们的结果就得到了10000,也就解决了这种类型的问题

    4.内存可见性

    直接看这个问题先

    1. class A extends Thread {
    2. public int i = 0;
    3. public void run() {
    4. while(i == 0){
    5. }
    6. }
    7. }
    8. public class Main {
    9. public static void main(String[] args) throws InterruptedException {
    10. A t1 = new A();
    11. Scanner sc = new Scanner(System.in);
    12. t1.start();
    13. t1.i = sc.nextInt();
    14. t1.join();
    15. }
    16. }

    这里我们执行的结果是

    可以看到当我们输入5之后,他并没有结束线程,这是为什么呢?

    因为我们的编译器进行了自动的优化,让我们的while循环中的判断只执行一次,如果在单线程的情况下,这样当然是没问题的了,但是在多线程的情况下,就会出问题,闹我们如何解决呢? 

    前面提到了volatile关键字,我们对代码进行修改

    1. class A extends Thread {
    2. public volatile int i = 0;
    3. public void run() {
    4. while(i == 0){
    5. }
    6. }
    7. }
    8. public class Main {
    9. public static void main(String[] args) throws InterruptedException {
    10. A t1 = new A();
    11. Scanner sc = new Scanner(System.in);
    12. t1.start();
    13. t1.i = sc.nextInt();
    14. t1.join();
    15. }
    16. }

     可以看到,当我们输入5之后,线程直接就停止了,volatile到底干了什么呢?

    其实很简单,他就是跟编译器说,这个变量,会有别的线程来读,就老老实实的去读数据,就那么简单,这样我们就解决了内存可见性的问题.

    5.指令重排序问题

    指令重排序也是编译器优化的一种,就是把一些执行的执行顺序给他改变了,这在单线程的情况下肯定任何问题,但是在多线程的情况下,就不一定了,多线程的情况下可能会导致wait()这种关键字得到错误的信息,那我们怎么办呢?

    还是加volatile关键字,给我们可能发生指令重排序的实例或者变量加上这个关键字的时候,就不会发生指令重排序的问题了,比如我们的线程安全的"懒汉"单例模式,就是通过volatile关键字

    经典的面试问题:

    简述volatile关键字的作用:

    1.保证内存的可见性,用屏障指令实现

    2.禁止指令重排序,编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。

  • 相关阅读:
    C,C++网络编程学习指南
    linux中的inode文件编号和软硬链接
    python数据分析—删除value=0的行
    linux开机自动启动java的jar包项目及开机自动启动Nacos的配置
    软件测试面试题汇总
    降C大法——第四式
    27寸2K显示器 - HKC G27H2
    某银行开发一个信用卡管理系统CCMS
    2023 编程资料合集汇总
    【Django】django.core.exceptions.AppRegistryNotReady: Apps aren‘t loaded yet.
  • 原文地址:https://blog.csdn.net/qq_55546526/article/details/126906902