JUC可重入锁ReentrantLock
1.概述
java.util.concurrent.locks.ReentrantLock,即可重入锁,是Lock接口的一个实现类,可以实现和synchronized一样的功能
以往我们都是使用synchronized解决线程的安全问题,在JUC中,java.util.concurrent.locks.Lock接口提供了比synchronized更多的功能,可以使用Lock接口实现手动上锁和释放锁,并且可以更加精准的设置条件(Condition)控制等待唤醒,它的特点以及和synchronized的区别是:
- synchronized是Java中的关键字,而Lock则是Java中的一个类。
- synchronized的上锁和释放锁都是自动完成,而使用Lock时必须手动释放锁,否则会造成死锁现象。
- Lock可以让等待锁的线程中断,而synchronized却不能,使用synchronized时,未得到锁的线程会一直等待下去不会中断。
- 通过Lock可以知道是否成功获取到锁,而synchronized却不能。
- Lock可以提高多个线程进行读操作的效率,在大量线程同时竞争时,效率远胜于synchronized。
2.获取锁和释放锁
使用lock.lock();方法实现上锁,用lock.unlock();方法释放锁,一般情况下,lock.unlock();的代码总是要写在finally中,以保证方法出现异常后也能释放锁,避免死锁。
例:10个线程卖票,每次只能有一个线程对票数进行修改,JUC中所有的例子都可以写成“线程操作资源类”的形式,本例创建10个线程共同操作资源类TicketTask,调用ticket()方法卖票
package example.juc.test2;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestLock {
public static void main(String[] args) {
TicketTask ticketTask = new TicketTask();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
ticketTask.ticket();
}, "线程" + i).start();
}
}
}
class TicketTask {
private int number = 500;
private Lock lock = new ReentrantLock();
public void ticket() {
while (true) {
lock.lock();
try {
if (number > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
number--;
System.out.println(Thread.currentThread().getName() + "完成售票:" + number);
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}3.公平与非公平
synchronized默认是非公平的,ReentrantLock也是默认非公平锁,比如以下例子运行结果会出现卖的票被同一个线程大量抢占的情况
package example.juc.test2;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestLock {
public static void main(String[] args) {
TicketTask ticketTask = new TicketTask();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
ticketTask.ticket();
}
}, "线程A").start();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
ticketTask.ticket();
}
}, "线程B").start();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
ticketTask.ticket();
}
}, "线程C").start();
}
}
class TicketTask {
private int number = 30;
private Lock lock = new ReentrantLock();
public void ticket() {
lock.lock();
try {
if (number > 0) {
try {
Thread.sleep(10);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
number--;
System.out.println(Thread.currentThread().getName() + "完成售票:" + number);
}
}
finally {
lock.unlock();
}
}
}出现这种情况的原因就在于,ReentrantLock默认是非公平的
java.util.concurrent.locks.ReentrantLock
public ReentrantLock() {
sync = new NonfairSync();
}
......
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}如果想要实现公平的效果,创建对象时需要加上一个初始化参数
private Lock lock = new ReentrantLock(true);公平锁的优点在于“公平”,缺点在于效率较低
4.可重入
可重入锁指的是,一个线程在一个方法外层获取了锁 ,进入方法内层会自动获取锁。
和synchronized一样,ReentrantLock也是可重入的,但是需要注意加锁和释放次数要匹配,否则其他线程无法获得锁,例如:
package example.juc3;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestReentrantLock {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(() -> {
lock.lock();
try {
System.out.println("外层");
lock.lock();
try {
System.out.println("内层");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
},"t1").start();
new Thread(() -> {
lock.lock();
try {
System.out.println("2");
} finally {
lock.unlock();
}
},"t2").start();
}
}5.响应中断
synchronized是无法支持响应中断的,一个线程获取不到锁,就会一直等着,程序无法结束,造成死锁。
而ReentrantLock支持通过tryLock()设置一个时间参数,到时间后自动放弃获取锁,如果不设置时间参数代表立即返回获取锁的结果,通过这个可以避免死锁现象
例1:派发1000个线程,每个为变量加1,结果抢不到的直接放弃,所以变量被加的远远不到1000
package example.juc2.test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestTryLock {
private static final Lock lock = new ReentrantLock();
private static int sum = 0;
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
if (lock.tryLock()) {
try {
sum ++;
System.out.println(Thread.currentThread().getName() +" - "+ sum);
}
catch (Exception e) {
throw new RuntimeException(e);
}
finally {
lock.unlock();
}
}
}).start();
}
}
}
例2:每个抢到锁的线程1秒钟才能执行完成,每个抢不到锁的线程最多等待5秒否则直接放弃争抢锁,因此程序只能打印5次左右
package example.juc2.test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestTryLock {
private static final Lock lock = new ReentrantLock();
private static int sum = 0;
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
sum ++;
System.out.println(Thread.currentThread().getName() +" - "+ sum);
TimeUnit.SECONDS.sleep(1);
}
catch (Exception e) {
throw new RuntimeException(e);
}
finally {
lock.unlock();
}
}
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
}
}6.精准唤醒
如果说Lock代替了synchronized的使用,Condition则是替代了Object的wait(),notify(),notifyAll()方法,java.util.concurrent.locks.Condition是一个接口,通过调用Lock对象的newCondition()方法获取具体Condition对象,将Condition绑定在Lock上,通过Condition的await(),signal(),signalAll()实现线程之间的通信,与传统Object作为同步监视器一样,Condition的await()也要总是出现在循环中,实现二次条件判断。
例:实现一个资源类AirConditioner,然后新建4个线程,每个线程对AirConditioner中的变量number交替改成0和1
package example.juc2.test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestConditionWaitNotify {
public static class AirConditioner {
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment() {
lock.lock();
try {
while (number != 0) {
condition.await();
}
number ++;
System.out.println(Thread.currentThread().getName()+" 修改为 "+number);
condition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public void decrement() {
lock.lock();
try {
while (number == 0) {
condition.await();
}
number --;
System.out.println(Thread.currentThread().getName()+" 修改为 "+number);
condition.signalAll();
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
AirConditioner airConditioner = new AirConditioner();
new Thread(() -> {
for (int i=0;i<10;i++) {
airConditioner.increment();
}
}, "T1").start();
new Thread(() -> {
for (int i=0;i<10;i++) {
airConditioner.decrement();
}
}, "T2").start();
new Thread(() -> {
for (int i=0;i<10;i++) {
airConditioner.increment();
}
}, "T3").start();
new Thread(() -> {
for (int i=0;i<10;i++) {
airConditioner.decrement();
}
}, "T4").start();
}
}运行结果:
T1 修改为 1
T2 修改为 0
T1 修改为 1
T2 修改为 0
T3 修改为 1
T2 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T3 修改为 1
T4 修改为 0
T1 修改为 1
T4 修改为 0
T1 修改为 1
T2 修改为 0
T1 修改为 1
T2 修改为 0
T1 修改为 1
T2 修改为 0
T1 修改为 1
T2 修改为 0
T1 修改为 1
T2 修改为 0
T1 修改为 1
T2 修改为 0
T1 修改为 1
T2 修改为 0上面的例子中,线程对资源类的操作是无序的,和加了synchronized的效果一样,而Condition有一个比synchronized更强大的地方,那就是能精确的控制等待唤醒。如果希望几个线程有序的交替执行,就可以获取多个监视器Condition绑定同一个Lock,实现精确的定制化通信。
例:三个线程ABC,按A-B-C顺序执行资源类ShareResource中的打印任务,并循环交替3轮(ABC-ABC-ABC),A执行时打印5次,B10次,C15次,通过lock.newCondition()为三个线程分别创建3个同步监视器:conditionA,conditionB, conditionC,再通过标志位变量flag判断当前该谁执行了,开始默认为A。
程序执行,A线程先获得执行权,执行第一轮,执行完成唤醒B,执行第一轮,B线程执行完成唤醒C,C也执行第一轮,完成再唤醒A,A开始执行第二轮,以此类推直到三轮任务全部完成,实现每一轮都是按照A-B-C的顺序执行。。
package example.juc2.test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestThreadOrderAccess {
public static class ShareResource {
private char flag = 'A';
private final Lock lock = new ReentrantLock();
private final Condition conditionA = lock.newCondition();
private final Condition conditionB = lock.newCondition();
private final Condition conditionC = lock.newCondition();
public void doA() {
lock.lock();
try {
while (flag != 'A') {
conditionA.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" +i);
}
flag = 'B';
conditionB.signal();
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
finally {
lock.unlock();
}
}
public void doB() {
lock.lock();
try {
while (flag != 'B') {
conditionB.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "\t" +i);
}
flag = 'C';
conditionC.signal();
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
finally {
lock.unlock();
}
}
public void doC() {
lock.lock();
try {
while (flag != 'C') {
conditionC.await();
}
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + "\t" +i);
}
System.out.println("*********");
flag = 'A';
conditionA.signal();
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
shareResource.doA();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
shareResource.doB();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 3; i++) {
shareResource.doC();
}
}, "C").start();
}
}最终运行结果:
A 0
A 1
A 2
A 3
A 4
B 0
B 1
B 2
B 3
B 4
B 5
B 6
B 7
B 8
B 9
C 0
C 1
C 2
C 3
C 4
C 5
C 6
C 7
C 8
C 9
C 10
C 11
C 12
C 13
C 14
*********
A 0
A 1
A 2
A 3
A 4
B 0
B 1
B 2
B 3
B 4
B 5
B 6
B 7
B 8
B 9
C 0
C 1
C 2
C 3
C 4
C 5
C 6
C 7
C 8
C 9
C 10
C 11
C 12
C 13
C 14
*********
A 0
A 1
A 2
A 3
A 4
B 0
B 1
B 2
B 3
B 4
B 5
B 6
B 7
B 8
B 9
C 0
C 1
C 2
C 3
C 4
C 5
C 6
C 7
C 8
C 9
C 10
C 11
C 12
C 13
C 14
*********7.总结
Lock和synchronized都是独占锁,可重入锁,但是synchronized获取/释放锁是JVM自动完成,Lock是需要开发者手动完成。synchronized不能响应中断,Lock可以响应中断,synchronized无法精准唤醒,Lock可以实现精准唤醒。
"如果文章对您有帮助,可以请作者喝杯咖啡吗?"
微信支付
支付宝