当前位置: 首页 > news >正文

多线程(四)----线程安全

线程安全问题的万恶之源就是多线程的抢占式执行所带来的随机性.

有了多线程, 此时抢占式执行下, 代码执行的顺序, 会出现更多的变数, 代码执行顺序的可能性就从一种情况变成了无数种情况. 只要有一种情况使得代码结果不正确, 都是视为bug, 线程不安全.

有线程安全的代码

以下是一个有线程安全的的代码:

class Counter{
    public int count;

    public void add(){
        count++;
    }
}

public static void main(String[] args) throws InterruptedException {
    Counter counter = new Counter();
    //搞两个线程, 两个线程分别针对counter 来调用5w次的add方法
    Thread t1 = new Thread(() -> {
        for(int i = 0;i < 50000;i++){
            counter.add();
        }
    });

    Thread t2 = new Thread(() -> {
        for(int i = 0;i < 50000;i++){
            counter.add();
        }
    });

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

    //等待两个线程结束
    try{
        t1.join();
        t2.join();
    } catch (InterruptedException e){
        e.printStackTrace();
    }

    //打印最终的 count 值
    System.out.println("count = "+counter.count);


}

代码结果:

从代码结果可以知道, 并不是和预期一样, 达到10w次.

为什么会出现这种情况呢?

原因就出现在 "count++" 这里

"++" 操作本质上要分成三步(类似于汇编)

第一步: 先把内存中的值, 读取到CPU的寄存器中 (load)

第二部: 把CPU寄存器里的数值进行 "+1" 运算 (add)

第三步: 把得到的结果写回到内存中 (save)

如果是两个线程并发执行, 此时就相当于两组load add save 进行执行, 此时不同的线程调度顺序就可能会产生一些结果上的差异.(将相当于6个操作, 可能出现的排列次序不同)

这是由于线程之间是随机调度的, 导致此处的调度顺序充满着其他的可能性

脏读

由于两个核心上处理同一个内存上的变量, 如果是按顺序完成自身的任务(load add save)后, 另一个线程在开始自增, 那么结果肯定是10w次, 但是由于随机调度, 就会出现一个线程自增后, 还没有保存, 而另一个线程已经读取的情况, 就会导致内存上只++1次, 没有按预期一样++2次.

线程安全问题的主要原因

线程安全问题的主要原因:

1. [根本原因] 抢占式执行, 随即调度,

2. 代码结构: 多个线程同时修改同一个变量

    一个线程修改一个变量不会出现安全问题

    多个线程读取同一个变量不会出现安全问题(例如:String是不可变对象, 天然是线程安全的)

    多个线程修改多个不同的变量不会出现安全问题

3.原子性:  如果修改操作是非原子的, 就会容易出现线程安全问题

    原子性: 比如汇编中的一条指令,或者说几条指令互不影响

    针对线程安全问题的解决, 最主要的手段就是从原子性入手, 把非原子的操作, 变为原子的

4. 内存可见性: 如果一个线程读, 一个线程修改 也会出现安全问题

5. 指令重排序: 编译器主动调整你的代码

线程安全问题的解决----Synchronized

//synchronize 是一个关键字, 表示加锁
synchronized public void add(){
    count++;
}

加了synchronized之后, 进入方法就会加锁, 出了方法就会解锁, 如果两个线程同时尝试加锁, 此时一个能获取锁成果, 另一个只能阻塞等待(BLOCKED) 一直阻塞到线程释放锁(解锁), 当前线程才能加锁成功.

加锁, 就是保证原子性, 加锁的本质是把并行, 变成了串行

加锁之后, 代码结果就变成了10w次

synchronized关键字----监视器monitor lock

synchronized 使用方法

1.修饰方法

  1) 修饰普通方法; 锁对象就是this

public synchronized void add(){
    count++;
}

直接把synchronized 修饰到方法上了, 此时相当于对 this 加锁.

Thread t1 = new Thread(() -> {
    for(int i = 0;i < 50000;i++){
        counter.add();
    }
});

Thread t2 = new Thread(() -> {
    for(int i = 0;i < 50000;i++){
        counter.add();
    }
});

        t1 执行 add 就加上锁了, 针对count这个对象加上锁了, t2 执行 add 的时候, 也尝试对counter加锁, 但是由于counter 已经被 t1 给占用了, 因此这里的加锁操作就会阻塞.

  2)修饰静态方法: 锁对象就是类对象(Counter,class)

     与1) 同理

2.修饰代码块

显式/手动指定锁对象

public void add(){
    synchronized (this){
        count++;
    }
}

  "( )" 内可以指定任意想指定的对象, 不一定非是 this, 进了代码块就加锁, 出了代码块就解锁

synchronized 的特性

1) 互斥

  进入 synchronized 修饰的代码块, 相当于 加锁

  推出synchronized 修饰的代码块, 相当于解锁

加锁

如果两个线程针对同一个对象进行加锁, 就会出现锁竞争/锁冲突, 一个线程能够获取到锁(先到先得)另一个线程阻塞等待, 等待到上一个线程解锁, 它才能获取锁成功

如果两个线程针对不同对象加锁, 此时不会发生锁竞争/锁冲突. 这俩线程都能获取到各自的锁, 不会阻塞等待了.

2) 可重入

synchronized 同步块对同一条线程来说是可重入的, 不会出现自己把自己锁死的问题.

Java标准库中的线程安全类

 

Java标准库中很多都是不是线程安全的. 例如以上, 这些类可能会涉及到多线程修改共享数据, 有没有任何加锁措施

还有一些是线程安全的, 使用了一些锁机制来控制

还有的虽然没加索, 但是不涉及"修改", 仍然是线程安全的-----String

相关文章:

  • 力扣刷题994. 腐烂的橘子
  • 比特币牛市还在不在
  • 「Wi-Fi学习」节能模式
  • Java常用类
  • Android第四次面试总结(基础算法篇)
  • LeetCode-274.H 指数
  • C#进阶(多线程相关)
  • SMT贴片机销售实战技巧解析
  • Python高级:GIL、C扩展与分布式系统深度解析
  • 汽车机械钥匙升级一键启动的优点
  • CentOS下安装ElasticSearch(日志分析)
  • 项目实战:基于瑞萨RA6M5构建多节点OTA升级-创建系统最小框架<三>
  • 【SpringMVC】深入解析基于Spring MVC与AJAX的用户登录全流程——参数校验、Session管理、前后端交互与安全实践
  • CXSMILES介绍
  • 【Linux】浅谈环境变量和进程地址空间
  • APP测试
  • c++初阶易错题(选择)
  • Linux: qemu-user-static 是如何工作的?
  • 初探自定义注意力机制:DAttention的设计与实现
  • 力扣128. 最长连续序列 || 452. 用最少数量的箭引爆气球
  • 新华时评:中国维护国际经贸秩序的立场坚定不移
  • 巴基斯坦首都及邻近城市听到巨大爆炸声
  • 韩德洙成为韩国执政党总统大选候选人
  • 马上评丨规范隐藏式车门把手,重申安全高于酷炫
  • 古埃及展进入百天倒计时,闭幕前168小时不闭馆
  • 上海国际电影节推出三大官方推荐单元,精选十部优秀影片