Java中的锁

Java中的锁

Lock接口

在Java5之前通过Synchronized关键字实现锁功能,Synchronized关键字隐式获取释放锁,不需要我们手动管理。但是对于一些需要灵活控制锁的获取与释放就需要使用Lock接口。

Lock接口核心API

方法签名描述
void lock()获取锁。若锁被其他线程持有,则当前线程阻塞直至锁释放。
void lockInterruptibly() throws InterruptedException获取锁(可中断)。等待锁的过程中可响应中断(抛出 InterruptedException)。
boolean tryLock()尝试获取锁(非阻塞)。立即返回:成功返回 true,失败返回 false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException尝试获取锁(带超时)。若在指定时间内未获取到锁,则返回 false,等待期间可被中断。
void unlock()释放锁。必须在 finally 块中调用,确保锁被释放。
Condition newCondition()返回一个与当前锁绑定的 Condition 对象,用于线程间的等待/通知机制(替代 wait()/notify())。

使用

在finally块中释放锁是为了保证锁最终能被释放。

Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
    lock.unlock();
}
特性SynchronizedLock 接口(如 ReentrantLock
语法Java 内置关键字,通过 monitor 隐式控制锁Java 5+ 的接口,需显式调用 lock()unlock()
锁管理自动获取/释放锁(JVM 管理)手动获取/释放锁(需在 finally 中释放)
锁类型非公平锁支持公平锁/非公平锁(构造函数指定)
可中断性不可中断可通过 lockInterruptibly() 中断等待
尝试锁不支持支持 tryLock()tryLock(timeout)
读写锁不支持支持(ReentrantReadWriteLock
异常处理异常时自动释放锁需手动释放锁,否则可能死锁
性能低竞争场景优化后效率高高竞争场景通过非阻塞特性可能更优
适用场景简单同步(方法/代码块)复杂同步(中断、超时、读写分离)

队列同步器(AQS)

AQS相当于构建锁的基础框架,主要有一个成员变量表示同步状态,FIFO队列。主要使用是子类继承同步器实现其抽象方法,用来构建各种同步器。同步器的设计是采用模版方法设计的。

同步器提供的模版方法

在这里插入图片描述

可重写的方法

方法签名描述
protected boolean tryAcquire(int arg)独占模式:尝试获取同步状态。若返回true,表示获取成功;否则失败。参数arg为获取状态的参数(如锁的重入次数)。实现示例:CAS操作将state从0改为1,表示获取锁。
protected boolean tryRelease(int arg)独占模式:尝试释放同步状态。若返回true,表示释放后其他线程可获取锁。参数arg为释放状态的参数。实现示例:将state减1,减至0时释放锁。
protected int tryAcquireShared(int arg)共享模式:尝试共享获取同步状态。返回值:负值表示失败;0表示成功但无剩余资源;正值表示成功且有剩余资源。实现示例Semaphore中获取许可时,返回剩余许可数。
protected boolean tryReleaseShared(int arg)共享模式:尝试共享释放同步状态。若返回true,表示释放后可唤醒后续等待线程。实现示例CountDownLatch中计数器减至0时唤醒所有等待线程。
protected boolean isHeldExclusively()判断当前同步器是否由当前线程独占。实现示例ReentrantLock中判断state是否大于0且当前线程是锁的持有者。

队列同步器的实现

同步器是如何完成线程同步的

  1. 同步队列
    当线程获取同步状态失败时,会被组装成节点通过(CAS方法)加入到同步队列当中,同步队列遵循FIFO。
    节点的属性与方法
    在这里插入图片描述

    同步队列
    在这里插入图片描述

  2. 独占式获取同步状态获取流程
    在这里插入图片描述

  3. 共享式同步状态获取与释放
    在这里插入图片描述

  4. 独占式超时获取同步状态
    在这里插入图片描述

重入锁

重入锁(ReentrantLock),可重入锁表示该锁能够支持一个线程对资源的重复加锁。

  1. 实现重入在内部有一个计数器,当线程重复获取锁时计数器加1,最后在释放锁时需要将计数器依次清0,即锁被获取n次,那么前(n-1)次tryRelease(int release)方法必须返回false。

  2. 公平与非公平锁非公平:当前线程锁释放之后随机唤醒一个等待线程,可能会造成线程饥饿。公平:当前线程锁释放之后唤醒处于同步队列队首节点,会有大量的线程切换,吞吐量低,

读写锁

ReentrantLock在同一时间只允许一个线程访问,而读写锁允许多个线程访问。在Java并发包中提供读写锁的实现是ReentrantReadWriteLock。

在这里插入图片描述

示例

import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;

public class ReadWriteLockExample {
    // 共享资源
    private int sharedData = 0;
    private int version = 0;
    
    // 创建读写锁
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final ReadLock readLock = rwLock.readLock();
    private final WriteLock writeLock = rwLock.writeLock();
    
    // 读取共享资源
    public int readData() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 获取读锁");
            // 模拟读取操作
            System.out.println(Thread.currentThread().getName() + " 读取数据: " + sharedData + " (版本: " + version + ")");
            return sharedData;
        } finally {
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + " 释放读锁");
        }
    }
    
    // 写入共享资源
    public void writeData(int newValue) {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 获取写锁");
            // 模拟写入操作
            sharedData = newValue;
            version++;
            System.out.println(Thread.currentThread().getName() + " 写入数据: " + sharedData + " (新版本: " + version + ")");
        } finally {
            writeLock.unlock();
            System.out.println(Thread.currentThread().getName() + " 释放写锁");
        }
    }
    
    public static void main(String[] args) {
        ReadWriteLockExample example = new ReadWriteLockExample();
        
        // 创建多个读线程
        Thread[] readers = new Thread[3];
        for (int i = 0; i < readers.length; i++) {
            readers[i] = new Thread(() -> {
                for (int j = 0; j < 5; j++) {
                    example.readData();
                    try {
                        Thread.sleep((long) (Math.random() * 500));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "Reader-" + i);
        }
        
        // 创建多个写线程
        Thread[] writers = new Thread[2];
        for (int i = 0; i < writers.length; i++) {
            final int writerId = i;
            writers[i] = new Thread(() -> {
                for (int j = 0; j < 3; j++) {
                    example.writeData(writerId * 10 + j);
                    try {
                        Thread.sleep((long) (Math.random() * 1000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "Writer-" + i);
        }
        
        // 启动所有线程
        for (Thread reader : readers) {
            reader.start();
        }
        for (Thread writer : writers) {
            writer.start();
        }
        
        // 等待所有线程完成
        try {
            for (Thread reader : readers) {
                reader.join();
            }
            for (Thread writer : writers) {
                writer.join();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("所有线程执行完毕");
    }
}

实现分析

  1. 读写设计
    读写锁同样依赖自定义同步器来实现同步功能,读写状态对应同步器的同步状态。而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。
    在这里插入图片描述

  2. 写锁获取与释放
    写锁是一个支持重入的排它锁。在获取写锁之前需要判断读锁是否已被获取,保证写操作对读操作可见。写锁的释放与ReentrantLock类似。

  3. 读锁的获取与释放读锁是一个支持重入的共享锁,能够被多个线程同时获取。当写锁被其他线程获取时,获取读锁将进入等待

  4. 锁降级锁降级是指写锁降级成读锁,当线程持有写锁,执行完写操作之后先获取读锁在释放写锁。
    锁降级的优势

    1. 数据一致性:确保在写操作完成后,线程可以继续读取自己修改的数据,不会被其他线程的写操作干扰

    2. 减少锁竞争:避免在写操作后释放锁,然后立即重新获取读锁的开销

    3. 提高并发性能:在保持数据一致性的同时,允许其他线程获取读锁

Condition接口

condition通过调用lock的newCondition方法创建,依赖于lock对象。

示例

在这里插入图片描述

Condition中的方法

在这里插入图片描述
在这里插入图片描述

有界队列

在这里插入图片描述

Condition的实现分析

ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。

  1. 等待队列当调用Condition.await()会将当前线程构造成节点加入到等待队列尾部。
    在这里插入图片描述

  2. 同步队列与等待队列
    并发包中的Lock拥有一个同步队列和多个等待队列,由于Condition实现属于Lock内部类(Condition condition = lock.newCondition()),因此每个实例都拥有其引用。
    在这里插入图片描述

  3. 等待
    当前线程加入Condition的等待队列过程
    在这里插入图片描述

  4. 通知
    在这里插入图片描述

### Java 中的机制概述 Java 提供了多种机制以支持多线程环境下的同步操作。这些机制主要包括内置(synchronized 关键字)、显示(ReentrantLock 类)以及一些高级特性如读写(ReadWriteLock)。以下是关于它们的具体介绍: --- ### 内置(Intrinsic Lock) Java 的 synchronized 关键字提供了一种简单的方式来进行线程间的同步。每个对象都有一个与之关联的监视器(monitor),当一个线程进入由 synchronized 修饰的方法或代码块时,它会自动获得该对象的监视器。 ```java public class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } } ``` 上述代码展示了如何使用 synchronized 方法来确保多个线程不会同时修改 `count` 变量[^1]。 --- ### 显示(Explicit Lock) 相比于内置,显示提供了更大的灵活性。`ReentrantLock` 是最常用的显示实现之一,它可以手动控制的获取和释放,并且支持可中断等待、超时尝试等功能。 #### 使用示例 ```java import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Worker { private final Lock lock = new ReentrantLock(); private int value = 0; public void updateValue(int newValue) throws InterruptedException { boolean isLocked = false; try { isLocked = lock.tryLock(1000, java.util.concurrent.TimeUnit.MILLISECONDS); if (isLocked) { value = newValue; } else { System.out.println("Failed to acquire the lock."); } } finally { if (isLocked) { lock.unlock(); } } } public int getValue() { lock.lock(); try { return value; } finally { lock.unlock(); } } } ``` 此代码片段演示了如何利用 `tryLock()` 方法在一定时间内尝试获取,如果无法获取,则可以选择其他逻辑路径[^1]。 --- ### 偏向(Biased Locking) 偏向是一种优化技术,旨在减少无竞争情况下的操作开销。它的基本思想是在第一次访问某个时将其绑定到当前线程,后续再次访问时不需额外验证即可直接执行。 然而,一旦检测到有其他线程尝试获取同一个,JVM 将撤销偏向并升级为轻量级甚至重量级。这一过程发生在全局安全点处,因为需要暂停所有活动线程以重新评估的状态[^3]。 --- ### 轻量级(Lightweight Locking) 轻量级通过自旋的方式来避免线程切换带来的性能损失。具体来说,当第一个线程请求时,它会在栈帧中保存指向记录的信息;第二个线程到达时发现已被占用,则启动循环不断检查是否可用。只有当自旋次数达到上限仍未能成功时,才会退化成重量级[^4]。 --- ### 解决 ABA 问题 在高并发场景下可能出现一种称为 ABA 的现象:某一变量先后经历了两次相同值的变化,但其间可能发生了不可忽略的动作。为此引入了带版本号的原子引用类 `AtomicStampedReference` 来加以防范[^5]。 ```java import java.util.concurrent.atomic.AtomicStampedReference; public class ABADemo { static AtomicStampedReference<Integer> atomicRef = new AtomicStampedReference<>(100, 0); public static void main(String[] args) { new Thread(() -> { int stamp = atomicRef.getStamp(); try {Thread.sleep(1);} catch (Exception ignored){} atomicRef.compareAndSet(100, 101, stamp, stamp + 1); atomicRef.compareAndSet(101, 100, stamp + 1, stamp + 2); }).start(); new Thread(() -> { int stamp = atomicRef.getStamp(); try {Thread.sleep(2);} catch (Exception ignored){} boolean result = atomicRef.compareAndSet(100, 101, stamp, stamp + 1); System.out.println(result ? "Success" : "Failure"); }).start(); } } ``` 以上例子表明即使目标数值看似未变,但由于附加了时间戳标记,依然能有效区分实际发生的改动序列[^5]。 --- ### 高效实践建议 - 对于短期定需求优先考虑内置; - 如果涉及复杂业务流程推荐选用显式以便更好地掌控资源分配状况; - 注意合理配置粒度大小以免造成不必要的争用瓶颈。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值