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 中的机制及类型 #### 显式 (Explicit Lock) 显式通过 `java.util.concurrent.locks.Lock` 接口及其具体实现类提供了一种更灵活的方式来管理线程间的同步[^1]。常见的实现类有 `ReentrantLock` 和 `ReadWriteLock`。与内置不同的是,显式允许开发人员更加精细地控制的行为,例如支持可中断等待、超时获取以及公平等功能。 以下是显式的一个简单示例: ```java import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ExplicitLockExample { private final Lock lock = new ReentrantLock(); public void performTask() { lock.lock(); // 获取 try { System.out.println(Thread.currentThread().getName() + " is performing the task."); } finally { lock.unlock(); // 释放 } } public static void main(String[] args) throws InterruptedException { ExplicitLockExample example = new ExplicitLockExample(); Thread t1 = new Thread(example::performTask, "Thread-1"); Thread t2 = new Thread(example::performTask, "Thread-2"); t1.start(); t2.start(); t1.join(); t2.join(); } } ``` #### 内置 (Synchronized Lock) 内置是一种隐式的定机制,在 Java 的语法层面由关键字 `synchronized` 提供支持[^2]。当一个方法或者代码块被声明为 `synchronized` 时,意味着同一时刻只允许一个线程执行该方法或代码块。这种机制适用于简单的场景,但对于复杂的多线程环境可能显得不够灵活。 下面是一个使用内置的例子: ```java public class SynchronizedLockExample { private int counter = 0; public synchronized void incrementCounter() { // 使用 synchronized 关键字 counter++; System.out.println(Thread.currentThread().getName() + ": Counter value -> " + counter); } public static void main(String[] args) throws InterruptedException { SynchronizedLockExample example = new SynchronizedLockExample(); Thread t1 = new Thread(() -> example.incrementCounter(), "Thread-1"); Thread t2 = new Thread(() -> example.incrementCounter(), "Thread-2"); t1.start(); t2.start(); t1.join(); t2.join(); } } ``` #### 常见类型 除了上述提到的显式和内置外,还有一些特定用途的: 1. **重入 (ReentrantLock)** 这是最常用的显式之一,允许多次进入同一个而不会死。它还支持公平性和非公平性的选项[^3]。 2. **读写 (ReadWriteLock)** 此类型的分为读和写两部分。多个线程可以同时持有读,但如果有一个线程持有了写,则不允许其他任何线程再获得读或写。这非常适合于那些读操作远大于写操作的应用程序。 3. **自旋 (SpinLock)** 自旋并不真正挂起线程而是让其忙等待直到可以获得为止。这种方式通常用于非常短时间内的竞争情况以减少上下文切换开销。 4. **信号量 (Semaphore)** 虽然严格意义上来说不算作传统意义上的“”,但它确实能用来协调对有限数量资源的竞争访问问题。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值