线程安全问题

关于线程安全

在多线程下,发现由于多线程执行,导致的bug,统称为"线程安全问题"如果某个代码,在单线程执行没有问题,在多线程下执行也没有问题,那么,就称之为"线程安全",反之则称为"线程不安全"
代码示例

class Counter {
    public int count = 0;
    //通过变量捕获进行对count进行修改,如果是局部变量,则不能进行局部变量修改
    void increase() {
        count++;
    }
}
public class demo3 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

通过上述的代码,我们可以发现,每次count的值都与我们预期的值不相同,对于这种现象,我们称为"bug".在多线程中执行导致的bug,统称为"线程安全问题"

其实出现bug的原因也很简单,通过上述代码我们可以发现我们每次都调用到了Counter类中的increase方法中的count++这个操作.
站在cpu指令的角度,count++本质上其实是3个步骤:
1.把内存中的数据加载到CPU的寄存器中(load)
2.把寄存器中的数据进行+1(add)
3.把寄存器中的数据写回到内存中(save)

如果上述的操作,在两个线程,或者多个线程并发执行的情况下,就可能会出现问题.

在这里插入图片描述
在左侧的图中首先的操作就是将count加载到t1的工作内存(寄存器)中,随后count变量也加载到t2中,t1在工作内存中修改count的值,t2也修改了count的值返回到内存中,count的值为1,最后t1中count值返回到内存中,count的值最终为1.虽然是自增两次,但是两个线程并发执行,就可能在一定的执行顺序下,导致运算的中间结果就被覆盖了,与我们预想的值不相同,而且得到的错误值一定是小于1w的,而在右侧的图中,代码的顺序性是没有问题的.执性的顺序是串行与我们预期值一样.在这5000次的循环中,有多次是这俩线程串行执行++的,不确定,因为线程的调度是随机的,抢占式的执行过程.

虽然一个cpu核心上寄存器只有一组但是多线程下,可以看作各自都有一组寄存器,本质上是"分时复用".

  • 线程之间的共享变量都存储在主内存中
  • 每个线程都有自己的"工作内存(寄存器)"
  • 当线程要读取共享变量的时候,会先把变量从主内存拷贝到工作内存(寄存器),再从工作内存中读取数据
  • 当线程要对一个共享变量进行修改的时候,会先修改工作内存中的副本(寄存器),在同步回主内存

由于每个线程都有自己独立的工作内存,这些工作内存相当于同一个共享变量的"副本",修改线程t1的工作内存值,t2不会及时发生改变.

线程安全问题的原因

1.线程调度性[根本原因]:多个线程之间的调度顺序是"随机的",操作系统使用"抢占式"执行的策略来调度线程,和单线程不同的是,多线程在执行顺序产生了更多的变化.以往单线程只需要在固定顺序下执行,现在则要考虑多线程下,N种执行顺序都要正确.
2. 修改共享数据(代码结构):多个线程同时修改同一个变量,容易产生线程安全问题.
3. 原子性:原子性的意思就是我们把一段代码想象成一个房间,每个线程就是要进入房间的人,如果房间没有任何的机制保护.假设现在由线程A进入房间,A还没有出来,线程B也可以进入房间,这样就打断了A在房间里面的隐私,这就是不具备原子性的,
那么我们其实可以把代码设置为原子性的,只需要房间里加上一把锁,当A线程进去后,给房间加上一把锁,其他线程就不会打断线程A,只有当A把锁解开的时候,其他线程才能进入房间.有时候也把这个现象叫做同步互斥,表示操作时互相排斥的.
一条Java语句不一定是原子的,也不一定只是一条语句,就比如我们刚才所看见的count++实际上是由三步操作完成的:1.从内存把数据读到寄存器2.进行数据更新3.把数据写回到内存种
4. 可见性:一个线程对共享变量值的修改,及时地被其他线程看见
5. 指令重排序:在多线程环境下,cpu和编译器会对代码进行重排序,但重排序的结果并不会影响单线程程序的执行结果,而在多线程环境下会受到影响

解决线程安全问题

//解决线程执行调度顺序,就需要把代码设置为原子性的,只需要给代码加上一把锁,synchronized(锁)
class Counter {
    public int count = 0;
    //通过变量捕获进行对count进行修改,如果是局部变量,则不能进行局部变量修改
    //进入方法就会加锁(lock)
    //出了方法就会解锁(unlock)
    synchronized void increase() {
        count++;
    }
}
public class demo3 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter.increase();//加锁成为串行
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}
//通过加锁操作,把并发执行->串行执行,但是不是整体的串行,for循环并没有涉及到线程安全问题,
//for循环中的i是栈上的一个局部变量,两个线程是有两个独立的栈空间,也就是完全不同的变量.
//两个线程中的i不是同一个变量,两个线程修改了两个不同的变量,没有线程安全问题,也就不需要加锁

当t1加锁后,t2如果也尝试加锁,就会陷入阻塞等待,这个阻塞会一直等到t1把锁释放后,t2才能加锁成功.就把"穿插执行"变成了"串行执行".

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值