让问题暴漏-线程安全问题
大家好,我是欧阳方超,微信公众号同名。
1 概述
使用多线程时,如果不引入同步机制的话,可能会出现线程安全问题,一般都是用存款、取款这个场景演示多线程中数据不一致的例子,本文也从这个场景出发,演示多线程中不引入同步机制时可能会出现的数据不一致问题,并与引入同步机制后数据不一致问题消失作对比。
2 场景设计
假设有两个线程:
- 第一个线程进行存款操作。
- 第二个线程进行取款操作。
两个线程同时运行,并且都对同一个账户的余额balance进行修改。然而,由于没有使用同步机制,可能会导致存款和取款的顺序混乱,从而造成数据不一致。
不带同步机制的BankAccount类
public class BankAccount {
private int balance = 0;
//存款
public void deposit(int amount) {
balance += amount;
}
public void withdraw(int amount) {
balance -= amount;
}
public int getBalance() {
return balance;
}
}
2.1 场景模拟
下面的代码中,模拟两个线程同时对同一账户进行存款和取款操作。由于没有同步机制,存款和取款操作会发生竞态条件,即多个线程同时修改balance,导致最终余额不正确。
public class RaceConditionTest {
public static void main(String[] args) throws InterruptedException {
BankAccount bankAccount = new BankAccount();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
bankAccount.deposit(10);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
bankAccount.withdraw(10);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(bankAccount.getBalance());
}
}
以下是可能的运行结果:
2.2 结果分析
程序中启动了两个线程,thread1执行存款操作,执行100次存款,每次存10元钱,共存了1000元钱,thread2执行取款操作,执行100次取款,每次取10元钱,共取了1000元钱,当两个线程都执行完毕后,查看账户的余额,理论上应该是0元,但实际情况却是跑出了上面的结果,是一个意外值。
2.2.1 为什么出现这个现象
- 线程之间的交错执行:当deposit和withdraw操作同时进行时,可能会发生两个线程同时对balance变量进行读/写操作。例如:
- 线程A(存款线程)读取了balance当前值(假设为0)。
- 线程B(取款线程)也读取了balance当前值(仍然是0)。
- 然后,线程A将0增加1,存储会balance。
- 接着,线程B将0减去1,存储会balance。
- 此时balance的值可能是-1,出现数据不一致。
- 缺乏原子性
deposit和withdraw方法并不是原子性的,它们都涉及多个步骤(读取、修改和写回balance),如果这两个操作没有互斥,多个线程就可能在同一时刻修改共享的balance,导致计算操作。
2.3 引入同步机制
可以通过适当的同步机制(如使用synchronized或ReentrantLock),解决这个问题,确保每次只有一个线程能够操作共享资源,从而保证线程安全。
将BankAccount类修改为如下形式:
class BankAccount {
private int balance = 0;
//存款
public synchronized void deposit(int amount) {
balance += amount;
}
public synchronized void withdraw(int amount) {
balance -= amount;
}
public int getBalance() {
return balance;
}
}
再次执行程序的main方法,无论执行多少次,余额始终为0。
2.4 关于为什么加Thread.sleep()
可能你已经看到在调用deposit()方法和withdraw()方法的下面,都出现了让线程休眠的sleep()方法,并且把sleep()方法去掉后,即使没有同步机制,程序运行结果很有可能也符合预期——余额为0。这是因为线程的调度非常快,在没有真正的耗时操作时(如存款或取款通常会涉及操作数据库或调用远程接口),存款和取款的操作没有干扰到彼此的结果,最后balance的值符合预期。
2.4.1 sleep()方法发生了什么
sleep()方法会使得线程在存款或取款操作时,暂停一定的时间,刻意打断线程的执行时序,从而让线程的调度和操作出现更多的交错,暴漏出竞态条件。线程调用sleep()方法时,它会暂停一定时间,并让出CPU资源,此时操作系统调度器可能会将CPU时间片分配给其他线程。正式因为sleep()的特性与实际当中线程执行I/O操作时(如数据库操作、文件读写、网络请求)进入阻塞状态,并释放掉当前CPU时间片的场景非常相似。
3 总结
介绍了只使用多线程不引入同步机制时可能会出现的数据不一致问题,并展示了通过使用synchronized解决多线程带来的数据不一致问题,synchronized关键字是解决同步问题的最基本手段,后面是陆续介绍,本篇先将线程安全问题暴露出来,毕竟先暴露出问题才能解决问题。
我是欧阳方超,把事情做好了自然就有兴趣了,如果你喜欢我的文章,欢迎点赞、转发、评论加关注。我们下次见。