Java线程同步语句原理及使用疑问解析
背景
正在学习Java内置锁(Intrinsic Lock)相关内容,已掌握synchronized方法、同步语句、synchronized静态方法的基础区别,现针对以下问题寻求解答:
疑问1:同步语句避免同步调用外部方法的逻辑示例
教程提到addName方法需要同步lastName和nameCount的修改,但要避免在同步块内调用其他对象的方法(否则可能引发活性问题),如果不用同步语句就得单独写非同步方法调用nameList.add。下面用代码示例拆解这个逻辑:
错误示例(可能引发活性问题)
public class NameManager { private List<String> nameList = new ArrayList<>(); private String lastName; private int nameCount; // 同步方法:整个方法都持有当前对象锁 public synchronized void addName(String name) { lastName = name; nameCount++; // 调用外部对象(nameList)的方法,此时仍持有当前对象锁 // 如果nameList的add方法内部也持有锁,或其他线程在等待nameList的锁,可能导致死锁或长时间阻塞 nameList.add(name); } }
这里的问题是:addName作为同步方法,整个执行过程都持有NameManager对象的锁,调用nameList.add时,若nameList是多线程访问的同步集合(比如Vector),或其他线程在等待nameList的锁,就可能出现锁嵌套导致的死锁,或当前线程长时间持有NameManager的锁,导致其他需要该锁的线程无法执行,引发饥饿等活性问题。
正确示例(用同步语句拆分锁范围)
public class NameManager { private List<String> nameList = new ArrayList<>(); private String lastName; private int nameCount; public void addName(String name) { // 仅在修改共享变量lastName和nameCount时持有当前对象锁 synchronized (this) { lastName = name; nameCount++; } // 调用外部对象方法时,已释放当前对象锁,避免锁持有时间过长或嵌套 nameList.add(name); } }
这个写法里,同步块只包裹需要保护的共享变量修改逻辑,执行完就释放锁,再调用nameList.add。这样即使nameList的操作耗时或涉及其他锁,也不会占用NameManager的锁,减少了锁竞争概率,避免活性问题。
疑问2:MsLunch类细粒度同步的深层逻辑
教程里的MsLunch类用两个独立锁分别保护无关的c1、c2字段,不只是简单的“独立锁对应变量”,深层逻辑包括:
- 减少锁竞争:如果用同一个锁保护所有字段,操作无关
c1和c2的线程也需要等待同一锁,并发度极低。用独立锁后,操作c1和c2的线程互不干扰,能同时执行,提升并发吞吐量。 - 降低锁持有时间:细粒度锁只会在操作对应字段时持有锁,不需要在整个方法执行期间持有,减少了其他线程等待锁的时间。
- 避免锁膨胀风险:如果一个锁被频繁竞争,JVM可能会把偏向锁升级为轻量级锁,再升级为重量级锁,性能下降。细粒度锁的竞争概率低,能维持更高效的锁状态。
- 逻辑清晰易维护:每个锁只负责保护一组相关的共享状态,符合单一职责原则,便于排查并发问题。
同步语句与同步方法的效率对比测试代码
下面是一个简单的对比测试,模拟多线程对两个独立共享变量的操作:
测试类定义
import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class SyncCompareTest { // 用同步方法的测试类 static class SyncMethodTest { private int count1 = 0; private int count2 = 0; public synchronized void incrementCount1() { count1++; } public synchronized void incrementCount2() { count2++; } } // 用同步语句的测试类 static class SyncStatementTest { private int count1 = 0; private int count2 = 0; private Object lock1 = new Object(); private Object lock2 = new Object(); public void incrementCount1() { synchronized (lock1) { count1++; } } public void incrementCount2() { synchronized (lock2) { count2++; } } } public static void main(String[] args) throws InterruptedException { int threadNum = 100; int loopNum = 100000; // 测试同步方法 SyncMethodTest methodTest = new SyncMethodTest(); CountDownLatch latch1 = new CountDownLatch(threadNum * 2); ExecutorService executor1 = Executors.newFixedThreadPool(threadNum); long start1 = System.currentTimeMillis(); for (int i = 0; i < threadNum; i++) { executor1.submit(() -> { for (int j = 0; j < loopNum; j++) { methodTest.incrementCount1(); } latch1.countDown(); }); executor1.submit(() -> { for (int j = 0; j < loopNum; j++) { methodTest.incrementCount2(); } latch1.countDown(); }); } latch1.await(); long end1 = System.currentTimeMillis(); executor1.shutdown(); System.out.println("同步方法耗时:" + (end1 - start1) + "ms"); // 测试同步语句 SyncStatementTest statementTest = new SyncStatementTest(); CountDownLatch latch2 = new CountDownLatch(threadNum * 2); ExecutorService executor2 = Executors.newFixedThreadPool(threadNum); long start2 = System.currentTimeMillis(); for (int i = 0; i < threadNum; i++) { executor2.submit(() -> { for (int j = 0; j < loopNum; j++) { statementTest.incrementCount1(); } latch2.countDown(); }); executor2.submit(() -> { for (int j = 0; j < loopNum; j++) { statementTest.incrementCount2(); } latch2.countDown(); }); } latch2.await(); long end2 = System.currentTimeMillis(); executor2.shutdown(); System.out.println("同步语句耗时:" + (end2 - start2) + "ms"); } }
测试结果说明
运行后会发现,同步语句的耗时明显少于同步方法:同步方法中两个increment方法共享同一个对象锁,线程操作count1和count2时互相阻塞;而同步语句用两个独立锁,操作count1和count2的线程可以同时执行,并发效率更高。
内容的提问来源于stack exchange,提问作者user22844207

