尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java多线程同步:synchronized原理与最佳实践

Java多线程同步:synchronized原理与最佳实践 1. 为什么我们需要synchronized当我在2013年第一次遇到多线程数据竞争问题时一个简单的计数器程序给了我深刻教训。当时我创建了10个线程同时对同一个计数器进行操作理论上应该得到10000但实际运行结果总是在8000-9000之间波动。这就是典型的线程安全问题而synchronized正是Java为解决这类问题提供的内置解决方案。synchronized关键字在Java中用于控制多线程对共享资源的访问它能确保同一时刻只有一个线程可以执行特定代码段或访问特定对象。这个特性对于银行转账、库存扣减等需要原子性操作的场景至关重要。注意即使是最简单的i操作在底层也会被拆分为读取-修改-写入三个步骤不加锁就会导致更新丢失。2. synchronized的三种使用方式2.1 实例方法同步这是最常见的用法直接在方法声明中添加synchronizedpublic synchronized void transfer(Account target, int amount) { if (this.balance amount) { this.balance - amount; target.balance amount; } }这种写法等价于用synchronized(this)包裹方法体。锁对象是当前实例不同实例间的操作不会互相阻塞。2.2 静态方法同步当需要同步静态方法时锁对象变成类的Class对象public static synchronized void updateConfig() { // 更新配置的操作 }这相当于synchronized(MyClass.class)。因为Class对象在JVM中唯一所以能保证全局同步。2.3 同步代码块最灵活的用法是指定任意对象作为锁private final Object lock new Object(); public void doSomething() { // 非同步代码 synchronized(lock) { // 临界区代码 } // 非同步代码 }这种方式的优势是锁粒度更细提升并发性能可以使用专用锁对象避免意外锁住this可以实现更复杂的同步策略3. 底层实现原理3.1 对象头与Mark Word每个Java对象在内存中都由三部分组成对象头实例数据对齐填充其中对象头包含Mark Word存储哈希码、GC分代年龄、锁状态等类型指针指向类元数据数组长度如果是数组在32位JVM中Mark Word结构如下锁状态25bit4bit1bit(偏向锁)2bit(锁标志)无锁哈希码分代年龄001偏向锁线程ID时间戳分代年龄101轻量级锁指向栈中锁记录的指针00重量级锁指向互斥量的指针10GC标记空113.2 锁升级过程现代JVM采用锁升级策略来优化同步性能无锁状态初始状态偏向锁第一个线程访问时在Mark Word中记录线程ID轻量级锁当有竞争时升级为CAS自旋锁重量级锁自旋超过阈值默认10次后升级为操作系统互斥锁这个升级过程是不可逆的目的是减少直接使用重量级锁带来的性能开销。3.3 字节码层面编译后的同步方法会多出两条指令monitorenter进入同步块monitorexit退出同步块JVM保证每个monitorenter必须有对应的monitorexit即使在异常情况下也会执行。4. 性能优化实践4.1 减小锁粒度错误的做法public synchronized void processOrder(Order order) { validate(order); calculate(order); save(order); notify(order); }优化方案public void processOrder(Order order) { validate(order); // 无需同步 synchronized(this) { calculate(order); save(order); } notify(order); // 无需同步 }4.2 分离读写锁对于读多写少的场景可以使用ReadWriteLockprivate final ReentrantReadWriteLock rwLock new ReentrantReadWriteLock(); public Data readData() { rwLock.readLock().lock(); try { return data; } finally { rwLock.readLock().unlock(); } } public void updateData(Data newData) { rwLock.writeLock().lock(); try { this.data newData; } finally { rwLock.writeLock().unlock(); } }4.3 避免死锁死锁的四个必要条件互斥条件请求与保持不剥夺条件循环等待预防措施按固定顺序获取锁设置锁超时时间使用tryLock()而非lock()5. 常见问题排查5.1 锁竞争问题诊断使用jstack查看线程状态jstack pid | grep -A 10 BLOCKED典型输出Thread-1 #12 prio5 os_prio0 tid0x00007f48740f8000 nid0x1e03 waiting for monitor entry [0x00007f486b7fe000] java.lang.Thread.State: BLOCKED (on object monitor) at com.example.Test.method(Test.java:15) - waiting to lock 0x000000076ab95c80 (a com.example.Test)5.2 锁膨胀问题当发现大量线程处于BLOCKED状态时可能是锁粒度过大同步代码执行时间过长锁分配不合理解决方案使用JProfiler或VisualVM分析热点考虑使用并发集合替代同步块评估是否可以用原子变量(AtomicInteger等)5.3 虚假唤醒问题典型错误代码synchronized(lock) { while(!condition) { lock.wait(); } // 处理逻辑 }必须使用while循环而非if判断因为操作系统可能产生虚假唤醒其他线程可能意外调用了notifyAll()6. 与volatile的比较特性synchronizedvolatile原子性保证不保证可见性保证保证有序性保证有限保证适用范围代码块/方法变量线程阻塞会不会编译器优化禁止有限禁止实际选择原则需要原子性操作 → synchronized只需可见性保证 → volatile既要可见性又要简单原子操作 → Atomic类7. 现代Java中的替代方案7.1 java.util.concurrent包对于高并发场景推荐使用ReentrantLock可中断、可定时、公平锁StampedLock乐观读锁Semaphore信号量控制CountDownLatch线程协调7.2 无锁编程CAS(Compare-And-Swap)实现的无锁数据结构AtomicInteger counter new AtomicInteger(0); // 线程安全的自增 counter.incrementAndGet();优点无阻塞无死锁风险高并发下性能更好缺点实现复杂ABA问题不适合复杂操作8. 实际案例线程安全的LRU缓存public class SynchronizedLRUCacheK, V { private final int capacity; private final LinkedHashMapK, V map; private final Object lock new Object(); public SynchronizedLRUCache(int capacity) { this.capacity capacity; this.map new LinkedHashMapK, V(capacity, 0.75f, true) { Override protected boolean removeEldestEntry(Map.EntryK, V eldest) { return size() capacity; } }; } public V get(K key) { synchronized(lock) { return map.get(key); } } public void put(K key, V value) { synchronized(lock) { map.put(key, value); } } }优化点使用专用锁对象而非this利用LinkedHashMap的访问顺序特性重写removeEldestEntry实现自动淘汰9. 面试常见问题synchronized和ReentrantLock的区别synchronized是JVM内置实现ReentrantLock是JDK实现ReentrantLock提供更灵活的锁机制公平锁、条件变量等synchronized会自动释放锁ReentrantLock需要手动unlock什么是锁粗化JVM会将相邻的同步块合并减少锁获取/释放的开销双重检查锁定问题经典的错误单例模式实现// 错误示范 public class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance null) { synchronized(Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; } }正确实现需要加volatileprivate static volatile Singleton instance;synchronized能否修饰构造方法语法上可以但没有实际意义因为构造方法本身是线程安全的如何选择锁对象对于实例同步通常用this对于静态同步用Class对象最佳实践是使用private final的专用锁对象10. 最佳实践总结锁对象选择优先使用private final对象避免锁String常量等可能被共享的对象同步范围只同步必要的代码块尽量缩短同步块执行时间异常处理synchronized(lock) { try { // 业务代码 } catch(Exception e) { // 处理异常 } }性能监控关注JVM的锁竞争统计使用JMX或第三方工具监控锁等待时间代码审查要点检查是否存在嵌套锁验证锁释放路径是否完整评估锁粒度是否合理我在实际项目中最深刻的体会是不要为了同步而同步。在最近的一个高并发项目中我们通过分析发现80%的同步块其实是不必要的移除后QPS提升了3倍。多线程编程的艺术在于找到安全与性能的平衡点。
返回列表