[toc]
前面部分,我们着重讨论了synchronized的使用和wait、notify及notifyAll方法等并发的基础部分。今天,我们来学习另外一种解决方案。
1.交替打印数字和阻塞队列
还记得,前面一篇文章《什么?面试官让我用ArrayList实现一个阻塞队列?》中,描述了一个关于实现两个线程交替打印以及实现阻塞队列的例子,那么今天,我们来看看另外一种解决办法---ReentrantLock。
代码如下:
package com.dhb.reentrantlocktest;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest {
private static final ReentrantLock lock = new ReentrantLock(true);
private static int count = 0;
public static final int MAX = 100;
public static void main(String[] args) {
new Thread(() -> {
while (count <= MAX) {
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+" "+count++);
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
},"T1").start();
new Thread(() -> {
while (count <= MAX) {
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+" "+count++);
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
},"T2").start();
}
}
上述代码执行之后如下:
T1 0
T2 1
T1 2
T2 3
T1 4
T2 5
T1 6
T2 7
T1 8
T2 9
T1 10
T2 11
T1 12
T2 13
T1 14
T2 15
T1 16
T2 17
T1 18
T2 19
T1 20
T2 21
T1 22
T2 23
T1 24
T2 25
T1 26
T2 27
T1 28
T2 29
T1 30
T2 31
T1 32
T2 33
T1 34
T2 35
T1 36
T2 37
T1 38
T2 39
T1 40
T2 41
T1 42
T2 43
T1 44
T2 45
T1 46
T2 47
T1 48
T2 49
T1 50
T2 51
T1 52
T2 53
T1 54
T2 55
T1 56
T2 57
T1 58
T2 59
T1 60
T2 61
T1 62
T2 63
T1 64
T2 65
T1 66
T2 67
T1 68
T2 69
T1 70
T2 71
T1 72
T2 73
T1 74
T2 75
T1 76
T2 77
T1 78
T2 79
T1 80
T2 81
T1 82
T2 83
T1 84
T2 85
T1 86
T2 87
T1 88
T2 89
T1 90
T2 91
T1 92
T2 93
T1 94
T2 95
T1 96
T2 97
T1 98
T2 99
可以看到用两个线程交替打印的功能很快就实现了。
那么阻塞队列,貌似也可以这样实现:
package com.dhb.reentrantlocktest;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class BlockQuene {
private static final int CAPACITY = 10;
private static final ReentrantLock lock = new ReentrantLock(true);
private static final List queue = new ArrayList<>();
private static int number = 0;
public static void main(String[] args) {
new Thread(new Producer(),"P1").start();
new Thread(new Consumer(),"C1").start();
}
private static class Producer implements Runnable{
@Override
public void run() {
while (true) {
lock.lock();
try {
if(queue.size() < CAPACITY) {
int num = number++;
queue.add(num);
System.out.println(Thread.currentThread().getName()+" Producer : "+num);
TimeUnit.SECONDS.sleep(1);
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}
private static class Consumer implements Runnable {
@Override
public void run() {
while (true) {
lock.lock();
try {
if(queue.size() > 0) {
int num = queue.remove(0);
System.out.println(Thread.currentThread().getName()+" Consumer : "+num);
}
}finally {
lock.unlock();
}
}
}
}
}
输出:
P1 Producer : 0
C1 Consumer : 0
P1 Producer : 1
C1 Consumer : 1
P1 Producer : 2
C1 Consumer : 2
P1 Producer : 3
C1 Consumer : 3
P1 Producer : 4
C1 Consumer : 4
这就是本文今天要介绍的主角,ReentrantLock。
上面两个案例实际上都是利用了ReentrantLock的公平锁来完成了业务需求,所谓公平锁,就是说被阻塞的队列中,获取锁的顺序不是随机的,而是依赖于进入等待队列的顺序,满足先进先出。这一点比较像我们之前学习的WaitSet中notify之后出队的状态。因此notify也是具有公平性的。
以上就是对ReentrantLock第一个特性的应用。这个地方在构造函数中,我们传入了true。如果不传入true,则默认创建非公平锁,这就与synchronized的方式类似了。
//公平锁
ReentrantLock lock1 = new ReentrantLock(true);
//非公平锁
ReentrantLock lock2 = new ReentrantLock();
2.可以被Interrupt
我们前面聊过,当线程在获取锁的时候,如果此时锁被其他线程获取,那就进入cxq和EntryList的阻塞队列,此时处于BLOCK状态,除非其他线程释放锁,否则这个状态是不能被Interrupt的。我们可以通过如下方式实验:
package com.dhb.reentrantlocktest;
import java.util.concurrent.TimeUnit;
public class SynchronizedInterruptTest {
public static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException{
long start = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
try {
synchronized (lock) {
TimeUnit.SECONDS.sleep(60);
}
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName()+" 被打断Interrupted。 cost:"+(System.currentTimeMillis()-start));
e.printStackTrace();
}
},"T1");
Thread it1 = new Thread(() -> {
try {
synchronized (lock) {
TimeUnit.SECONDS.sleep(60);
}
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName()+" 被打断Interrupted。 cost:"+(System.currentTimeMillis()-start));
e.printStackTrace();
}
},"IT1");
t1.start();
it1.start();
TimeUnit.SECONDS.sleep(1);
it1.interrupt();
long cost = System.currentTimeMillis()-start;
t1.join();
it1.join();
}
}
执行结果:
IT1 被打断Interrupted。 cost:60061
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.dhb.reentrantlocktest.SynchronizedInterruptTest.lambda$main$1(SynchronizedInterruptTest.java:25)
at java.lang.Thread.run(Thread.java:748)
可以看到,IT1到60秒之后才被打断。这说明,在T1没有释放锁的时候,IT1处于BLOCK状态是不能被打断的。
我们再来试试ReentrantLock。ReentrantLock提供了两种方式,当使用lockInterruptibly的时候,可以被打断。
package com.dhb.reentrantlocktest;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockInterruptTest {
public static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException{
long start = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
try {
lock.lock();
TimeUnit.SECONDS.sleep(60);
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName()+" 被打断Interrupted。 cost:"+(System.currentTimeMillis()-start));
e.printStackTrace();
}finally {
lock.unlock();
}
},"T1");
Thread it1 = new Thread(() -> {
try {
lock.lock();
TimeUnit.SECONDS.sleep(60);
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName()+" 被打断Interrupted。 cost:"+(System.currentTimeMillis()-start));
e.printStackTrace();
}finally {
lock.unlock();
}
},"IT1");
Thread it2 = new Thread(() -> {
try {
lock.lockInterruptibly();
TimeUnit.SECONDS.sleep(60);
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName()+" 被打断Interrupted。 cost:"+(System.currentTimeMillis()-start));
e.printStackTrace();
}finally {
lock.unlock();
}
},"IT2");
t1.start();
it1.start();
it2.start();
TimeUnit.SECONDS.sleep(1);
it1.interrupt();
it2.interrupt();
}
}
上述代码执行结果:
"D:\Program Files\Java\jdk1.8.0_231\bin\java.exe" "-javaagent:D:\Program Files\ideaIU-2020.2.1.win\lib\idea_rt.jar=58930:D:\Program Files\ideaIU-2020.2.1.win\bin" -Dfile.encoding=UTF-8 -classpath "D:\Program Files\Java\jdk1.8.0_231\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\deploy.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\access-bridge-64.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\cldrdata.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\dnsns.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\jaccess.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\jfxrt.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\localedata.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\nashorn.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\sunec.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\sunjce_provider.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\sunmscapi.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\sunpkcs11.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\ext\zipfs.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\javaws.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\jfxswt.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\management-agent.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\plugin.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_231\jre\lib\rt.jar;D:\workspace-git\MyProject\target\classes;D:\m2\log4j\log4j\1.2.17\log4j-1.2.17.jar;D:\m2\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;D:\m2\org\slf4j\slf4j-log4j12\1.7.25\slf4j-log4j12-1.7.25.jar;D:\m2\org\apache\commons\commons-lang3\3.9\commons-lang3-3.9.jar;D:\m2\org\openjdk\jol\jol-core\0.10\jol-core-0.10.jar" com.dhb.reentrantlocktest.ReentrantLockInterruptTest
IT2 被打断Interrupted。 cost:1061
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.dhb.reentrantlocktest.ReentrantLockInterruptTest.lambda$main$2(ReentrantLockInterruptTest.java:39)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "IT2" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
at com.dhb.reentrantlocktest.ReentrantLockInterruptTest.lambda$main$2(ReentrantLockInterruptTest.java:45)
at java.lang.Thread.run(Thread.java:748)
IT1 被打断Interrupted。 cost:60059
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.dhb.reentrantlocktest.ReentrantLockInterruptTest.lambda$main$1(ReentrantLockInterruptTest.java:29)
at java.lang.Thread.run(Thread.java:748)
可以看到,IT1被打断需要61秒,而IT2被打断只需要1秒。这说明,对于ReentrantLock而言。等待锁的过程中,是支持Interrupt的。
3.支持超时
对,Reentrant还支持超时。可以通过tryLock方法返回是否能获得锁的状态。
package com.dhb.reentrantlocktest;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTimeOutTest {
private static final ReentrantLock lock = new ReentrantLock(true);
public static void main(String[] args) throws Exception{
long start = System.currentTimeMillis();
new Thread(() -> {
lock.lock();
try {
TimeUnit.SECONDS.sleep(60);
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
System.out.println(Thread.currentThread().getName()+" 执行完毕。 cost:"+(System.currentTimeMillis()-start));
}
},"T1").start();
new Thread(() -> {
boolean isLock = false;
try {
isLock = lock.tryLock(10,TimeUnit.SECONDS);
System.out.println(Thread.currentThread().getName()+" 执行完毕。 cost:"+(System.currentTimeMillis()-start));
} catch (Exception e) {
System.out.println(Thread.currentThread().getName()+" 出现异常。 cost:"+(System.currentTimeMillis()-start));
e.printStackTrace();
}finally {
if(isLock) {
lock.unlock();
}
}
},"T2").start();
}
}
上述代码执行结果:
T2 执行完毕。 cost:10059
T1 执行完毕。 cost:60057
T2等待了10秒之后,没有获得锁,就结束执行了。这也是synchronized无法提供的功能。
4.配合Condition
我们再回到一开始的面试题,两个线程交替打印的问题,现在将题目改一下,每个线程分别各自打印10次。不再用之前公平锁的方式怎么实现?对,就是Condition可以解决这个问题:
package com.dhb.reentrantlocktest;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;
public class ConditionTest {
public static final ReentrantLock lock = new ReentrantLock();
public static final Condition con1 = lock.newCondition();
public static final Condition con2 = lock.newCondition();
public static int count = 0;
public static void main(String[] args) {
long start = System.currentTimeMillis();
new Thread(() -> {
while (true) {
lock.lock();
try {
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName() +" "+count++);
TimeUnit.SECONDS.sleep(1);
}
con2.signal();
con1.await();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
},"T1").start();
new Thread(() -> {
while (true) {
lock.lock();
try {
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName() +" "+count++);
TimeUnit.SECONDS.sleep(1);
}
con1.signal();
con2.await();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
},"T2").start();
}
}
上述代码执行结果:
T1 0
T1 1
T1 2
T1 3
T1 4
T1 5
T1 6
T1 7
T1 8
T1 9
T2 10
T2 11
T2 12
T2 13
T2 14
T2 15
T2 16
T2 17
T2 18
T2 19
T1 20
T1 21
T1 22
T1 23
T1 24
T1 25
T1 26
T1 27
T1 28
T1 29
T2 30
T2 31
T2 32
T2 33
T2 34
T2 35
T2 36
T2 37
T2 38
T2 39
T1 40
T1 41
T1 42
T1 43
T1 44
T1 45
T1 46
T1 47
T1 48
T1 49
T2 50
T2 51
T2 52
T2 53
T2 54
T2 55
T2 56
T2 57
T2 58
T2 59
T1 60
T1 61
T1 62
T1 63
T1 64
T1 65
T1 66
T1 67
T1 68
T1 69
可以看到Condition可以根据lock产生条件变量,之后通过await进入等待队列,再通过signal可以唤醒。这与wait和notify机制非常像。但是Condition的优势就在于,可以指定多个条件变量。上面的案例中我们就指定了2个条件变量。我们可以根据需要设置我们所需要的条件。这就比wait和notify机制要灵活得多。
5.可重入性
现在对ReentrantLock和Synchronized进行重入性测试:
package com.dhb.reentrantlocktest;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class ReentTest {
private static final ReentrantLock lock = new ReentrantLock();
public static final Object lock1 = new Object();
public static void main(String[] args) throws Exception{
new Thread(() -> {
try {
lock.lock();
TimeUnit.SECONDS.sleep(1);
for(int i=0;i<5;i++) {
lock.lock();
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"重入:" + i);
lock.unlock();
}
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
},"T1").start();
TimeUnit.SECONDS.sleep(10);
new Thread(() -> {
try {
synchronized (lock1){
TimeUnit.SECONDS.sleep(1);
for(int i=0;i<5;i++) {
synchronized (lock1) {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"重入:" + i);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
},"T2").start();
}
}
执行结果:
T1重入:0
T1重入:1
T1重入:2
T1重入:3
T1重入:4
T2重入:0
T2重入:1
T2重入:2
T2重入:3
T2重入:4
可见,两种锁都是支持重入的。
6.总结
本文介绍了ReentrantLock的用法以及和synchronized进行比较,其对比如下:
- 1.公平/非公平锁:ReentrantLock可以支持公平和非公平两种锁。而synchronized是非公平的。
- 2.reentrantLock再BLOCK状态的时候可以被Interrupt,而synchronized处于BLOCK状态的时候不支持Interrupt。
- 3.ReentrantLock可以通过tryLock设置获取锁的时间,而synchronized不支持。
- 4.ReentrantLock可以配合Conditaion,实现多个条件变量。而synchronized只能够wait/notify实现一个条件。
- 5.两种锁都是支持重入的。
以上就是对这两种锁的总结,总的来说,ReentrantLock会更加灵活。但是在通常情况下,如果我们并不需要这些灵活配置,那么使用synchronized,尤其是在jdk1.8之后,jvm进行过专门的优化,可能效率会更好。