多线程带来的的风险-线程安全

❣️关注专栏:: JavaEE


这篇文章将为大家描述线程安全问题的原因和解决方案。线程安全是多线程编程中最难的地方,也是重要的地方,还是一个最容易出错的地方,也是面试中容易考的要点,同样也是我们以后工作中经常爱出错的地方,所以线程安全问题大家一定要注意!!!

🌴1线程安全的概念

想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该得到的结果,则说这个程序是线程安全的。

🌴2典型的线程安全问题

🌴2.1线程不安全案例

观察以下代码,了解什么是线程的不安全状态,线程的不安全问题会带来什么结果呢?

class Counter { // 实现 count 累加的类
    public int count = 0;

    public void add() { // 实现 count 累加的方法
        count++;
    }
}

public class ThreadDemo13 {
    public static void main(String[] args) {
        Counter counter = new Counter();

        // 搞两个线程。2个线程分别对 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);
    }
}

按照我们所想,对 count 进行了两次 5w次的累加,最终的结果应该是10w才对,那么我们运行一下结果却不一样(结果太多就不截图举例了),并且每次运行后的结果都不一样,为什么呢?
造成这样的结果,罪魁祸首就是多线程的抢占式执行所带来的随机性。
  如果没有多线程,此时程序的执行顺序只有一条,也就是固定的顺序,那么程序的执行结果就是固定的。如果有了多线程,此时抢占式执行下,代码的顺序会有很多种的变化,代码的执行顺序会有无数种,所以代码的执行结果就会不正确,就视为有 bug ,此时线程就是不安全的。

🌴2.2案列不安全的原因

这个案例它之所以不安全,主要与 count++ 有关,因为++操作本质上是要分为3步完成的。

  1. 先把内存中的值,读取到 CPU 的寄存器中。(load)
  2. 把 CPU 寄存器里的数值进行 +1 运算。(add)
  3. 把得到的结果写回到内存中保存起来。(save)

这3个操作就是 CPU 上执行的三个指令,为机器语言。
如果两个线程并发执行的 count++,此时就相当于是两组 load、add、save 进行执行,那么不同的线程调度顺序就可能会产生一些结果上的差异。
接下来以图示展示可能出现的执行顺序。
在这里插入图片描述
在众多中的情况下,只有1和2这两个调度顺序才是安全的
因为1和2原理相同,只对1进行分析:可见线程是安全的。在这里插入图片描述
下面对第3种情况,一个不安全的执行调度顺序进行分析:由此可见,得到的结果不是我们想要的结果。预期结果是2,实际结果确实1,所以出现了 bug。
在这里插入图片描述
其实这里的安全问题和事务中的读未提交(read uncommitted)的原理差不多。相当于 t1 读到了 t2 还没来得及提交的脏数据,就成了“脏读”。多线程安全问题和并发事务本质上都是“并发编程”的问题。
一个线程要是要执行,就需要先编译成许多的 CPU 指令,我们所写的任何一个代码,都是要编译成很多 CPU 指令的。由于线程的抢占式执行,导致当前执行到任意一个指令的时候,线程都可能被调度走,这时 CPU 会让其他的线程来执行。

🌴3线程不安全的原因

🌴3.1抢占式执行

抢占式执行是线程不安全的根本原因,导致随机调度。

🌴3.2代码结构

  1. 多个线程同时修改同一个变量,是不安全的。
    上述的线程不安全是由于多个线程同时修改共享数据。涉及到多个线程针对 counter.count 变量进行修改,此时这个 counter.count 是一个多个线程都能访问到的 “共享数据”,counter.count 这个变量就是在堆上,因此可以被多个线程共享访问。
  2. 一个线程修改一个变量,是没事的,安全的。
  3. 多个线程读取同一个变量,是没事的,安全的。
  4. 多个线程修改不同的变量,是没事的,安全的。

因此我们可以通过调整代码的结构来规避这个问题,但是这种调整不一定都能使用,所以这不是一个普适性的方案。

🌴3.3原子性

  如果修改的操作是原子(不可拆分的单位)的,那问题不大。如果不是,那么出现的问题概率就非常高。
  但是原子性并不会保证线程一定就是安全的。如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。这点也和线程的抢占式调度密切相关. 如果线程不是 “抢占” 的, 就算没有原子性, 也问题不大。
  针对线程的安全问题,最主要的手段就是把这个非原子的操作编程原子的,对它进行加锁操作。

🌴3.4可见性

可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到

🌴3.5代码顺序性

这里说的是“指令重排序”。重排序指的是单个线程里,编译器在保证逻辑不变的情况下将代码进行调整,代码顺序发生改变从而加快程序的执行效率,线程123可能调整为321。

🌴4解决线程不安全问题----加锁

解决不安全的额问题最主要的就是加锁,加锁的关键字是:synchronized,给 add() 方法前边加入关键字synchronized,就可以实现了加锁。
在这里插入图片描述
加了 synchronized 之后,进入方法就会加速,出了方法就会解锁。
synchronized用的锁是存在Java对象头里的。
在这里插入图片描述其他线程的排队等待就是“阻塞等待”。
就用刚才的案例解释“加锁”,对一个线程加锁,其实是让3个操作在执行过程中不进行调度,而是让其他线程在阻塞等待。
加锁的本质就是把 并发 变成了 串行。

🌴4.1synchronized

synchronized 关键字-监视器锁

🌴4.1.1特性

1.互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象,此时synchronized 就会阻塞等待.

  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁

2.可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。什么是可重入?一个线程对同一个对象,连续加两次锁,是否有问题?如果没有问题,就是可重入的,如果有问题,就是不可重入的。

🌴4.1.2使用方法

  1. 修饰方法
    进入方法就是加锁,离开方法就是解锁。
    (1)修饰普通方法
    (2)修饰静态方法
  2. 修饰代码块

但是这两种方法,加锁的“对象”不同。
 修饰普通方法,锁对象就是 this
 修饰静态方法,锁对象就是类对象
 修饰代码块,显示/手动指定锁对象
加锁时一定要明确对哪个对象加锁。
如果两个线程针对同一个对象加锁,会产生阻塞等待(锁竞争/锁冲突);如果两个线程对不同的对象进行加锁,不会阻塞等待,不会锁竞争/锁冲突。
简单地理解就是:不管加锁的对象是谁,只要锁对象相同,就会产生锁竞争(产生阻塞等待),锁对象不同就不会产生锁竞争(产生阻塞等待)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值