线程安全(JAVA)

线程安全对于我们编写多线程代码是非常重要的。

什么是线程安全?

在我们平时的代码中有些代码在单线程程序中可以正常执行,但如果同样的代码放在在多个线程中执行就会引发BUG,而这种现象我们一般称为 "线程安全问题" 或 "线程不安全"。

例如:使用两个线程对 count 变量进行自增操作,每个线程10000次。

java 复制代码
private static int count;
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(()->{
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    });
    Thread t2 = new Thread(()->{
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    });

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

    t1.join();
    t2.join();
    System.out.println(count);
}

结果可以看到和我们预期的并不相同,而且当我们多运行几次后,每次的结果还都不相同,这就是一个典型的 线程安全问题
为什么会出现上述情况呢?

  • 自增操作本质上其实分为三步

    -- 从内存把数据读到 CPU

    -- 进行加一操作

    -- 把新数据写回到 CPU

  • 两个线程是并发执行

所以就会引发下面这种状况(程序按照时间线从上往下执行 ):

这里只是简单画了六种,由于线程的调度是无序的所以这里会有无数种情况,但是在这无数种情况中,只有当两个线程的调度每次都满足前两种情况才不会发生BUG。

引发线程安全的原因

一般引发线程安全都有以下原因:

  1. 操作系统中线程的调度是随机的(抢占式执行,罪魁祸首)
  2. 多个线程针对同一个变量进行修改
  3. 修改操作不是原子的
  4. 内存可见性问题
  5. 指令重排序问题

想要解决线程安全问题就需要从上面这几点出发,由于我们上述的代码不涉及4和5所以无需考虑它们,而第一点是系统原因是客观存在的无法更改。

我们此时有两种解决方法:

  • 将这个代码改为单线程(解决多个线程针对同一个变量进行修改的问题);
  • 让该自增操作变为原子的(解决修改操作不是原子的问题)

这两种方法都可以解决此代码的线程安全问题,第一种很好实现,那么我们该怎样让这个自增操作变为原子的呢?加锁!

synchronized 关键字

synchronized 关键字是JAVA提供的一种常用的加锁工具。

注:

  • synchronized关键字在使用时需要搭配()和{};
  • 程序执行进入 { 加锁 离开 } 解锁 ,{} 里面就是被加锁的代码块;
  • ()里面用来表示一个加锁的对象(这个对象是啥不重要,它的主要功能就是用来区分多个线程是否在竞争同一个锁)。

如果多个线程对同一个线程尝试进行加锁操作就会产生锁竞争(其中一个线程就会发生阻塞等待),如果是不同对象就不会产生锁竞争,仍然是并发执行。

我们先随便创建一个Object类型的对象,命名为lock,将count++放入{}中

java 复制代码
private static int count;
public static void main(String[] args) throws InterruptedException {
    Object lock = new Object();
    Thread t1 = new Thread(()->{
        for (int i = 0; i < 10000; i++) {
            synchronized(lock) {
                count++;
            }
        }
    });
    Thread t2 = new Thread(()->{
        for (int i = 0; i < 10000; i++) {
            synchronized(lock) {
                count++;
            }
        }
    });

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

    t1.join();
    t2.join();
    System.out.println(count);
}

由于我们对count++加了锁所以线程t1和t2就会在执行过程中相互影响。

当t1线程在执行++操作时,如果t2线程也想执行++操作就会发生阻塞等待,当t1线程执行完++操作出了 } 后会解锁,此时 t2 才会继续向下执行。

此时这个程序的执行顺序就只会是这类正确的类型:

相关推荐
独立开发之道几秒前
【Three.js教程】 图元速查:官方内置的立体图形
开发语言·javascript·ecmascript
一次旅行1 分钟前
2026‑08‑28 AI产业深度解读|生成视频模型迭代、AI安全全线收紧、国产大模型生态加速落地
人工智能·安全
Htr_3 分钟前
Python面向对象编程:类与对象基础
开发语言·python
RD_daoyi3 分钟前
谷歌改写了76%的标题:超60字符的,95%会被谷歌自己重写
大数据·服务器·开发语言·前端·搜索引擎·html
weixin_307779134 分钟前
AI生成代码的隐患与治理:人工审查流程及提示词驱动的修复策略
开发语言·人工智能·算法
Java后端的Ai之路8 分钟前
09、Python组合模式
开发语言·人工智能·python·docker·组合模式
Escalating_xu14 分钟前
【C++入门基础(上)】从发展历程、学习路线到命名空间与输入输出
开发语言·c++
jimy117 分钟前
C++ 中的rValue的判断---是否能“隐式移动”
开发语言·c++
xxy2224120 分钟前
TCP 网络编程(3):从基础通信到并发服务器与 HTTP 实战
开发语言·php
zcmodeltech21 分钟前
垃圾发电厂沙盘模型控制系统设计与实现:多设备协同联动方案
java·大数据·数据库·人工智能·stm32·嵌入式硬件·制造