synchronized的功能扩展:重入锁
同步控制是并发程序必不可少的手段,它决定了一个线程是否可以访问临界区资源。synchronized关键字就是一个简单的同步控制方式,而重入锁ReentrantLock可以完全替代synchronized关键字。在jdk 5.0的早期版本中,重入锁的性能远远好于synchronized;jdk 6.0开始,synchronized得到了大量优化,使得两者性能差距并不大。
ReentrantLock使用java.util.concurrent.locks.ReentrantLock
实现,不同于synchronized,它支持显式地获得锁和释放锁,中断响应、锁申请等待限时、公平锁。
ReentrantLock使用示例
package reenterLock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Time : 2019/05/05 下午 02:07
* @Author : xiuc_shi
**/
public class ReenterLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
public void run() {
for(int j = 0;j < 10000000;j++){
lock.lock();
try{
i++;
}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);
}
}
重入锁之所以叫重入锁,它允许同一个线程两次获得同一把锁,其实synchronized也支持。
lock.lock();
lock.lock();
try{
i++;
}finally {
lock.unlock();
lock.unlock();
}
获得多少次就得释放多少次。
- 获得次数 > 释放次数:线程没有完成释放锁,还持有着锁。
- 获得次数 < 释放次数:抛出
java.lang.IllegalMonitorStateException
异常。
重入锁显式加锁,释放锁的特点使得它的灵活性很高,除此之外,它还提供了一些高级的功能,这是它区别于synchronized的地方。
中断响应
对于synchronized,如果一个线程在等待锁,那么它要么获得锁后继续执行,要么保持等待。而重入锁则有第三种选择,那就是中断该线程,这对处理死锁有一定的帮助。
以下代码制造死锁状态,并中断线程2以释放锁lock2,让线程1可以获得锁lock2继续执行。
package reenterLock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Time : 2019/05/05 下午 03:13
* @Author : xiuc_shi
**/
public class IntLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
public IntLock(int lock){
this.lock = lock;
}
/**
* 通过lock控制线程申请lock1和lock2的顺序,从而制造死锁的情况
*/
public void run() {
try{
if(lock == 1){
lock1.lockInterruptibly();
try {
Thread.sleep(500);
} catch (InterruptedException e) { }
lock2.lockInterruptibly();
System.out.println("线程1执行完成");
}else{
lock2.lockInterruptibly();
try {
Thread.sleep(500);
} catch (InterruptedException e) { }
lock1.lockInterruptibly();
System.out.println("线程2执行完成");
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
if(lock1.isHeldByCurrentThread()){
lock1.unlock();
}
if(lock2.isHeldByCurrentThread()){
lock2.unlock();
}
System.out.println(Thread.currentThread().getId() + ":线程退出");
}
}
public static void main(String[] args) throws InterruptedException {
IntLock r1 = new IntLock(1); //先申请lock1,再申请lock2
IntLock r2 = new IntLock(2); //先申请lock2,再申请lock1
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
Thread.sleep(1000);
t2.interrupt(); //中断线程2
}
}
>>>>>结果
线程1执行完成
java.lang.InterruptedException
10:线程退出
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
9:线程退出
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at reenterLock.IntLock.run(IntLock.java:36)
at java.lang.Thread.run(Thread.java:748)
t2.interrupt()
执行后,线程2的lock2.lockInterruptibly()
响应中断从而释放lock2
.
lockInterruptibly()
方法可以对中断进行响应的锁申请动作,即在等待锁时可以响应中断。
锁申请等待限时
除了等待外部通知,限时等待锁也可以避免死锁。通常我们无法判断线程为何迟迟未能获得锁,或死锁,或饥饿。因此给定一个等待时间,在该时间内未能获得锁则让线程自动放弃。
以下代码线程对锁的等待时长为3秒,线程1获得锁后持有锁5秒钟,线程2不可能在3秒内获得锁,因此放弃。
package reenterLock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Time : 2019/05/05 下午 03:50
* @Author : xiuc_shi
**/
public class TimeLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public void run() {
try {
if(lock.tryLock(3,TimeUnit.SECONDS)){
Thread.sleep(5000);
}else{
System.out.println("获取锁失败");
}
} catch (InterruptedException 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();
}
}
tryLock()
方法接受两个参数:1.等待时长 2.计时单位。
该例线程对锁的等待时长为3秒,线程获得锁后会持有5秒,故当一个线程获得锁后,另一个线程不可能在3秒内获得该锁,因此请求锁失败。
如果使用tryLock()
方法的无参数版本,当线程尝试获得锁,若锁未被占用,则申请锁成功,返回true
,否则不等待直接返回false
。
公平锁
大多数情况下,锁的申请都是非公平的。即,线程1先申请锁A,接着线程2也申请锁A。那么当锁A可用时,系统会从这个锁的等待队列钟随机挑选一个线程获得锁A。而公平锁则讲究先来后到,按申请先后给予锁。所以公平锁不会产生饥饿现象。只要排队,最终还是会获得锁的。
重入锁通过如下有参构造函数设置锁的公平与否:
public ReentrantLock(boolean fair);
当参数fair为true
时,表示锁是公平的。
公平锁需要维护一个有序队列,因此实现成本比较高,性能相对非常低下,因此默认情况下锁是非公平的。
小结
-
ReentrantLock
需要显式加锁、释放锁,支持中断响应,锁申请等待限时,公平锁。 -
ReentrantLock
的几个重要方法整理:-
lock()
:获得锁,若锁被占用,则等待。 -
lockInterruptibly()
:获得锁,但优先响应中断。 -
tryLock()
:尝试获得锁,成功返回true
,失败返回false
,不等待。 -
tryLock(long time, TimeUnit unit)
:在给定时间内尝试获得锁。 -
unlock()
:释放锁。
-
- 重入锁实现的三个要素:
- 原子状态。使用CAS操作来存储当前锁的状态,判断锁是否已被其他线程占用。
- 等待队列。所有未请求到锁的线程会进入等待队列。待锁释放,系统从等待队列中唤醒一个线程获得锁继续运行。
- 阻塞原语
park()
和unpark()
,用来挂起和恢复线程。
网友评论