您现在的位置是:首页 > 编程 > 

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

2025-07-26 15:38:21
Byteman 让 i++ 百分百线程不安全 在我早期的文章当中,我使用过一个插件 vmlens 实现让 i++ 展现了百分百的线程不安全。在演示示例中,使用了两个线程并发执行 i++,然后就看到了线程不安全的全过程。但是 vmlens 当时是个付费软件,作者给白嫖用户两周的体验期,虽然我我提了一个 BUG ,也没得到任何的优待。所以很快进行了简单的尝试之后,就放弃探索 vmlens 。最近开始研

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

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

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

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

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

i++ 为什么不安全

不安全

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

  1. 读取变量值:从内存中读取变量 i 的当前值。
  2. 自增操作:将读取的值加1。
  3. 写回变量值:将更新后的值存回内存中。

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

解决方法

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

代码语言:javascript代码运行次数:0运行复制
synchronized(this) {
    i++;
}

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

代码语言:javascript代码运行次数:0运行复制
AtomicInteger i = new AtomicInteger(0);
i.incrementAndGet();  // 相当于 i++

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

测试代码

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

代码语言:javascript代码运行次数:0运行复制
package com.;


public class FunTester {

    static int i = 0;

    public static void test() {
        i++;
        println(().getame() + "     " + 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 脚本的内容:

代码语言:javascript代码运行次数:0运行复制
RULE sync test
CLASS com..FunTester
METHOD test
HELPER _mesh.byteman.helper.FunHelper
AT ETRY
IF TRUE
DO setThreadame()
EDRULE


RULE async test
CLASS com..FunTester
METHOD test
HELPER _mesh.byteman.helper.FunHelper
AT WRITE i
IF checkThreadame()
DO println(().getame() + "     持有锁");
EDRULE

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

第一条规则:sync test
  • RULE 名称sync test,给这条规则命名为 sync test
  • CLASS:目标类是 com..FunTester,这条规则作用于该类。
  • METHOD:这条规则针对 test() 方法,表示要拦截这个方法。
  • HELPER:指定了一个辅助类 _mesh.byteman.helper.FunHelper,其中定义了一些辅助方法来支持规则逻辑。
  • AT ETRY:该规则触发的时机是在 test() 方法的入口处,也就是方法一开始执行时。
  • IF TRUE:条件始终为 TRUE,意味着无条件执行。
  • **DO setThreadame()**:在 test() 方法执行时,调用 FunHelper 中的 setThreadame() 方法。这通常是用于记录或设置当前线程的名称。

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

第二条规则:async test
  • RULE 名称async test,命名为 async test
  • CLASS:同样作用于类 com..FunTester
  • METHOD:针对 test() 方法。
  • HELPER:依然是 _mesh.byteman.helper.FunHelper,同样使用辅助类来提供额外功能。
  • AT WRITE i:表示该规则在变量 i 被写入时触发。也就是说,当 i 的值发生改变时,规则会被执行(对应于 i++ 时的写操作)。
  • **IF checkThreadame()**:该规则只有在辅助类中的 checkThreadame() 返回 true 时才会触发。这个方法可能会根据线程名称来判断当前线程是否符合某种条件。
  • **DO println(().getame() + " 持有锁")**:如果条件为 true,则会打印当前线程的名字,并显示 "持有锁",表示当前线程正在执行对共享变量 i 的修改操作。

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

思路

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

实践效果

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

代码语言:javascript代码运行次数:0运行复制
Thread-     7
Thread-     8
Thread-2     9
Thread-     10
Thread-2     11
setThreadame  Thread-2
setThreadame  Thread-2
Thread-     持有锁
Thread-     12
setThreadame  Thread-
Thread-2     持有锁
Thread-2     12
setThreadame  Thread-2
Thread-     持有锁
Thread-     1
setThreadame  Thread-
Thread-2     持有锁
Thread-2     1
setThreadame  Thread-2

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

关于 FunHelper

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

本文参与 腾讯云自媒体同步曝光计划,分享自。原始发表:2024-12-02,如有侵权请联系 cloudcommunity@tencent 删除变量多线程脚本线程安全

#感谢您对电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格的认可,转载请说明来源于"电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格

本文地址:http://www.dnpztj.cn/biancheng/1132550.html

相关标签:无
上传时间: 2025-07-18 20:50:12
留言与评论(共有 16 条评论)
本站网友 孔雀大卫城
6分钟前 发表
当 test() 方法中的共享变量 i 被写入时(即 i++ 发生时)
本站网友 信质电机
28分钟前 发表
多个线程可能会在读取和写入 i 时发生冲突
本站网友 脑干
9分钟前 发表
HELPER:指定了一个辅助类 _mesh.byteman.helper.FunHelper
本站网友 stormii
28分钟前 发表
每个线程每隔一秒对共享变量 i 进行递增操作
本站网友 底部
12分钟前 发表
有兴趣的可以加好友一起交流一下 Byteman 相关技术话题
本站网友 少时诵诗书
24分钟前 发表
然后在分别开始执行赋值过程
本站网友 晨之科
4分钟前 发表
意味着无条件执行
本站网友 感叹号的作用
3分钟前 发表
以便后续跟踪哪个线程正在执行
本站网友 芭乐的功效与作用
27分钟前 发表
用来记录每次的递增操作
本站网友 孕周计算器
10分钟前 发表
代码语言:javascript代码运行次数:0运行复制AtomicInteger i = new AtomicInteger(0); i.incrementAndGet(); // 相当于 i++ 这样可以避免多个线程同时修改变量时导致的不一致性
本站网友 新再见艳阳天
6分钟前 发表
也就是方法一开始执行时
本站网友 怎么去掉老年斑
21分钟前 发表
初始值为 0
本站网友 短租平台哪个最好
17分钟前 发表
使用了一个循环创建了 两个线程
本站网友 天津星美国际影城
14分钟前 发表
这个设计基本上可以保障后来的进程先赋值
本站网友 我好色
15分钟前 发表
再来分享一篇文章