1. 理解ReentrantLock在Android线程安全中的核心地位
在Android开发中,多线程编程是提升应用性能的关键手段,但随之而来的线程安全问题却让不少开发者头疼。记得去年我们团队遇到一个线上崩溃:在用户抢购场景下,库存计数器偶尔会出现负数。经过通宵排查,最终发现问题出在没有正确使用线程同步机制上。这个教训让我深刻认识到,掌握ReentrantLock这样的并发工具对Android开发者有多重要。
与传统的synchronized关键字相比,ReentrantLock提供了更灵活的线程控制能力。它属于java.util.concurrent.locks包下的显式锁,具有可重入、可中断、公平性选择等特性。在Android环境下,虽然Handler、AsyncTask等组件能解决部分线程间通信问题,但当遇到复杂的共享资源竞争场景时,ReentrantLock往往能提供更精细的控制。
2. ReentrantLock基础用法与核心机制
2.1 基本使用模板
正确的ReentrantLock使用必须遵循"获取锁-操作-释放锁"的范式。下面是一个标准的代码模板:
java复制ReentrantLock lock = new ReentrantLock();
// ...
public void performThreadSafeOperation() {
lock.lock(); // 获取锁
try {
// 临界区代码
sharedResource.doSomething();
} finally {
lock.unlock(); // 确保锁被释放
}
}
这里有几个关键点需要注意:
- 必须在finally块中释放锁,确保即使操作抛出异常也能释放锁
- 临界区代码应尽可能精简,减少锁持有时间
- 避免在临界区内调用可能阻塞的操作(如网络请求)
2.2 可重入特性解析
ReentrantLock的可重入特性允许同一个线程多次获取同一把锁。这个特性在递归调用场景中尤为重要:
java复制public class RecursiveExample {
private final ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock();
try {
inner(); // 递归调用
} finally {
lock.unlock();
}
}
private void inner() {
lock.lock(); // 同一线程可以再次获取锁
try {
// ...
} finally {
lock.unlock();
}
}
}
每个lock()调用都必须有对应的unlock()调用,否则会导致锁无法释放。Android Studio的代码检查工具可以帮助发现这类问题,但开发者仍需保持警惕。
3. 高级特性与实战技巧
3.1 公平锁与非公平锁的选择
ReentrantLock提供了公平性选择的功能,通过构造函数的参数指定:
java复制// 公平锁 - 按照请求顺序获取锁
ReentrantLock fairLock = new ReentrantLock(true);
// 非公平锁 - 默认选项,允许插队
ReentrantLock unfairLock = new ReentrantLock(false);
在Android开发中,非公平锁通常是更好的选择:
- 性能优势:减少线程切换开销
- 吞吐量高:允许当前持有锁的线程连续获取锁
- 实际测试表明,在典型场景下非公平锁的性能比公平锁高出5-10倍
但要注意,非公平锁可能导致某些线程长时间获取不到锁(线程饥饿)。如果业务对公平性有严格要求,才考虑使用公平锁。
3.2 可中断与超时控制
ReentrantLock提供了比synchronized更灵活的锁获取方式:
java复制// 可中断获取
public void performInterruptibleOperation() throws InterruptedException {
lock.lockInterruptibly(); // 可被Thread.interrupt()中断
try {
// ...
} finally {
lock.unlock();
}
}
// 带超时的尝试获取
public boolean tryPerformOperation(long timeout, TimeUnit unit)
throws InterruptedException {
if (lock.tryLock(timeout, unit)) {
try {
return doOperation();
} finally {
lock.unlock();
}
}
return false; // 超时未获取锁
}
这些特性在Android中特别有用,比如:
- 防止ANR:在UI线程中尝试获取锁时设置合理超时
- 优雅退出:通过中断机制取消后台任务
- 死锁预防:超时机制可以打破潜在的循环等待
4. Condition对象的精妙运用
4.1 基本等待/通知机制
ReentrantLock的Condition对象提供了比Object.wait()/notify()更灵活的线程协调能力:
java复制class BoundedBuffer {
final ReentrantLock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 等待不满条件
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); // 通知不空条件
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 等待不空条件
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal(); // 通知不满条件
return x;
} finally {
lock.unlock();
}
}
}
这种模式在Android中的典型应用包括:
- 生产者-消费者场景(如图片加载队列)
- 线程池任务调度
- 事件总线实现
4.2 多条件变量的优势
与synchronized的单一等待集不同,一个ReentrantLock可以创建多个Condition对象。这在复杂同步场景中特别有用:
java复制class OrderProcessor {
private final ReentrantLock lock = new ReentrantLock();
private final Condition newOrder = lock.newCondition();
private final Condition orderProcessed = lock.newCondition();
public void waitForNewOrder() throws InterruptedException {
lock.lock();
try {
newOrder.await();
} finally {
lock.unlock();
}
}
public void notifyNewOrder() {
lock.lock();
try {
newOrder.signalAll();
} finally {
lock.unlock();
}
}
public void waitForProcessing() throws InterruptedException {
lock.lock();
try {
orderProcessed.await();
} finally {
lock.unlock();
}
}
public void notifyProcessingComplete() {
lock.lock();
try {
orderProcessed.signalAll();
} finally {
lock.unlock();
}
}
}
这种设计使得线程可以精确地等待特定条件,避免了不必要的唤醒(虚假唤醒),提高了系统效率。
5. 性能优化与避坑指南
5.1 锁粒度的控制
在Android开发中,不合理的锁粒度会严重影响应用性能:
java复制// 不推荐 - 锁粒度过大
public synchronized void updateAllData() {
updateUserInfo();
updateOrderList();
updateInventory();
}
// 推荐做法 - 细粒度锁控制
public void updateAllData() {
updateUserInfo(); // 内部使用独立锁
updateOrderList(); // 内部使用独立锁
updateInventory(); // 内部使用独立锁
}
优化建议:
- 尽量减小临界区范围
- 对不同的共享资源使用不同的锁
- 读写分离:读多写少的场景使用ReadWriteLock
5.2 避免常见陷阱
在实际项目中,我遇到过不少ReentrantLock使用不当的情况:
-
锁泄漏:忘记释放锁会导致系统死锁
java复制// 错误示例 public void riskyMethod() { lock.lock(); if (someCondition) { return; // 直接返回导致锁未释放 } lock.unlock(); } -
嵌套锁顺序:多个锁的获取顺序不一致可能导致死锁
java复制// 线程1 lockA.lock(); lockB.lock(); // 线程2 lockB.lock(); lockA.lock(); // 可能导致死锁 -
条件变量使用不当:await()调用前必须持有锁
java复制// 错误示例 public void wrongAwait() throws InterruptedException { condition.await(); // 抛出IllegalMonitorStateException }
5.3 Android特有的注意事项
在Android平台上使用ReentrantLock还需要特别注意:
-
UI线程限制:绝对不能在UI线程中执行可能阻塞的锁操作
java复制// 危险代码 new Handler(Looper.getMainLooper()).post(() -> { reentrantLock.lock(); // 如果锁被其他线程持有,可能导致ANR try { // ... } finally { reentrantLock.unlock(); } }); -
内存消耗:每个ReentrantLock实例大约消耗200字节内存,在大量创建时需要谨慎
-
跨进程限制:ReentrantLock只适用于同一进程内的线程同步,跨进程通信需要使用Android的Binder机制
6. 与Android架构组件的结合
6.1 在ViewModel中的应用
在MVVM架构中,ViewModel常需要处理多线程数据访问:
java复制public class ProductViewModel extends AndroidViewModel {
private final ReentrantLock dataLock = new ReentrantLock();
private List<Product> productList;
public LiveData<List<Product>> getProducts() {
MutableLiveData<List<Product>> liveData = new MutableLiveData<>();
new Thread(() -> {
dataLock.lock();
try {
if (productList == null) {
productList = loadFromDatabase();
}
liveData.postValue(productList);
} finally {
dataLock.unlock();
}
}).start();
return liveData;
}
}
这种模式确保了数据加载的线程安全,同时避免了阻塞UI线程。
6.2 与Kotlin协程的配合
在Kotlin项目中,ReentrantLock可以与协程很好地协同工作:
kotlin复制class SharedResourceManager {
private val lock = ReentrantLock()
private val data = mutableMapOf<String, Any>()
suspend fun <T> withLock(block: suspend () -> T): T {
lock.lock()
return try {
block()
} finally {
lock.unlock()
}
}
suspend fun updateData(key: String, value: Any) = withLock {
data[key] = value
withContext(Dispatchers.IO) {
saveToDatabase(key, value) // 可以在锁内执行挂起操作
}
}
}
这种模式结合了ReentrantLock的明确同步语义和协程的轻量级特性,特别适合复杂的并发场景。
7. 实战案例:线程安全的缓存实现
最后,让我们看一个完整的ReentrantLock实战案例 - 线程安全的图片缓存:
java复制public class ImageCache {
private final ReentrantLock lock = new ReentrantLock();
private final LruCache<String, Bitmap> memoryCache;
private final DiskLruCache diskCache;
public ImageCache(Context context, int maxMemorySize) {
memoryCache = new LruCache<>(maxMemorySize);
diskCache = DiskLruCache.open(context.getCacheDir(), 1, 1, 50 * 1024 * 1024);
}
public Bitmap get(String url) {
// 先尝试从内存获取
Bitmap bitmap = memoryCache.get(url);
if (bitmap != null) return bitmap;
lock.lock();
try {
// 双重检查
bitmap = memoryCache.get(url);
if (bitmap != null) return bitmap;
// 从磁盘加载
bitmap = loadFromDisk(url);
if (bitmap != null) {
memoryCache.put(url, bitmap);
}
return bitmap;
} finally {
lock.unlock();
}
}
public void put(String url, Bitmap bitmap) {
memoryCache.put(url, bitmap);
lock.lock();
try {
saveToDisk(url, bitmap);
} finally {
lock.unlock();
}
}
private Bitmap loadFromDisk(String url) {
// 实现省略...
}
private void saveToDisk(String url, Bitmap bitmap) {
// 实现省略...
}
}
这个实现展示了ReentrantLock的几个最佳实践:
- 只在必要时加锁(内存缓存无锁访问)
- 使用双重检查减少锁竞争
- 确保所有退出路径都释放锁
- 分离读写操作,优化性能
在Android开发中,合理使用ReentrantLock可以构建出既安全又高效的并发系统。掌握它的各种特性和使用场景,是成为高级Android开发者的必经之路。
