锁的基本介绍

并发编程的一个最基本问题就是原子性地执行一系列指令。锁有助于直接解决这一问题。

锁的基本思想

锁就是一个变量。这个变量保存了锁在某一时刻的状态。它要么是可用的,表示没有线程持有锁,要么是被占用的,表示有线程持有锁,正处于临界区(访问共享资源的那部分代码)。

Pthread锁

POSIX库将锁称为互斥量(mutex),因为它被用来提供线程之间的互斥。当一个线程在临界区,它能阻止其它线程进入到本地线程直到本线程离开临界区。

设计锁

一个锁应当能保证不会有第二个线程进入临界区。另外,还应当保证公平性,所有的线程都有机会公平抢到锁。在设计锁时还应该考虑到抢锁和释放锁的开销。

为了实现上面两点,我们需要使用硬件和操作系统的支持。

控制中断

在临界区关闭中断。这个方案是为单处理系统开发的。

原子交换(测试并设置)

控制中断无在多处理器上工作,系统设计者开始让硬件支持锁。最简单的硬件支持就是测试并设置指令(test-and-set instruction),也叫做原子交换。

typedef struct lock_t { int flag; } lock_t;

void init(lock_t* mutex)
{
    mutex->flag = 0;
}

void lock(lock_t* mutex)
{
    while (mutex->flag == 1) //测试
        ; //锁被其它线程占用,该线程陷入自旋

    mutex->flag = 1; //设置
}

void unlock(lock_t* mutex)
{
    mutex->flag = 0;
}

这种设置也是有问题的,首先它不能完成让线程互斥的基本任务。考虑这种情况,当线程1在测试锁时(只是通过了测试并未设置),发生了中断切换的线程2,随后线程2执行了完整的测试和设置,此时再次发生中断切换到线程1。由于先前线程1已经通过了测试语句,接下来会执行设置代码。这样一来,就有两个线程同时持有锁了。

在性能上,如果一个线程在等待已经被持有的锁时,会一直自旋,不停地检查锁。这会浪费许多CPU时间。

实现这种锁,需要一条硬件指令支持。在x86上是xchg指令(原子交换)。下面的代码片段说明了该指令的工作方式:

int TestAndSet(int* old_ptr, int new)
{
    int old = *old_ptr;
    *old_ptr = new;
    return old;
}

这些代码都是原子地执行。利用这类原子交换指令可以实现自旋锁。

typedef struct lock_t
{
    int flag;
} lock_t;

void init(lock_t* lock)
{
    lock->flag = 0;
}

void lock(lock_t* lock)
{
    while (TestAndSet(lock, 1) == 1)
        ;//自旋
}

void unlock(lock_t* lock)
{
    lock->flag = 0;
}

比较并交换

另一个硬件支持是比较并交换指令。

int CompareAndSwap(int *ptr, int expected, int new)
{
    int actual = *ptr;
    if (actual == expected)
        *ptr = new;

    return actual;
}

链接的加载和条件式存储指令

int LoadLinked(int* ptr)
{
    return *ptr;
}

int StoreConditional(int* ptr, int value)
{
    if (no one has updated *ptr since the LoadLinked to this address)
    {
        *ptr = value;
        return 1;
    }
    else
    {
        return 0;
    }
}

获取并增加

int FetchAndAdd(int *ptr)
{
    int old = *ptr;
    *ptr = old + 1;
    return old;
}

typedef struct lock_t
{
    int ticket;
    int turn;
} lock_t;

void lock_init(lock_t *lock)
{
    lock->ticket = 0;
    lock->turn = 0;
}

void lock(lock_t* lock)
{
    int myturn = FetchAndAdd(&lock->ticket);
    while (lock->turn != myturn)
        ; //spin
}

void unlock(lock_t* lock)
{
    FetchAndAdd(&lock->turn);
}

解决自旋时的浪费

在自旋的时候,线程应该放弃CPU,从运行状态变为就绪状态。而且我们应该控制释放锁时谁可以抢到锁,这是为了避免性能上的浪费以及饿死的问题。

为此,我们需要操作系统维护一个队列保存正在等待锁的线程队列。这样我们就可以在锁被占用时休眠线程,在锁可用时从队头中唤醒一个线程。

<think>(Lock)在Java中是一种用于控制多个线程对共享资源进行访问的同步机制。在多线程环境下,可以防止多个线程同时访问共享资源,从而避免数据不一致和竞态条件。Java提供了多种机制,包括内置(synchronized关键字)和显式(java.util.concurrent.locks包中的)。 ### 1. 内置(synchronized) 内置是Java中最基本机制,通过`synchronized`关键字实现。它可以用于同步方法或同步代码块。 #### 同步方法 ```java public synchronized void synchronizedMethod() { // 同步方法体,是当前对象实例(对于实例方法)或类对象(对于静态方法) } ``` #### 同步代码块 ```java public void method() { // 非同步代码 synchronized (lockObject) { // 同步代码块,是lockObject对象 } } ``` **特点**: - 内置是可重入的:同一个线程可以多次获取同一把。 - 自动释放:当同步块执行完毕或发生异常时,会自动释放。 - 不支持中断:线程在等待的过程中无法被中断。 - 不支持超时:无法设置获取的超时时间。 - 非公平:默认情况下,内置是非公平的,不保证等待线程的获取顺序。 ### 2. 显式(Lock接口) Java 5引入了`java.util.concurrent.locks`包,提供了更灵活的机制,主要实现是`ReentrantLock`。 #### 使用示例 ```java Lock lock = new ReentrantLock(); public void method() { lock.lock(); // 获取 try { // 同步代码块 } finally { lock.unlock(); // 必须在finally块中释放 } } ``` **显式的优势**: 1. **可中断**:`lockInterruptibly()`方法允许在等待的过程中响应中断。 2. **超时机制**:`tryLock(long time, TimeUnit unit)`可以设置超时时间。 3. **公平**:`ReentrantLock`可以设置为公平(按等待时间顺序获取)。 4. **条件变量**:通过`Condition`对象,可以实现更精细的线程等待和唤醒。 #### 条件变量(Condition) ```java Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); public void await() throws InterruptedException { lock.lock(); try { condition.await(); // 释放并等待 } finally { lock.unlock(); } } public void signal() { lock.lock(); try { condition.signal(); // 唤醒一个等待线程 } finally { lock.unlock(); } } ``` ### 3. 读写(ReadWriteLock) `ReadWriteLock`接口定义了一对:读(共享)和写(排他)。适用于读多写少的场景。 #### 使用示例 ```java ReadWriteLock rwLock = new ReentrantReadWriteLock(); Lock readLock = rwLock.readLock(); Lock writeLock = rwLock.writeLock(); // 读操作 public void read() { readLock.lock(); try { // 读操作 } finally { readLock.unlock(); } } // 写操作 public void write() { writeLock.lock(); try { // 写操作 } finally { writeLock.unlock(); } } ``` **特点**: - 读是共享的:多个线程可以同时持有读。 - 写是排他的:同一时间只有一个线程可以持有写,且写获取时不能有读或写存在。 ### 4. 的最佳实践 - 总是使用`try-finally`确保被释放。 - 尽量减小同步代码块的范围,提高并发性能。 - 避免嵌套,防止死。 - 考虑使用`java.util.concurrent`包中的高级并发工具(如`Semaphore`, `CountDownLatch`等)。 ### 死及避免 死是指两个或更多线程互相持有对方所需的而无限等待的状态。死发生的四个必要条件: 1. 互斥条件 2. 请求与保持条件 3. 不可剥夺条件 4. 循环等待条件 **避免死的方法**: - 按顺序获取(所有线程以相同的顺序获取)。 - 使用超时机制(如`tryLock`)。 - 避免嵌套。 ### 总结 是Java多线程编程中同步访问共享资源的重要工具。内置(synchronized)简单易用,而显式(Lock)提供了更灵活和高级的功能。根据具体场景选择合适的机制,并遵循最佳实践以避免死和性能问题。 §§相关问题§§: 1. synchronized和ReentrantLock有什么区别? 2. 什么是可重入? 3. 如何检测和避免死? 4. 读写适用于什么场景?</think>### Java机制详解 是Java并发编程中控制多线程访问共享资源的核心同步机制,主要解决**线程安全**和**可见性**问题。Java提供了多种实现,可分为两大类: #### 一、内置(隐式) 通过`synchronized`关键字实现,JVM自动管理的获取和释放。 **1. 同步方法** ```java public synchronized void syncMethod() { // 对象为当前实例(this) } ``` **2. 同步代码块** ```java public void method() { synchronized (lockObject) { // 显式指定对象 // 临界区代码 } } ``` **3. 静态同步** ```java public static synchronized void staticSync() { // 对象为Class对象(MyClass.class) } ``` **特性**: - 可重入性:线程可重复获取已持有的 - 互斥性:同一时刻仅一个线程可持有 - 非公平性:不保证等待线程的获取顺序 - 自动释放:代码块结束或异常时自动释放 #### 二、显式(`java.util.concurrent.locks`包) 提供更灵活的控制,核心接口`Lock`。 **1. ReentrantLock(可重入)** ```java Lock lock = new ReentrantLock(); void criticalSection() { lock.lock(); // 手动获取 try { // 临界区代码 } finally { lock.unlock(); // 必须手动释放 } } ``` **优势**: - 公平选项:`new ReentrantLock(true)` - 等待超时:`tryLock(long timeout, TimeUnit unit)` - 可中断:`lockInterruptibly()` - 条件变量支持:`Condition condition = lock.newCondition()` **2. ReadWriteLock(读写)** ```java ReadWriteLock rwLock = new ReentrantReadWriteLock(); Lock readLock = rwLock.readLock(); Lock writeLock = rwLock.writeLock(); // 读操作(共享) void readData() { readLock.lock(); try { /* 并发读操作 */ } finally { readLock.unlock(); } } // 写操作(排他) void writeData() { writeLock.lock(); try { /* 独占写操作 */ } finally { writeLock.unlock(); } } ``` #### 三、特殊类型 1. **StampedLock(JDK8+)** 乐观读实现: ```java StampedLock sl = new StampedLock(); // 乐观读 long stamp = sl.tryOptimisticRead(); if (!sl.validate(stamp)) { // 检查是否被修改 stamp = sl.readLock(); // 升级为悲观读 } try { /* 读操作 */ } finally { sl.unlockRead(stamp); } ``` 2. **降级(写→读)** ```java writeLock.lock(); try { // 写操作... readLock.lock(); // 保持写时获取读 } finally { writeLock.unlock(); } try { /* 读操作 */ } finally { readLock.unlock(); } ``` #### 四、的内存语义 - **可见性保证**:释放前将本地内存刷新到主内存 - **Happens-Before原则**:解操作先于后续加操作 #### 五、优化技术 1. **消除**:JIT编译器移除不可能竞争的 2. **粗化**:合并相邻同步块减少开销 3. **偏向**:无竞争时消除同步开销 4. **适应性自旋**:动态调整自旋等待时间 #### 六、死预防 使用`tryLock`避免死: ```java Lock lockA = new ReentrantLock(); Lock lockB = new ReentrantLock(); void safeOperation() { while (true) { if (lockA.tryLock()) { try { if (lockB.tryLock()) { // 尝试获取第二把 try { /* 操作资源 */ } finally { lockB.unlock(); } } } finally { lockA.unlock(); } } Thread.sleep(10); // 避免活 } } ``` #### 七、的选择原则 | 场景 | 推荐类型 | |------|------------| | 简单同步 | `synchronized` | | 需要超时/中断 | `ReentrantLock` | | 读多写少 | `ReadWriteLock` | | 极高性能读 | `StampedLock` | | 无竞争同步 | `Atomic`原子类 | ### 总结 Java机制从`synchronized`到`StampedLock`经历了显著的演进,核心目标是平衡**线程安全**与**性能开销**。正确选择类型需考虑:竞争强度、读写比例、是否需要公平性等。始终遵循"最小化临界区"原则,并优先考虑`java.util.concurrent`包中的高级并发工具类。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值