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()方法的核心流程
这部分是线程的执行逻辑,分两步走,设计思路很合理:
非临界区的独立计算:
int zm = 0; for (int j = index; j < zakres; j++) { zm += tablica.get(j); }先在当前线程内计算负责区间的元素和,结果存在局部变量
zm里。这一步完全不需要共享资源,所以放在锁外面,避免了长时间占用锁,大大提升了并发效率——这是并行计算的正确姿势:能并行的计算都放外面,只在修改共享资源时加锁。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

