什么是显示锁?显示锁的作用是?
答:Java
并发包中的显示锁接口和类位于包java.util.concurrent.locks
下,主要接口和类有:- 锁接口
Lock
,主要实现类是ReentrantLock
。 - 读写锁接口
ReadWriteLock
,主要实现类是ReentrantReadWriteLock
。
- 锁接口
Java
并发包中的显示锁可以解决synchronized
的一些限制。
显示锁接口
Lock
的定义是?
答:1
2
3
4
5
6
7
8public interface Lock {
void lock(); // 普通的获取锁方法,会阻塞直到成功
void lockInterruptibly() throws InterruptedException; // 与 lock() 不同的是,它可以响应中断,如果被其他线程中断了,则抛出 InterruptedException 异常
boolean tryLock(); // 只是尝试获取锁,立即返回,不阻塞,如果获取成功,返回 true,否则返回 false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 先尝试获取锁,如果能成功则立即返回 true,否则阻塞等待,但等待的最长时间由指定的参数设置,在等待的同时响应中断,如果发生了中断,则抛出 InterruptedException,如果在等待的时间内获得了锁,返回 true,否则返回 false
void unlock(); // 普通的释放锁方法
Condition newCondition; // 新建一个条件,一个 Lock 可以关联多个条件
}
显示锁接口
Lock
与synchronized
相比如何?
答:相比synchronized
,显示锁支持以非阻塞方式获取锁、可以响应中断、可以限时,这使它灵活很多。可重入锁
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;
}
}
使用
tryLock()
可以避免死锁吗?
答:- 使用
tryLock()
可以避免死锁。 - 在持有一个锁获取另一个锁而获取不到的时候,可以释放已持有的锁,给其他线程获取锁的机会,然后重试获取所有锁。
- 使用
【笔试题】手写一个使用
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
35
36// 表示账户的类
public 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();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 在账户之间转账,需要两个账户都锁定,如果不使用 tryLock(),而直接使用 lock,则转账的错误写法如下
public 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
30
31
// 模拟账户转账的死锁过程
public 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();
}
}
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
// 使用 tryLock() 进行修改
public 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。不管怎样,结束都会释放所有锁
1
2
3
4
5
6
7
8
9
10
// transfer() 方法可以循环调用 tryTransfer() 以避免死锁
public 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);
}
ReentrantLock
的实现原理?
答:- 在最底层,它依赖于
CAS
方法。 - 另外,它依赖于类
LockSupport
和AQS
中的一些方法。
- 在最底层,它依赖于
LockSupport
的基本方法有哪些?
答:1
2
3
4
5
6
7
8
9// 使当前线程放弃 CPU,进入等待状态(WAITING),操作系统不再对它进行调度
// 不同于 Thread.yield(),yield() 只是告诉操作系统可以先让其他线程运行,但自己依然是可运行状态
// park() 会放弃调度资格,使线程进入 WAITING 状态
// park() 是响应中断的,当有中断发生时,park() 会返回,线程的中断状态会被设置
// park() 可能会无缘无故地返回,线程应该重新检查 park() 等待的条件是否满足
public static void park()
public static void parkNanos(long nanos) // 可以指定等待的最长时间,参数是相对于当前时间的纳秒数
public static void parkUntil(long deadline) // 可以指定最长等到什么时候,参数是绝对时间,是相对于纪元时的毫秒数。当等待超时的时候,它们也会返回
public static void unpark(Thread thread) // 通知操作系统调度,使参数指定的线程恢复可运行状态
这些
park()/unpark()
是怎样实现的?
答:- 与
CAS
方法一样,它们也调用了Unsafe
类中对应的方法。 Unsafe
类最终调用了操作系统的API
,从程序员的角度,我们可以认为LockSupport
中的这些方法就是基本操作。
- 与
什么是
AQS
?
答:- 利用
CAS
和LockSupport
提供的基本方法,就可以用来实现ReentrantLock
了。 Java
中还有很多其他并发工具,如ReentranReadWriteLock
、Semaphore
、CountDownLatch
,它们的实现有很多类似的地方。- 为了复用代码,
Java
提供了一个抽象类AbstractQueuedSynchronizer
,简称AQS
。AQS
简化了并发工具的实现,AQS
的整体实现比较复杂。 AQS
封装了一个状态,给子类提供了查询和设置状态的方法。- 用于实现锁时,
AQS
可以保存锁的当前持有线程,提供了方法进行查询和设置。 ReentrantLock
内部使用AQS
。
- 利用
ReentrantLock
的lock()
方法的基本过程是怎样的?
答:能获得锁就立即获得,否则加入等待队列,被唤醒后检查自己是否是第一个等待的线程,如果是且能获得锁,则返回,否则继续等待。这个过程中如果发生了中断,lock()
会纪录中断标志位,但不会提前返回或抛出异常。ReentrantLock
的带参数的构造方法为什么默认不保证公平?
答:- 保证公平整体性能比较低,低的原因不是这个检查慢,而是会让活跃线程得不到锁,进入等待状态,引起频繁上下文切换,降低了整体的效率。
- 通常情况下,谁先运行关系不大,而且长时间运行,从统计角度而言,虽然不保证公平,也基本是公平的。
- 需要说明的是,即使
fair
为true
,ReentrantLock
中不带参数的tryLock()
方法也是不保证公平的,它不会检查是否有其他等待时间更长的线程。
对比一下
ReentrantLock
和synchronized
?
答:- 相比
synchronized
,ReentrantLock
可以实现与synchronized
相同的语义,而且支持以非阻塞方式获取锁,可以响应中断,可以限时,更为灵活;不过,synchronized
的使用更为简单,写的代码更少,也更不容器出错。 synchronized
代表一种声明式编程思维,程序员更多的是表达一种同步声明,由Java
系统负责具体实现,程序员不知道其实现细节;显示锁代表一种命令式编程思维,程序员实现所有细节。- 声明式编程的好处除了简单,还在于性能,在较新版本的
JVM
上,ReentranLock
和synchronized
的性能是接近的,但Java
编译器和虚拟机可以不断优化synchronized
的实现。比如,自动分析synchronized
的使用,对于没有锁竞争的场景,自动省略对锁获取/释放的调用。 - 简单总结下,能用
synchronized
就用synchronized
,不满足要求时再考虑ReentranLock
。
- 相比