【JavaEE】多线程之线程安全(上)

发布于:2025-08-16 ⋅ 阅读:(17) ⋅ 点赞:(0)

!线程安全是整个多线程最关键的要点!
tips:本节莫得笑话,全是干货,还请坐好扶稳,我们出发喽~

一、线程安全

前面我们已经学习了如何创建线程、启动线程以及Thread类中比较重要的属性和方法等,现在我们来看一段代码:

package thread;

public class Demo11 {

    private static long count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });

        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });

        t1.start();
        t2.start();

        ///main阻塞等待t1和t2
        t1.join();
        t2.join();

        System.out.println(count);
    }
}

相信这段代码对于大家来说都不复杂,凭直觉来说代码的运行结果是10000,静态变量、两个线程、两个循环一个5000次、加在一起10000!ok皆大欢喜~~

但是,当我们真正把程序运行起来就会发现,哪里好像不太对,,,,

8938、5382、7465……好像结果除了10000什么都有可能出现,为什么呢?

我们再来详细分析一下这段代码,明确几个问题

  1. 一共有几个线程?三个,t1、t2和main线程
  2. 线程执行顺序?随机调度
  3. 线程结束顺序?t1和t2先结束,main线程最后结束

emmm,似乎和我们理解的也没什么出入,不存在由于main线程先结束导致count打印结果有误的问题

so,why?答案是:count++操作

还记得我们之前谈到过,编程语言是程序员理解使用的语言,对于计算机来说,要将其翻译成它能理解的语言才能进一步执行

在计算机中,count++这行语句其实对应了三行命令

  1. load:将内存中count变量的值读取到CPU寄存器
  2. add:把指定寄存器中的值进行+ 1操作
  3. save:将修改后的寄存器的值写回到内存中

如果是单线程程序,我们自然不用担心,这三行命令一定是按顺序依次执行的,可是在多线程程序中就要考虑一个问题:如果一个线程的这三行命令执行了一半,CPU资源就分给了另一个线程,这时候会发生什么事情呢?

我们先来模拟一下两个线程由于线程的随机调度可能出现的count+结果(仅一次,count初始值为0)

第一种

t1线程 t2线程
load(t1线程加载count的值为0)
add(t1线程对count的值进行++)
save(t1线程将count的值写回内存,此时count=1)
load(t2线程加载count的值为1)
add(t2线程对count的值进行++)
save(t2线程将count的值写回内存,此时count=2)

结果:count=2
第二种

t1线程 t2线程
load(t1线程加载count的值为0)
load(t2线程加载count的值为0)
add(t2线程对count的值进行++)
save(t2线程将count的值写回内存,此时count=1)
add(t1线程对count的值进行++)
save(t1线程将count的值写回内存,此时count=1)

结果:count=2
第三种

t1线程 t2线程
load(t1线程加载count的值为0)
load(t2线程加载count的值为0)
add(t1线程对count的值进行++)
add(t2线程对count的值进行++)
save(t1线程将count的值写回内存,此时count=1)
save(t2线程将count的值写回内存,此时count=1)

结果:count=1

当然,随机调度的结果还有很多,这里就不一一列举
由此可以看出,两个线程分别对count进行仅仅一次++操作,就有可能出现两种结果,那两个线程分别对count进行5000次++,出现若干种结果也就不算出人意料了

这种程序运行结果和预期不匹配的效果当然是我们不愿意见到的,也就是我们常说的bug,在这里属于一种线程安全问题

1.1线程安全问题产生的原因

线程安全问题产生原因主要是以下几点:

  1. 根本原因:操作系统对于线程的调度是随机的,不同线程抢占式执行
  2. 多个线程修改了同一个变量
  3. 修改操作不是原子的(不是不可再分的)
  4. 内存可见性问题
  5. 指令重排序
    在不同的代码中我们可能涉及到不同的线程安全问题,这点需要我们在实际编程中认真体会~

1.2如何解决线程安全问题

开头的例子我们可以稍作修改,将最后几条语句改为

t1.start();
t1.join();
t2.start();
t2.join();

这样先启动t1线程,main线程阻塞等待直到t1线程运行结束;再启动t2线程,main线程阻塞等待直到t2线程运行结束
这样调正代码结构,将并行执行的两个线程改为串行执行,规避了线程不安全的代码
这可以是一种解决线程安全问题的方案,但是缺点也显而易见,这种方法不够通用,改为串行也降低了程序的运行效率

我们一般使用另一种更加通用的方案——加锁

加锁——通过加锁操作将不是原子的操作打包成一个原子的操作,避免其他线程的操作在当前线程执行过程中插队

Java中使用synchronized这样的关键字搭配代码块来实现加锁操作

synchronized(锁对象){ //进入代码块,就相当于加锁
//执行一些要保护的逻辑
} //出了代码块,就相当于解锁

欲知后事如何,且听下回分解~


网站公告

今日签到

点亮在社区的每一天
去签到