synchronized关键字相关

synchronized是Java中用于解决并发问题的核心关键字,它通过确保多个线程对共享资源的互斥访问,来避免线程安全问题(如竞态条件、数据不一致等)。

synchronized的核心特性

  • 原子性(Atomicity):确保一个或多个操作要么全部执行成功,要么全部执行失败。在synchronized代码块中的代码是不可中断的,同一时刻只有一个线程能执行。
  • 可见性(Visibility):保证一个线程对共享变量的修改,对于其他后续进入同步代码块的线程是可见的。当线程释放锁时,会将私有内存中的变量值刷新回主内存。
  • 有序性(Ordering):虽然编译器和处理器为了优化会进行指令重排,但synchronized可以保证多线程程序在逻辑上的执行顺序,即“同步块内的代码在执行上具有先后顺序”。
  • 可重入性:可重入性是指一个线程已经获取到某个锁后,再次请求该锁时可以直接获取,无需重新竞争。synchronized是可重入锁,其内部通过计数器记录锁的持有次数(初始为0,获取锁时加1,释放锁时减1,计数器为0时锁才被真正释放)。这一特性避免了线程在递归调用同步方法/代码块时出现死锁。例如,一个同步方法A调用另一个同步方法B(两者锁对象相同),线程获取A的锁后,调用B时可直接获取锁。

synchronized的使用方式

synchronized的使用灵活,可修饰不同的代码结构,核心是明确“锁对象”——线程竞争的是锁对象,只有获取到锁对象的线程才能执行同步代码,常见使用方式有3种。
  1. 修饰实例方法(对象锁),语法:public synchronized void methodName() { ... }
  • 锁对象:当前类的实例对象(this)。
  • 特点:不同实例对象的锁相互独立,即多个线程访问同一个实例的同步实例方法时会竞争锁;访问不同实例的同步实例方法时,因锁对象不同,不会竞争。
  • 示例:同一User实例的add()方法被多线程调用时互斥,不同User实例的add()方法可并行执行。
  1. 修饰静态方法(类锁)语法:public static synchronized void methodName() { ... }
  • 锁对象:当前类的Class对象(每个类在JVM中只有一个Class对象,是全局唯一的)。
  • 特点:类锁是全局锁,无论创建多少个类的实例,所有线程访问该类的同步静态方法时,都会竞争同一个Class对象锁。
  • 注意:类锁与对象锁相互独立,即同步静态方法和同步实例方法的锁对象不同,线程访问时不会竞争。
  1. 修饰代码块(自定义锁对象),语法:synchronized (锁对象) { ... 同步代码 ... }
  • 锁对象:可自定义,支持两种类型:① 实例对象(this或其他实例);② Class对象(类名.class)。
  • 特点:粒度最细,可精准控制需要同步的代码片段(而非整个方法),减少锁竞争,提高程序性能。
  • 常见场景:
    • 锁当前实例:synchronized (this) { ... },效果与修饰实例方法一致,但仅同步代码块内的逻辑。
    • 锁Class对象:synchronized (User.class) { ... },效果与修饰静态方法一致。
    • 锁自定义对象:private Object lock = new Object(); synchronized (lock) { ... },通过独立的锁对象,避免与其他同步逻辑竞争锁,灵活性最高。

synchronized的锁机制

  • Java 6及以后对synchronized进行了大幅优化,引入了“偏向锁、轻量级锁、重量级锁”三种锁状态,目的是根据线程竞争的激烈程度动态切换锁状态,平衡性能与线程安全。锁机制的核心是“对象头”——Java对象在内存中的布局包括对象头、实例数据、对齐填充,其中对象头存储了锁的状态信息(Mark Word)、类元数据指针等。
  • 三种锁状态的优先级:无锁 < 偏向锁 < 轻量级锁 < 重量级锁,随着线程竞争的加剧,锁会从低级别向高级别升级,且升级过程不可逆(一旦升级为重量级锁,无法回退为轻量级锁或偏向锁)。
    • 无锁(No Lock):初始状态。
    • 偏向锁(Biased Lock):当只有一个线程访问同步块时,直接在对象头记录线程ID,下次该线程进入时无需CAS操作,性能极高。
    • 轻量级锁(Lightweight Lock):当出现竞争,但竞争不激烈时,通过CAS自旋来尝试获取锁,避免线程阻塞。
    • 重量级锁(Heavyweight Lock):当自旋超过一定次数或竞争非常激烈时,升级为重量级锁。此时未获取到锁的线程会进入阻塞(Blocked)状态,交给操作系统管理。
锁优化补充
  • 锁消除:JVM的即时编译器(JIT)在运行时,会对一些“不可能存在竞争的锁”进行消除。例如,局部变量作为锁对象(每个线程都有独立的局部变量,无共享),JVM会直接删除该synchronized修饰,避免不必要的锁开销。
  • 锁粗化:当多个连续的synchronized代码块使用同一个锁对象时,JVM会将这些代码块合并为一个大的同步代码块,减少锁的获取/释放次数(每次获取/释放锁都有开销)。例如,循环内多次调用同步方法,JVM可能将锁粗化到循环外部。

CAS (Compare And Swap,比较并交换) 是并发编程中实现原子操作的核心算法,是一种乐观锁的实现策略。

  1. CAS的工作原理,包含三个核心参数:
  • 内存地址 V (Memory Location):变量在内存中的实际值。
  • 期望值 A (Expected Value):线程认为该变量当前应该是什么值。
  • 新值 B (New Value):线程想要更新成的值。
  • 执行逻辑:当且仅当内存地址 V 的值等于期望值 A 时,处理器才会将 V 的值更新为 B。否则,说明该变量已被其他线程修改,当前线程什么都不做,通常会进入自旋(死循环重试)。
  1. CAS的优缺点
    优点:
  • 非阻塞性:CAS 是一种非阻塞算法(Non-blocking),它不需要像 synchronized 那样挂起和恢复线程。
  • 性能高:在低、中度竞争的情况下,由于减少了线程上下文切换的开销,效率远高于重量级锁。
    缺点:
  • 循环时间长(自旋开销):如果高并发下竞争激烈,CAS 会频繁失败并不断自旋,这会给 CPU 带来巨大的计算压力。
  • 只能保证一个共享变量的原子操作:对于多个变量的操作,仍需使用 synchronized 或 ReentrantLock。
  • ABA 问题(最经典的缺点)。
  1. 什么是 ABA 问题?
  • 如果变量初始值为A,在线程1准备修改它的过程中,线程2快速地将其改成了B,然后又改回了A。 现象:线程1观察到值依然是A,认为它没变过,于是CAS成功。 风险:虽然数值没变,但变量的状态(或对象内部的属性)可能已经发生了变化,导致逻辑错误。
  • Java提供了AtomicStampedReference类,通过引入版本号(Stamp)来解决: 每次变量更新时,不仅更新值,还增加一个版本号。只有值和版本号都一致,CAS才会成功。
  • 还可以设置时间戳来解决。
  1. CAS 在Java中的实现
  • 在Java中,CAS主要由 sun.misc.Unsafe 类提供支持。该类中的方法(如 compareAndSwapInt)是 native 的,直接调用硬件底层的指令。

ReentrantLock(可重入锁)

  • ReentrantLock是 Java java.util.concurrent.locks 包下的可重入锁实现,基于 AQS(抽象队列同步器)构建,是 synchronized 的 “增强版”—— 既保留了 synchronized 的可重入特性,又提供了更灵活的同步控制能力。
两者对比:
特性 synchronized ReentrantLock
实现层面 JVM 层面(关键字),由 C++ 实现 JDK 层面(API),由 Java 编写(基于 AQS)
锁的释放 自动释放(代码执行完或异常后) 手动释放(必须在 finally 中调用 unlock())
灵活性 低(不可中断,无超时机制) 高(支持尝试获取、超时获取、可中断获取)
公平性 只支持非公平锁 支持公平锁与非公平锁(默认非公平)
等待队列 只能关联 1 个 等待队列(wait/notify) 可以绑定 多个 Condition(精细化唤醒)
ReentrantLock的特有高级功能
  1. 响应中断 (lockInterruptibly)
  • synchronized一旦进入阻塞等待,除非拿到锁,否则无法被中断。而ReentrantLock允许线程在等待锁的过程中响应Thread.interrupt(),从而避免死等。
  1. 超时机制 (tryLock)
  • 线程可以尝试获取锁,如果锁被占用,立即返回 false 或者等待一段时间后返回,而不是一直阻塞。这在预防死锁时非常有用。
  1. 公平锁 (Fairness)
  • 公平锁:按照线程请求锁的顺序分配,先到先得。
  • 非公平锁(默认):允许“插队”。如果新来的线程正好碰到锁释放,它可以直接抢占,性能通常比公平锁高。
  1. 多个 Condition 对象
  • 通过lock.newCondition(),你可以创建多个等待集。例如在阻塞队列中,可以定义 notFull 和 notEmpty 两个条件,实现比 notifyAll 更精准的线程唤醒。
基本使用示例:
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    // 创建非公平锁(默认),若需公平锁:new ReentrantLock(true)
    private static final ReentrantLock lock = new ReentrantLock();

    public static void doTask() {
        // 1. 普通获取锁(不可中断)
        lock.lock();
        try {
            // 临界区代码(线程安全)
            System.out.println(Thread.currentThread().getName() + " 执行任务");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            // 必须在finally中释放锁,否则锁永远无法释放
            lock.unlock();
        }
    }

    // 超时获取锁示例
    public static void tryLockWithTimeout() {
        try {
            // 尝试在2秒内获取锁,获取成功返回true,失败返回false
            if (lock.tryLock(2, java.util.concurrent.TimeUnit.SECONDS)) {
                try {
                    System.out.println(Thread.currentThread().getName() + " 超时获取锁成功");
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println(Thread.currentThread().getName() + " 超时获取锁失败");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) {
        // 测试普通获取锁
        new Thread(ReentrantLockDemo::doTask, "线程1").start();
        new Thread(ReentrantLockDemo::doTask, "线程2").start();
        
        // 测试超时获取锁
        new Thread(ReentrantLockDemo::tryLockWithTimeout, "线程3").start();
    }
}
posted @ 2026-01-13 22:24  我会替风去  阅读(4)  评论(0)    收藏  举报