类级同步块未生效?多线程环境下计数输出非100倍数问题排查
首先,你的同步块逻辑本身是正确的——使用CallRestServiceWorker.class作为锁对象,确实能保证所有线程同一时间只有一个进入同步块执行c=c+1和后续的判断打印操作,理论上输出的c应该都是100的倍数。但你遇到了异常结果,可能的原因和对应的解决办法如下:
1. 类加载器导致锁不共享
如果你的应用使用了多个类加载器加载CallRestServiceWorker类(比如Web应用中的不同模块、自定义类加载器场景),每个类加载器会生成一个独立的CallRestServiceWorker.class对象,这意味着不同线程持有的是不同的锁,同步机制完全失效。此时多个线程会同时修改c,导致c的递增不是原子操作,甚至可能出现c的值被覆盖,进而出现非预期的输出(比如重复打印100,或者计数跳变)。
解决办法:
- 确保
CallRestServiceWorker类只被一个类加载器加载,比如将其放在应用的核心类路径下,避免自定义类加载器重复加载。 - 换用一个全局唯一的锁对象,比如在类中定义一个静态的锁对象:
然后同步块改为:private static final Object GLOBAL_LOCK = new Object();
这种方式不受类加载器的影响,锁对象是全局唯一的。synchronized (GLOBAL_LOCK) { c = c + 1; if (c % 100 == 0) System.out.println(c); }
2. 未处理的异常导致计数中断
在读取S3文件内容时,fileContent.nextLine()可能抛出NoSuchElementException(比如文件流被意外关闭)或者IO异常,导致线程提前退出,后续的计数操作无法执行。不过这种情况只会导致c的总数比预期少,但已经打印的c依然是100的倍数,除非异常发生在同步块执行过程中,但同步块内的代码很少抛出异常。
解决办法:
- 给文件读取逻辑加上异常捕获,确保线程能正常执行完计数逻辑:
这里使用了try-with-resources自动关闭Scanner,避免资源泄漏。try (Scanner fileContent = new Scanner(s3FileObject.getObjectContent())) { while (fileContent.hasNextLine()) { try { fileContent.nextLine(); synchronized (CallRestServiceWorker.class) { c = c + 1; if (c % 100 == 0) System.out.println(c); } } catch (NoSuchElementException e) { // 处理读取到文件末尾的异常 break; } } } catch (IOException e) { // 处理S3文件流的IO异常 e.printStackTrace(); }
3. 内存可见性问题(优化建议)
虽然同步块的进入和退出会强制刷新主内存,保证c的可见性,但如果c没有被声明为volatile,在极端情况下,某些线程可能会读取到c的缓存值。不过这种情况一般不会导致输出非100倍数的问题,但可以优化代码来避免潜在风险。
解决办法:
- 给静态变量
c加上volatile修饰:static volatile int c = 0;
4. 其他线程意外修改了c
检查你的代码中是否有其他地方(比如S3Client父类的方法、其他工具类)修改了静态变量c,如果有,那同步块的保护范围就不完整,会导致c的值被意外修改,进而出现非预期的输出。
解决办法:
- 将
c的访问权限改为private,并提供静态的同步方法来修改它,避免直接访问:
然后在run方法中调用这个方法:private static volatile int c = 0; private static synchronized void incrementAndCheck() { c++; if (c % 100 == 0) { System.out.println(c); } }
这样能完全封装incrementAndCheck();c的修改逻辑,避免外部意外修改。
额外建议:使用原子类替代手动同步
Java提供了java.util.concurrent.atomic包下的原子类,比如AtomicInteger,可以更安全、高效地实现多线程下的计数,无需手动写同步块:
private static final AtomicInteger c = new AtomicInteger(0); // 在run方法中替换同步块的代码: int current = c.incrementAndGet(); if (current % 100 == 0) { // 这里println是线程安全的,但如果要保证打印顺序,可以加锁 synchronized (System.out) { System.out.println(current); } }
AtomicInteger的incrementAndGet()方法是原子操作,保证了计数的正确性,同时也提供了内存可见性。如果需要保证打印的顺序(比如按c的递增顺序输出),可以给System.out加锁,避免多个线程的打印内容交错。
内容的提问来源于stack exchange,提问作者Don Jose

