JAVA的并发包:Lock和Conidtion

再造管程的意义

java的管程的关键字是synchronized,在1.6后对管程做了升级,引入了偏向锁,轻量级锁的概念,性能几乎和Lock相近。然而在某些场景还是没法取到Lock。
主要集中synchronized如下的几个痛点

  1. 无法主动响应中断:在synchronzed代码块中,如果已经获取了资源a,在获取b失败后,就直接进入了阻塞,无法主动响应中断,这时候遇到死锁我们是无法处理的;
  2. 无法设置过期时间:synchronzed无法设置过期时间;
  3. 无法在非阻塞情况下竞争锁资源:
1
2
3
4
void lockInterruptibly() throws InterruptedException;
void lock(long time, TimeUnit unit)
boolean tryLock(long time, TimeUnit unit)
boolean tryLock();

lock的原理

我们以ReentrantLock为例子,类似管程,每个资源都一个等待队列。它是用一个双向链表实现的等待队列,同时每个链表的节点包含一个volatile的变量state作为锁标记位:0-release,1-lock。在调用lock/unlock方法中会通过cas修改volatile来实现state的变化。他是如何来保证在上锁解锁时候的可见性的呢?我们用下面的代码说明
rtl.lock上锁–>之后value+=1–>解锁,他们在并发场景下根据HB原则.

  1. 顺序原则:线程1中:rtl.lock (HB于) value+=1 ;value+=1(HB于)rtl.unlock()
  2. volatile的原则:此情况是针对线程1的rtl.unlock已经修改state成功但是由于可见性线程2的rtl.lock也申请锁的时候是否会失败?答案是不会,因为但当线程1的rtl.unlock修改state()和线程2的lock修改sate()发生冲突时候,由于rlt.lock需要先读取state,根据volatile的原则,写HB于读,所以unlock一定HB于lock,这时候lock一定读到的是最新的值
  3. 传递性原则:由于线程1的value+=1(HB于)rtl.unlock(),所以线程1的value+=1一定(HB于)于线程2的rtl.lock()
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();
}
}
}

lock的特点

可重入锁

lock和synchronized一样都是可重入锁:即同一个线程在获取锁之后,可以重复获取锁资源。(不可重入锁:即在此尝试获取锁资源会进入阻塞)

公平锁和非公平数锁

lock可以实现贡公平锁,如:ReentrantLock的构造方法,见下面代码,具体可以看Sync的代码,如果是非公平锁,有可能最后获取资源的反而最先释放。详细可以去看
ReentrantLock内部类Sync的tryAcquire俩种实现。其中FairSync里有一个判断hasQueuedPredecessors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}

/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

Lock中的等待通知机制-Condition

管程中最主要的一个功能就是wait的等待通知机制。那么lock的等待通知机制则是Condition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ReentrantLock lock=new ReentrantLock();
Condition condition=lock.newCondition();

public void test(){

//阻塞等同于 object.wait()
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}

//阻塞等同于 object.notify() object.notifyAll();
condition.signal();
condition.signalAll();
}

操作异步转同步

有些操作本身是异步的,但是我们在使用中需要同步的场景,比如dubbo中我们的客户端通过tcp发送数据给服务端后,我们需要接收到返回值才能继续处理请求。而tcp本身就是异步的。这时候就需要同步转异步操作是如何实现的呢,见DefaultFeature,伪代码如下

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
public class Request {

private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();

private Object ret;

public Request get() {
lock.lock();
try {
//等待结果转同步
if (!isDone()) {
condition.await(1000L,TimeUnit.MILLISECONDS);
}

if (!isDone()) {
throw new RuntimeException("timeout");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}

return this;
}

private boolean isDone() {
return true;
}


public void doReceived(Object response) {
lock.lock();
try {
this.ret = response;
//在别的线程获取结果后异步通知
condition.signalAll();
} finally {
lock.unlock();
}
}
}

lock的最佳实践

永远只用于锁要修改的成员表变量
永远只在访问可变的成员标量加锁
永远不要在调用其他对象的方法加锁—因为你不确定他内部是如何实现的

推荐阅读:

Doug Lea《Java 并发编程:设计原则与模式》