目录
1.1、ReentrantLock与synchronized比较
一、概述
在Java5之前,Java多线程中可以使用synchronized隐式锁实现线程之间同步互斥。Java5中提供了Lock类(显示锁)也可以实现线程间的同步,而且在使用上更加方便。本文主要研究ReentrantLock的使用。
ReentrantLock是一个独占的、可重入的、公平&非公平的锁。
实现原理:AQS+CAS+park/unpark+自旋
1.1、ReentrantLock与synchronized比较
1)ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。
2)ReentrantLock和synchronized都是可重入的。synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。
3)和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。在创建ReentrantLock的时候通过传进参数true
创建公平锁,如果传入的是false
或没传参数则创建的是非公平锁。
4)当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()
。该方法可以用来解决死锁问题。
5)ReentrantLock还给我们提供了获取锁限时等待的方法tryLock()
,可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题。
6)使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制。ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。
1.2、公平锁的执行过程
1.3、部分方法
1、lock() 获得锁
2、lockInterruptibly()获得可中断锁
3、tryLock()只有在调用时其他线程没有持有锁的情况下才获取锁
4、tryLock(long timeout, TimeUnit unit)如果在给定的等待时间内没有其他线程持有锁,则获取该锁。
5、unlock()释放锁
二、源码解析
2.1、公平锁与非公平锁
公平锁部分源码:
final void lock() {
acquire(1);
}protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}......
return false;
}
非公平锁部分源码:
final void lock() {
// 第一次尝试争抢锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {// 第二次尝试挣抢锁,不判断等待队列是否有线程等待获得锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
......
return false;
}
可以看出,非公平锁在加锁过程中会进行两次锁的争抢,它不管AQS等待队列中存不存在等待锁的线程,都会尝试去持有锁。
2.2、lock与lockInterruptibly
它们的不同点是,在等待获取锁的过程中,对其中断后的处理方式不同。
lock部分源码
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}static void selfInterrupt() {
Thread.currentThread().interrupt();
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
......
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
lockInterruptibly部分源码
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
......
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
三、示例
3.1、公平锁
package demo8;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest {
static Lock lock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++){
new Thread(new ThreadDemo(i)).start();
}
}
static class ThreadDemo implements Runnable {
Integer id;
public ThreadDemo(Integer id) {
this.id = id;
}
@Override
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<2;i++){
lock.lock();
System.out.println("获得锁的线程:"+id);
lock.unlock();
}
}
}
}
执行结果:
获得锁的线程:3
获得锁的线程:4
获得锁的线程:1
获得锁的线程:2
获得锁的线程:0
获得锁的线程:3
获得锁的线程:4
获得锁的线程:1
获得锁的线程:2
获得锁的线程:0
3.2、非公平锁
package demo8;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest {
static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++){
new Thread(new ThreadDemo(i)).start();
}
}
static class ThreadDemo implements Runnable {
Integer id;
public ThreadDemo(Integer id) {
this.id = id;
}
@Override
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<2;i++){
lock.lock();
System.out.println("获得锁的线程:"+id);
lock.unlock();
}
}
}
}
执行结果:
获得锁的线程:0
获得锁的线程:0
获得锁的线程:4
获得锁的线程:4
获得锁的线程:2
获得锁的线程:2
获得锁的线程:3
获得锁的线程:3
获得锁的线程:1
获得锁的线程:1
说明:公平锁和非公平锁该如何选择
- 如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁,这就是非公平锁的“饥饿”问题。
- 大部分情况下我们使用非公平锁,因为其性能比公平锁好很多。但是公平锁能够避免线程饥饿,某些情况下也很有用。
3.3、可中断锁
package com.flychuer.demo3;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread() {
public void run() {
try {
lock.lockInterruptibly();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
try {
// 无限循环,一直占用锁
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
};
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
Thread t2 = new Thread() {
public void run() {
try {
// 因线程t1一直占用锁,t2一直无法获得锁
lock.lockInterruptibly();
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (InterruptedException e1) {
e1.printStackTrace();
} finally {
lock.unlock();
}
}
};
t2.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
Thread t3 = new Thread() {
public void run() {
System.out.println("准备中断t2");
// 中断t2
t2.interrupt();
System.out.println("中断t2");
}
};
t3.start();
}
}
执行结果:
准备中断t2
中断t2
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(Unknown Source)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(Unknown Source)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(Unknown Source)
at com.flychuer.demo3.CountDownLatchTest$2.run(CountDownLatchTest.java:39)
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(Unknown Source)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(Unknown Source)
at java.util.concurrent.locks.ReentrantLock.unlock(Unknown Source)
at com.flychuer.demo3.CountDownLatchTest$2.run(CountDownLatchTest.java:50)
从执行结果可以看出,在线程t2无限等待锁的过程中,被线程t3中断了,lockInterruptibly()则直接抛出中断异常,由上层调用者区去处理中断。
将lockInterruptibly()换成lock()后,再执行以下代码
package com.flychuer.demo3;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread() {
public void run() {
lock.lock();
try {
// 无限循环,一直占用锁
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
};
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
Thread t2 = new Thread() {
public void run() {
try {
// 因线程t1一直占用锁,t2一直无法获得锁
lock.lock();
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
};
t2.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
Thread t3 = new Thread() {
public void run() {
System.out.println("准备中断t2");
// 中断t2
t2.interrupt();
System.out.println("中断t2");
}
};
t3.start();
}
}
执行结果如下,并且程序一直执行,因为线程t1是死循环,一直不释放锁,所以t2被中断也不会立即抛出异常,只有等t2获得锁后才会抛出异常。
准备中断t2
中断t2
结论:ReentrantLock的中断和非中断加锁模式的区别在于:线程尝试获取锁操作失败后,在等待过程中,如果该线程被其他线程中断了,它是如何响应中断请求的。lock方法会忽略中断请求,继续获取锁直到成功;而lockInterruptibly则直接抛出中断异常来立即响应中断,由上层调用者处理中断。
那么,为什么要分为这两种模式呢?这两种加锁方式分别适用于什么场合呢?根据它们的实现语义来理解,我认为lock()适用于锁获取操作不受中断影响的情况,此时可以忽略中断请求正常执行加锁操作,因为该操作仅仅记录了中断状态(通过Thread.currentThread().interrupt()操作,只是恢复了中断状态为true,并没有对中断进行响应)。如果要求被中断线程不能参与锁的竞争操作,则此时应该使用lockInterruptibly方法,一旦检测到中断请求,立即返回不再参与锁的竞争并且取消锁获取操作(即finally中的cancelAcquire操作)。
3.4、tryLock锁
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
package com.flychuer.demo3;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest1 {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread() {
public void run() {
if(lock.tryLock()) {
try {
System.out.println("t1获得了锁");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
else {
System.out.println("t1未获得锁");
}
}
};
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
Thread t2 = new Thread() {
public void run() {
if(lock.tryLock()) {
try {
System.out.println("t2获得了锁");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
else {
System.out.println("t2未获得锁");
}
}
};
t2.start();
Thread t3 = new Thread() {
public void run() {
if(lock.tryLock()) {
try {
System.out.println("t3获得了锁");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
else {
System.out.println("t3未获得锁");
}
}
};
t3.start();
}
}
执行结果
t1获得了锁
t2未获得锁
t3未获得锁
可以看出,锁lock被线程t1占用,这时线程t2、t3尝试获取锁失败,并没有继续等待获取锁,而是继续执行。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
package com.flychuer.demo3;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest1 {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread() {
public void run() {
if(lock.tryLock()) {
try {
System.out.println("t1获得了锁");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
else {
System.out.println("t1未获得锁");
}
}
};
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
Thread t2 = new Thread() {
public void run() {
try {
if(lock.tryLock(2000, TimeUnit.MILLISECONDS)) {
try {
System.out.println("t2获得了锁");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
else {
System.out.println("t2未获得锁");
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t2.start();
Thread t3 = new Thread() {
public void run() {
try {
if(lock.tryLock(6000, TimeUnit.MILLISECONDS)) {
try {
System.out.println("t3获得了锁");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
else {
System.out.println("t3未获得锁");
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t3.start();
}
}
运行结果
t1获得了锁
t2未获得锁
t3获得了锁
当t2试图获得锁时,t1还在占用锁,t2等待2s仍未获得锁,t2将不再等待锁,并继续执行后续代码。t3在等待的5s内,t1释放了锁,最终t3获得了锁。