Skip to content

fix: ActionHandler.action线程安全问题#325

Merged
MisakaTAT merged 3 commits into
MisakaTAT:mainfrom
Chowhound-ch:main
Jul 18, 2025
Merged

fix: ActionHandler.action线程安全问题#325
MisakaTAT merged 3 commits into
MisakaTAT:mainfrom
Chowhound-ch:main

Conversation

@Chowhound-ch

@Chowhound-ch Chowhound-ch commented Jul 17, 2025

Copy link
Copy Markdown
Contributor

ActionHandler.action多线程下会出现线程安全问题,返回错误响应retCode=-1。
image

原因

    1. 每次调用ActionHandler.action会创建一个新的PayloadSender,而PayloadSender中锁对象非静态,不同PayloadSender对象会持有不同锁,导致无效上锁。
image image
    1. ActionHandler.callback中key由int类型的echo++获得,而多线程下echo++操作会有线程安全问题,导致多个回调相互覆盖。

效果

修改前:
image

修改后:
image

测试代码:

@Slf4j
@SpringBootApplication
public class ShiroTestApplication implements ApplicationRunner {
    @Autowired
    private BotContainer container;

    @Override
    public void run(ApplicationArguments args) {
        int des = 10;
        AtomicInteger count = new AtomicInteger(0);
        container.robots.forEach((key, bot) -> {
            Runnable friendListRunnable = () -> {
                ActionList<FriendInfoResp> list = bot.getFriendList();
                int i = count.addAndGet(1);
                if (i == des * 2) {
                    log.info("测试结束");
                }
                if (list == null) {
                    log.warn("timeout-{}", i);
                    return;
                }
                if (list.getData() == null) {
                    log.error("fail-{}: 获取好友列表失败", i);
                } else {
                    log.info("success-{}: 好友列表长度为{}", i, list.getData().size());
                }

            };
            Runnable groupListRunnable = () -> {
                ActionList<GroupInfoResp> list = bot.getGroupList();
                int i = count.addAndGet(1);
                if (i == des * 2) {
                    log.info("测试结束");
                }
                if (list == null) {
                    log.warn("timeout-{}", i);
                    return;
                }
                if (list.getData() == null) {
                    log.error("fail-{}: 获取群组列表失败", i);
                } else {
                    log.info("success-{}: 群组列表长度为{}", i, list.getData().size());
                }
            };

            for (int i = 0; i < des; i++) {
                new Thread(friendListRunnable).start();
                new Thread(groupListRunnable).start();
            }
        });
    }

    public static void main(String[] args) {
        SpringApplication.run(ShiroTestApplication.class, args);
    }

}

好的,这是翻译成中文的 pull request 总结:

## Sourcery 总结

通过将非原子回显计数器替换为 AtomicInteger 并将 PayloadSender 中的锁合并到静态实例,确保 ActionHandler.action 是线程安全的。

Bug 修复:
- 将整数回显字段替换为 AtomicInteger,以便在有效负载生成中进行原子回显递增
- 将 PayloadSender 锁更改为 static final,以便所有实例共享一个锁

<details>
<summary>Original summary in English</summary>

## Summary by Sourcery

Ensure ActionHandler.action is thread-safe by replacing the non-atomic echo counter with AtomicInteger and consolidating the lock in PayloadSender to a static instance.

Bug Fixes:
- Replace integer echo field with AtomicInteger for atomic echo increments in payload generation
- Change PayloadSender lock to static final so all instances share a single lock

</details>

@sourcery-ai

sourcery-ai Bot commented Jul 17, 2025

Copy link
Copy Markdown

Reviewer's Guide (审查者指南)

Addressed thread safety in ActionHandler and PayloadSender by replacing the non-atomic echo counter with AtomicInteger and unifying the PayloadSender lock into a static field. (通过将非原子回显计数器替换为 AtomicInteger 并将 PayloadSender 锁统一到静态字段中,解决了 ActionHandler 和 PayloadSender 中的线程安全问题。)

ER diagram for ActionHandler echo field update (ActionHandler 回显字段更新的 ER 图)

erDiagram
    ACTION_HANDLER {
        INT echo
    }
    ACTION_HANDLER ||--o{ PAYLOAD : uses
    PAYLOAD {
        INT echo
    }
Loading

Class diagram for thread safety changes in ActionHandler and PayloadSender (ActionHandler 和 PayloadSender 中线程安全更改的类图)

classDiagram
    class ActionHandler {
        - AtomicInteger echo
        + ActionHandler(WebSocketProperties wsProp, RateLimiterProperties rateLimiterProps, RateLimiter rateLimiter)
        ...
    }
    class PayloadSender {
        - static Lock lock
        - Condition condition
        ...
    }
Loading

File-Level Changes (文件级别更改)

Change (更改) Details (详情) Files (文件)
Switched the echo counter in ActionHandler from a plain int to AtomicInteger for atomic increments. (将 ActionHandler 中的回显计数器从普通 int 切换到 AtomicInteger 以实现原子递增。)
  • Changed "private int echo" to "private final AtomicInteger echo" (将 "private int echo" 更改为 "private final AtomicInteger echo")
  • Replaced echo++ with echo.getAndAdd(1) (将 echo++ 替换为 echo.getAndAdd(1))
src/main/java/com/mikuac/shiro/handler/ActionHandler.java
Consolidated the ReentrantLock in PayloadSender into a static field so all instances share the same lock. (将 PayloadSender 中的 ReentrantLock 合并到静态字段中,以便所有实例共享同一个锁。)
  • Updated lock declaration from instance-level to static final (将锁声明从实例级别更新为静态 final)
  • Retained Condition creation using the shared lock (保留使用共享锁创建 Condition)
src/main/java/com/mikuac/shiro/common/utils/PayloadSender.java

Tips and commands (提示和命令)

Interacting with Sourcery (与 Sourcery 交互)

  • Trigger a new review: Comment @sourcery-ai review on the pull request. (触发新的审查: 在拉取请求上评论 @sourcery-ai review。)
  • Continue discussions: Reply directly to Sourcery's review comments. ( 继续讨论: 直接回复 Sourcery 的审查评论。)
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it. ( 从审查评论生成 GitHub 问题: 通过回复审查评论,要求 Sourcery 从审查评论创建一个问题。您也可以回复审查评论并使用 @sourcery-ai issue 从中创建一个问题。)
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time. ( 生成拉取请求标题: 在拉取请求标题中的任何位置写入 @sourcery-ai 以随时生成标题。您也可以在拉取请求上评论 @sourcery-ai title 以随时(重新)生成标题。)
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time. ( 生成拉取请求摘要: 在拉取请求正文中的任何位置写入 @sourcery-ai summary 以随时在您想要的位置生成 PR 摘要。您也可以在拉取请求上评论 @sourcery-ai summary 以随时(重新)生成摘要。)
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time. ( 生成审查者指南: 在拉取请求上评论 @sourcery-ai guide 以随时(重新)生成审查者指南。)
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore. ( 解决所有 Sourcery 评论: 在拉取请求上评论 @sourcery-ai resolve 以解决所有 Sourcery 评论。如果您已经解决了所有评论并且不想再看到它们,这将非常有用。)
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review! ( 驳回所有 Sourcery 审查: 在拉取请求上评论 @sourcery-ai dismiss 以驳回所有现有的 Sourcery 审查。如果您想从新的审查开始,这将特别有用 - 不要忘记评论 @sourcery-ai review 以触发新的审查!)

Customizing Your Experience (自定义您的体验)

Access your dashboard to: (访问您的 仪表板 以:)

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others. (启用或禁用审查功能,例如 Sourcery 生成的拉取请求摘要、审查者指南等。)
  • Change the review language. (更改审查语言。)
  • Add, remove or edit custom review instructions. (添加、删除或编辑自定义审查说明。)
  • Adjust other review settings. (调整其他审查设置。)

Getting Help (获取帮助)

Original review guide in English

Reviewer's Guide

Addressed thread safety in ActionHandler and PayloadSender by replacing the non-atomic echo counter with AtomicInteger and unifying the PayloadSender lock into a static field.

ER diagram for ActionHandler echo field update

erDiagram
    ACTION_HANDLER {
        INT echo
    }
    ACTION_HANDLER ||--o{ PAYLOAD : uses
    PAYLOAD {
        INT echo
    }
Loading

Class diagram for thread safety changes in ActionHandler and PayloadSender

classDiagram
    class ActionHandler {
        - AtomicInteger echo
        + ActionHandler(WebSocketProperties wsProp, RateLimiterProperties rateLimiterProps, RateLimiter rateLimiter)
        ...
    }
    class PayloadSender {
        - static Lock lock
        - Condition condition
        ...
    }
Loading

File-Level Changes

Change Details Files
Switched the echo counter in ActionHandler from a plain int to AtomicInteger for atomic increments.
  • Changed "private int echo" to "private final AtomicInteger echo"
  • Replaced echo++ with echo.getAndAdd(1)
src/main/java/com/mikuac/shiro/handler/ActionHandler.java
Consolidated the ReentrantLock in PayloadSender into a static field so all instances share the same lock.
  • Updated lock declaration from instance-level to static final
  • Retained Condition creation using the shared lock
src/main/java/com/mikuac/shiro/common/utils/PayloadSender.java

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Chowhound-ch - 我已经查看了你的更改,它们看起来很棒!

AI 代理的提示
请解决此代码审查中的评论:
## 单独评论

### 评论 1
<location> `src/main/java/com/mikuac/shiro/handler/ActionHandler.java:157` </location>
<code_context>
         JSONObject payload = new JSONObject();
         payload.put("action", action.getPath());
-        payload.put("echo", echo++);
+        payload.put("echo", echo.getAndAdd(1));
         if (params != null && !params.isEmpty()) {
             payload.put("params", params);
</code_context>

<issue_to_address>
考虑使用 getAndIncrement() 以提高清晰度。

getAndIncrement() 更加符合习惯用法,并且在递增 1 时提高了可读性。
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
        payload.put("echo", echo.getAndAdd(1));
=======
        payload.put("echo", echo.getAndIncrement());
>>>>>>> REPLACE

</suggested_fix>

### 评论 2
<location> `src/main/java/com/mikuac/shiro/common/utils/PayloadSender.java:36` </location>
<code_context>

     private JSONObject resp;

-    private final Lock lock = new ReentrantLock();
+    private static final Lock lock = new ReentrantLock();

     private final Condition condition = lock.newCondition();
</code_context>

<issue_to_address>
将锁设为静态可能会在实例之间引入并发问题。

静态锁导致所有 PayloadSender 实例共享同一把锁,从而增加了处理多个会话时的争用和死锁风险。如果锁保护特定于实例的状态,请保持锁为非静态。
</issue_to_address>

Sourcery 对开源是免费的 - 如果你喜欢我们的评论,请考虑分享它们 ✨
帮助我变得更有用!请点击每个评论上的 👍 或 👎,我将使用反馈来改进你的评论。
Original comment in English

Hey @Chowhound-ch - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `src/main/java/com/mikuac/shiro/handler/ActionHandler.java:157` </location>
<code_context>
         JSONObject payload = new JSONObject();
         payload.put("action", action.getPath());
-        payload.put("echo", echo++);
+        payload.put("echo", echo.getAndAdd(1));
         if (params != null && !params.isEmpty()) {
             payload.put("params", params);
</code_context>

<issue_to_address>
Consider using getAndIncrement() for clarity.

getAndIncrement() is more idiomatic and improves readability when incrementing by one.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
        payload.put("echo", echo.getAndAdd(1));
=======
        payload.put("echo", echo.getAndIncrement());
>>>>>>> REPLACE

</suggested_fix>

### Comment 2
<location> `src/main/java/com/mikuac/shiro/common/utils/PayloadSender.java:36` </location>
<code_context>

     private JSONObject resp;

-    private final Lock lock = new ReentrantLock();
+    private static final Lock lock = new ReentrantLock();

     private final Condition condition = lock.newCondition();
</code_context>

<issue_to_address>
Making lock static may introduce concurrency issues across instances.

A static lock causes all PayloadSender instances to share the same lock, increasing contention and risk of deadlocks when handling multiple sessions. Keep the lock non-static if it protects instance-specific state.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/main/java/com/mikuac/shiro/handler/ActionHandler.java Outdated

private JSONObject resp;

private final Lock lock = new ReentrantLock();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): 将锁设为静态可能会在实例之间引入并发问题。

静态锁导致所有 PayloadSender 实例共享同一把锁,从而增加了处理多个会话时的争用和死锁风险。如果锁保护特定于实例的状态,请保持锁为非静态。

Original comment in English

issue (bug_risk): Making lock static may introduce concurrency issues across instances.

A static lock causes all PayloadSender instances to share the same lock, increasing contention and risk of deadlocks when handling multiple sessions. Keep the lock non-static if it protects instance-specific state.

@Chowhound-ch

Copy link
Copy Markdown
Contributor Author

又补了一点,ActionHandler.callback为HashMap,HashMap线程不安全,而并发下会在ActionHandler.onReceiveActionResp方法大量get,会导致大量并发下最后几个请求没法被condition.signalAll()唤醒(20次不会,200次则可能最后几个会出问题),换成ConcurrentHashMap则不会出现。
可能多线程访问的情况还是用ConcurrentHashMap吧,HashMap容易出现奇奇怪怪的问题。

提高代码可读性

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
@MisakaTAT

Copy link
Copy Markdown
Owner

LGTM, Thanks!

@MisakaTAT
MisakaTAT merged commit 98c8bfb into MisakaTAT:main Jul 18, 2025
Chowhound-ch added a commit to Chowhound-ch/Chowh-Bot-Shiro that referenced this pull request Jul 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants