Java EE - 线程安全的产生及解决方法

目录

  • 1.线程安全
    • [1.1 线程安全例子](#1.1 线程安全例子)
    • [1.2 出现不安全的原因](#1.2 出现不安全的原因)
  • 2.解决方法
    • [2.1 串行执行](#2.1 串行执行)
    • [2.2 加锁](#2.2 加锁)
  • 3.小结

1.线程安全

1.1 线程安全例子

线程安全问题是在多线程并发的情况下,程序执行的结果与预期的结果不一致导致出现的问题,可以先看以下例子:创建两个线程t1 和 t2,定义一个变量count,在两个线程中对变量count分别执行累加操作(count++)5000次,然后输出count变量,预期输出结果是10000.

java 复制代码
public class Main{
    //定义变量
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //创建线程t1
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });

        //创建线程t2
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });

        //启动线程
        t1.start();
        t2.start();

        //确保累计操作完成执行完成:两个线程都销毁才执行输出的操作
        while(t1.isAlive() && t2.isAlive());

        //输出
        System.out.println("count = " + count);
    }
}

程序多次输出的结果如下:7764,7337,8377.



通过程序的执行结果可以看出,每一次的输入都是不一样的,此时的线程就是不安全的。

1.2 出现不安全的原因

为什么会程序这种情况呢?

1.操作系统在给线程分配资源的时候是随机的,可能先一段时间执行t1线程,再一段时间执行t2线程,也有可能t1执行完在分配给t2线程,或者t2先执行完后执行t1线程,两个线程并发的过程中就可能存在时间上的间隙(不在同一时间上执行)。

2.两个线程都是执行count++操作,而count++虽然是一条语句,但是在真正执行过程中,++操作对应的是3条指令,1.load指令(从内存中将变量count读取到cup中);2.add指令(在cpu中对count变量执行+1操作);3.save指令(将count变量重新存储到内存中);

由于线程的执行时间可能存在间隙,而count++操作对应的是多个指令,所以在并发执行过程中就有可能出现错误的执行结果,以下通过图解来做具体展示。

在并发程序执行的过程中可能出现的几种情况:1,2,3,4


观察情况1和情况2可知,两种情况都是先执行完一个线程后再执行另一个线程,以情况1做具体的介绍;在执行load操作时,本质是将变量从内存读取后存放在cup的寄存器中,以下通过寄存器1和寄存器2作为t1 和 t2线程读取变量存储的位置,如下图:

开始执行语句:执行开启线程t1语句,操作系统先给t1线程分配资源;

执行load指令,将count从内存中读取到寄存器1中。

执行add指令,将寄存器1中的值累加1,值由0变为1.

执行save指令,将寄存器1的值存放到内存中,count = 1.

执行t1.interupt()(不是真的调用中断线程方法,此处是表示累加操作完成),完成累加操作。

执行启动线程t2,为t2线程分配资源。

执行load指令,将count变量从内存中读取到寄存器2中。

执行add指令,将寄存器2中的值累加,由1变为2.

执行save语句,将寄存器2中的值加载到内存中,count = 2.

通过以上执行操作是可以得到正确的结果的,但是如果执行的是3和4情况,就会出现错误的结果,以情况3作为具体的展示。

执行t1线程开启,为线程分配资源,执行load语句,将count从内存读取到寄存器1中。

执行t2线程的启动和load指令,将count从内存读取到寄存器2中。

执行线程t2的add操作,将寄存器2中的值累加,由0变为1.

执行线程2的save指令,将寄存器2的值加载到内存中,count = 1.

执行t1线程的add操作,将寄存器1的值累加,由0变为1.

执行t1线程的save操作,将寄存器1的值加载到内存中,此时count = 1.

最后执行t1线程和t2线程的终止,可以看出经过两次累加操作操作,count 最终的结果是1,与预计出现的2不同,此时就发生了线程不安全问题。

线程安全产生的原因:

1)操作系统对线程的分配是随机调度的,即抢占式执行;

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

3)修改的操作不是原子性的;

在以上累加的例子中可以看出,count++这个操作是可以分为多条指令的,因此累加操作不是原子性的,同时t1和t2线程都是在修改count变量,同时在抢占式执行系统资源,所以导致累加的操作是不安全的。

2.解决方法

2.1 串行执行

将并发执行的过程改为串行执行,即先执行一个线程,等线程执行完成后再执行下一个线程;

例如:先执行线程t1的累加操作后,再执行t2线程的累加操作,最终可以得到10000.

java 复制代码
class Main{
    //定义变量
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //创建线程t1
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });

        //创建线程t2
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });

        //启动线程
        t1.start();
        t1.join();//等t1执行完,再执行下面操作

        t2.start();
        t2.join();//等t2执行完,再执行下面操作

        //确保累计操作完成执行完成:两个线程都销毁才执行输出的操作
        while(t1.isAlive() && t2.isAlive());

        //输出
        System.out.println("count = " + count);
    }
}

2.2 加锁

第一种方法虽然能解决问题,但是在多线程中还是希望能够并发执行的,所以就可以使用锁来解决,可以创建一个锁对象locker,对线程中的累加操作加锁,就能解决累加过程中出现的线程安全问题。

java 复制代码
class Main{
    //定义变量
    static int count = 0;

//    class Count{
//        synchronized  static void add(){
//            count++;
//        }
//    }
    //锁对象
    static Object locker = new Object();

    public static void main(String[] args) throws InterruptedException {
        //创建线程t1
        Thread t1 = new Thread(() -> {
        //加锁
            synchronized (locker){
              for (int i = 0; i < 5000; i++) {
                  count++;//or Count.add(),此时就不需要加锁,默认锁对象是Count
              }
            }
        });

        //创建线程t2
        Thread t2 = new Thread(() -> {
        //加锁
            synchronized (locker){
              for (int i = 0; i < 5000; i++) {
                      count++;//or Count.add(),此时就不需要加锁
                  }
              }
        });

        //启动线程
        t1.start();
        t2.start();


        //确保累计操作完成执行完成:两个线程都销毁才执行输出的操作
        //while(t1.isAlive() && t2.isAlive());//加锁后不能使用这个条件,因为可能其中一个线程先执行完,导致提前输出
        Thread.sleep(1000);//累加10000是不需要消耗1000毫秒时间

        //输出
        System.out.println("count = " + count);
    }
}

3.小结

线程安全问题通常出现在多线程并发执行的过程中,由于多个线程同时修改一个变量,且修改操作不是原子性的导致出现错误的情况,除了以上情况外,内存可见性问题和指令重排序也会使线程出现不安全的情况,如果需要解决线程安全问题可以通过加锁操作来解决或者将线程改为串行执行。

加锁操作不一定能解决线程安全的问题,而且可能导致程序一直进入阻塞等待,即死锁,下一篇文章将讲解死锁的产生和如何解决和预防出现死锁。

欢迎在评论区分享你的想法或问题,我们将在后续内容中继续探讨。

期待在评论区看到你的见解,下期内容更精彩。

相关推荐
Pluchon1 小时前
硅基计划6.0 伍 JavaEE 网络原理
网络·网络协议·学习·tcp/ip·udp·java-ee·信息与通信
せいしゅん青春之我1 小时前
【JavaEE初阶】网络层-IP协议
java·服务器·网络·网络协议·tcp/ip·java-ee
学习编程的Kitty1 小时前
JavaEE进阶——Spring Boot项目
数据库·spring boot·java-ee
Han.miracle1 小时前
Java ee初阶——定时器
java·java-ee
飞鱼&2 小时前
HashMap相关问题详解
java·hashmap
没有bug.的程序员2 小时前
Spring Cloud Alibaba 生态总览
java·开发语言·spring boot·spring cloud·alibaba
快乐非自愿3 小时前
Java垃圾收集器全解:从Serial到G1的进化之旅
java·开发语言·python
树在风中摇曳3 小时前
Java 静态成员与继承封装实战:从报错到彻底吃透核心特性
java·开发语言
芳草萋萋鹦鹉洲哦6 小时前
【Windows】tauri+rust运行打包工具链安装
开发语言·windows·rust