摘录自 极客时间

管程介绍

Java采用了管程(Monitor)技术,synchronized关键字以及wait()notify()notifyAll()都是管程的组成部分。

管程解决互斥问题就是将共享变量以及对共享变量的操作统一封装起来。
管程中引入了条件变量的概念,每一个条件变量都对应了一个等待队列。条件变量和等待队列的作用就是为了解决线程同步的问题。

下图为MESA模型

p1

如图所示,如果线程从条件变量等待队列出来,那么它会回到入口等待队列中。

下面是一段样例代码

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
37
38
39
40
41
42
43
44
45
46
public class BlockedQueue<T>{
final Lock lock =
new ReentrantLock();
// 条件变量:队列不满
final Condition notFull =
lock.newCondition();
// 条件变量:队列不空
final Condition notEmpty =
lock.newCondition();

// 入队
void enq(T x) {
lock.lock();
try {
// 一定要用while,使用if的话会出问题,
// 因为java mesa模型线程被唤醒后是回到入口等待队列去竞争的,
// 可能拿到锁以后又不满足条件了,所以必须使用while
while (队列已满){
// 等待队列不满
// await和wait()语义相同
notFull.await();
}
// 省略入队操作...
// 入队后, 通知可出队
// signal()和notify()语义相同
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 出队
void deq(){
lock.lock();
try {
while (队列已空){
// 等待队列不空
notEmpty.await();
}
// 省略出队操作...
// 出队后,通知可入队
notFull.signal();
}finally {
lock.unlock();
}
}
}

这里要注意的是线程等待和通知需要调用 await()signal()signalAll(),语义和wait()notify()notifyAll()相同。

wait正确姿势

1
2
3
while(条件不满足) {
wait();
}

notify()和notifyAll()

尽量使用notify(),除非

  • 所有等待线程拥有相同的等待条件
  • 所有等待线程被唤醒后,执行相同的操作
  • 只需要唤醒一个线程

PS: Java内置的管程(synchronized)对MESA模型进行了精简。Java语言内置的管程中只有一个条件变量。

并发包中的管程

并发编程的两大问题:互斥、同步。
有了synchronized还要存在Lock的原因是synchronized申请资源失败会进入阻塞,也不会释放线程已经占有的资源。不能实现破坏不可抢占条件
重新设计一把互斥锁来解决这个问题,有三种方案:

  • 能够响应中断
  • 支持超时
  • 非阻塞地获取锁

这三个方案是“重复造轮子”的原因,刚好对应了Lock接口的三个方法。

1
2
3
4
5
6
7
8
// 支持中断的 API
void lockInterruptibly()
throws InterruptedException;
// 支持超时的 API
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
// 支持非阻塞获取锁的 API
boolean tryLock();

保证可见性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class X {
private final Lock rtl = new ReentrantLock();
int value;
public void addOne() {
// 获取锁
rtl.lock();
try {
value+=1;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}

可见性的保证来自于volatile相关的Happens-Before规则。Java SDK 里面的ReentrantLock内部有一个volatile的成员变量state,获取锁的时候回写它的值,解锁的时候也会写它的值。
根据下面这三条规则:

  • 顺序性规则:对于线程 T1,value+=1 Happens-Before 释放锁的操作 unlock()
  • volatile变量规则:由于 state = 1 会先读取 state,所以线程 T1 的 unlock() 操作 Happens-Before 线程 T2 的 lock() 操作;
  • 传递性规则:线程 T1 的 value+=1 Happens-Before 线程 T2 的 lock() 操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class SampleLock {
    volatile int state;
    // 加锁
    lock() {
    // 省略代码无数
    state = 1;
    }
    // 解锁
    unlock() {
    // 省略代码无数
    state = 0;
    }
    }

公平锁和费公平锁

fail

1
2
3
4
5
6
7
8
9
// 无参构造函数:默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 根据公平策略参数创建锁
public ReentrantLock(boolean fair){
sync = fair ? new FairSync()
: new NonfairSync();
}

入口等待队列的锁都对应着一个等待队列,如果一个线程没有获得锁,就会进入等待队列,当有线程释放锁的时候,就需要从等待队列中唤醒一个等待的线程。如果是公平锁,唤醒的策略就是谁等待的时间长,就唤醒谁,很公平;如果是非公平锁,则不提供这个公平保证,有可能等待时间短的线程反而先被唤醒。

用锁的最佳实践

  • 永远只在更新对象的成员变量时加锁
  • 永远只在访问可变的成员变量时加锁
  • 永远不在调用其他对象的方法时加锁

思考小题

tryLock() 支持非阻塞方式获取锁,下面这段关于转账的程序就使用到了 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
class Account {
private int balance;
private final Lock lock
= new ReentrantLock();
// 转账
void transfer(Account tar, int amt){
while (true) {
if(this.lock.tryLock()) {
try {
if (tar.lock.tryLock()) {
try {
this.balance -= amt;
tar.balance += amt;
} finally {
tar.lock.unlock();
}
}//if
} finally {
this.lock.unlock();
}
}//if
}//while
}//transfer
}

存在活锁,A转B,B转A就血G。