今天在看java并发编程一书时,看到关于显示锁的介绍,受益良多,在此做一个总结整理。
首先是Lock接口中的方法:
与内置的加锁机制不同的是Lock的加锁和解锁都是显示的。ReentrantLock实现了Lock,并提供了与synchronized相同的可重入性,互斥性和内存可见性。
1.ReentrantLock的使用方法:
Lock lock = new ReentrantLock();
lock.lock();
try {
//
} finally {
lock.unlock();
}
必须要在finally块中释放锁,否则不会自动清除锁,锁将永远得不到释放
2 为什么要创建和内置锁如此相似的新加锁机制呢?
ReentrantLock并不是一种替代内置加锁的方法,而是当内置锁机制不适用时,作为一种可选择的高级功能。
在大多数情况下,内置锁都能够很好的解决工作,但在功能上存在一些局限性,例如:无法中断一个正在等待获取锁的线程,无法在请求获取一个锁时无限的等待下去。
利用轮询锁和定时锁来避免死锁的发生: 利用Lock的tryLock机制来实现轮询锁。
(在内置锁中死锁是一个严重的问题,恢复程序的唯一的方法就是重新启动程序,而防止死锁的唯一方式就是构造程序时避免不一致的锁顺序。关于死锁的介绍和如何引发,我在线程基本介绍 二 中有相关的介绍)。
使用tryLock方法来避免死锁:
package com.wc.study.lock.demo1;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
public static Lock lock = new ReentrantLock();
private String name;
private double money;
public void addMoney(double money){
lock.lock();
try {
this.money += money;
} finally {
lock.unlock();
}
}
public void subtractMoney(double money){
lock.lock();
try {
this.money -= money;
} finally {
lock.unlock();
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public static void main(String[] args) {
//测试方法的可行性
Account account = new Account();
account.setName("张三");
account.setMoney(100);
Thread thread1 = new Thread( () -> {
account.addMoney(100);
});
Thread thread2 = new Thread( () -> {
account.addMoney(100);
});
Thread thread3 = new Thread( () -> {
account.addMoney(100);
});
Thread thread4 = new Thread( () -> {
account.subtractMoney(100);
});
thread1.start();
thread2.start();
thread4.start();
thread3.start();
System.out.println(account.getName() + "账户上的钱为: " +account.getMoney()); //张三账户上的钱为: 200.0
}
}
package com.wc.study.lock.demo1;
import java.util.concurrent.TimeUnit;
public class ReentrantLockDemoTest {
private boolean transferMoney(Account fromAccount,Account toAccount,double money,long timeout,TimeUnit timeUnit) throws InterruptedException{
long startTime = System.currentTimeMillis();
long stopTime = timeUnit.toNanos(timeout);
while(true){
if(fromAccount.lock.tryLock()){
try {
if(toAccount.lock.tryLock()){
try {
//同时获取到了两个锁
fromAccount.subtractMoney(money);
toAccount.addMoney(money);
return true;
} finally {
toAccount.lock.unlock();
}
}
} finally {
fromAccount.lock.unlock();
}
}
long endTime = System.currentTimeMillis();
if(timeUnit.toNanos(endTime - startTime) > stopTime){
return false;
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemoTest reentrantLockDemoTest = new ReentrantLockDemoTest();
Account fromAccount = new Account();
fromAccount.setName("张三");
fromAccount.setMoney(2000);
Account toAccount = new Account();
toAccount.setName("李四");
toAccount.setMoney(200);
boolean flag = reentrantLockDemoTest.transferMoney(fromAccount, toAccount, 500, 2, TimeUnit.SECONDS);
System.out.println("转账结果:" + flag + " 转账方账户: " + fromAccount.getMoney() + "收款人 :" + toAccount.getMoney());
}
}
可以看到在上面的例子中使用tryLock来获取两个锁,如果不能获取到两个锁那么就回退重新尝试,长时间获取不到锁,在循环中根据程序的运行时间来限制避免阻塞太长的时间,使该操作平缓的失败 。而使用内置锁之后,在开始请求锁之后,操作无法取消,因此内置锁很难实现带有时间限制的操作。
3. 内置锁和显示锁的优缺点:
在内置锁中,锁的释放和获取都是在代码块中的,自动释放的所操作简化了代码分析,避免了可能的编码错误。 但是也可能存在需要更细粒度的加锁规则。使用内置锁在开始请求之后,这个操作无法取消。而且在使用内置锁的时候不可中断的阻塞机制使得实现可以取消的任务变得复杂。
使用显示锁。可以使用tryLock来实现带有时间限制的加锁,定时的tryLock可以响应中断或者使用lock的lockInterruptibly(), 此方法可以在获取到锁的同时保持对中断的响应。灵活的加锁释放锁机制可以实现更细力度的加锁规则,提高代码的可伸缩性。但同时可能忘记释放锁造成程序的运行错误。
4. 在synchronized和ReentrantLock之间做出选择
ReentrantLock在加锁和内存上提供的语义与内置锁相同,此外他还提供了一些其他的功能:定时的锁等待,可中断的锁等待,公平性,实现非快结构的加锁。性能在java6之后略有胜出,既然这么多好处为什么不放弃使用snchronized呢?与显示锁相比,内置锁仍然后很大的好处,内置锁为许多开发人员所熟悉,在很多的程序中已经使用了内置锁,再换使用显示锁会造成好大的困扰,也容易发生错误。而且ReentrantLock的危险性更高,如果忘记在finally中释放锁,代码虽然表面上可以运行,但是实际上埋下了一颗“炸弹”. 。 所以可以在一些内置锁无法实现的功能时再使用ReentrantLock,将其作为一种高级工具来使用。