Java锁机制知识点

发布于:2025-06-29 ⋅ 阅读:(19) ⋅ 点赞:(0)

一、锁的基础概念

1.1 什么是锁

在并发编程中,锁是用于控制多个线程对共享资源进行访问的机制。锁可以保证在同一时刻最多只有一个线程访问共享资源,从而保证数据的一致性。

1.2 锁的分类
  • 可重入锁 vs 不可重入锁:可重入锁允许同一个线程多次获得同一把锁,如synchronizedReentrantLock;不可重入锁要求线程在释放锁后才能再次获得锁。
  • 公平锁 vs 非公平锁:公平锁保证线程按照申请锁的顺序依次获得锁,类似于“先来先得”,如ReentrantLock在构造时指定true可实现公平锁;非公平锁不保证线程获取锁的顺序,可能会出现线程插队现象,ReentrantLock默认实现为非公平锁。
  • 独占锁 vs 共享锁:独占锁保证在同一时刻,只有一个线程可以获得锁,如写锁;共享锁允许多个线程同时获得锁,如读写锁中的读锁。
  • 乐观锁 vs 悲观锁:悲观锁假设线程间会发生资源争用,在访问共享资源前先加锁,如synchronizedReentrantLock;乐观锁假设线程间不会发生冲突,不加锁,访问数据时判断是否发生了冲突,若冲突则重试,如CAS(Compare-And-Swap)。
  • 偏向锁、轻量级锁、重量级锁:这三种锁特指synchronized锁的状态,它们通过JVM内部的对象头(Mark Word)来控制锁的状态。偏向锁适用于线程没有竞争的场景;轻量级锁通过CAS操作来避免线程阻塞,适合多线程之间竞争较少的场景;重量级锁通过操作系统的同步机制来实现,线程获取不到锁时会被阻塞,适合竞争激烈且锁持有时间较长的场景。锁的升级路径为:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。

二、常见锁机制详解

2.1 synchronized关键字
  • 基本使用
//修饰实例方法
public class SynchronizedExample {
    public synchronized void instanceMethod() {
        // 方法体
    }
}
//修饰静态方法
public class SynchronizedExample {
    public static synchronized void staticMethod() {
        // 方法体
    }
}
//修饰代码块
public class SynchronizedExample {
    public void blockMethod() {
        synchronized (this) {
            // 同步代码块
        }
    }
}
  • 实现原理synchronized的实现原理主要包括对象头中的Mark Word、monitor entermonitor exit,以及锁的升级过程(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)。
  • 特性与缺点
    • 隐式锁:由JVM自动管理锁的获取与释放,程序员不需要手动操作。
    • 阻塞性:线程在获取不到锁时会被阻塞,直到锁被释放。
    • 不可中断:线程一旦获得锁就会一直持有,直到执行完同步代码块或方法才会释放锁。
    • 性能问题:由于是阻塞锁,在高并发环境下可能会引起较大的性能损失,尤其是在锁竞争激烈时。
  • 适用场景:适用于简单的同步场景,当需要同步的代码块较小,且竞争不激烈时;也适合锁粒度较粗的同步需求。
2.2 ReentrantLock
  • 基本使用
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock(); // 获取锁
        try {
            count++;
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    // 支持超时的获取锁方式
    public boolean tryIncrement() {
        if (lock.tryLock()) { // 尝试获取锁
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false;
    }
}
  • 高级特性
    • 可中断锁lock.lockInterruptibly()方法允许线程在等待获取锁时响应中断。
    • 公平锁:可以创建公平锁,确保按照请求锁的顺序进行,等待时间最长的线程先获得锁。
    • 条件变量:提供Condition接口,用于线程间的协调。
  • 特性与优缺点
    • 优点:支持可中断的锁等待、锁超时、非阻塞获取锁;可以创建公平锁;提供了更多的控制选项。
    • 缺点:需要手动获取和释放锁,代码容易出错。
  • 适用场景:需要可中断锁的场景;需要公平锁的场景;需要尝试锁的场景。
2.3 ReadWriteLock
  • 概述ReadWriteLock提供了读写分离的锁机制,允许多个线程同时读取资源,但在写入时只能有一个线程获取写锁。其实现类通常是ReentrantReadWriteLock
  • 工作原理
    • 读锁:多个读线程可以同时获取,读锁不会阻塞其他读线程。
    • 写锁:写锁是独占的,写锁获取时会阻塞所有读线程和写线程。
  • 特性与优缺点
    • 优点:在读多写少的场景下,能够极大地提高并发性能。
    • 缺点:如果写操作较为频繁,性能提升不明显;可能会出现写饥饿问题。
  • 适用场景:适用于读多写少的场景,如缓存实现。
2.4 StampedLock
  • 核心特性
    • 乐观读:通过tryOptimisticRead()实现无锁读取,校验数据版本(邮戳)。若校验失败(期间有写操作),再升级为悲观读锁。
    • 三种模式:写锁、悲观读锁、乐观读。
  • 代码示例
import java.util.concurrent.locks.StampedLock;

public class StampedLockExample {
    private final StampedLock stampedLock = new StampedLock();

    // 乐观读
    public void optimisticRead() {
        long stamp = stampedLock.tryOptimisticRead();
        // 读取数据
        if (!stampedLock.validate(stamp)) {
            stamp = stampedLock.readLock();
            try {
                // 重新读取数据
            } finally {
                stampedLock.unlockRead(stamp);
            }
        }
    }

    // 写操作
    public void write() {
        long writeStamp = stampedLock.writeLock();
        try {
            // 修改数据
        } finally {
            stampedLock.unlockWrite(writeStamp);
        }
    }
}
  • 缺点:不可重入,同一线程重复获取锁会导致死锁;API复杂,需手动处理锁升级和邮戳验证。
  • 适用场景:需要极高读并发且写冲突少的场景,如实时数据分析。

三、锁的优化技术

3.1 锁粗化

将多个紧邻的小范围加锁操作合并为一次较大的加锁操作,从而减少锁的频繁获取和释放,降低锁的开销。例如:

// 优化前
for (int i = 0; i < 100; i++) {
    synchronized (lock) {
        // 执行一些操作
    }
}

// 优化后
synchronized (lock) {
    for (int i = 0; i < 100; i++) {
        // 执行一些操作
    }
}
3.2 锁消除

JVM在JIT编译时,通过逃逸分析判断加锁的对象是否只在当前线程内使用,如果确定不会发生线程竞争,JVM会自动将这些锁消除,从而避免不必要的锁操作。例如:

public String concatenate(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

在上述代码中,StringBuffer对象只在方法内部使用,不会被其他线程访问,因此JVM可以消除锁操作。

3.3 偏向锁

偏向于第一个获取它的线程,如果该线程再次进入同步块,锁不会进行竞争,直接获取锁。这种优化适用于锁竞争较少的场景,可以减少加锁的开销。

3.4 轻量级锁

核心思想是避免线程阻塞,它通过CAS操作来获取锁,从而避免了线程的上下文切换。如果线程竞争激烈,轻量级锁会升级为重量级锁。

3.5 自旋锁与自适应自旋

自旋锁是指当线程尝试获取锁而失败时,不会立即进入阻塞状态,而是进行短暂的忙等待(自旋),等待锁的释放后再尝试获取。Java中使用了自适应自旋技术,会根据前一次自旋的结果动态调整自旋次数。

四、死锁问题

4.1 死锁产生的条件
  • 互斥使用:资源一次只能被一个线程独占使用。
  • 不可抢占:资源请求者不能强制从资源占有者手中抢夺资源,资源只能由占有者主动释放。
  • 请求和保持:当资源请求者在请求其他资源的同时保持对原因资源的占有。
  • 循环等待:多个线程存在环路的锁依赖关系而永远等待下去。
4.2 死锁的预防方法
  • 破坏“循环等待”条件 - 锁顺序化:强制所有线程以全局一致的固定顺序获取锁。
  • 避免持有并等待:线程在获取所有需要的资源之前,不占用任何资源。
  • 允许抢占:允许线程在必要时抢占其他线程的资源。

网站公告

今日签到

点亮在社区的每一天
去签到