概述

  1. 按实现层面划分
    • [内置锁]:synchronized是JVM层面实现的,无需手动释放锁,属于内置锁。
    • [显式锁]:ReentrantLock为代表的显式锁,需要手动释放锁,功能更加灵活,位于java.util.concurrent.locks包,Java代码层面实现。
  2. 按锁的竞争机制划分
    • [悲观锁]:synchronizedReentrantLock都为悲观锁,每次操作资源都会加锁,阻塞其他线程。
    • [乐观锁]:不加锁直接操作,通过CAS验证操作是否成功,失败则重试,例如AtomicInteger
  3. 按锁的可重入性划分
    • [可重入锁]:对于已获取到锁的线程可再次获取同一把锁(避免自身死锁),synchronizedReentrantLock均为可重入锁。
    • [不可重入锁]:持有锁的线程无法再次获取这把锁,容易引起自身死锁。
  4. 锁的升级
    • 偏向锁 -> 轻量级锁 -> 重量级锁(JDK1.6优化后,synchronized会根据竞争激烈程度自动升级,逐步提升并发性能)

具体

一、synchronized

1. 用法

public class SynchronizedDemo { 
	// 1. 修饰实例方法:锁的是「当前类的实例对象」(this) 
	public synchronized void instanceMethod() { 
		// 线程安全的实例方法逻辑 
		System.out.println("修饰实例方法,锁:" + this); 
	}
	
	// 2. 修饰静态方法:锁的是「当前类的 Class 对象」(SynchronizedDemo.class)
	public static synchronized void staticMethod() { 
		// 线程安全的静态方法逻辑 
		System.out.println("修饰静态方法,锁:" + SynchronizedDemo.class); 
	}
	
	// 3. 修饰同步代码块:锁的是「括号内指定的对象」(灵活可控) 
	public void codeBlockMethod() { 
		// 可选锁对象:this(实例对象)、Class 对象、自定义任意对象 
		Object lock = new Object(); 
		synchronized (lock) { 
		// 线程安全的代码块逻辑 
		System.out.println("修饰代码块,锁:" + lock); 
		}
	}
}

synchronized 竞争的是一个资源对象,无论是修饰的实例方法、静态方法还是代码块,只是对象的类型不同而已。(可以是方法当前类的实例对象、class对象、自定义的任意对象)

2. 实现原理

依赖于「Java对象头」和「监视器锁」,不同JDK版本略有差异

  • JDK1.6之前:仅支持「重量级锁」,依赖操作系统的「互斥量(Mutex)」实现,线程竞争时会触发「用户态 <-> 内核态」的上下文切换,开销极大,性能较差。
  • JDK1.6之后:引入了「锁升级」机制(偏向锁 -> 轻量级锁 -> 重量级锁),根据竞争激烈程度逐步升级,大幅优化性能:
    1. 偏向锁:无多线程竞争时,锁偏向第一个获取锁的线程,后续该线程再次获取锁时,无需任何竞争操作,直接获取锁(开销极低)。
    2. 轻量级锁:出现少量线程竞争时,偏向锁升级为轻量级锁,通过「CAS 操作」实现锁的获取与释放,无需阻塞线程,仅存在少量自旋开销。
    3. 重量级锁:出现大量线程竞争时,轻量级锁升级为重量级锁,依赖操作系统互斥量实现,竞争失败的线程会被阻塞挂起,避免空耗 CPU,此时开销最大,但能保证高并发场景下的稳定性。
    4. ⭐锁升级是单向的,无法降级。

3. 核心特性

  • 可重入性:线程持有锁后,可再次获取同一把锁(如同步方法中调用另一同步方法),避免自身死锁。
  • 隐式释放锁:无需手动调用释放方法,同步代码块 / 方法执行完毕、抛出异常时,JVM 会自动释放锁,降低资源泄露风险。
  • 非公平锁:默认采用非公平锁策略(线程获取锁时,不遵守先到先得的顺序,可能存在插队现象),吞吐量更高(公平锁会带来额外的排队开销)。

二、ReentrantLock

1. 用法

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    // 1. 创建 ReentrantLock 实例(默认非公平锁,传入 true 为公平锁)
    private static final ReentrantLock lock = new ReentrantLock(false);

    public void doTask() {
        // 2. 获取锁(lock() 方法,阻塞式获取)
        lock.lock();
        try {
            // 3. 执行线程安全的业务逻辑
            System.out.println(Thread.currentThread().getName() + " 已获取锁,执行任务");
        } finally {
            // 4. 释放锁(必须在 finally 中,保证无论是否异常,都能释放锁)
            lock.unlock();
            System.out.println(Thread.currentThread().getName() + " 已释放锁");
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo demo = new ReentrantLockDemo();
        // 启动 2 个线程竞争锁
        new Thread(demo::doTask, "线程1").start();
        new Thread(demo::doTask, "线程2").start();
    }
}

finally释放锁是必须的,防止try抛出异常,锁无法释放,会导致其他线程永久阻塞,引发死锁。

2. 核心功能扩展

ReentrantLock提供了synchronized不具备的高级功能:

  • 支持公平锁/非公平锁:创建实例的时候可以设置采用公平锁还是非公平锁,默认非公平锁(false)
  • 支持获取锁超时:通过trylock(long timeout, TimeUnit unit)方法,设置锁获取超时时间,超时后自动放弃获取锁,返回false,避免长时间阻塞。
  • 支持可中断获取锁:通过lockInterruptibly()方法,获取锁的线程可被其他线程中断(调用thread.interrupt()),中断后抛出 InterruptedException,并释放锁,灵活控制线程状态。
  • 提供锁状态查询:如isLocked()(判断锁是否被持有)、isHeldByCurrentThread()(判断当前线程是否持有该锁)、getHoldCount()(获取当前线程持有该锁的次数),方便监控和调试。

3. 原理

ReentrantLock 的底层基于 AQS(AbstractQueuedSynchronizer,抽象队列同步器) 实现,AQS 是 JUC 包中所有显式锁、同步工具类的核心框架。 核心逻辑:

  1. AQS 内部维护一个「volatile 状态变量 state」和一个「双向链表同步队列」。
  2. state 用于标记锁的状态:state=0 表示锁未被持有,state>0 表示锁被持有(state 的值等于线程持有锁的次数,体现可重入性)。
  3. 线程获取锁时,通过 CAS 操作修改 state:若 state=0,则将 state 改为 1,获取成功;若 state>0 且是当前线程持有,则 state 加 1(可重入),否则加入同步队列阻塞等待。
  4. 线程释放锁时,将 state 减 1,当 state=0 时,释放锁并唤醒同步队列中的下一个线程。

4. 核心特性

  1. 可重入性:和 synchronized 一样,支持线程重复获取同一把锁,通过 state 变量计数实现。
  2. 显式释放锁:必须手动调用 unlock() 释放,依赖开发者规范,灵活性高但风险也高(容易遗漏释放)。
  3. 功能灵活:支持公平锁、超时获取、可中断等高级功能,适配复杂并发场景。
  4. 性能优异:在高并发场景下,性能略优于 synchronized(JDK 1.6 后 synchronized 优化,两者性能差距不大)。

三、synchronized锁升级

锁的载体 -- Java对象头synchronized 锁的是「对象」,而锁状态的信息,就存储在 Java 对象头(Object Header) 中(仅针对普通对象,数组对象的对象头额外包含数组长度)。 Java 对象头在 32 位 JVM 和 64 位 JVM 中长度分别为 8 字节和 16 字节,核心包含两部分(以 64 位 JVM 为例):

  1. Mark Word(标记字段,8字节):存储对象的锁状态、哈希码、GC年龄、偏向线程ID等核心信息,是锁升级的「核心载体」。
  2. Klass Pointer(类型指针,8字节):指向对象所属类的Class对象,用于确定对象的类型。
锁状态Mark Word 核心字段(64 位)
无锁哈希码(25 位)+ GC 年龄(4 位)+ 无锁标记(1 位)
偏向锁偏向线程 ID(54 位)+ 偏向时间戳(2 位)+ GC 年龄(4 位)+ 偏向锁标记(1 位)
轻量级锁指向栈中锁记录的指针(63 位)+ 轻量级锁标记(1 位)
重量级锁指向监视器锁(Monitor)的指针(63 位)+ 重量级锁标记(1 位)
第一步:无锁 -> 偏向锁(无竞争场景)
  1. 第一个线程获取锁时,JVM 通过 CAS 操作,将 Mark Word 中的「偏向线程 ID」设置为当前线程的 ID,同时将「锁标记位」改为「偏向锁标记」。
  2. 该线程后续再次获取该对象的锁时,无需再执行 CAS 操作或阻塞,只需简单检查 Mark Word 中的:
    • 偏向线程 ID 是否为当前线程 ID;
    • 偏向锁标记是否有效。
  3. 若两者都满足,直接获取锁成功(「偏向」的含义就是锁会「偏爱」这个线程),全程无额外开销,效率极高。
第二步:偏向锁 → 轻量级锁(少量线程竞争,无阻塞)
  1. 加锁流程
  • 线程获取锁时,先在自己的栈帧中创建一个「锁记录(Lock Record)」,用于存储对象 Mark Word 的副本(称为「Displaced Mark Word」)。
  • 线程通过 CAS 操作,将对象 Mark Word 中的内容替换为「指向当前线程栈中锁记录的指针」。
  • 若 CAS 成功,说明锁获取成功,将 Mark Word 的锁标记位改为「轻量级锁标记」,线程继续执行同步代码。
  • 若 CAS 失败,说明有其他线程正在竞争该锁,当前线程不会被阻塞,而是进入「自旋」(反复执行 CAS 操作,尝试获取锁)。
  1. 解锁流程
  • 线程执行完同步代码后,通过 CAS 操作,将对象 Mark Word 中的「锁记录指针」替换回原来的「Displaced Mark Word」(栈中锁记录存储的副本)。
  • 若 CAS 成功,说明无其他线程竞争,解锁成功,锁状态回到无锁。
  • 若 CAS 失败,说明有其他线程在自旋竞争,此时会将轻量级锁升级为重量级锁,同时唤醒自旋的线程。
第三步:轻量级锁 → 重量级锁(大量线程竞争,阻塞式竞争)
  1. 加锁流程
  • 锁升级为重量级锁后,对象 Mark Word 中会存储「指向 Monitor 的指针」,Monitor 是一个用于管理锁竞争的核心数据结构。
  • 线程获取锁时,会尝试获取 Monitor 的「持有权」(Monitor 中的 owner 字段记录持有锁的线程)。
  • 若获取成功,owner 字段设置为当前线程,线程执行同步代码。
  • 若获取失败,当前线程会被加入 Monitor 的「阻塞队列」,被操作系统挂起(从用户态切换到内核态),放弃 CPU 执行权,直到被唤醒。
  1. 解锁流程
  • 线程执行完同步代码后,释放 Monitor 的持有权(将 owner 字段置为 null)。
  • 唤醒阻塞队列中的一个线程(通过操作系统的唤醒机制),让其尝试获取锁。
  • 锁状态保持为重量级锁,不会降级(单向升级)。
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com