多线程05

张开发
2026/5/24 23:06:19 15 分钟阅读
多线程05
线程安全与锁从底层指令到 synchronized目录前言1 线程安全1.1 案例引入1.2 深入理解指令1.3 线程随机调度与抢占式执行1.3.1 有序交替理想情况1.3.2 随机切分的指令现实情况1.4 为什么多线程修改变量会不安全1.5 操作的原子性2 锁Locking2.1 引入锁的概念2.2 关键字 synchronized 的使用对象锁方法锁总结前言通过这篇文章我们来深入讨论多线程开发中绕不开的两个核心话题“线程安全”与“锁”。1 线程安全1.1 案例引入我们先通过一段代码来观察一个奇特的现象。假设我们有一个全局变量countpublicstaticintcount0;Threadt1newThread(()-{System.out.println(我是折纸);for(inti0;i50000;i){count;}},折纸);Threadt2newThread(()-{System.out.println(我是三叶);for(inti0;i50000;i){count;}},三叶);t1.start();t2.start();t1.join();t2.join();System.out.println(count count);System.out.println(main线程 结束);运行分析按常识来说两个线程各加 5 万次结果应该是 10 万。但实际运行后你会发现结果往往小于 10 万且每次运行的结果都不同。这是为什么呢1.2 深入理解指令问题的关键在于count这条语句。在计算机底层它并不是一次性完成的而是拆分成了三条 CPU 指令LOAD将count的值从内存读取到CPU 寄存器中。ADD在寄存器中将数值加 1。SAVE将寄存器中修改后的值重新写回内存。模拟冲突过程假设当前count为 0折纸和三叶两个线程同时操作折纸执行LOAD拿到 0 到自己的寄存器。三叶抢占 CPU也执行LOAD拿到 0 到自己的寄存器。折纸执行ADD寄存器里变成 1。三叶执行ADD寄存器里也变成 1。折纸执行SAVE内存count变 1。三叶执行SAVE内存count再次被写入 1。结果明明执行了两次count内存里的结果却是 1。三叶的努力被折纸覆盖了这就是典型的线程安全问题。线程不安全的原因总结线程随机调度抢占式执行。多个线程同时修改同一个变量。修改操作并非原子性的。内存可见性问题。指令重排序。1.3 线程随机调度操作系统为了公平采用“抢占式执行”一个线程正执行得好好的系统可能在任何瞬间哪怕一条指令还没跑完将其挂起换另一个线程上场。1.4 为什么多线程修改变量会不安全如果只是多个线程同时读取一个变量并不会报错因为“看一眼”不会改变数据。但一旦涉及修改由于操作被拆分数据的一致性就会被破坏。1.5 操作的原子性原子性指的是一个操作是不可分割的。在数据库中事务也强调原子性要么全部成功要么全部失败。count包含三步指令因此它不具备原子性。要解决这个问题我们需要把这三步“打包”成一个不可分割的整体。关于指令重排性和内存可见性我并不打算在这篇文章里讲解2 锁2.1 引入锁锁Lock的概念其实很好理解。想象一个公共厕所门口有一把锁。当“滑稽老铁”进去时会顺手把门锁上。此时其他滑稽老铁只能在外面排队等待直到里面的人出来并释放锁。在 Java 中我们可以使用synchronized关键字来给代码加锁。ObjectlockernewObject();// 创建一个锁对象Threadt1newThread(()-{for(inti0;i50000;i){synchronized(locker){// 加锁count;}// 自动释放锁}},折纸);加锁后count的三条指令被“打包”了。当折纸正在执行这三步时三叶如果也想执行会被阻塞Blocked在外面直到折纸彻底完成SAVE操作。2.2 关键字 synchronized 的使用对象锁基本形式如下synchronized(对象){// 需要锁定的代码逻辑}注意只有当多个线程竞争的是同一个对象时锁才会生效。如果你给折纸分了locker1给三叶分了locker2那锁就形同虚设依然会发生冲突。形象比喻小美是你的女同桌你向她表白成功她成了你的女朋友你对“小美”这个对象加了锁。其他男生其他线程想追她必须等你和她分手释放锁之后。方法锁synchronized也可以直接写在方法声明上静态方法加锁publicstaticsynchronizedvoidadd(){count;}这等价于synchronized(类名.class)。普通实例方法加锁publicsynchronizedvoidadd(){count;}这等价于synchronized(this)。加了方法锁后整个方法内的逻辑被视为一个整体。在计算机中锁是无法被强行破坏的其他线程必须等持有锁的线程主动释放后才有机会获取锁。总结线程安全问题本质上是由抢占式执行和操作非原子性导致的。count看似一步实则包含 LOAD、ADD、SAVE 三步。synchronized通过互斥机制将多步操作打包成“原子”操作。使用锁时务必确保多个线程竞争的是同一个锁对象否则无法起到同步效果。

更多文章