写点什么

Byteman 让 i++ 百分百线程不安全

作者:FunTester
  • 2024-12-03
    河北
  • 本文字数:3116 字

    阅读完需:约 10 分钟

在我早期的文章当中,我使用过一个插件 vmlens 实现让 i++ 展现了百分百的线程不安全。在演示示例中,使用了两个线程并发执行 i++,然后就看到了线程不安全的全过程。


但是 vmlens 当时是个付费软件,作者给白嫖用户两周的体验期,虽然我我提了一个 BUG ,也没得到任何的优待。所以很快进行了简单的尝试之后,就放弃探索 vmlens


最近开始研究 Byteman 的官方文档过程中,当我看到了关于多线程管理的部分,原来可以控制多个故障的多线程同步,突然意识到有可能找到了 vmlens 一样的套路。如果我们可以控制访问一个变量的线程访问(读/写)顺序,那我们应该可以很容易模仿出线程不安全的场景。


既然如此,那我将重现一下 i++ 百分百线程不安全的远古神级。

i++ 为什么不安全

不安全

i++ 是线程不安全的,因为它不是一个原子操作。i++ 其实包含了三个步骤:


  1. 读取变量值:从内存中读取变量 i 的当前值。

  2. 自增操作:将读取的值加 1。

  3. 写回变量值:将更新后的值存回内存中。


在单线程环境下,这个过程不会有问题,但在多线程环境中,如果多个线程同时执行 i++,可能会发生竞态条件。例如,两个线程都读取了相同的初始值,但都还没来得及写回时,导致最终只会增加一次,而不是两次。

解决方法

  1. 使用同步机制:可以通过使用 synchronized 关键字来确保每次只有一个线程能够访问这个变量进行 i++ 操作。


   synchronized(this) {       i++;   }
复制代码


  1. 使用原子类:Java 提供了 AtomicInteger 来处理类似的操作,它保证了 i++ 的原子性。


   AtomicInteger i = new AtomicInteger(0);   i.incrementAndGet();  // 相当于 i++
复制代码


这样可以避免多个线程同时修改变量时导致的不一致性。

测试代码

下面是我的测试代码,逻辑非常简单。代码创建了两个线程,每个线程每隔一秒对共享变量 i 进行递增操作,并输出当前值。


package com.funtest.temp;

public class FunTester {
static int i = 0;
public static void test() { i++; System.out.println(Thread.currentThread().getName() + " " + i); }
public static void main(String[] args) { for (int j = 0; j < 2; j++) { new Thread(() -> { while (true) { try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } test(); } }).start(); }
}
}
复制代码


这个代码的逻辑可以简单梳理为以下几点:


  1. 静态变量 i:定义了一个静态变量 i,初始值为 0。这是所有线程共享的变量,用来记录每次的递增操作。

  2. test() 方法test() 方法的作用是对 i 进行自增操作,然后输出当前线程的名字和自增后的值。在原代码中,i++ 是线程不安全的,多个线程可能会在读取和写入 i 时发生冲突。

  3. main() 方法:在 main() 方法中,使用了一个循环创建了 两个线程,每个线程会进入一个无限循环(while (true))。每个线程在执行时,都会每隔 1 秒Thread.sleep(1000))调用一次 test() 方法,执行自增操作,并输出线程名称和当前的 i 值。

  4. 多线程执行:两个线程同时运行,不断对 i 进行递增操作,由于 i++ 不是原子操作,线程可能会发生数据竞争,导致递增结果不正确(输出值可能不连续或重复)。

总结:

代码创建了两个线程,每个线程每隔一秒对共享变量 i 进行递增操作,并输出当前值。然而,由于 i++ 操作不是线程安全的,程序可能出现竞态条件,导致输出结果不符合预期。

Byteman rule 脚本

下面是我的 Byteman 脚本的内容:


RULE sync testCLASS com.funtest.temp.FunTesterMETHOD testHELPER org.chaos_mesh.byteman.helper.FunHelperAT ENTRYIF TRUEDO setThreadName()ENDRULE

RULE async testCLASS com.funtest.temp.FunTesterMETHOD testHELPER org.chaos_mesh.byteman.helper.FunHelperAT WRITE iIF checkThreadName()DO System.out.println(Thread.currentThread().getName() + " 持有锁");ENDRULE
复制代码


这个 Byteman 脚本的作用是通过对类 com.funtest.temp.FunTestertest 方法进行增强,借助 Byteman 的规则动态监控和修改线程执行时的行为。主要目标是通过 FunHelper 来记录和监控线程的名字,并在访问共享变量 i 时输出线程持有锁的信息。下面逐步解释每个规则的含义:

第一条规则:sync test

  • RULE 名称sync test,给这条规则命名为 sync test

  • CLASS:目标类是 com.funtest.temp.FunTester,这条规则作用于该类。

  • METHOD:这条规则针对 test() 方法,表示要拦截这个方法。

  • HELPER:指定了一个辅助类 org.chaos_mesh.byteman.helper.FunHelper,其中定义了一些辅助方法来支持规则逻辑。

  • AT ENTRY:该规则触发的时机是在 test() 方法的入口处,也就是方法一开始执行时。

  • IF TRUE:条件始终为 TRUE,意味着无条件执行。

  • DO setThreadName():在 test() 方法执行时,调用 FunHelper 中的 setThreadName() 方法。这通常是用于记录或设置当前线程的名称。


test() 方法执行时,无论什么情况下,都会调用 setThreadName(),可能用于记录每个线程的名称,以便后续跟踪哪个线程正在执行。

第二条规则:async test

  • RULE 名称async test,命名为 async test

  • CLASS:同样作用于类 com.funtest.temp.FunTester

  • METHOD:针对 test() 方法。

  • HELPER:依然是 org.chaos_mesh.byteman.helper.FunHelper,同样使用辅助类来提供额外功能。

  • AT WRITE i:表示该规则在变量 i 被写入时触发。也就是说,当 i 的值发生改变时,规则会被执行(对应于 i++ 时的写操作)。

  • IF checkThreadName():该规则只有在辅助类中的 checkThreadName() 返回 true 时才会触发。这个方法可能会根据线程名称来判断当前线程是否符合某种条件。

  • DO System.out.println(Thread.currentThread().getName() + " 持有锁"):如果条件为 true,则会打印当前线程的名字,并显示 "持有锁",表示当前线程正在执行对共享变量 i 的修改操作。


test() 方法中的共享变量 i 被写入时(即 i++ 发生时),Byteman 会检查当前线程的名字。如果线程名字满足 checkThreadName() 的条件,就会输出该线程已经持有锁的信息。这可以用于调试或监控,查看哪个线程正在修改共享变量 i,避免竞态条件。

思路

通过这两个脚本,我们就可以在 i++ 赋值的过程中,第一个线程等待第二个线程进来,就能模式两个线程同时完成 i+ 计算,然后在分别开始执行赋值过程。我自己的思路就是在赋值之前做一个阻塞的设置,当一个线程到达,必须等另外一个线程过去,然后自己再执行。这个设计基本上可以保障后来的进程先赋值,因为我再等待的方法中加上了神迹 Thread.sleep(10);

实践效果

下面是注入 Byteman 脚本前后,控制台输出日志变化情况:


Thread-3     7Thread-3     8Thread-2     9Thread-3     10Thread-2     11setThreadName  Thread-2setThreadName  Thread-2Thread-3     持有锁Thread-3     12setThreadName  Thread-3Thread-2     持有锁Thread-2     12setThreadName  Thread-2Thread-3     持有锁Thread-3     13setThreadName  Thread-3Thread-2     持有锁Thread-2     13setThreadName  Thread-2
复制代码


可以看出,注入前,看着似乎是线程安全的,但是注入之后,每个线程输出的值都是一样的,百分百线程不安全了。

关于 FunHelper

这里实现比较简单,而且粗糙,目前各种实践中积累一些好的设计和场景。打算从 Byteman 源码中再汲取一些营养。后面等我感觉代码成熟了,再来分享一篇文章。有兴趣的可以加好友一起交流一下 Byteman 相关技术话题。

发布于: 刚刚阅读数: 4
用户头像

FunTester

关注

公众号:FunTester,800篇原创,欢迎关注 2020-10-20 加入

Fun·BUG挖掘机·性能征服者·头顶锅盖·Tester

评论

发布
暂无评论
Byteman 让 i++ 百分百线程不安全_FunTester_InfoQ写作社区