Java并发编程系列:ReentrantLock

简介

ReentrantLocksynchronized都是提供了同步的功能,JDK1.6之后对synchronized性能进行了优化,所以两者的性能上几乎没什么区别,但是ReentrantLock提供了了一些高级功能。

  • 等待可中断:在synchronized中,如果一个线程在等待锁,他只用两种结果,要么获得锁执行完,要么一直保持等待。可中断的等待是通知正在等待的线程,告诉他没必要再等待后。

  • 实现公平锁:公平锁:会按照时间的先后顺序,保证先到先得。特点是它不会产生饥饿现象。而synchroized关键字进行所控制时,锁是非公平的。而重入锁可以设置为公平锁。 public ReetranLock(boolean fair)fairtrue时,表示锁是公平的。实现公平锁必然要求系统维护一个有序队列,因此公平锁的成本比较高,性能也非常低向。默认情况下锁是非公平的。

  • 绑定多个条件:类似于Object类的waitnotify方法,它是与ReentrantLock绑定的条件,可以绑定多个条件。

一个简单的例子

注意:退出临界区要释放锁,否则其他线程就没有机回访问临界区了。

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
public class TestReentrantLock implements Runnable {
public static ReentrantLock rlock = new ReentrantLock();
public static int i=0;
@Override
public void run(){
for(int j=0;j<1000000;j++){
rlock.lock();
try{
i++;
}finally {
rlock.unlock();
}
}
}

public static void main(String args[]) throws InterruptedException {
TestReentrantLock tl = new TestReentrantLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t2.start();
//表示当前线程等待t1执行完
t1.join();
t2.join();
System.out.println(i);
}
}

注意:退出临界区要释放锁,否则其他线程就没有机回访问临界区了。

Lock接口

Lock接口是JDK1.5新加的同步工具接口,它的实现类有ReentrantLockWriteLock等,接口中定义了通用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
void lock();
void unlock();
// 可中断获取锁,与lock()不同之处在于可响应中断操作,即在获取锁的过程中可中断
// synchronized在获取锁时是不可中断的
void lockInterruptibly() throws InterruptedException;
//尝试非阻塞获取锁,调用该方法后立即返回结果,如果能够获取则返回true,否则返回false
boolean tryLock();
//根据传入的时间段获取锁,在指定时间内没有获取锁则返回false,如果在指定时间内当前线程未被中并断获取到锁则返回true
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//获取等待通知组件,该组件与当前锁绑定,当前线程只有获得了锁
//才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
Condition newCondition();

其中lockunlock方法提供了synchronized的功能,其他方法使得同步过程更加的灵活。

什么叫重入锁

一个线程可以多次进入,当然必须多次释放锁。

1
2
3
4
5
6
7
8
9
10
rlock.lock();
rlock.lock();
try{
i++;
}finally {
rlock.unlock();
rlock.unlock();
//如果释放次数多,则回抛出java.lang.IllegalMonitorStateException异常
//rlock.unlock();
}

下面根据案例主要介绍ReentrantLock的用法,在后面的文章中介绍它的实现原理。

中断响应

如果一个线程正在等待锁,那么它可以收到一个通知,被告知无序再等待,可以停止工作了。在synchronized中,
如果一个线程在等待锁,他只用两种结果,要么获得锁执行完,要么一直保持等待。

下面通过一个死锁的例子,介绍中断响应的过程。

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
47
48
public class DeathLock implements Runnable{
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
public DeathLock(int lock){
this.lock = lock;
}
@Override
public void run() {
try{
if(lock==1){
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+" get lock1");
try{
Thread.sleep(1000);
}catch(Exception e){}
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+" get lock2");
}else{
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+" get lock2");
try{
Thread.sleep(1000);
}catch(InterruptedException e){}
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+" get lock1");
}
}catch(InterruptedException e){
e.printStackTrace();
}finally {
//判断当前线程是否拥有该锁
if(lock1.isHeldByCurrentThread())
lock1.unlock();
if(lock2.isHeldByCurrentThread())
lock2.unlock();
System.out.println(Thread.currentThread().getName()+" 退出!");
}
}
public static void main(String args[]) throws InterruptedException{
//这里new出两个实现Runnable的对象是因为为了传进去不同的lock值
Thread thread1 = new Thread(new DeathLock(1),"thread1");
Thread thread2 = new Thread(new DeathLock(1),"thread2");
thread1.start();
thread2.start();
Thread.sleep(1000);
thread2.interrupt();
}
}

执行过程是thread1占用lock1,休眠500毫秒,然后想占用lock2,与此同时,thread2占用lock2,休眠1000毫秒后在请求lock1。可是当thread1,想请求lock2时,已经被thread2占用,因此只能进入阻塞状态,thread2也同理进入阻塞状态。因此进入死锁。但是这里使用了lockInterruptibly()方法。这是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中可以响应中断。在thred2调用interrupt()方法,thread2线程被中断,thread2放弃对lock的申请,同时释放已获得的lock2,所以thread1可以得到lock2继续执行下去。

结果为:

http://static.cyblogs.com/20170809094510953.png

thread2先中断,抛出异常,跳入finally块,释放资源,最终退出。

锁申请等待限时

如果给定一个等待时间,超过时间,让线程自动放弃。

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
public class TimeLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
if(lock.tryLock(2, TimeUnit.SECONDS)){
Thread.sleep(5000);
}else{
System.out.println(Thread.currentThread().getName()+" get lock failed");
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
if(lock.isHeldByCurrentThread())
lock.unlock();
}
}
public static void main(String args[]) {
TimeLock t = new TimeLock();
Thread thread1 = new Thread(t,"thread1");
Thread thread2 = new Thread(t,"thread2");
thread1.start();
thread2.start();
}
}

tryLock()两个参数分别表示等待时长和计时单位,表示线程在请求锁的过程中,最多等待5秒,如果超过改时间则返回false,如果成果获得锁,则返回true
该程序中首先任意一个线程先获得锁,然后休眠5秒,然而它一直占有锁,因此另一个线程无法再2秒内获得锁,因此失败。

tryLock()方法也可以不带参数,这种情况下,当前线程会尝试获得锁,如果锁未被其他线程占用,则申请锁会成功,把那个返回true,如果锁被其他线程占用,则当前线程不会等待,而是立即返回false。这种模式下不会引起线程等待,因此也不会产生死锁。

公平锁

大多数情况下,锁的申请都是非公平的,也就是说,线程1首先申请锁A,接着线程2也请求了锁A,当锁A可用时,线程1,2都有可能获得锁,系统只是在等待队列中随机挑选一个,因此不能保证公平性。
所以有了公平锁,公平锁:会按照时间的先后顺序,保证先到先得。特点是它不会产生饥饿现象。而synchroized关键字进行所控制时,锁是非公平的。而重入锁可以设置为公平锁。
public ReetranLock(boolean fair)
fairtrue时,表示锁是公平的。实现公平锁必然要求系统维护一个有序队列,因此公平锁的成本比较高,性能也非常低向。默认情况下锁是非公平的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FairLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while(true)
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+" get lock !");
}finally{
lock.unlock();
}
}
public static void main(String args[]) {
FairLock f = new FairLock();
Thread thread1 = new Thread(f,"thread1");
Thread thread2 = new Thread(f,"thread2");
thread1.start();
thread2.start();
}
}

部分结果为:

http://static.cyblogs.com/20170809094651573.png

可以看出两个线程基本上是交替获得锁。

Condition条件(搭配重入锁使用)

Condition类似于wait()notify()的功能,它是与重入锁关联使用的。Lock接口中提供了newCondition()方法,该方法可以返回绑定到此LockCondition实例。

方法解释:
await方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()signalAll()方法时,线程会重新获得锁并继续执行,当线程被中断时,也能跳出等待。与Objectwait()方法相似。
singal()方法用于唤醒一个在等待中的线程。
注意:以上连个方法调用之前必须当前线程拥有锁。否则抛出IllegalMonitorStateException异常

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
public class ConditionDemo implements Runnable {
public static ReentrantLock lock = new ReentrantLock(true);
public static Condition condition = lock.newCondition();
@Override
public void run() {
try {
lock.lock();
//当前线程释放锁,进入等待状态
condition.await();
System.out.println(Thread.currentThread().getName()+" get lock !");
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
public static void main(String args[]) throws InterruptedException {
ConditionDemo t = new ConditionDemo();
Thread thread1 = new Thread(t,"thread1");
thread1.start();
Thread.sleep(2000);
//当thread1进入处于等待状态,main线程获得锁
lock.lock();
System.out.println(Thread.currentThread().getName() + " get lock !");
condition.signal();
lock.unlock();
}
}
/* 结果:
main get lock !
thread1 get lock !
*/

thread1线程调用await时,要求线程持有相关的重入锁,调用后,线程释放这把锁,同理signal方法调用时,也要求线程先获得相关的锁,在signal方法调用后,系统会从当前的Condition对象的等待队列中唤醒一个线程,一旦线程唤醒,它会重新尝试获得之前绑定的锁,一旦成功获取await方法返回,继续执行。在调用signal后先睡眠2秒,并且保持了锁,释放了锁之后,await方法获取锁后才得以返回继续执行。因此打印出来的时间差为2000毫秒。

最后

上面结合例子介绍了ReentrantLock主要的用法,还有一些很有意思的用法,比如正在等待锁的线程,当前线程是否拥有锁等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//查询当前线程保持此锁的次数。
int getHoldCount()
//返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。
protected Thread getOwner();
//返回一个 collection,它包含可能正等待获取此锁的线程,其内部维持一个队列,这点稍后会分析。
protected Collection<Thread> getQueuedThreads();
//返回正等待获取此锁的线程估计数。
int getQueueLength();
// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
protected Collection<Thread> getWaitingThreads(Condition condition);
//返回等待与此锁相关的给定条件的线程估计数。
int getWaitQueueLength(Condition condition);
// 查询给定线程是否正在等待获取此锁。
boolean hasQueuedThread(Thread thread);
//查询是否有些线程正在等待获取此锁。
boolean hasQueuedThreads();
//查询是否有些线程正在等待与此锁有关的给定条件。
boolean hasWaiters(Condition condition);
//如果此锁的公平设置为 true,则返回 true。
boolean isFair()
//查询当前线程是否保持此锁。
boolean isHeldByCurrentThread()
//查询此锁是否由任意线程保持。
boolean isLocked()

进入ReentrantLock的源码发现,ReentrantLock类的绝大部分功能是通过它的内部类Sync来实现的,而Sync又继承了AbstractQueuedSynchronizer类。这就是大名鼎鼎的AQSDoug Lea最著名的作品,后面的文章分析它的精华所在。

参考地址

如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员简栈文化-小助手(lastpass4u),他会拉你们进群。

简栈文化服务订阅号