synchronized的功能扩展:重入锁 (ReentrantLock)
重入锁使用java.util.concurrent.locks.ReentrantLock
类来实现,使用案例如下所示:
package test;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author zry
* @Date 2022/6/13
* @ApiNote
*/
public class ReenterLock implements Runnable{
// 创建重入锁实例对象
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
// 使用重入锁保护临界区资源i,确保多线程对i操作的安全性
lock.lock();
try {
i++;
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLock tl = new ReenterLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
System.out.println(i);
}
}
-
重入锁与synchronized相比,有着明显的操作过程,开发必须手动指定何时加锁,注意,退出临界区的时候,必须释放锁,否则其他线程就没有机会访问临界区了。
重入锁是可以**反复进入**的,**仅限一个线程**!
-
重入锁提供了无条件的、可轮询的、定时的以及可中断的锁获取操作。
lock.lock();
lock.lock();
try {
i++;
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
lock.unlock();
}
在这种情况下,一个线程连续获得同一把锁,这是被允许的,否则同一个线程就会在第二次获得锁的时候与自己产生死锁。
注意:如果同一个线程多次获得锁,那么在释放锁的时候也必须释放相同的次数!
如果释放的多了,就会得到一个java.lang.IllegalMonitorStateException
异常;反之,则相当于线程还持有这个锁,因此其他线程无法进入临界区。
中断响应
对于synchronized来说,如果一个线程在等待锁,则结果只有两种:
-
获得这个锁继续执行
-
保持等待
而是用重入锁,则提供了另一种可能:线程可以被中断!
也就是在等待所的过程中,程序可以根据需要取消对锁的请求。这种情况对处理死锁是有帮助的。
示例如下:
lock1.lockInterruptibly();
使用上述代码申请锁,可以进行中断,即这是一个可以对中断进行相应的锁申请动作。
锁申请等待限时
我们可以使用tryLock()方法进行一次限时的等待。
tryLock(等待时间m,计时单位t)
:表示线程在这个锁请求中最多等待m,如果超过m还没有得到锁,就会返回false,反之会返回true
tryLock()
:如果锁未被其他线程占用,则申请锁会成功,直接返回true;否则会直接返回false。
示例代码如下:
package test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author zry
* @Date 2022/6/14
* @ApiNote
*/
public class TimeLock implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
// tryLock(等待时常,计时单位)
if (lock.tryLock(5, TimeUnit.SECONDS)){
Thread.sleep(6000);
}else {
System.out.println("get lock failed");
}
}catch (Exception e){
e.printStackTrace();
}finally {
if (lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
public static void main(String[] args) {
TimeLock tl = new TimeLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t2.start();
}
}
上述代码中,由于线程获得锁之后会休眠6s,即占用该锁6s,导致其他线程无法在规定时间内获得锁,因此,请求锁会失败。
公平锁
大多是情况下,锁申请并不是公平的,而是看运气,系统会在这个锁的等待队列中随机挑选一个。因此不能保证公平性。
如果我们使用synchronized进行锁控制,那么产生的锁就是非公平的,而重入锁允许我们对其公平性进行设置。其构造函数如下:
public ReentrantLock(boolean fair)
当参数fair为true时,表示锁是公平的。
实现公平锁必须要求系统维护一个有序队列,因此公平锁的成本较高、性能较低!
公平锁一大特点:不会产生饥饿现象
公平锁示例代码如下:
package test;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author zry
* @Date 2022/6/14
* @ApiNote
*/
public class FairLock implements Runnable{
// 指定锁是公平的
public static ReentrantLock fairLock = new ReentrantLock(true);
@Override
public void run() {
while (true){
try {
fairLock.lock();
System.out.println(Thread.currentThread().getName()+"获得锁");
}catch (Exception e){
e.printStackTrace();
}finally {
fairLock.unlock();
}
}
}
public static void main(String[] args) {
FairLock rl = new FairLock();
Thread t1 = new Thread(rl,"Thread_t1");
Thread t2 = new Thread(rl,"Thread_t2");
t1.start();
t2.start();
}
}
上述代码运行结果如下图:
可以看到都是交替获得锁的,几乎不会遇到一个线程多次获得锁的情况,这就是公平锁。
而不使用公平锁的话,则会由一个线程呢个多次获得一个锁。这种分配方式高效但是无公平性可言。
ReentrantLock的几个重要方法如下:
-
lock():获得锁,如果锁已经被占用,则等待
-
lockInterruptibly():获得锁,但优先响应中断
-
tryLock():尝试获得锁,如果成功返回true,失败返回false,该方法不等待,立即返回!
-
tryLock(long time,TimeUnit.unit):在给定时间内尝试获得锁
-
unlock():释放锁
在重入锁的实现中,主要包含三个要素:
-
是原子状态,原子状态使用CAS操作来存储当前锁的状态,判断锁是否已经被别的线程持有。
-
是等待队列,所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作。
-
是阻塞原语park()和unpark(),用来挂起和恢复线程。没有得到锁的线程会被挂起。
参考:《实战Java高并发程序设计》 葛一鸣 郭超 编著