文章目录
锁接口和类
前面我们介绍了Java的原生锁——基于对象的锁,通常与synchronized
关键字一起使用。然而,Java在java.util.concurrent.locks
包下还提供了更为强大和灵活的锁类和接口,这些锁可以提供更高的性能和更多的功能。
synchronized
的不足之处
尽管synchronized
是最早的同步机制之一,但它在处理多线程时存在一些不足:
- 单线程执行限制:即使临界区是只读操作,
synchronized
也会限制同一时间只有一个线程执行,这可能导致不必要的性能瓶颈。 - 锁状态不可查询:
synchronized
无法查询当前线程是否成功获得了锁。 - 线程阻塞问题:如果临界区因为IO操作或
sleep
等原因阻塞,而当前线程没有释放锁,其他线程会被迫等待,这可能会导致系统的整体吞吐量降低。
这些问题是java.util.concurrent.locks
包中的锁类所能解决的。
锁的几种分类
锁的分类可以基于多种方式进行,下面详细介绍几种主要的分类方法:
可重入锁和非可重入锁
-
可重入锁:允许同一线程对同一资源重复加锁,即使已经持有该锁的线程再次请求该锁也不会发生死锁。
synchronized
关键字实现的锁就是可重入的。例子:一个线程在一个synchronized
方法中调用另一个本实例的synchronized
方法时,能够继续获得锁而不产生死锁。 -
非可重入锁:不允许同一线程对资源进行重复加锁。如果线程尝试重复加锁,可能会导致死锁或线程阻塞。
ReentrantLock
是一个典型的可重入锁实现,允许线程对同一资源进行重复加锁。
公平锁与非公平锁
-
公平锁:按照线程请求锁的顺序来分配锁,遵循FIFO原则。公平锁能够避免线程饥饿,但可能会带来较高的开销。
-
非公平锁:不保证按照请求顺序分配锁,可能会导致某些线程长时间得不到锁,从而提高了锁的吞吐量,但可能会引起线程饥饿现象。
ReentrantLock
支持两种模式:公平锁和非公平锁。公平锁的开销相对较高,但在一些对公平性要求较高的场景下是必需的。
读写锁和排它锁
-
排它锁:在同一时刻只允许一个线程访问临界区。
synchronized
和ReentrantLock
都是排它锁。 -
读写锁:允许多个线程同时读取,但在写入时会阻塞所有的读取和其他写入。
ReentrantReadWriteLock
是读写锁的实现,内部维护了一个读锁和一个写锁,以提升“读多写少”场景下的性能。注意,虽然读写锁在读取时允许多个线程并发,但在写操作时仍然会阻塞所有读线程和其他写线程。
JDK中有关锁的一些接口和类
Java JDK中提供了许多与锁相关的类和接口,主要集中在java.util.concurrent.locks
包下。
以下是几个重要的类和接口:
抽象类AQS/AQLS/AOS
-
AQS (AbstractQueuedSynchronizer):提供了基本的队列同步器功能,内部使用一个
int
类型的资源表示。在JDK 1.6中引入了AQLS
(AbstractQueuedLongSynchronizer)来支持更大范围的资源计数。AQS
和AQLS
都继承自AOS
(AbstractOwnableSynchronizer),后者用于表示锁与持有者之间的关系。AOS
提供了设置和获取锁持有线程的方法。// 独占模式,锁的持有者 private transient Thread exclusiveOwnerThread; // 设置锁持有者 protected final void setExclusiveOwnerThread(Thread t) { exclusiveOwnerThread = t; } // 获取锁的持有线程 protected final Thread getExclusiveOwnerThread() { return exclusiveOwnerThread; }
接口Condition/Lock/ReadWriteLock
- Lock接口:提供了获取和释放锁的方法,以及获取
Condition
对象的方法。
- Condition接口:提供类似于
Object
的wait/notify
机制的方法,但功能更强大。Condition
允许多个等待队列,并支持中断、超时等待等功能。
每个对象都可以用继承自Object
的wait/notify方法来实现等待/通知机制。而Condition接口也提供了类似Object监视器的方法,通过与Lock配合来实现等待/通知模式。
那为什么既然有Object的监视器方法了,还要用Condition呢?这里有一个二者简单的对比:
对比项 | Object监视器 | Condition |
---|---|---|
前置条件 | 获取对象的锁 | 调用Lock.lock获取锁,调用Lock.newCondition获取Condition对象 |
调用方式 | 直接调用,比如object.notify() | 直接调用,比如condition.await() |
等待队列的个数 | 一个 | 多个 |
当前线程释放锁进入等待状态 | 支持 | 支持 |
当前线程释放锁进入等待状态,在等待状态中不中断 | 不支持 | 支持 |
当前线程释放锁并进入超时等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态直到将来的某个时间 | 不支持 | 支持 |
唤醒等待队列中的一个线程 | 支持 | 支持 |
唤醒等待队列中的全部线程 | 支持 | 支持 |
Condition和Object的wait/notify基本相似。其中,Condition的await方法对应的是Object的wait方法,而Condition的signal/signalAll方法则对应Object的notify/notifyAll()。但Condition类似于Object的等待/通知机制的加强版。我们来看看主要的方法:
方法名称 | 描述 |
---|---|
await() | 当前线程进入等待状态直到被通知(signal)或者中断;当前线程进入运行状态并从await()方法返回的场景包括:(1)其他线程调用相同Condition对象的signal/signalAll方法,并且当前线程被唤醒;(2)其他线程调用interrupt方法中断当前线程; |
awaitUninterruptibly() | 当前线程进入等待状态直到被通知,在此过程中对中断信号不敏感,不支持中断当前线程 |
awaitNanos(long) | 当前线程进入等待状态,直到被通知、中断或者超时。如果返回值小于等于0,可以认定就是超时了 |
awaitUntil(Date) | 当前线程进入等待状态,直到被通知、中断或者超时。如果没到指定时间被通知,则返回true,否则返回false |
signal() | 唤醒一个等待在Condition上的线程,被唤醒的线程在方法返回前必须获得与Condition对象关联的锁 |
signalAll() | 唤醒所有等待在Condition上的线程,能够从await()等方法返回的线程必须先获得与Condition对象关联的锁 |
-
ReadWriteLock接口:提供了获取读锁和写锁的方法。
public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }
ReentrantLock
ReentrantLock
是Lock
接口的默认实现,支持公平锁和非公平锁。它通过内部的同步器实现了锁的基本功能,并且支持可重入特性。其内部结构包括两个同步器类:NonfairSync
和FairSync
,分别用于非公平锁和公平锁。
ReentrantLock 是 Java 中用于线程同步的一种可重入锁 (reentrant lock)。它是在 java.util.concurrent.locks
包中定义的,提供了比 synchronized
关键字更灵活和更强大的锁机制。ReentrantLock 是 Lock
接口的实现类,通过它可以显式地获取和释放锁,从而控制多个线程对共享资源的访问。
主要特点
-
可重入性:
ReentrantLock 是一种可重入锁,即同一个线程可以多次获取同一把锁,而不会导致死锁。这意味着如果一个线程已经持有了该锁,并且再次请求获取该锁,ReentrantLock 会允许这个线程成功获取锁,并维护一个计数器,记录获取锁的次数。在释放锁时,必须与获取锁的次数匹配,这样锁才真正被释放。 -
公平锁与非公平锁:
ReentrantLock 可以选择公平锁或非公平锁。默认情况下,ReentrantLock 是非公平的,这意味着线程可能不会按照请求锁的顺序获取锁,而是可能会发生“插队”现象。公平锁则严格按照请求的顺序来分配锁,从而避免线程饥饿问题。在创建 ReentrantLock 对象时,可以通过构造函数指定锁的公平性:ReentrantLock lock = new ReentrantLock(true); // 公平锁 ReentrantLock lock = new ReentrantLock(false); // 非公平锁 (默认)
-
可中断的锁获取:
使用synchronized
关键字时,线程在等待锁时无法被中断。而 ReentrantLock 提供了可以响应中断的锁获取方式,允许线程在等待锁的过程中被中断:try { lock.lockInterruptibly(); } catch (InterruptedException e) { // 处理中断 }
-
尝试获取锁:
ReentrantLock 提供了tryLock()
方法,它不会像lock()
那样无限期等待锁,而是立即返回获取锁的结果。该方法可以设置超时时间,当锁不可用时,可以选择等待一段时间,如果在指定时间内无法获取锁,则返回false
:if (lock.tryLock()) { // 成功获取锁 } else { // 未能获取锁 } if (lock.tryLock(1, TimeUnit.SECONDS)) { // 成功获取锁 } else { // 在指定时间内未能获取锁 }
-
条件变量(Condition):
ReentrantLock 可以与 Condition 对象结合使用,提供比Object
类的wait()
和notify()
更强大的等待/通知机制。Condition 可以让线程在等待某个条件时释放锁,其他线程可以在条件满足时通知等待的线程继续执行:Condition condition = lock.newCondition(); lock.lock(); try { while (!conditionMet) { condition.await(); // 释放锁并进入等待状态 } // 执行条件满足后的逻辑 } finally { lock.unlock(); } lock.lock(); try { condition.signal(); // 唤醒等待中的线程 } finally { lock.unlock(); }
使用示例
基本使用
以下是一个使用 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 int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
// 创建多个线程来同时访问共享资源
for (int i = 0; i < 10; i++) {
new Thread(example::increment).start();
}
// 主线程等待所有子线程执行完毕
try {
Thread.sleep(1000); // 简单的等待
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印最终计数结果
System.out.println("Final count: " + example.getCount());
}
}
在这个示例中,多个线程同时调用 increment()
方法增加 count
变量。为了保证线程安全,使用了 ReentrantLock 来同步对共享资源 count
的访问。最终,在所有线程执行完毕后,count
的值应该是线程数的总和。
Condition 使用 :生产者-消费者模式
下面是一个使用 ReentrantLock
和 Condition
的代码示例。这个示例演示了如何使用 Condition
来实现线程间的协作,类似于生产者-消费者模式。生产者负责生成数据,消费者负责消费数据,Condition
用来管理生产者和消费者之间的同步。
package io.github.dunwu.javacore.concurrent.artisan;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.LinkedList;
import java.util.Queue;
/**
* 生产者消费者问题的条件变量示例类
* 本类展示了如何使用条件变量来解决生产者消费者问题
* 通过ReentrantLock和Condition来同步生产和消费操作
*/
public class ProducerConsumerConditionExample {
// 定义一个重入锁,用于同步对队列的访问
private final ReentrantLock lock = new ReentrantLock();
// 当队列不为空时,notEmpty条件被触发,用于唤醒消费者
private final Condition notEmpty = lock.newCondition();
// 当队列不为满时,notFull条件被触发,用于唤醒生产者
private final Condition notFull = lock.newCondition();
// 使用LinkedList作为队列的基础实现
private final Queue<Integer> queue = new LinkedList<>();
// 定义队列的容量
private final int capacity = 10;
/**
* 生产者方法,负责生成数据并放入队列中
* 模拟生产者的行为,当队列满时等待,当队列不空时可以消费
*
* @throws InterruptedException 如果生产过程中被中断
*/
public void produce() throws InterruptedException {
int value = 0;
while (true) {
lock.lock();
try {
// 当队列满时,生产者等待
while (queue.size() == capacity) {
System.out.println("Queue is full, producer is waiting");
notFull.await(); // 通过调用await方法使生产者线程等待
}
// 生产数据并放入队列
System.out.println("Produced " + value);
queue.offer(value++);
notEmpty.signal(); // 通知消费者队列不为空
} finally {
lock.unlock();
}
// 模拟生产延迟
Thread.sleep(5000);
}
}
/**
* 消费者线程不断尝试从队列中消费元素的方法
* 该方法通过使用条件变量实现生产者和消费者之间的同步
*
* @throws InterruptedException 如果线程被中断
*/
public void consume() throws InterruptedException {
while (true) {
// 获取锁以确保线程安全
lock.lock();
try {
// 如果队列为空,则消费者等待
while (queue.isEmpty()) {
System.out.println("Queue is empty, consumer is waiting");
notEmpty.await(); // 队列空了,消费者等待
}
// 从队列头部取出一个元素并消费
int value = queue.poll();
System.out.println("Consumed " + value);
// 通知生产者队列不满
notFull.signal(); // 通知生产者队列不满
} finally {
// 释放锁
lock.unlock();
}
// 模拟消费过程中的延迟
Thread.sleep(1000); // 模拟消费的延迟
}
}
/**
* 程序的入口点 主要用于演示生产者-消费者问题的解决方案
* 通过使用条件变量来同步生产和消费操作
*
* @param args 命令行参数
*/
public static void main(String[] args) {
// 创建生产者-消费者示例对象
ProducerConsumerConditionExample example = new ProducerConsumerConditionExample();
// 创建并启动生产者线程
Thread producerThread = new Thread(() -> {
try {
// 生产者方法,负责生产数据
example.produce();
} catch (InterruptedException e) {
// 如果线程被中断,重新设置中断标志
Thread.currentThread().interrupt();
}
});
// 创建并启动消费者线程
Thread consumerThread = new Thread(() -> {
try {
// 消费者方法,负责消费数据
example.consume();
} catch (InterruptedException e) {
// 如果线程被中断,重新设置中断标志
Thread.currentThread().interrupt();
}
});
// 启动生产者线程
producerThread.start();
// 启动消费者线程
consumerThread.start();
}
}
代码说明
-
锁和条件变量:
ReentrantLock lock
:用于控制对共享资源(queue
)的访问。Condition notFull
:生产者在队列满时等待的条件。Condition notEmpty
:消费者在队列为空时等待的条件。
-
生产者方法 (
produce
):- 当队列已满(
queue.size() == capacity
)时,生产者线程调用notFull.await()
进入等待状态,直到被通知队列有空间可用。 - 生产者将生成的数据添加到队列中后,调用
notEmpty.signal()
通知等待的消费者线程。
- 当队列已满(
-
消费者方法 (
consume
):- 当队列为空时,消费者线程调用
notEmpty.await()
进入等待状态,直到被通知队列有数据可消费。 - 消费者从队列中取出数据后,调用
notFull.signal()
通知等待的生产者线程。
- 当队列为空时,消费者线程调用
-
线程启动:
- 创建并启动生产者线程和消费者线程,使它们在不同的线程中运行
produce()
和consume()
方法。
- 创建并启动生产者线程和消费者线程,使它们在不同的线程中运行
运行结果
- 当队列为空时,消费者会等待,直到生产者添加了新数据。
- 当队列已满时,生产者会等待,直到消费者消耗了数据。
这个例子展示了 ReentrantLock
和 Condition
在线程间协作中的强大功能,特别是在需要更加细粒度控制的场景中,比起 synchronized
和 wait/notify
提供了更大的灵活性。
ReentrantReadWriteLock
ReentrantReadWriteLock
实现了ReadWriteLock
接口,支持读写锁。它内部维护了两个同步器,分别是ReadLock
和WriteLock
。这两个锁共享同一个同步器实例,用于实现读写锁的功能。ReentrantReadWriteLock
也支持公平锁和非公平锁。
与传统的互斥锁(如 ReentrantLock
)不同,ReentrantReadWriteLock
允许多个线程同时读取数据(只要没有线程在写数据),但在写操作进行时,会阻塞所有的读操作和其他写操作。这种机制在读多写少的场景下非常有用,可以提高并发性能。
主要组件
ReentrantReadWriteLock
由两个锁组成:
-
读锁(ReadLock):
- 允许多个线程同时获取,前提是没有线程持有写锁。
- 如果有线程持有写锁,则读锁会被阻塞。
-
写锁(WriteLock):
- 只允许一个线程获取,且当写锁被持有时,所有的读锁和写锁请求都会被阻塞。
主要方法
lock.readLock().lock()
和lock.readLock().unlock()
:获取和释放读锁。lock.writeLock().lock()
和lock.writeLock().unlock()
:获取和释放写锁。
特性
- 重入性:
ReentrantReadWriteLock
是可重入的,这意味着线程可以多次获得它已经持有的读锁或写锁,而不会被阻塞。 - 锁降级:从写锁降级为读锁是允许的,前提是写锁在持有时获取读锁。锁的降级可以用来在写操作完成后安全地继续读取操作。
读写锁的使用
下面是一个使用 ReentrantReadWriteLock
的示例,演示了读写锁如何在读写操作中协调多个线程的访问。
package io.github.dunwu.javacore.concurrent.artisan;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 读写锁示例类
*/
public class ReadWriteLockExample {
// 创建一个读写锁对象
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 获取读锁
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
// 获取写锁
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
// 共享变量,初始值为0
private int value = 0;
/**
* 写操作:更新值
*
* @param newValue 新的值
*/
public void writeValue(int newValue) {
// 获取写锁
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " writing " + newValue);
// 更新值
value = newValue;
System.out.println(Thread.currentThread().getName() + " wrote " + newValue);
} finally {
// 释放写锁
writeLock.unlock();
}
}
/**
* 读操作:读取值
*
* @return 当前值
*/
public int readValue() {
// 获取读锁
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " reading " + value);
// 返回当前值
return value;
} finally {
// 释放读锁
readLock.unlock();
}
}
/**
* 主函数入口
*
* @param args 命令行参数
*/
public static void main(String[] args) {
ReadWriteLockExample example = new ReadWriteLockExample();
// 启动多个线程进行读操作
for (int i = 0; i < 3; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) {
example.readValue();
}
}, "Reader-" + i).start();
}
// 启动一个线程进行写操作
new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.writeValue(i);
try {
Thread.sleep(100); // 模拟写操作的延迟
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "Writer").start();
}
}
代码说明
-
写锁使用:
- 在
writeValue()
方法中,写操作通过writeLock.lock()
加锁,确保在写操作期间没有其他线程能够进行读或写操作。 - 写操作完成后,调用
writeLock.unlock()
释放写锁。
- 在
-
读锁使用:
- 在
readValue()
方法中,读操作通过readLock.lock()
加锁,确保多个线程能够同时读取,只要没有写操作进行。 - 读操作完成后,调用
readLock.unlock()
释放读锁。
- 在
-
线程的并发执行:
- 启动多个读线程和一个写线程,读线程可以并发读取,而写线程在写入时会阻塞所有的读操作。
适用场景
ReentrantReadWriteLock
适合以下场景:
- 读多写少:如果应用程序中读操作远多于写操作,使用
ReentrantReadWriteLock
可以提高并发性能。 - 需要锁降级:在某些场景中,写操作完成后继续读取数据是有必要的,这时可以利用
ReentrantReadWriteLock
的锁降级特性。
StampedLock
StampedLock简介
StampedLock
是 Java 8 引入的一个新的锁机制,用于优化读写锁的性能。它设计的目的是为了减少读操作和写操作之间的竞争,提高并发性能。StampedLock
与传统的 ReadWriteLock
在功能和性能上有所不同。
设计背景和目的
传统的 ReadWriteLock
允许多个读线程并发读取,但写线程在写入时需要独占锁。虽然 ReadWriteLock
能够较好地处理读多写少的场景,但它的性能可能会受到读操作和写操作的锁争用的影响。StampedLock
引入了一个时间戳机制来管理锁的获取,从而在高并发的情况下提供更好的性能。
基本功能和使用方式
StampedLock
提供了三种主要的锁模式:
-
乐观读模式(Optimistic Read):在乐观读模式下,线程可以读取共享数据而不需要获取锁。乐观读模式不会阻塞写线程,因此性能通常优于传统的读锁。当读取完成后,线程需要验证读取期间是否有写操作发生,如果有,则需要重新读取数据。
-
悲观读模式(Pessimistic Read):线程在悲观读模式下获取一个读锁,确保在读操作期间没有其他线程写数据。与
ReadWriteLock
的读锁类似,悲观读锁也会阻塞写线程。 -
写模式(Write):线程在写模式下获取一个写锁,保证写操作期间没有其他线程读或写数据。写锁是独占的。
锁的获取和释放
-
乐观读:
long stamp = stampedLock.tryOptimisticRead();
stampedLock.validate(stamp);
stampedLock.unlock(stamp);
(如果需要) -
悲观读:
long stamp = stampedLock.readLock();
stampedLock.unlockRead(stamp);
-
写锁:
long stamp = stampedLock.writeLock();
stampedLock.unlockWrite(stamp);
优缺点
优点:
- 性能:在高并发的读操作场景中,乐观读模式能显著提高性能。
- 灵活性:可以在乐观读和悲观读之间切换,提高了灵活性。
缺点:
- 复杂性:使用乐观读模式时需要处理验证过程,增加了编程复杂度。
- 死锁风险:如果使用不当,尤其是在锁的升级和降级时,可能会导致死锁。
示例代码
package io.github.dunwu.javacore.concurrent.artisan;
import java.util.concurrent.locks.StampedLock;
public class StampedLockExample {
// 声明一个StampedLock对象,用于实现乐观读、悲观读和写锁
private final StampedLock stampedLock = new StampedLock();
// 声明一个可被多个读取器共享的值
private int value = 0;
/**
* 乐观读取方法
* 首先尝试进行无锁读取,如果在读取期间检测到写操作,则转换为有锁读取
* @return 当前的value值
*/
public int optimisticRead() {
// 尝试乐观读取,不需要锁
long stamp = stampedLock.tryOptimisticRead();
int currentValue = value;
// 检查读取期间是否有写操作发生
if (!stampedLock.validate(stamp)) {
// 发现写操作,转换为有锁读取
stamp = stampedLock.readLock();
try {
currentValue = value;
} finally {
stampedLock.unlockRead(stamp);
}
}
return currentValue;
}
/**
* 悲观读取方法
* 在读取之前获取一个读锁,确保在读取期间不会有写操作发生
* @return 当前的value值
*/
public int pessimisticRead() {
// 获取一个读锁
long stamp = stampedLock.readLock();
try {
return value;
} finally {
stampedLock.unlockRead(stamp);
}
}
/**
* 写操作方法
* 在写操作之前获取一个写锁,确保在写操作期间不会有其他读或写操作发生
* @param newValue 要写入的新值
*/
public void write(int newValue) {
// 获取一个写锁
long stamp = stampedLock.writeLock();
try {
value = newValue;
} finally {
stampedLock.unlockWrite(stamp);
}
}
public static void main(String[] args) {
StampedLockExample example = new StampedLockExample();
// 测试写操作
System.out.println("Writing value 10");
example.write(10);
// 测试乐观读取
System.out.println("Optimistic Read (initial): " + example.optimisticRead());
// 模拟另一个线程的写操作
System.out.println("Writing value 20 (simulate concurrent modification)");
example.write(20);
// 测试乐观读取在修改后的结果
int optimisticValue = example.optimisticRead();
System.out.println("Optimistic Read (after write): " + optimisticValue);
// 测试悲观读取
System.out.println("Pessimistic Read: " + example.pessimisticRead());
// 最后的写操作
System.out.println("Final write value 30");
example.write(30);
System.out.println("Final value (pessimistic read): " + example.pessimisticRead());
}
}
与其他锁机制的比较
-
与
ReadWriteLock
的比较:StampedLock
提供的乐观读模式比ReadWriteLock
的读锁性能更好,尤其是在读操作远多于写操作的情况下。StampedLock
的锁获取和释放操作较为复杂,而ReadWriteLock
更直观易用。
-
与
ReentrantLock
的比较:StampedLock
的读写分离机制比ReentrantLock
的互斥锁机制在读多写少的场景下能提供更高的并发性能。StampedLock
允许悲观和乐观读锁,而ReentrantLock
只提供互斥锁功能,不能直接优化读操作。