:cloud:1.线程安全概述
:snowflake:1.1什么是线程安全问题
首先我们需要明白操作系统中线程的调度是抢占式执行的,或者说是随机的,这就造成线程调度执行时线程的执行顺序是不确定的,有一些代码执行顺序不同不影响程序运行的结果,但也有一些代码执行顺序发生改变了重写的运行结果会受影响,这就造成程序会出现bug,对于多线程并发时会使程序出现bug的代码称作线程不安全的代码,这就是线程安全问题。
下面,将介绍一种典型的线程安全问题实例,整数自增问题。
:snowflake:1.2一个存在线程安全问题的程序
有一天,老师布置了这样一个问题:使用两个线程将变量 count
自增 10
万次,每个线程承担 5
万次的自增任务,变量 count
的初始值为 0
。 这个问题很简单,最终的结果我们也能够口算出来,答案就是 10
万。 小明同学做事非常迅速,很快就写出了下面的一段代码:
class Counter { private int count; public void increase() { ++this.count; } public int getCount() { return this.count; } } public class Main11 { private static final int CNT = 50000; private static final Counter counter = new Counter(); public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { for (int i = 0; i < CNT; i++) { counter.increase(); } }); Thread thread2 = new Thread(() -> { for (int j = 0; j < CNT; j++) { counter.increase(); } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(counter.getCount()); } } 复制代码
按理来说,结果应该是 10
万,我们来看看运行结果:
运行的结果比 10
万要小,你可以试着运行该程序你会发现每次运行的结果都不一样,但绝大部分情况,结果都会比预期的值要小,下面我们就来分析分析为什么会这样。
:cloud:2.线程加锁与线程不安全的原因
:snowflake:2.1案例分析
上面我们使用多线程运行了一个程序,将一个变量值为0的变量自增10万次,但是最终实际结果比我们预期结果要小,原因就是线程调度的顺序是随机的,造成线程间自增的指令集交叉,导致运行时出现两次自增但值只自增一次的情况,所以得到的结果会偏小。
我们知道一次自增操作可以包含以下几条指令:
load add save
我们来画一条时间轴,来总结一下常见的几种情况:
:star:️情况1:线程间指令集,无交叉,运行结果与预期相同,图中寄存器A表示线程1所用的寄存器,寄存器B表示线程2所用的寄存器,后续情况同理。
:star:️情况2: 线程间指令集存在交叉,运行结果低于预期结果。:star:️情况3: 线程间指令集完全交叉,实际结果低于预期。根据上面我们所列举的情况,发现线程运行时没有交叉指令的时候运行结果是正常的,但是一旦有了交叉会导致自增操作的结果会少 1
,综上可以得到一个结论,那就是由于自增操作不是原子性的,多个线程并发执行时很可能会导致执行的指令交叉,导致线程安全问题。
那如何解决上述线程不安全的问题呢?当然有,那就是对对象加锁。
:snowflake:2.2线程加锁
:zap:️2.2.1什么是加锁
为了解决由于“抢占式执行”所导致的线程安全问题,我们可以对操作的对象进行加锁,当一个线程拿到该对象的锁后,会将该对象锁起来,其他线程如果需要执行该对象的任务时,需要等待该线程运行完该对象的任务后才能执行。
举个例子,假设要你去银行的ATM机存钱或者取款,每台ATM机一般都在一间单独的小房子里面,这个小房子有一扇门一把锁,你进去使用ATM机时,门会自动的锁上,这个时候如果有人要来取款,那它得等你使用完并出来它才能进去使用ATM,那么这里的“你”相当于线程,ATM相当于一个对象,小房子相当于一把锁,其他的人相当于其他的线程。