浅谈线程安全问题的原因和解决方案
- 手机
- 2025-09-07 10:48:02

1. 观察线程不安全 class Counter { public int count = 0; public void increase() { count++; } } // 线程安全问题演示. public class Demo12 { public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.count); } }
上述的代码中两个线程, 针对同一个变量, 进行循环自增. 各自自增5w次,预期最终应该是10w, 但实际上,并不是这样的结果. 每次运行的结果都不一样, 并且还都是错的.
在多线程下,发现由于多线程执行,导致的bug, 统称为"线程安全问题". 如果某个代码, 在单线程下执行没有问题, 多个线程下执行也没问题, 则称为"线程安全",反之就可以称为"线程不安全".
那么啥是bug呢? bug是一个非常广义的概念. bug 的中文名,可以翻译成"幺蛾子". 只要是实际运行效果和预期效果(需求中的效果)不一致,就可以称为是一个bug.
线程安全和线程不安全的区别也就是多线程代码是否有bug.
那么上述的代码为啥会出现bug呢?
如果上述操作, 在两个线程或者多个线程并发执行的情况下, 就可能会出现问题.
如果上述两个线程是这样串行执行的, 那么结果就会是对的. 但是真的能这样吗? 上述图片中虽然是只是自增两次,但是由于两个线程并发执行, 就可能在一定的执行顺序下,导致运算的中间结果就被覆盖了. 在这5w次的循环过程中, 有多少次这俩线程执行++是"串行的”?,有多少次会出现覆盖结果的? 这些都不确定. 因为线程的调度是随机的, 是抢占式执行的过程.
上述的过程就是结果被覆盖的例子. 此处这两个线程的调度是不确定的, 这两组对应的操作也会有差异. 而且上述代码得到的结果一定是小于100000的, 因为有结果被覆盖掉了.
2. 线程安全问题的原因1) [根本原因]多个线程之间的调度顺序是"随机的", 操作系统使用"抢占式"执行的策略来调度线程.这就是罪魁祸首,万恶之源.
和单线程不同的是, 在多线程下, 代码的执行顺序,产生了更多的变化. 以往只需要考虑代码在一个固定的顺序下执行,执行正确即可. 现在则要考虑多线程下, N种执行顺序下,代码执行结果都得正确. 这件事情,木已成舟,咱们无力改变.当前主流的操作系统,都是这样的抢占式执行的.
2) 多个线程同时修改同一个变量就容易产生线程安全问题.
一个线程修改一个变量, 没事.
多个线程读取同一个变量, 没事.
多个线程修改多个变量, 没事.
3) 进行的修改, 不是"原子的".
如果修改操作,能够按照原子的方式来完成, 此时也不会有线程安全问题. count++ 不是原子的~ = 直接赋值, 可以视为原子. if = 先判定, 再赋值, 也不是原子的~~
所以解决线程安全, 最主要的切入手段就是"加锁".
"加锁"相当于是把一组操作, 给打包成一个"原子"的操作. 事务的那个原子操作, 主要是靠回滚. 此处这里的原子, 则是通过锁进行"互斥", 也就是这个线程进行工作的时候, 其他线程无法进行工作.
那根据上面的例子和代码, 我们就可以知道要给count++加锁, 使用synchronized关键字即可.
于是乎代码变动成了这样.
class Counter { public int count = 0; synchronized public void increase() { count++; } } // 线程安全问题演示. public class Demo12 { public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.count); } }
那么就有一个问题了, 通过加锁操作之后, 把并发执行=>串行执行了. 此时, 多线程还有存在的意义嘛?
必然是有的. 代码中的线程并不是只做了count++这一件事, for循环并没有加锁, for循环中操作的变量i是栈上的一个局部变量. 两个线程, 是有两个独立的栈空间, 也就是完全不同的变量, 就不涉及到线程安全问题. 因此,这两个线程,有一部分代码是串行执行的, 有一部分是并发执行的, 就仍然要比纯粹的串行执行效率要高.
synchronized进行加锁解锁, 其实是以“对象"为维度进行展开的.
加锁目的是为了互斥使用资源.(互斥的修改变量)
synchronized每次加锁,也是针对某个特定的对象加锁!
如果两个线程针对同一个对象进行加锁 就会出现锁竞争/锁冲突(一个线程能加锁成功,另一个线程阻塞等待), 那么就可以解决线程安全问题.
具体是针对哪个对象加锁,不重要. 重要的是, 两个线程, 是不是针对同一个对象加锁.
就比如更改一下代码, 也一样可有算出正确答案.
class Counter { public int count = 0; private Object locker = new Object(); public void increase() { synchronized (locker) { count++; } } public void increase2() { synchronized (locker) { count++; } } } // 线程安全问题演示. public class Demo12 { public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.increase(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 50000; i++) { counter.increase2(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.count); } }
4) 内存可见性,引起的线程安全问题.
5) 指令重排序,引起的线程安全问题.
浅谈线程安全问题的原因和解决方案由讯客互联手机栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“浅谈线程安全问题的原因和解决方案”