java之ReentrantLock详解
前言
如果一个代码块被synchronized修饰了,当一个线程获取了相应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的释放,现在有这么一种情况,这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程只能干巴巴地等着,在这种情况下,非常影响程序执行效率
所以Lock应运而生,可以不让等待的线程一直等待下去(比如只等待一定的时间或者能够响应中断)
一、Lock接口
(1)与synchronzed区别
synchronized是JVM层面的内置锁,而Lock则是java层面的显示锁,Lock提供了一种可重入的、可轮询的、定时的以及可中断的锁获取操作。
采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
(2)lock接口源码详解
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
Condition newCondition();
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
}
lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。 newCondition()方法返回一个Condition对象
lock():此方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
tryLock():此方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
tryLock(long time, TimeUnit unit):此方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
二、ReentrantLock
(1)模拟可中断的锁获取
Lock中的lockInterruptibly() 可以在获得锁的同时保持对中断的响应,但是内置锁synchronized却很难实现这个功能。
synchronized
如下程序,创建一任务,假设该任务需要执行很长时间才能结束(使用死循环来模拟时长)。现在有两个线程竞争该资源的内置锁,在等待一段时间后,想要终止线程t2的锁获取等待操作,使用t2.interrupt(); 尝试中断线程t2。遗憾的是,此时t2根本不会响应这个中断操作,它会继续等待直到获得资源锁。
public class InterruptedLockTest implements Runnable{
public synchronized void doCount(){
//使用死循环表示此操作要进行很长的一段时间才能结束
while(true){}
}
@Override
public void run() {
doCount();
}
}
public static void main(String[] args) throws InterruptedException {
InterruptedLockTest test = new InterruptedLockTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.start();
t2.start();
//等待两秒,尝试中断线程t2的等待
TimeUnit.SECONDS.sleep(2);
t2.interrupt();
//等待1秒,让 t2.interrupt(); 执行生效
TimeUnit.SECONDS.sleep(1);
System.out.println("线程t1是否存活:" + t1.isAlive());
System.out.println("线程t2是否存活:" + t2.isAlive());
}
console打印:
线程t1是否存活:true
线程t2是否存活:true
Lock
public class LockDemo implements Runnable {
@Override
public void run() {
try {
doCount();
} catch (InterruptedException e) {
System.out.println("被中断....");
}
}
Lock lock = new ReentrantLock();
public void doCount() throws InterruptedException {
lock.lockInterruptibly();
try {
while (true){
}
}catch (Exception e){
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
LockDemo lockDemo = new LockDemo();
Thread t1 = new Thread(lockDemo);
Thread t2 = new Thread(lockDemo);
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(2);
t2.interrupt();
//等待1秒,让 t2.interrupt(); 执行生效
TimeUnit.SECONDS.sleep(1);
System.out.println("线程t1是否存活:" + t1.isAlive());
System.out.println("线程t2是否存活:" + t2.isAlive());
}
}
console打印:
被中断....
线程t1是否存活:true
线程t2是否存活:false
(2)模拟可轮询(避免死锁)
相比于synchronized内置锁的无条件锁获取模式,Lock提供了tryLock() 实现可定时和可轮询的锁获取模式,这也使Lock具有更完善的错误恢复机制。在内置锁中,死锁是一个很严重的问题,造成死锁的原因之一可能是,锁获取顺序不一致导致程序死锁。比如说,线程1持有A对象锁,正在等待获取B对象锁;线程2持有B对象锁,正在等待获取A对象锁。这样,两个线程都会由于获取不到想要的锁而陷入死锁的境地。解决办法可以是,两个线程要么同时获取两个锁,要么一个锁都不获取。Lock 的可定时和可轮询锁就可以很好的满足该条件,从而避免死锁的发生
synchronized死锁问题
public class TestDeadLock implements Runnable{
int flag ;
static Object o1 = new Object();
static Object o2 = new Object();
@Override
public void run() {
if(flag==0){
synchronized (o1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("flag==0");
}
}
}
else if (flag==1){
synchronized (o2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("flag==1");
}
}
}
}
public static void main(String[] args) {
TestDeadLock td1 = new TestDeadLock();
TestDeadLock td2 = new TestDeadLock();
td1.flag = 0;
td2.flag = 1;
Thread t1 = new Thread(td1);
Thread t2 = new Thread(td2);
t1.start();
t2.start();
}
}
ReentrantLock可轮询
// 资源类
public class Resource {
//资源总和
private int resourceNum;
// 显示锁
public Lock lock = new ReentrantLock();
public Resource(int resourceNum){
this.resourceNum = resourceNum;
}
//返回此资源的总量
public int getResourceNum(){
return resourceNum;
}
}
public class LockTest1 {
//传入两个资源类和预期操作时间,在此期间内返回两个资源的数量总和
public int getResource(Resource resourceA, Resource resourceB, long timeout, TimeUnit unit)
throws InterruptedException {
// 获取当前时间,算出操作截止时间
long stopTime = System.nanoTime() + unit.toNanos(timeout);
while(true){
try {
// 尝试获得资源A的锁
if (resourceA.lock.tryLock()) {
try{
// 如果获得资源A的锁,尝试获得资源B的锁
if(resourceB.lock.tryLock()){
//同时获得两资源的锁,进行相关操作后返回
return getSum(resourceA, resourceB);
}
}finally {
resourceB.lock.unlock();
}
}
}finally {
resourceA.lock.unlock();
}
// 判断当前是否超时,规定-1为错误标识
if(System.nanoTime() > stopTime)
return -1;
//睡眠1秒,继续尝试获得锁
TimeUnit.SECONDS.sleep(1);
}
}
// 获得资源总和
public int getSum(Resource resourceA,Resource resourceB){
return resourceA.getResourceNum()+resourceB.getResourceNum();
}
}
(3)公平锁
背景
CPU在调度线程的时候是在等待队列里随机挑选一个线程,由于这种随机性所以是无法保证线程先到先得的(synchronized控制的锁就是这种非公平锁)。但这样就会产生饥饿现象,即有些线程(优先级较低的线程)可能永远也无法获取cpu的执行权,优先级高的线程会不断的强制它的资源。那么如何解决饥饿问题呢,这就需要公平锁了。
公平锁可以保证线程按照时间的先后顺序执行,避免饥饿现象的产生。但公平锁的效率比较地,因为要实现顺序执行,需要维护一个有序队列。
// 也可以指定公平性
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//默认创建非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
Demo
package com.jalja.base.threadTest;
import java.util.concurrent.locks.ReentrantLock;
public class LockFairTest implements Runnable{
//创建公平锁
private static ReentrantLock lock=new ReentrantLock(true);
public void run() {
while(true){
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"获得锁");
}finally{
lock.unlock();
}
}
}
public static void main(String[] args) {
LockFairTest lft=new LockFairTest();
Thread th1=new Thread(lft);
Thread th2=new Thread(lft);
th1.start();
th2.start();
}
}
console打印:
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
分析结果可看出两个线程是交替执行的,几乎不会出现同一个线程连续执行多次。
三、Synchronized与Lock的区别
锁类型
-
可重入锁:在执行对象中所有同步方法不用再次获得锁
-
可中断锁:在等待获取锁过程中可中断
-
公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利
-
读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写
区别
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个类 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入 不可中断 非公平 | 可重入 可中断 可公平(两者皆可) |
性能 | 少量同步 | 大量同步 |
关于读写锁
我们知道,当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。