Java Sychronized详解与实战应用

Synchronized是Java中最基本且最常用的线程同步机制,它能够有效解决多线程环境下的共享资源竞争问题。本文将全面解析Synchronized的工作原理、使用方式、优化策略以及实际应用场景。

一、Synchronized基本概念与作用

Synchronized是Java语言原生支持的关键字,用于实现线程同步,保证多线程环境下对共享资源的安全访问。它的核心作用可以概括为三点:

  1. 保证线程互斥访问:同一时刻最多只有一个线程能执行被Synchronized修饰的方法或代码块,其他线程必须等待当前线程执行完毕后才能获取锁并执行
  2. 保证可见性:确保一个线程对共享变量的修改能够立即对其他线程可见。在退出Synchronized块时,JVM会将工作内存中的变量刷新回主内存;在进入时,会强制从主内存重新读取变量值
  3. 保证有序性:防止编译器和CPU对同步代码块内的指令进行重排序,确保代码按程序顺序执行

不使用同步机制的典型后果是数据不一致。例如两个线程同时执行count++操作,由于count++实际上包含"读取-修改-写入"三个步骤,在并发情况下可能导致最终结果小于预期值

二、Synchronized的四种使用方式

1. 修饰实例方法(对象锁)

arduino 复制代码
public synchronized void method() {
    // 方法体
}

这种方式锁住的是当前实例对象​(this),同一个实例的多个同步方法会互斥,不同实例的同步方法不会相互干扰。例如银行账户操作:

arduino 复制代码
public class BankAccount {
    private double balance;
    
    public synchronized void deposit(double amount) {
        balance += amount;
    }
    
    public synchronized void withdraw(double amount) {
        balance -= amount;
    }
}

2. 修饰静态方法(类锁)

arduino 复制代码
public static synchronized void method() {
    // 方法体
}

这种方式锁住的是类对象​(Class对象),所有实例共享同一把锁,即使不同实例调用静态同步方法也会互斥。例如:

arduino 复制代码
public class Counter {
    private static int count = 0;
    
    public static synchronized void increment() {
        count++;
    }
}

3. 同步代码块(指定锁对象)

javascript 复制代码
synchronized(obj) {
    // 代码块
}

这种方式可以灵活指定锁对象,obj可以是任何对象实例。通常使用共享资源作为锁对象。例如:

typescript 复制代码
public class TicketService {
    private int tickets;
    private final Object lock = new Object();
    
    public void sellTicket() {
        synchronized(lock) {
            if(tickets > 0) {
                tickets--;
            }
        }
    }
}

4. 同步静态代码块(类锁)

javascript 复制代码
synchronized(ClassName.class) {
    // 代码块
}

这种方式同样锁住类对象,与静态同步方法效果相同。例如:

typescript 复制代码
public class Logger {
    public static void log(String message) {
        synchronized(Logger.class) {
            // 写入日志文件
        }
    }
}

三、Synchronized实现原理

1. 监视器锁(Monitor)机制

Synchronized的底层实现依赖于JVM的对象监视器锁(Monitor)​机制。每个Java对象都与一个Monitor相关联,Monitor包含以下关键字段:

  • _owner:记录持有锁的线程
  • _recursions:锁的重入次数
  • _EntryList:存放等待锁的阻塞线程队列
  • _WaitSet:存放调用wait()后等待的线程队列

当线程执行到同步代码块时:

  1. 执行monitorenter指令尝试获取Monitor所有权
  2. 获取成功则进入同步代码块执行,Monitor进入数+1
  3. 执行完毕后执行monitorexit指令,Monitor进入数-1
  4. 当进入数为0时完全释放锁,唤醒等待线程

对于同步方法,JVM通过方法访问标志ACC_SYNCHRONIZED实现同步,原理与代码块同步类似

2. 锁升级优化过程

JDK1.6后对Synchronized进行了重大优化,引入了锁升级机制,根据竞争情况自动调整锁状态:

  1. 无锁状态:初始状态,没有线程竞争锁
  2. 偏向锁:当第一个线程访问同步块时,JVM会将对象头Mark Word标记为偏向该线程ID。后续该线程进入同步块时无需任何同步操作,只需检查线程ID是否匹配
  3. 轻量级锁 :当第二个线程尝试获取锁时,偏向锁升级为轻量级锁。线程通过CAS操作尝试获取锁,失败则短暂自旋等待,避免线程阻塞切换
  4. 重量级锁:当多个线程竞争激烈时,轻量级锁升级为重量级锁。竞争失败的线程会进入阻塞状态,依赖操作系统线程调度机制

锁升级是不可逆的,一旦升级为重量级锁就无法降级

3. 其他优化策略

  • 锁消除:JVM检测到不可能存在共享数据竞争时,会自动消除同步锁
  • 锁粗化:将多个连续的加锁/解锁操作合并为一个更大范围的锁操作,如将循环内的锁移到循环外
  • 自适应自旋:根据以往自旋等待的成功率,动态调整自旋时间

四、Synchronized的特性与限制

1. 可重入性

Synchronized是可重入锁,同一线程在外层方法获取锁后,内层方法可以直接再次获取该锁。JVM通过记录锁的持有线程和进入次数实现这一特性。例如:

arduino 复制代码
public class ReentrantDemo {
    public synchronized void method1() {
        method2(); // 可重入
    }
    
    public synchronized void method2() {
        // ...
    }
}

2. 不可中断性

一旦锁被其他线程获取,当前线程只能选择等待或阻塞,无法主动中断获取锁的过程。这与Lock类的可中断特性形成对比

3. 非公平锁

Synchronized是非公平锁,不保证等待时间最长的线程优先获取锁,任何尝试获取锁的线程都有机会立即获取锁

五、Synchronized的缺陷与替代方案

1. 主要缺陷

  1. 效率问题:在竞争激烈时,重量级锁会导致频繁的线程阻塞和唤醒,性能开销大
  2. 灵活性不足:无法设置超时、无法中断等待、无法实现读写分离等复杂同步需求
  3. 单条件限制:每个锁只能有一个等待条件(通过wait/notify),无法满足复杂条件判断

2. 替代方案

  1. Lock接口:提供了更灵活的加锁机制,支持尝试获取锁、可中断锁、超时锁等
  2. 读写锁(ReentrantReadWriteLock)​:实现读读共享、读写互斥、写写互斥,提高读多写少场景的性能
  3. CAS操作:基于乐观锁的无锁算法,如AtomicInteger等原子类

六、实战应用场景与最佳实践

1. 典型应用场景

  1. 单例模式实现:确保多线程环境下只创建一个实例
typescript 复制代码
public class Singleton {
    private static volatile Singleton instance;
    
    public static Singleton getInstance() {
        if(instance == null) {
            synchronized(Singleton.class) {
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  1. 线程安全的计数器:保证计数的原子性
arduino 复制代码
public class SafeCounter {
    private int count;
    
    public synchronized void increment() {
        count++;
    }
}
  1. 资源池管理:如数据库连接池的获取和释放
csharp 复制代码
public class ConnectionPool {
    private List<Connection> pool = new ArrayList<>();
    
    public synchronized Connection getConnection() {
        if(pool.isEmpty()) return createConnection();
        return pool.remove(pool.size()-1);
    }
    
    public synchronized void release(Connection conn) {
        pool.add(conn);
    }
}

2. 最佳实践

  1. 减小同步范围:尽量只同步必要的代码块,而非整个方法
  2. 分离读写锁:读多写少场景考虑使用读写锁替代
  3. 避免锁嵌套:防止死锁发生
  4. 使用私有锁对象:而非直接锁this或类对象,提高封装性
typescript 复制代码
public class PrivateLock {
    private final Object lock = new Object();
    
    public void method() {
        synchronized(lock) {
            // ...
        }
    }
}

七、Synchronized与volatile比较

特性 Synchronized volatile
原子性 保证代码块/方法的原子性 仅保证单次读/写的原子性
可见性 保证 保证
有序性 保证 有限保证
互斥性 支持 不支持
性能 较高开销 较低开销
使用复杂度 较高 较低

volatile适用于单一变量的可见性控制,而Synchronized适用于复杂操作的原子性控制

总结

Synchronized作为Java最基本的同步机制,虽然存在一些性能缺陷,但在JDK不断优化下仍然是大多数并发场景的首选方案。理解其底层原理和适用场景,能够帮助开发者编写出更高效、更安全的并发程序。对于更复杂的并发需求,可以考虑结合Lock、CAS等机制实现

相关推荐
九日卯贝3 小时前
字符串处理函数
java
RainbowSea3 小时前
5. Prompt 提示词
java·spring·ai编程
enzi_max3 小时前
IntelliJ IDEA / Android Studio 里直接跑 Cursor(不用来回切窗口)
java·android studio·intellij-idea·cursor
lalala_Zou3 小时前
虾皮后端一面
java·面试
我没想到原来他们都是一堆坏人3 小时前
java 动态代理
java·开发语言·动态代理
Coding_Doggy3 小时前
java面试day5 | 消息中间件、RabbitMQ、kafka、高可用机制、死信队列、消息不丢失、重复消费
java·开发语言·面试
GreatSQL社区3 小时前
GreatSQL 优化技巧:最值子查询与窗口函数相互转换
java·服务器·数据库
天生励志1234 小时前
【学习笔记】黑马Java+AI智能辅助编程视频教程,java基础入门
java·笔记·学习
这周也會开心4 小时前
Spring-MVC响应
java·spring·mvc