You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java中的阻塞型Semaphore(信号量)及Watek线程类技术问询

解析Java中用阻塞型Semaphore实现线程安全累加的Watek线程类

咱们直接拆解这个Watek线程类的逻辑,看看它是怎么用Semaphore保证线程安全的,同时聊聊里面的亮点和潜在问题:

一、类的核心定位

这个类是基于Runnable实现的工作线程,职责是分块计算链表元素的和,再把结果安全合并到共享计数器中——核心是用Semaphore解决多线程修改共享资源的竞态问题。

二、成员变量与构造函数解析

先逐个看成员的作用:

  • Semaphore sem = new Semaphore(1);:初始化了一个仅含1个许可的信号量,本质就是互斥锁(Mutex),同一时间只能有一个线程拿到许可进入临界区。
  • index & zakres:分别是当前线程要处理的链表起始索引和结束边界(循环用j < zakres,属于左闭右开区间),用来拆分计算任务,实现并行分块求和。
  • Counter sum:共享的计数器对象,是所有线程要修改的临界资源——也是线程安全问题的核心点。
  • LinkedList<Integer> tablica:待计算的数据源链表。

构造函数就是把外部传入的任务参数、共享对象赋值给成员变量,逻辑很直接,但后面我要提一个致命的Semaphore使用坑!

三、run()方法的核心流程

这部分是线程的执行逻辑,分两步走,设计思路很合理:

  1. 非临界区的独立计算:

    int zm = 0;
    for (int j = index; j < zakres; j++) {
        zm += tablica.get(j);
    }
    

    先在当前线程内计算负责区间的元素和,结果存在局部变量zm里。这一步完全不需要共享资源,所以放在锁外面,避免了长时间占用锁,大大提升了并发效率——这是并行计算的正确姿势:能并行的计算都放外面,只在修改共享资源时加锁。

  2. Semaphore保护的临界区操作:

    sem.acquire();
    sum.add(zm);
    sem.release();
    
    • sem.acquire():尝试获取信号量许可,如果此时已有线程持有许可,当前线程会阻塞等待,直到许可被释放。
    • sum.add(zm):把局部计算的和加到共享计数器里——这一步必须被保护,否则多个线程同时修改sum会出现丢更新、值错乱等竞态问题。
    • sem.release():释放许可,让其他等待的线程可以进入临界区修改共享计数器。

四、Semaphore在这里的作用

这里用的是二进制信号量(许可数=1),功能和synchronized、ReentrantLock的互斥作用完全一致,都是保证同一时间只有一个线程能执行临界区代码。相比synchronized,Semaphore更灵活——比如以后如果需要允许N个线程同时修改(比如计数器支持批量更新),只要把Semaphore的许可数改成N就行,但当前场景是互斥,所以1个许可刚好。

五、代码里的坑与改进建议

1. 致命问题:Semaphore是线程私有而非共享!

现在sem是Watek类的成员变量,意味着每个线程实例都有自己的信号量——这等于没锁!多个线程各自拿自己的许可,完全不会互斥,sum.add(zm)还是会被并发执行,线程安全直接失效。

修复方案:把Semaphore作为构造函数的参数传入,让所有Watek线程共用同一个Semaphore实例,比如:

public class Watek implements Runnable { 
    Semaphore sem; // 不再初始化,由外部传入
    // ... 其他成员不变
    public Watek(int pivot_index, int chunk, Counter Atomic_var, LinkedList<Integer> tab, Semaphore sharedSem) { 
        index = pivot_index; 
        zakres = chunk; 
        sum = Atomic_var; 
        tablica = tab; 
        sem = sharedSem; // 共用同一个信号量
    }
    // ... run方法不变
}

2. 异常处理不严谨

原代码的catch块没写完,而且sem.release()直接放在sum.add(zm)后面,如果add方法抛出异常,许可就永远不会被释放,导致其他线程一直阻塞。

修复方案:把release()放到finally块里,确保无论临界区代码是否抛出异常,都会释放许可:

try {
    sem.acquire();
    try {
        sum.add(zm);
    } finally {
        sem.release();
    }
} catch (InterruptedException e) {
    // 处理线程中断,比如记录日志、终止任务
    Thread.currentThread().interrupt(); // 恢复中断状态
}

3. LinkedList的get效率问题

LinkedList.get(j)是O(n)时间复杂度,因为要从链表头遍历到指定索引,如果链表很长、分块很多,这部分会很慢。建议换成ArrayList(get是O(1)),或者用迭代器遍历指定区间,避免重复遍历链表。

4. Counter的线程安全冗余

如果你的Counter类本身已经是线程安全的(比如内部用AtomicInteger实现add方法),那其实不需要Semaphore,直接调用sum.add(zm)就行——Semaphore在这里就是多余的了。


内容的提问来源于stack exchange,提问作者QWERTY1234567890

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 10:10:19