前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java并发编程之重入锁(ReentrantLock)

Java并发编程之重入锁(ReentrantLock)

作者头像
布禾
发布2021-04-20 11:03:04
5860
发布2021-04-20 11:03:04
举报
简介

ReentrantLock重入锁,由于它的加锁和解锁操作需要手动来完成,所以也称为显式锁

ReentrantLock是基于AQS独占模式实现的独占锁,同时只能有一个线程能获取到该锁,获取不到锁的线程将被放入该锁的AQS阻塞队列中等待。

ReentrantLock的简单使用

一个ReentrantLock使用示例,对临界资源进行加锁,当线程访问临界资源时需要先获得锁,如果锁已经被其他线程所持有,则需要等待其他线程释放锁后再重新获取锁,直到获取锁之后才能继续执行。

代码语言:javascript
复制
public class ReentrantLockDemo {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();

        Runnable runnable = () -> {
            try{
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName() + "获得了锁");
                Thread.sleep(3000);
            }catch (InterruptedException ex){
                ex.printStackTrace();
            }finally {
                reentrantLock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放了锁");
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

/*
 * 输出结果:
 * Thread-1获得了锁
 * Thread-1释放了锁
 * Thread-0获得了锁
 * Thread-0释放了锁
 */
可重入性

可重入性是指线程获得锁之后,没有释放锁,当再次需要获得同一把锁的时候仍然可以成功。如果锁是不可重入的,第二次获得锁的时候就会导致死锁。

state变量记录了锁的重入次数,每次lock()方法的调用会使state加1,每次unlock()方法的调用会使state减1,当state为0时锁被释放,将锁的持有者设置为null。

如下示例,多次调用lock()方法是可以的,但是对应的unlock()方法也要调用对应次数。

代码语言:javascript
复制
public class ReentrantLockDemo {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();

        Runnable runnable = () -> {
            try{
                reentrantLock.lock();
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName() + "获得了锁");
                Thread.sleep(3000);
            }catch (InterruptedException ex){
                ex.printStackTrace();
            }finally {
                reentrantLock.unlock();
                reentrantLock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放了锁");
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

/*
 * 输出结果:
 * Thread-1获得了锁
 * Thread-1释放了锁
 * Thread-0获得了锁
 * Thread-0释放了锁
 */

当unlock()方法次数少于lock()方法调用次数时,state变量值大于0,相当于当前线程还持有该锁,其他线程依然不能获取到锁,只能继续等待。如下所示:

代码语言:javascript
复制
public class ReentrantLockDemo {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();

        Runnable runnable = () -> {
            try{
                reentrantLock.lock();
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName() + "获得了锁");
                Thread.sleep(3000);
            }catch (InterruptedException ex){
                ex.printStackTrace();
            }finally {
                reentrantLock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放了锁");
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

/*
 * 输出结果:
 * Thread-1获得了锁
 * Thread-1释放了锁
 */

当unlock()方法次数大于lock()方法调用次数时,相当于锁已经被释放,锁的持有者不再是当前线程,此时再次调用unlock()方法将抛出IllegalMonitorStateException异常。

代码语言:javascript
复制
public class ReentrantLockDemo {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();

        Runnable runnable = () -> {
            try{
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName() + "获得了锁");
                Thread.sleep(3000);
            }catch (InterruptedException ex){
                ex.printStackTrace();
            }finally {
                reentrantLock.unlock();
                reentrantLock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放了锁");
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

输出结果:

代码语言:javascript
复制
Thread-0获得了锁
Thread-1获得了锁
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
	at java.base/java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:149)
	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1302)
	at java.base/java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:439)
	at com.buhe.demo.demos.reen.ReentrantLockDemo.lambda$main$0(ReentrantLockDemo.java:18)
	at java.base/java.lang.Thread.run(Thread.java:834)
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
	at java.base/java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:149)
	at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1302)
	at java.base/java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:439)
	at com.buhe.demo.demos.reen.ReentrantLockDemo.lambda$main$0(ReentrantLockDemo.java:18)
	at java.base/java.lang.Thread.run(Thread.java:834)
公平策略

使用无参构造函数创建的ReentrantLock默认是非公平锁,带参数的构造函数可以指定公平策略,true表示公平锁,false表示非公平锁。

公平锁是指先请求锁的线程一定先获得锁,反之非公平锁则是随机的。

示例:

代码语言:javascript
复制
public class ReentrantLockDemo {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();

        Runnable runnable = () -> {
            for(int i = 0; i < 2; i++) {
                try {
                    reentrantLock.lock();
                    System.out.println(Thread.currentThread().getName() + "获得了锁");
                    Thread.sleep(3000);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                } finally {
                    reentrantLock.unlock();
                }
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        Thread thread3 = new Thread(runnable);
        Thread thread4 = new Thread(runnable);
        Thread thread5 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
    }
}

锁的获取是随机的,即使刚释放锁的线程也有可能立即获得锁。

输出结果:

代码语言:javascript
复制
Thread-0获得了锁
Thread-0获得了锁
Thread-1获得了锁
Thread-1获得了锁
Thread-2获得了锁
Thread-2获得了锁
Thread-3获得了锁
Thread-4获得了锁
Thread-4获得了锁
Thread-3获得了锁

修改ReentrantLock的创建方式,改为公平锁

代码语言:javascript
复制
ReentrantLock reentrantLock = new ReentrantLock(true);

先请求锁的线程一定先获得锁,锁的获取是有序的。

输出结果:

代码语言:javascript
复制
Thread-2获得了锁
Thread-1获得了锁
Thread-0获得了锁
Thread-3获得了锁
Thread-4获得了锁
Thread-2获得了锁
Thread-1获得了锁
Thread-0获得了锁
Thread-3获得了锁
Thread-4获得了锁

使用公平锁不会产生饥饿现象,由于要维护线程的顺序,使公平锁的效率低下,所以没特殊情况应该使用非公平锁。

和synchronized的比较

synchronized和ReentrantLock都属于独占锁,两者性能相差不大,但是在使用上,ReentrantLock要更灵活。

  • synchronized的加锁和解锁是自动进行的,易于操作,但不够灵活。ReentrantLock的加锁和解锁是手动进行的,不易操作,但更灵活。
  • ReentrantLock支持公平锁和非公平锁,而synchronized只支持非公平锁。
  • ReentrantLock可以关联多个条件队列,而synchronized最多只能关联一个条件队列。
  • 在等待锁的过程中,使用ReentrantLock能使等待的线程响应中断,而使用synchronized时,线程只能保持等待直到获取到锁后继续执行。
  • ReentrantLock支持锁申请等待超时,当在给定的时间内还没有获取到锁,则主动放弃获取锁。
  • ReentrantLock支持尝试获取锁,如果锁没有被其他线程占用,则获得锁成功并立即返回true,如果锁被其他线程所占用,则锁获取失败直接返回false,不会进行等待。
  • ReentrantLock和synchronized都是可重入锁。
源码分析

ReentrantLock和AQS的关系如下图:

ReentrantLock的内部类Sync继承自AQS,它的子类NonfairSync和FairSync分别实现了非公平锁和公平锁策略。

通过ReentrantLock的构造函数参数来指定公平策略:

代码语言:javascript
复制
public ReentrantLock(boolean fair) {
	sync = fair ? new FairSync() : new NonfairSync();
}

下面以非公平锁为例分析下加锁和解锁的过程。

加锁

非公平锁加锁调用的是NonfairSync的lock方法,默认state为0,通过CAS将state设置为1,设置成功则表示加锁成功,并设置当前线程为锁的持有者。设置失败则调用AQS的acquire方法。

代码语言:javascript
复制
//java.util.concurrent.locks.ReentrantLock.NonfairSync#lock
final void lock() {
	//CAS设置状态值
	if (compareAndSetState(0, 1))
		//设置当前线程为锁的持有者
		setExclusiveOwnerThread(Thread.currentThread());
	else
		//调用AQS的acquire方法
		acquire(1);
}

acquire方法,当tryAcquire方法返回false时,会把当前线程放入AQS阻塞队列。

代码语言:javascript
复制
//java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

tryAcquire方法由AQS的子类实现,这里就是Sync以及它的子类来实现。

tryAcquire方法中会判断state值,如果state为0,则会尝试获取锁,使用CAS设置state的值,设置成功返回true,设置失败返回false。

如果state大于0,则判断持有锁的线程是不是当前线程,如果是则需要更新重入次数,重新设置state的值后返回true,反之则表示锁已经被其他线程所持有,直接返回false。

代码语言:javascript
复制
//java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
	return nonfairTryAcquire(acquires);
}
代码语言:javascript
复制
//java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	//获取锁的状态值
	int c = getState();
	//判断锁的状态,为0则表示锁没有被任何线程所持有
	if (c == 0) {
		//CAS设置状态值
		if (compareAndSetState(0, acquires)) {
			/设置当前线程为锁的持有者
			setExclusiveOwnerThread(current);
			return true;
		}
	}
	//判断持有锁的线程是否为当前线程
	else if (current == getExclusiveOwnerThread()) {
		int nextc = c + acquires;
		if (nextc < 0) // overflow
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	return false;
}
解锁

非公平锁和公平锁的解锁过程是一致的,解锁unlock方法调用的是AQS的release方法,release方法会调用tryRelease方法尝试释放锁,释放锁成功之后的处理由AQS完成。同tryAcquire方法,tryRelease方法也是需要AQS子类实现的。

代码语言:javascript
复制
//java.util.concurrent.locks.AbstractQueuedSynchronizer#release
public final boolean release(int arg) {
	if (tryRelease(arg)) {
		Node h = head;
		if (h != null && h.waitStatus != 0)
			unparkSuccessor(h);
		return true;
	}
	return false;
}
代码语言:javascript
复制
//java.util.concurrent.locks.ReentrantLock.Sync#tryRelease
protected final boolean tryRelease(int releases) {
	//减少重入次数
	int c = getState() - releases;
	//如果不是锁的持有者调用unlock方法则抛出异常
	if (Thread.currentThread() != getExclusiveOwnerThread())
		throw new IllegalMonitorStateException();
	boolean free = false;
	//如果重入次数为0,则清空锁的持有线程
	if (c == 0) {
		free = true;
		setExclusiveOwnerThread(null);
	}
	setState(c);
	return free;
}

如果当前持有锁的线程不是当前线程则直接抛出异常,否则继续判断state的值,如果state为0,则清空锁的持有线程,如果state不为0,则减少重入次数。

参考资料: 《Java并发编程之美》 一文彻底理解ReentrantLock可重入锁的使用

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-04-17 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • ReentrantLock的简单使用
  • 可重入性
  • 公平策略
  • 和synchronized的比较
  • 源码分析
    • 加锁
      • 解锁
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档