同步
使用多线程是需要小心翼翼使用的,不恰当的使用,就可能带来并发问题,结果不幂等。一般来说,并发问题是由于程序运行的步骤不是原子性的,这使得不同的线程运行同一个步骤时,就有可能会出现数据的丢失或者错误读取等情况。
Java使用锁对象,防止代码块受并发访问的干扰。
-
synchronized关键字
-
ReentrantLock类
本文对ReentrantLock进行简要的介绍和学习。
ReentrantLock的使用
作为锁对象,ReentrantLock锁住一个代码块,使其在不同线程调用该代码块实例时,都以串行的形式服务,虽降低了效率,但保证了原子性。用ReentrantLock保护代码块的基本结构如下:
private Lock myLock = new ReentrantLock();
...
myLock.lock();
try {
// do something...
} finally {
myLock.unlock();
}
ReentrantLock是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数(hold count)来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。这个特性是避免同一个线程多次获取同一个锁导致死锁。
ReentrantLock还可以是公平锁,其带boolean参数的构造函数提供了创建公平锁的机制。公平锁偏爱等待时间最长的线程。默认为非公平锁。
需要注意的是,即使使用公平锁,也无法保证线程调度器是公平的。且公平锁太慢,因此不建议使用。
条件对象
通常,线程进入临界区,却发现某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。
以转账为例。我们要避免使用如下代码,因为其有可能在检测通过后被中断,其他线程进行了修改,导致再次运行时,实际检测条件已经不通过。
if(bank.getBalance(from) >= amount) {
bank.transfer(from, to, amount);
}
我们可以通过锁来保护检查与转账动作:
public void transfer(int from, int to, int amount) {
bankLock.lock();
try {
while (accounts[from] < amount) {
// wait...
}
// transfer...
} finally {
bankLock.unlock();
}
}
仅仅使用上面的代码结构,因为ReentrantLock的排他性,其他线程无法获取锁往账户转账,就会使得账户余额一直不足,因而死循环...因此我们需要等待时释放锁。这就是条件对象的作用。
一个锁对象可以有一个或多个相关的条件对象,你可以用newCondition方法获得一个条件。使用条件对象的await方法阻塞该线程并放弃锁。使用对同一个条件对象调用signalAll方法唤醒所有被阻塞线程。
条件对象调用await方法后如果没有收到signalAll的唤醒(或者signal方法,该方法随机唤醒一个线程,容易死锁,慎用),即使当前锁可用,也会一直处于阻塞状态。
通常,对await的调用应该在如下形式的循环体中:
while(!(ok to proceed)) {
condition.await();
}
因此转账代码应保持如下结构:
public void transfer(int from, int to, int amount) {
bankLock.lock();
try {
while (accounts[from] < amount) {
sufficentFunds.await(); // call condition object await
}
// transfer...
sufficentFunds.signalAll(); // call condition object signalAll
} finally {
bankLock.unlock();
}
}