1. 什么是显示锁,显示锁的作用是
Java 并发包中的显示锁接口和类位于包
java.util.concurrent.locks
下,主要接口和类有- 锁接口
Lock
,主要实现类是ReentrantLock
- 读写锁接口
ReadWriteLock
,主要实现类是ReentrantReadWriteLock
- 锁接口
Java 并发包中的显示锁可以解决
synchronized
的一些限制
2. 显示锁接口 Lock
的定义是
1 | public interface Lock { |
3. 显示锁接口 Lock
与 synchronized
相比如何
- 相比
synchronized
,显示锁接口Lock
有如下特点- 显示锁支持以非阻塞方式获取锁
- 可以响应中断
- 可以限时
4. 可重入锁 ReentrantLock
的基本用法是
Lock
接口的主要实现类是ReentrantLock
,它的基本用法lock()/unlock()
实现了与synchronized
一样的语义,包括- 可重入,一个线程在持有一个锁的前提下,可以继续获得该锁
- 可以解决竞态条件问题
- 可以保证内存可见性
ReentrantLock
有两个构造方法:public ReentrantLock()/public ReentrantLock(boolean fair)
- 参数
fair
表示是否公平,不指定的情况下,默认为false
,表示不公平 - 公平是指,等待时间最长的线程优先获得锁
- 保证公平会影响性能,一般也不需要,所以默认不保证。
synchronized
锁也是不保证公平的
- 参数
使用显示锁,一定要记得调用
unlock()
。一般而言,应该将lock()
之后的代码包装到try
语句内,在finally
语句内释放锁。比如,使用ReentrantLock
实现Counter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class Counter {
private final Lock lock = new ReentrantLock();
private volatile int count;
public void incr() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
5. 使用 tryLock()
可以避免死锁吗
- 可以
- 原因:在持有一个锁获取另一个锁而获取不到的时候,可以释放已持有的锁,给其他线程获取锁的机会,然后重试获取所有锁
6. 写一个使用 tryLock()
避免死锁的银行转账 Demo
表示账户的类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35public class Account {
private Lock lock = new ReentrantLock();
private volatile double money; //当前账户余额
public Account(double initialMoney) {
this.money = initialMoney;
}
public void add(double money) { //存钱
lock.lock();
try {
this.money += money;
} finally {
lock.unlock();
}
}
public void reduce(double money) { //取钱
lock.lock();
try {
this.money -= money;
} fianlly {
lock.unlock();
}
}
public double getMoney() {
return money;
}
void lock() {
lock.lock();
}
void unlock() {
lock.unlock();
}
boolean tryLock() {
return lock.tryLock();
}
}在账户之间转账,需要两个账户都锁定,如果不使用
tryLock()
,而直接使用lock()
,则转账的错误写法如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class AccountMgr {
public static class NoEnoughMoneyException extends Exception {}
public static void transfer(Account from, Account to, double money) throws NoEnoughMoneyException {
from.lock();
try {
to.lock();
try {
if(from.getMoney() >= money) {
from.reduce(money);
to.add(money);
} else {
throw new NoEnoughMoneyException();
}
} finally {
to.unlock();
}
} finally {
from.unlock();
}
}
}- 这么写是有问题的,如果两个账户都同时给对方转账,都先获取了第一个锁,则会发生死锁
模拟账户转账的死锁过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30public static void simulatDeadLock() {
final int accountNum = 10;
final Account[] accounts = new Account[accountNum]; //10 个账户
final Random rnd = new Random();
for(int i = 0; i<accountNum; i++) {
account[i] = new Account(rnd.nextInt(10000));
}
int threadNum = 100;
Thread[] threads = new Thread[threadNum]; //100 个线程
for(int i=0; i<threadNum; i++) {
thread[i] = new Thread() {
public void run() {
int loopNum = 100;
for(int k=0; k<loopNum; k++) { //100 次循环
int i = rnd.nextInt(accountNum);
int j = rnd.nextInt(accountNum);
int money = rnd.nextInt(10);
if(i!=j) {
try {
transfer(accounts[i], accounts[j], money); //每次循环中,随机挑选两个账户进行转账
} catch(NoEnoughMoneyException e) {
}
}
}
}
};
thread[i].start();
}
}使用
tryLock()
进行修改1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public static boolean tryTransfer(Account from, Account to, double money) throws NoEnoughMoneyException {
if(from.tryLock()) {
try {
if(to.tryLock()) {
try {
if(from.getMoney() >= money) {
from.reduce(money);
to.add(money);
} else {
throw new NoEnoughMoneyException();
}
return true;
} finally {
to.unlock();
}
}
} finally {
from.unlock();
}
}
return false;
}- 如果两个锁都能够获得,且转账成功,则返回
true
,否则返回false
。不管怎样,结束都会释放所有锁
- 如果两个锁都能够获得,且转账成功,则返回
transfer()
方法可以循环调用tryTransfer()
以避免死锁1
2
3
4
5
6
7
8
9public static void transfer(Account from, Account to, double money) throws NoEnoughMoneyException {
boolean success = false;
do {
success = tryTransfer(from, to, money);
if(!success) {
Thread.yield();
}
} while (!success);
}
7. ReentrantLock
的实现原理
- 在最底层,它依赖于
CAS
方法 - 另外,它依赖于类
LockSupport
和AQS
中的一些方法
8. LockSupport
的基本方法有哪些
1 | //使当前线程放弃 CPU,进入等待状态(WAITING),操作系统不再对它进行调度 |
9. 这些 park()/unpark()
是怎样实现的
- 与
CAS
方法一样,它们也调用了Unsafe
类中对应的方法 Unsafe
类最终调用了操作系统的 API,从程序员的角度,我们可以认为LockSupport
中的这些方法就是基本操作
10. 什么是 AQS
- 利用
CAS
和LockSupport
提供的基本方法,就可以用来实现ReentrantLock
了 - Java 中还有很多其他并发工具,如
ReentranReadWriteLock
、Semaphore
、CountDownLatch
,它们的实现有很多类似的地方 - 为了复用代码,Java 提供了一个抽象类
AbstractQueuedSynchronizer
,简称AQS
。AQS
简化了并发工具的实现,AQS
的整体实现比较复杂 AQS
封装了一个状态,给子类提供了查询和设置状态的方法- 用于实现锁时,
AQS
可以保存锁的当前持有线程,提供了方法进行查询和设置 ReentrantLock
内部使用AQS
11. ReentrantLock
的 lock()
方法的基本过程是怎样的
- 能获得锁就立即获得,否则加入等待队列
- 被唤醒后检查自己是否是第一个等待的线程,如果是且能获得锁,则返回,否则继续等待
- 这个过程中如果发生了中断,
lock()
会纪录中断标志位,但不会提前返回或抛出异常
12. ReentrantLock
的带参数的构造方法为什么默认不保证公平
- 保证公平整体性能比较低,低的原因不是这个检查慢,而是会让活跃线程得不到锁,进入等待状态,引起频繁上下文切换,降低了整体的效率
- 通常情况下,谁先运行关系不大,而且长时间运行,从统计角度而言,虽然不保证公平,也基本是公平的
- 需要说明的是,即使
fair
为true
,ReentrantLock
中不带参数的tryLock()
方法也是不保证公平的,它不会检查是否有其他等待时间更长的线程
13. 对比一下 ReentrantLock
和 synchronized
- 相比
synchronized
,ReentrantLock
可以实现与synchronized
相同的语义,而且支持以非阻塞方式获取锁,可以响应中断,可以限时,更为灵活;不过,synchronized
的使用更为简单,写的代码更少,也更不容器出错 synchronized
代表一种声明式编程思维,程序员更多的是表达一种同步声明,由 Java 系统负责具体实现,程序员不知道其实现细节;显示锁代表一种命令式编程思维,程序员实现所有细节- 声明式编程的好处除了简单,还在于性能,在较新版本的 JVM 上,
ReentranLock
和synchronized
的性能是接近的,但 Java 编译器和虚拟机可以不断优化synchronized
的实现。比如,自动分析synchronized
的使用,对于没有锁竞争的场景,自动省略对锁获取/释放的调用 - 简单总结下,能用
synchronized
就用synchronized
,不满足要求时再考虑ReentranLock