影响线程安全问题的因素有很多
包括但不限于:
- 抢占式执行(主要原因)
- 多个线程修改同一个变量
- 修改操作,不是原子的(这里所说的原子,是指不可分割)
本篇将通过实例对上述原因进行讲解
🔎1.示例
🌻示例代码
class Counter { public int count; public void add() { count++; } } public class Test1 { public static void main(String[] args) { int n = 50_000; Counter cnt = new Counter(); Thread t1 = new Thread(() -> { for (int i = 0; i < n; i++) { cnt.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < n; i++) { cnt.add(); } }); t1.start(); t2.start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(cnt.count); } }
代码描述:
- 上述代码启动了 t1,t2两个线程
- 每个线程分别执行cnt.add()50000次
- 最后打印出count的值
🌻运行结果
运行结果
运行结果
运行结果
上图表示代码的运行结果
对于上述这段代码,由于是多线程进行运行,所以其运行结果不一定是10W.
也可以通俗的理解为一个简单的BUG
🌻原因分析
那么是因为什么导致的这个BUG呢?
主要原因有3个
- 1.抢占式执行(主要原因),这个可能不太好理解请看下图
- 2.多个线程修改同一个变量(这里由于 t1,t2两个线程同时对count值进行修改,所以结果是不确定的)那么什么操作时安全的呢?
- 一个线程修改同一个变量(安全)
- 多个线程读取同一个变量(安全)
- 多个线程修改多个不同的变量(安全)
- 3.修改操作,不是原子的(原子性只得是不可再被分割)
修改操作可以被划分为load(读取)add(修改)save(保存)
那么什么是原子性的操作呢?一个简单的例子就是赋值(a = 1)
🌼抢占式执行的几种可能
load(读取),线程读取数据的值
add(修改),线程修改读取到的数据的值
save(保存),线程将修改的数据值返回
这3个步骤必须连续,结果才能是正确的
case1
case2
case3
case4
case5
注意:以上所罗列的只是部分情况,并非全部
对于上述情况,只有case4和case5的执行结果是正确的
🌼模拟case
下面来模拟实现case1
case1
🌼原因分析
对于case1
(1)多个线程对同一个变量进行修改
(2)修改操作不是原子性的
(3)线程之间进行了抢占式执行(操作顺序不同)
最终导致了BUG的发生
🌻解决方案
为该方法加锁
当为该操作加锁后,一个线程执行该方法时,另一个线程就需要进行等待,就不再会出现抢占式执行
Java中,synchronized{}代表锁,在括号体内部就是为该对象加锁,出了括号就代表解锁
运行结果
🌼加锁的实现方式
public void add() { synchronized(this) { count++; } }
只为count++操作进行加锁,哪个对象调用的add()方法,this就指代哪个对象
synchronized public void add() { count++; }
为add()方法加锁
synchronized public static void add() { }
为静态方法加锁
public static void add() { synchronized(Counter.class) { } };
为静态方法加锁
需要注意的是,synchronized修饰sttatic(静态方法)的时候,是给类对象进行加锁
🔎结尾
创作不易,如果对您有帮助,希望您能点个免费的赞👍
大家有什么不太理解的,可以私信或者评论区留言,一起加油