并发干扰
使用多线程开发可以很好地提高代码效率,但是在多线程对同一数据资源的共享中,许多线程需要的同一个数据做读写修改操作,因此必然会存在竞争问题,而且这些问题通常会带来灾难性的后果,我们来看一个例子:
现在有一个多线程的银行账户管理系统,我的账户内有余额 1000 元,现在有两个线程对我的账户进行操作:
线程 A :存入500元
线程 B :取出200元
我们应该知道,在大多服务器操作系统中都采用抢占式调度(线程状态及属性),每个线程只有一个时间片来执行任务,当时间片用完后会立刻暂停运行交由其他线程,然后再重新排队
在这种情况下,线程 A 和线程 B,可能就会产生冲突:
我们会发现,到最后我的账户只剩了800元,完全忽略了存钱的操作,这肯定不行!
锁对象
为解决代码块并发访问干扰,有两种机制:一种是 Java 提供的 synchronized 关键字,另一种是 ReentrantLock 类(唯一实现了 Lock 接口的对象)。我们来看看利用ReentrantLock 类如何解决这一问题
ReentrantLock
用 ReentrantLock 保护代码块的基本结构:
myLock.lock(); // 一个 ReentrantLock 对象
try{
...
}
finally{
myLock.unlock(); // 确保即使出现异常,也能解锁
}
这种方法理解起来其实很简单,我们可以把 lock 到 unlock 之间的区域想象成一个封锁区,任何一个线程一旦调用 lock,那么在它 unlock 之前, 其他任何线程都不能进入这段封锁区
如果有这么一个锁对象来保护我的代码,那么之前的问题就会变成这样:
这样一来,多线程共享资源就安全多了
我们要注意,在这里的锁对象只是针对我的账户的对象,不同的账户会有自己不同的锁对象,而线程操作不同的账户时,线程之间互不影响
还要注意,锁对象可以嵌套使用,线程可以重复获得已经持有的锁,锁会保持一个持有计数,用来跟踪 lock 方法的嵌套调用,线程每调用一次 lock 方法都还要调用 unlock 方法来解锁
ReentrantLock( ):构建一个可以被用来保护临界区的可重入的锁
ReentrantLock( boolean fair ):构建一个带有公平策略的锁,一个公平锁会偏向于等待时间长的线程,但是这一公平机制会大大降低性能,所以,默认情况下,锁没有被强制为公平的
ReentrantReadWriteLock(读/写锁)
Lock readLock( ) : 得到一个可以被多个读操作共用的读锁,但会排斥所有写操作
Lock writeLock( ):得到一个写锁,排斥所有其他读操作和写操作
使用 ReentrantReadWriteLock 必要步骤:
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock readLock = rwl.readLock();
private Lock readLock = rwl.writeLock();
// 对所有读取方法加读锁
public double getTotalBalance() {
readLock.lock();
try {...}
finally { readLock.unlock(); }
}
// 对所有修改方法加写锁
public void transfer(...) {
writeLock.lock();
try {...}
finally { writeLock.unlock(); }
}