对于JAVA多线程重入锁ReentrantLock应用感兴趣的读者,本文将会是一篇不错的选择,我们将详细介绍java多线程上锁,并为您提供关于J.U.C|可重入锁ReentrantLock、java并
对于JAVA多线程重入锁ReentrantLock应用感兴趣的读者,本文将会是一篇不错的选择,我们将详细介绍java多线程上锁,并为您提供关于J.U.C|可重入锁ReentrantLock、java 并发系列 (三)-----ReentrantLock (重入锁) 功能详解和应用演示、Java 并发(九):重入锁 ReentrantLock、Java 显示锁 之 重入锁 ReentrantLock(七)的有用信息。
本文目录一览:- JAVA多线程重入锁ReentrantLock应用(java多线程上锁)
- J.U.C|可重入锁ReentrantLock
- java 并发系列 (三)-----ReentrantLock (重入锁) 功能详解和应用演示
- Java 并发(九):重入锁 ReentrantLock
- Java 显示锁 之 重入锁 ReentrantLock(七)
JAVA多线程重入锁ReentrantLock应用(java多线程上锁)
package concurrent; import java.util.concurrent.*; import java.util.concurrent.locks.reentrantlock; /** * @Auther:zhl * @Date:2019/7/13 * @Description: 并发测试,重入锁reentrantlock解决并发问题 */ public class ConcurrentSample { //并发线程数量 private static int users = 100; //访问次数 private static int count = 10000; //访问总量 private static int number = 0; //private static CyclicBarrier cyclicBarrier = new CyclicBarrier(10000); private static reentrantlock reentrantlock = new reentrantlock(); public static void main(String[] args) throws InterruptedException { //定义线程池 ExecutorService executorService = Executors.newCachedThreadPool(); //并发量 //Semaphore semaphore = new Semaphore(users); CountDownLatch countDownLatch = new CountDownLatch(count); for (int i = 0; i < count; i++) { executorService.execute(() -> { try { //semaphore.acquire(); add(); countDownLatch.countDown(); //semaphore.release(); } catch (Exception e) { e.printstacktrace(); } }); } countDownLatch.await(); executorService.shutdown(); System.out.println("计数器:" + number); } public static void add() { //加锁 reentrantlock.lock(); number++; //解锁 reentrantlock.unlock(); } }
J.U.C|可重入锁ReentrantLock
一、写在前面
前几篇我们具体的聊了AQS原理以及底层源码的实现,具体参见
《J.U.C|一文搞懂AQS》
《J.U.C|同步队列(CLH)》
《J.U.C|AQS独占式源码分析》
《J.U.C|AQS共享式源码分析》
本章我们来聊一聊其实现之一 可重入锁ReentrantLock的实现原理以及源码分析。
注 :本章主要讲解非公平锁的实现流程和源码解析,其中涉及到AQS底层的实现因在前面几章都已经详细聊过在这会一笔带过。
二、什么是重入锁
可重入锁 ReentrantLock ,顾名思义,支持重新进入的锁,其表示该锁能支持一个线程对资源的重复加锁。
Java API 描述
一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。ReentrantLock 将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁并返回。如果当前线程已经拥有该锁,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。
ReentrantLock还提供了公平锁和非公平锁的选择, 其构造方法接受一个公平参数(默认是非公平方式),当传入ture时表示公平锁, 否则为非公平锁。其两者的主要区别在于公平锁获取锁是有顺序的。但是其效率往往没有非公平锁的效率高,在多线程的访问时往往表现很低的吞吐量(即速度慢,常常急慢)。
来张图缓解下
三、源码分析
我们先来看一段代码
ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
// 业务代码
} finally {
lock.unlock();
}
这一段代码相信学过Java的同学都非常熟悉了,今天我们就以此为入口一步一步的带你深入其底层世界。
共享状态的获取(锁的获取)
lock()方法
// ReentrantLock --> lokc() 实现Lock 接口的方法
public void lock() {
// 调用内部类sync 的lock方法, 这里有两种实现,公平锁(FairSync)非公平锁(NonfairSync)这里我们来主要说 NonfairSync
sync.lock();
}
ReentrantLock 的lock 方法, sync 为ReentrantLock的一个内部类,其继承了AbstractQueuedSynchronizer(AQS), 他有两个子类公平锁FairSync 和非公平锁NonfairSync
ReentrantLock 中其中大部分的功能的实现都是委托给内部类Sync实现的,在Sync 中定义了abstract void lock()
留给子类去实现, 默认实现了final boolean nonfairTryAcquire(int acquires)
方法,可以看出其为非公平锁默认实现方式,下面我讲下给看下非公平锁lock方法。
NonfairSync.lock()
// ReentrantLock$NonfairSync
final void lock() {
if (compareAndSetState(0, 1))
// 非公平原则, 上来就插队来尝试下获取共享状态,如果成功则设置当前持有锁线程为自己,获取锁成功。
setExclusiveOwnerThread(Thread.currentThread());
else
//如果失败则调用AQS中的acquire方法
acquire(1);
}
首先就尝试获取同步状态(体现非公平锁上来就插队)如果成功则将持有锁线程设置为自己,失败则走AQS中的acquire方法。
AQS.acquire(int arg)
// AQS中的acquire方法,在AQS中已经讲过,首先会调用tryAcquire(arg)方法,tryAcquire(arg)方法会有具体由子类去实现。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里AQS中的源码我就不再过多的讲解了(无非就是尝试获取同步状态成功直接返回,失败加入同步队列等待被唤醒),主要来将留给子类实现的tryAcquire(arg)方法。
如有对AQS不明白的请看文章头中列出的几篇文章过一下或者锁搜引擎中锁搜下。
Nonfairync.tryAcquire(int acquires)
protected final boolean tryAcquire(int acquires) {
// 非公平锁的tryAcquire(arg)实现,委托给Sync.nonfairTryAcquire(int acquires)具体处理
return nonfairTryAcquire(acquires);
}
ReentrantLock中非公平锁tryAcquire(int acquires)的实现,具体调用其父类Sync中默认实现的(上面已经提过)。
Sync.nonfairTryAcquire(int acquires)
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 获取共享状态
int c = getState();
if (c == 0) {
// 如果共享状态为0,说明锁空闲,利用CAS来获取锁(将共享状态值改为1)
if (compareAndSetState(0, acquires)) {
// 如果设置成功,则表明获取锁成功,将持有锁线程设置为自己
setExclusiveOwnerThread(current);
return true;
}
}
// 如果c != 0 则说明锁已经被线程持有,判断持有锁的线程是不是自己(这里就是可重入锁的具体体现)
else if (current == getExclusiveOwnerThread()) {
// 如果当前持有锁的线程是自己,说明可重入,将共享状态值加1,返回ture
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
主要逻辑:
- 首先判断同步状态 state == 0 ?,
- 如果state == 0 则说明该锁处于空闲状态,直接通过CAS设置同步状态,成功将持有锁线程设置为自己返回ture,
- 如果state !=0 判断锁的持有者是否是自己,是则说明可重入将state 值加1 返回ture,
- 否则返回false.
来张图加深下理解
注:此图只是体现了RenntrantLock中的状态,其中涉及到AQS中的状态流转没有在这体现。
锁的释放
锁的释放逻辑就比较简单
ReentrantLock.unlock()
public void unlock() {
sync.release(1);
}
同样在ReentrantLock.unlock()方法中将具体释放逻辑委托给了内部类Sync来实现, 在这Sync 同样没有去实现release(1)而是使用其父类AQS的默认实现。
AQS.release(1)
// 调用AQS中的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;
}
AQS释放锁的逻辑比较简单,同样就不解释了(无非就是释放锁,唤醒后继节点)具体来看下需要自类实现的tryRelease(arg) 释放共享状态的方法。
Sync.tryRelease(int releases)
protected final boolean tryRelease(int releases) {
// 共享状态值减去releases
int c = getState() - releases;
// 如果持有锁的线程线程不是自己,则抛出异常(很好理解不能把别人的锁释放了)
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 共享状态 state = 0 则表明释放锁成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
释放共享状态(锁)的逻辑比较简单,主要是将共享状态的值减去releases,减后共享状态值为0表示释放锁成功将持有锁线程设置为null 返回 ture。
四、总结
最后我们来对ReentrantLock加锁和释放锁做个简单总结,ReentrantLock 是一个可重入锁提供了两种实现方式公平锁和非公平锁。
非公平锁获取锁流程:
1: 首先不管三七二一就来个 CAS 尝试获取锁。
2: 成功则皆大欢喜。
3: 失败,再次获取下共享状态(万一这会有人释放了尼)判断是否为0
4: 如果为0 则说明锁空闲,再次CAS获取锁成功将持有锁线程设置为自己并返回ture
5:不为0,判断持有者是否是自己、是自己表明可重入state + 1 返回ture 否则返回false(就去同步队列中排队去)。
非公平锁释放锁流程
很简单state - 1 = 0 则释放成功否则失败。
java 并发系列 (三)-----ReentrantLock (重入锁) 功能详解和应用演示
1. ReentrantLock 简介
jdk 中独占锁的实现除了使用关键字 synchronized 外,还可以使用 ReentrantLock。虽然在性能上 ReentrantLock 和 synchronized 没有什么区别,但 ReentrantLock 相比 synchronized 而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。
2. ReentrantLock 和 synchronized 的相同点
2.1 ReentrantLock 是独占锁且可重入的
- 例子
public class ReentrantLockTest {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
for (int i = 1; i <= 3; i++) {
lock.lock();
}
for(int i=1;i<=3;i++){
try {
} finally {
lock.unlock();
}
}
}
}
上面的代码通过 lock()
方法先获取锁三次,然后通过 unlock()
方法释放锁 3 次,程序可以正常退出。从上面的例子可以看出,ReentrantLock 是可以重入的锁,当一个线程获取锁时,还可以接着重复获取多次。在加上 ReentrantLock 的的独占性,我们可以得出以下 ReentrantLock 和 synchronized 的相同点。
-
1.ReentrantLock 和 synchronized 都是独占锁,只允许线程互斥的访问临界区。但是实现上两者不同:synchronized 加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用 synchronized 的就够了;ReentrantLock 需要手动加锁和解锁,且解锁的操作尽量要放在 finally 代码块中,保证线程正确释放锁。ReentrantLock 操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。
-
2.ReentrantLock 和 synchronized 都是可重入的。synchronized 因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而 ReentrantLock 在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。
3. ReentrantLock 相比 synchronized 的额外功能
3.1 ReentrantLock 可以实现公平锁
公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。而非公平锁则随机分配这种使用权。和 synchronized 一样,默认的 ReentrantLock 实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。在创建 ReentrantLock 的时候通过传进参数 true
创建公平锁,如果传入的是 false
或没传参数则创建的是非公平锁。
ReentrantLock lock = new ReentrantLock(true);
继续跟进看下源码
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
可以看到公平锁和非公平锁的实现关键在于成员变量 sync
的实现不同,这是锁实现互斥同步的核心。以后有机会我们再细讲。
- 一个公平锁的例子
public class ReentrantLockTest {
static Lock lock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++){
new Thread(new ThreadDemo(i)).start();
}
}
static class ThreadDemo implements Runnable {
Integer id;
public ThreadDemo(Integer id) {
this.id = id;
}
@Override
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<2;i++){
lock.lock();
System.out.println("获得锁的线程:"+id);
lock.unlock();
}
}
}
}
公平锁结果:
我们开启 5 个线程,让每个线程都获取释放锁两次。为了能更好的观察到结果,在每次获取锁前让线程休眠 10 毫秒。可以看到线程几乎是轮流的获取到了锁。如果我们改成非公平锁,再看下结果
非公平锁结果:
线程会重复获取锁。如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁。这就是非公平锁的 “饥饿” 问题。
公平锁和非公平锁该如何选择?
大部分情况下我们使用非公平锁,因为其性能比公平锁好很多。但是公平锁能够避免线程饥饿,某些情况下也很有用。
3.2 .ReentrantLock 可响应中断
当使用 synchronized 实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而 ReentrantLock 给我们提供了一个可以响应中断的获取锁的方法 lockInterruptibly()
。该方法可以用来解决死锁问题。
- 响应中断的例子
public class ReentrantLockTest {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
thread.start();
thread1.start();
thread.interrupt();//是第一个线程中断
}
static class ThreadDemo implements Runnable {
Lock firstLock;
Lock secondLock;
public ThreadDemo(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
firstLock.lockInterruptibly();
TimeUnit.MILLISECONDS.sleep(10);//更好的触发死锁
secondLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()+"正常结束!");
}
}
}
}
结果
构造死锁场景:创建两个子线程,子线程在运行时会分别尝试获取两把锁。其中一个线程先获取锁 1 在获取锁 2,另一个线程正好相反。如果没有外界中断,该程序将处于死锁状态永远无法停止。我们通过使其中一个线程中断,来结束线程间毫无意义的等待。被中断的线程将抛出异常,而另一个线程将能获取锁后正常结束。
3.3 获取锁时限时等待
ReentrantLock 还给我们提供了获取锁限时等待的方法 tryLock()
, 可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true 表示获取锁成功,false 表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题。
- 更好的解决死锁的例子
public class ReentrantLockTest {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
thread.start();
thread1.start();
}
static class ThreadDemo implements Runnable {
Lock firstLock;
Lock secondLock;
public ThreadDemo(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
while(!lock1.tryLock()){
TimeUnit.MILLISECONDS.sleep(10);
}
while(!lock2.tryLock()){
lock1.unlock();
TimeUnit.MILLISECONDS.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()+"正常结束!");
}
}
}
}
结果
线程通过调用 tryLock()
方法获取锁,第一次获取锁失败时会休眠 10 毫秒,然后重新获取,直到获取成功。第二次获取失败时,首先会释放第一把锁,再休眠 10 毫秒,然后重试直到成功为止。线程获取第二把锁失败时将会释放第一把锁,这是解决死锁问题的关键,避免了两个线程分别持有一把锁然后相互请求另一把锁。
4. 结合 Condition 实现等待通知机制
使用 synchronized 结合 Object 上的 wait 和 notify 方法可以实现线程间的等待通知机制。ReentrantLock 结合 Condition 接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。
4.1 Condition 使用简介
Condition 由 ReentrantLock 对象创建,并且可以同时创建多个
static Condition notEmpty = lock.newCondition();
static Condition notFull = lock.newCondition();
Condition 接口在使用前必须先调用 ReentrantLock 的 lock () 方法获得锁。之后调用 Condition 接口的 await () 将释放锁,并且在该 Condition 上等待,直到有其他线程调用 Condition 的 signal () 方法唤醒线程。使用方式和 wait,notify 类似。
- 一个使用 condition 的简单例子
public class ConditionTest {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
lock.lock();
new Thread(new SignalThread()).start();
System.out.println("主线程等待通知");
try {
condition.await();
} finally {
lock.unlock();
}
System.out.println("主线程恢复运行");
}
static class SignalThread implements Runnable {
@Override
public void run() {
lock.lock();
try {
condition.signal();
System.out.println("子线程通知");
} finally {
lock.unlock();
}
}
}
}
运行结果
4.2 使用 Condition 实现简单的阻塞队列
阻塞队列是一种特殊的先进先出队列,它有以下几个特点
1. 入队和出队线程安全
2. 当队列满时,入队线程会被阻塞;当队列为空时,出队线程会被阻塞。
- 阻塞队列的简单实现
public class MyBlockingQueue<E> {
int size;//阻塞队列最大容量
ReentrantLock lock = new ReentrantLock();
LinkedList<E> list=new LinkedList<>();//队列底层实现
Condition notFull = lock.newCondition();//队列满时的等待条件
Condition notEmpty = lock.newCondition();//队列空时的等待条件
public MyBlockingQueue(int size) {
this.size = size;
}
public void enqueue(E e) throws InterruptedException {
lock.lock();
try {
while (list.size() ==size)//队列已满,在notFull条件上等待
notFull.await();
list.add(e);//入队:加入链表末尾
System.out.println("入队:" +e);
notEmpty.signal(); //通知在notEmpty条件上等待的线程
} finally {
System.out.println("****************队列有数据或队列满了,需要通知出列线程干活");
lock.unlock();
}
}
public E dequeue() throws InterruptedException {
E e;
lock.lock();
try {
while (list.size() == 0)//队列为空,在notEmpty条件上等待
notEmpty.await();
e = list.removeFirst();//出队:移除链表首元素
System.out.println("出队:"+e);
notFull.signal();//通知在notFull条件上等待的线程
return e;
} finally {
System.out.println("****************出列线程干活,需要通知入队列线程干活");
lock.unlock();
}
}
}
- 测试代码
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(2);
for (int i = 0; i < 5; i++) {
int data = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
queue.enqueue(data);
} catch (InterruptedException e) {
}
}
}).start();
}
for(int i=0;i<5;i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Integer data = queue.dequeue();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
运行结果
可以看到执行 condition.await () 或 condition.signal () 之后,必须执行 lock.unlock (),因为这个相当于大门锁,而 condition 相当于进了大门后的各个房间的小锁。
5. 总结
ReentrantLock 是可重入的独占锁。比起 synchronized 功能更加丰富,支持公平锁实现,支持中断响应以及限时等待等等。可以配合一个或多个 Condition 条件方便的实现等待通知机制。
Java 并发(九):重入锁 ReentrantLock
先做总结:
1、为什么要用 ReentrantLock?
(1)ReentrantLock 与 synchronized 具有相同的功能和内存语义;
(2)synchronized 是重量级锁,性能不好。ReentrantLock 性能好;
(3)ReentrantLock 可操作性强,如:实现条件 Condition,读写锁,可轮询,使用更灵活。
2、ReentrantLock 实现原理
(1)ReentrantLock 的属性 sync 是一个 Sync(继承了 AQS)对象
(2)获取锁的标志:
sync.state>0 (0 代表没有被占用,大于 0 代表有线程持有当前锁 (锁可以重入,每次重入都 + 1))
sync.exclusiveOwnerThread == Thread.currentThread()
(3)Sync 重写了 tryAcquire () 方法(获取锁)和 tryRelease () 方法(释放锁),其实就是对 sync.state 和 sync.exclusiveOwnerThread 的操作。
(4)获取不到锁的线程加入 sync 的同步队列,上一篇说过 Java 并发(八):AbstractQueuedSynchronizer
3、公平锁与非公平锁
ReentrantLock 只能时公平锁和非公平锁中的一个
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
不同:
(1)非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁。
(2)非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法。在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁(即使同步队列中有线程等待),而公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
因此,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
两个不同的源码:
/**
* 不同一:
* 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁。
*/
// NonfairSync
final void lock() {
if (compareAndSetState(0, 1)) // 不同一:NonfairSync会CAS尝试获取锁
setExclusiveOwnerThread(Thread.currentThread()); // 如果拿到锁就设置当前线程
else
acquire(1);
}
// FairSync(FairSync不会先尝试拿锁)
final void lock() {
acquire(1);
}
/**
* 不同二:
* 如果发现锁这个时候被释放了(state == 0),
* 非公平锁会直接 CAS 抢锁(即使同步队列中有线程等待),而公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
*/
// NonfairSync
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 锁没有被占用,直接获取(不管同步队列中有没有等待线程)
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;
}
// FairSync
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 即使锁没有被占用,也要排在同步队列中等待的线程之后
if (!hasQueuedPredecessors() && // CLH队列为空或者队列头结点是当前线程节点 才能获得锁
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
一、ReentrantLock 类结构
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync; // 锁 大部分功能都是委托给Sync来实现的
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class FairSync extends Sync {}
static final class NonfairSync extends Sync {}
}
二、以 NonfairSync 为例解析重入锁
获取锁标志:
(NonfairSync extends Sync extends AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer)
1.AbstractQueuedSynchronizer.state>0 (0 代表没有被占用,大于 0 代表有线程持有当前锁 (锁可以重入,每次重入都 + 1))
2.AbstractOwnableSynchronizer.exclusiveOwnerThread == Thread.currentThread()
获取锁:
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();// 默认是非公平锁
lock.lock();
}
// ReentrantLock
public void lock() {
sync.lock();
}
// NonfairSync
final void lock() {
if (compareAndSetState(0, 1)) // 尝试获取锁
setExclusiveOwnerThread(Thread.currentThread()); // 如果拿到锁就设置当前线程
else
acquire(1);
}
// AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 没有获取到锁,将线程加入同步队列(参考上一篇AbstractQueuedSynchronizer)
selfInterrupt();
}
// NonfairSync
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
/**
* 获取锁标志:
* (NonfairSync extends Sync extends AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer)
* 1.AbstractQueuedSynchronizer.state>0(0代表没有被占用,大于0代表有线程持有当前锁(锁可以重入,每次重入都+1))
* 2.AbstractOwnableSynchronizer.exclusiveOwnerThread == Thread.currentThread()
* Sync(NonfairSync没有重写nonfairTryAcquire)
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 所没有被占用,直接获取
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;
}
释放锁:
// ReentrantLock
public void unlock() {
sync.release(1);
}
// AbstractQueuedSynchronizer
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒队列下一个节点线程 参考上一篇:AbstractQueuedSynchronizer
return true;
}
return false;
}
// Sync
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false; // 重入锁,直到state==0才算释放
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
三、公平锁与非公平锁
// FairSync(NonfairSync会先尝试拿锁,FairSync不会)
final void lock() {
acquire(1);
}
// FairSync
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && // CLH队列为空或者队列头结点是当前线程节点 才能获得锁
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
/**
* AbstractQueuedSynchronizer
* true - CLH队列为空或者队列头结点是当前线程节点
*/
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
以上代码可以看出,公平锁和非公平锁只有两处不同:
(1)非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁。
(2)非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法。在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,而公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
因此,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
四、ReentrantLock 优势
ReentrantLock 与 synchronized 具有相同的功能和内存语义。
1、与 synchronized 相比,ReentrantLock 提供了更多,更加全面的功能,具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票。
2、ReentrantLock 还提供了条件 Condition,对线程的等待、唤醒操作更加详细和灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock 更加适合。
3、ReentrantLock 提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而 synchronized 则一旦进入锁请求要么成功要么阻塞,所以相比 synchronized 而言,ReentrantLock 会不容易产生死锁些。
4、ReentrantLock 支持更加灵活的同步代码块,但是使用 synchronized 时,只能在同一个 synchronized 块结构中获取和释放。注:ReentrantLock 的锁释放一定要在 finally 中处理,否则可能会产生严重的后果。
5、ReentrantLock 支持中断处理,且性能较 synchronized 会好些。
参考资料 / 相关推荐
【死磕 Java 并发】—–J.U.C 之重入锁:ReentrantLock
一行一行源码分析清楚 AbstractQueuedSynchronizer
Java 并发(八):AbstractQueuedSynchronizer
Java 显示锁 之 重入锁 ReentrantLock(七)
ReentrantLock 重入锁简介
重入锁 ReentrantLock,顾名思义,就是支持同一个线程对资源的重复加锁。另外,该锁还支持获取锁时的公平与非公平性的选择。
重入锁 ReentrantLock,只支持独占方式的获取操作,因此它只实现了 tryAcquire、tryRelease 和 isHeldExclusively 方法。
ReentrantLock 如何实现锁重入
锁重入是指任意线程在获取到锁之后,能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决两个问题:
1、线程再次获取锁
需要识别获取锁的线程是否为当期占有锁的线程,如果是,则便获取锁成功
2、锁最终得到释放
同一个线程重复 n 次获得了锁,在第 n 次释放该锁后,其它线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于 0 时,表示锁已经成功释放
ReentrantLock 是通过自定义同步器来实现所得获取和释放,默认使用非公平性,下面就以非公平性获取锁为例,学习重入锁是如何实现的:
final boolean nonfairTryAcquire(int acquires) {
//当前线程
final Thread current = Thread.currentThread();
//当前同步状态
int c = getState();
//当前同步状态等于0,说明当前线程可以获取同步状态
if (c == 0) {
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;
}
通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程,则将同步状态值进行累加并返回 true,表示获取同步状态成功。
成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求在释放同步状态时递减同步状态的值。如下,释放锁的代码:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
如果该锁被获取了 n 次, 那么前 (n- 1) 次 tryRelease ( int releases) 方法必须返回 false,只有同步状态完全释放了,才能返回 tru。 该方法将同步状态 是否为 0 作为作为最终释放的条件,当同步状态为 0 时,将该锁的拥有者线程设置为 null,并返回 true,表示成功释放。
ReentrantLock 如何实现锁的公平性与非公平性
公平性与否是针对获取锁而言的,如果一个锁时公平的,那么锁的获取顺序就应该服务请求时间的顺序,也就是 FIFO。
由上文介绍的非公平获取锁方法 nonfairTryAcquire (int acquires) 可知,只要 CAS 设置同步状态成功,则表示线程获取了锁,而公平锁则不同,如代码所示:
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
该方法与 nonfairTryAcquire (int acquires) 的区别在于多了 hasQueuedPredecessors () 方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回 true,则表示有线程比当前线程更早的请求了锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
关于JAVA多线程重入锁ReentrantLock应用和java多线程上锁的介绍现已完结,谢谢您的耐心阅读,如果想了解更多关于J.U.C|可重入锁ReentrantLock、java 并发系列 (三)-----ReentrantLock (重入锁) 功能详解和应用演示、Java 并发(九):重入锁 ReentrantLock、Java 显示锁 之 重入锁 ReentrantLock(七)的相关知识,请在本站寻找。
本文标签: