Java多线程:ReentrantLock重入锁(五)

导读:本篇文章讲解 Java多线程:ReentrantLock重入锁(五),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一、定义

可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁
jdk中独占锁(排它锁)的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。

二、synchronized和ReentrantLock区别

2.1 区别

1)可中断锁
响应中断的锁,Lock是可中断锁(体现在lockInterruptibly()方法),synchronized不是。如果线程A正在执行锁中代码,线程B正在等待获取该锁。时间太长,线程B不想等了,可以让它中断自己。
2)公平锁
尽量以请求锁的顺序获取锁。比如同时有多个线程在等待一个锁,当锁被释放后,等待时间最长的获取该锁,跟京牌司法拍卖一个道理。非公平锁可能会导致有些线程永远得不到锁,synchronized是非公平锁,ReentrantLock是选非公平锁和公平锁
3)读写锁
读写锁将对一个资源(如文件)的访问分为2个锁,一个读锁,一个写锁;读写锁使得多个线程的读操作可以并发进行,不需同步。而写操作就得需要同步,提高了效率,ReadWriteLock就是读写锁,是一个接口,ReentrantReadWriteLock实现了这个接口。可通过readLock()获取读锁,writeLock()获取写锁
4)绑定多个条件
一个ReentrantLock可以绑定多个Condition对象,仅需多次调用new Condition()即可;而在synchronized中锁锁对象的wait()、notify()/notifyAll()可以实现一个隐含的条件,如果要和多余的条件关联,就不得不额外的增加一个锁。

2.2 性能比较

大量线程同时竞争,ReentrantLock要远胜于synchronized。

JDK5中,synchronized是性能低效的,因为这是一个重量级操作,对性能的最大影响是阻塞的实现,挂起线程和恢复线程的操作,都需要转入内核态中完成,给并发带来了很大压力。
JDK6中synchronized加入了自适应无锁、偏向锁、轻量级锁、重量级锁等一系列优化,官方也支持synchronized,提倡在synchronized能实现需求的前提下,优先考虑synchronized来进行同步。

偏向锁:当一个线程第一次获取到锁之后,再次申请就可以直接取到锁。
轻量级锁:没有多线程竞争,但有多个线程交替执行。
重量级锁:有多线程竞争,线程获取不到锁进入阻塞状态。

示例:

public class ReetrantLockTest1 {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();

        for (int i = 1; i <= 3; i++) {
            lock.lock();
        }

        for(int i=1;i<=2;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在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁

三、ReentrantLock公平锁和非公平锁

公平锁指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。而非公平锁随机分配这种使用权。和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止线程饥饿,某些情况下也很有用。在创建ReentrantLock的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁
在这里插入图片描述
可以看到公平锁和非公平锁的实现关键在于成员变量sync的实现不同,这是锁实现互斥同步的核心。
公平锁示例

public class ReetrantLockTest2 {

    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毫秒。可以看到线程几乎是轮流的获取到了锁。如果我们改成非公平锁,再看下结果:
在这里插入图片描述
综上可以看出线程会重复获取锁。在非公平锁中如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁。这就是非公平锁的“饥饿”问题

公平锁和非公平锁该如何选择
大部分情况下我们使用非公平锁,因为其性能比公平锁好很多。但是公平锁能够避免线程饥饿,某些情况下也很有用。

四、ReentrantLock可响应中断

当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()。该方法可以用来解决死锁问题
示例:

public class ReetrantLockTest3 {

    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,另一个线程正好相反。如果没有外界中断,该程序将处于死锁状态永远无法停止。我们通过使其中一个线程中断,来结束线程间毫无意义的等待。被中断的线程将抛出异常,而另一个线程将能获取锁后正常结束。

五、获取锁限时等待

ReentrantLock还给我们提供了获取锁限时等待的方法tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题

示例:

public class ReetrantLockTest4 {

    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
		//该线程先获取锁1,再获取锁2
        Thread thread = new Thread(new ThreadDemo(lock1, lock2));
        //该线程先获取锁2,再获取锁1
        Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));
        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() + "正常结束!");
            }
        }
    }
}

在这里插入图片描述

六、结合Condition实现线程等待通知

使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制。ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。
Condition由ReentrantLock对象创建,并且可以同时创建多个

示例:

static Condition notEmpty = lock.newCondition();
static Condition notFull = lock.newCondition();

Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁。之后调用Condition接口的await()将释放锁挂,等待唤醒,并且在该Condition上等待,直到有其他线程调用Condition的signal()方法唤醒线程。使用方式和wait,notify类似。

示例:

public class ReetrantLockTest5 {

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    static class SignalThread implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                condition.signal();
                System.out.println("子线程通知");
            } finally {
                lock.unlock();
            }
        }
    }

    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("主线程恢复运行");
    }
}

在这里插入图片描述

七、ReetrantLock源码分析

ReentrantLock可以有公平锁和非公平锁的不同实现,只要在构造它的时候传入不同的布尔值,继续跟进下源码我们就能发现,关键在于实例化内部变量sync的方式不同,如下所示:
在这里插入图片描述
非公平锁继承结构
在这里插入图片描述

公平锁继承结构
在这里插入图片描述
公平锁内部是FairSync,非公平锁内部是NonfairSync。而不管是FairSync还是NonfariSync,都间接继承自AbstractQueuedSynchronizer这个抽象类。

7.1 AbstractQueuedSynchronizer类(AQS)

AbstractQueuedSynchronizer,简称AQS, 是一个用于构建锁、同步器等线程协作工具类的框架,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件为构建不同的同步组件提供了可扩展的基础框架常见的有:ReentrantLock、CountDownLatch等,如下图所示。
在这里插入图片描述
AQS以模板方法模式在内部定义了获取和释放同步状态的模板方法,并留下钩子函数供子类继承时进行扩展,由子类决定在获取和释放同步状态时的细节,从而实现满足自身功能特性的需求。除此之外,AQS通过内部的同步队列管理获取同步状态失败的线程,向实现者屏蔽了线程阻塞和唤醒的细节

7.2 AQS的设计思想

1) AQS中同步等待队列的实现是一个带头尾指针的双向链表。
在这里插入图片描述
head队列首元素线程结点
tail队列尾线程结点
2) 内部类Node设计
在这里插入图片描述

  • prev指向前一个结点的指针
  • next指向后一个结点的指针
  • thread当前结点表示的线程,因为同步队列中的结点内部封装了之前竞争锁失败的线程,故而结点内部必然有一个对应线程实例的引用
  • waitStatus对于重入锁而言,主要有3个值
    0:初始化状态;
    -1(SIGNAL):当前结点表示的线程在释放锁后需要唤醒后续节点的线程;
    1(CANCELLED):在同步队列中等待的线程等待超时或者被中断,取消继续等待;

    同步队列示意图
    在这里插入图片描述
    为了更好的理解加锁和解锁过程的源码,说下同步队列的特性:
    • 1.同步队列是个先进先出(FIFO)队列,获取锁失败的线程将构造结点并加入队列的尾部,并阻塞自己
    • 2.队列首结点可以用来表示当前正获取锁的线程
    • 3.当前线程释放锁后将尝试唤醒后续处结点中处于阻塞状态的线程
    • 4.这个同步队列是FIFO队列,也就是说先在队列中等待的线程将比后面的线程更早的得到锁

7.3 AQS中的state属性

在这里插入图片描述
这是一个带volatile(保证了参数的可见性)前缀的int值,是一个类似计数器的东西。在不同的同步组件中有不同的含义。以ReentrantLock为例,state可以用来表示该锁被线程重入的次数
       当state为0表示该锁不被任何线程持有;
       当state为1表示线程恰好持有该锁1次(未重入);
       当state大于1则表示锁被线程重入state次。
       因为这是一个会被并发访问的量,为了防止出现可见性问题要用volatile进行修饰

7.4 exclusiveOwnerThread

该属性存在AbstractQueuedSynchronizer父类中
在这里插入图片描述
如注释所言,这是在独占同步模式下标记持有同步状态线程ReentrantLock就是典型的独占同步模式,该变量用来标识锁被哪个线程持有

7.5 AQS加锁

由于非公平锁在实际开发中用的比较多,就简单说说非公平锁加锁流程。非公平锁的代码如下:

public class NoFairLockTest {

    public static void main(String[] args) {
        //创建非公平锁
        ReentrantLock lock = new ReentrantLock(false);
        try {
            //加锁
            lock.lock();
            //模拟业务处理用时
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();
        }
    }
}

流程开始
1. 非公平锁Lock方法
在这里插入图片描述
2. 真正的加锁入口
在这里插入图片描述
首先尝试快速获取锁,以cas的方式将state的值更新为1,只有当state的原值为0时更新才能成功,因为state在ReentrantLock的语境下等同于锁被线程重入的次数,这意味着只有当前锁未被任何线程持有时该动作才会返回成功。若获取锁成功,则将当前线程标记为持有锁的线程,然后整个加锁流程就结束了
若获取锁失败,则执行acquire方法
3. 获取锁失败acquire分支
在这里插入图片描述
该方法主要的逻辑都在if判断条件中,这里面有3个重要的方法
tryAcquire(),
addWaiter(),
acquireQueued(),

这三个方法中分别封装了加锁流程中的主要处理逻辑,理解了这三个方法到底做了哪些事情,整个加锁流程就清晰了。
4. tryAcquire方法
tryAcquireAQS中定义的钩子方法,如下所示:
在这里插入图片描述
该方法默认会抛出异常,强制同步组件通过扩展AQS来实现同步功能的时候必须重写该方法,ReentrantLock在公平和非公平模式下对此有不同实现,非公平模式的实现如下:
在这里插入图片描述
底层调用了nonfairTryAcquire()从方法名上我们就可以知道这是非公平模式下尝试获取锁的方法,具体方法实现如下:
在这里插入图片描述
这是非公平模式下获取锁的通用方法它包括了当前线程在尝试获取锁时的所有可能情况
• 1.当前锁未被任何线程持有(state=0),则以cas方式获取锁,若获取成功则设置exclusiveOwnerThread为当前线程,然后返回成功的结果;若cas失败,说明在得到state=0和cas获取锁之间有其他线程已经获取了锁,返回失败结果。
• 2.若锁已经被当前线程获取(state>0,exclusiveOwnerThread为当前线程),则将锁的重入次数加1(state+1),然后返回成功结果。因为该线程之前已经获得了锁,所以这个累加操作不用同步。
• 3.若当前锁已经被其他线程持有(state>0,exclusiveOwnerThread不为当前线程),则直接返回失败结果。

因为我们用state来统计锁被线程重入的次数,所以当前线程尝试获取锁的操作是否成功可以简化为:state值是否成功累加1,是则尝试获取锁成功,否则尝试获取锁失败。

其实这里还可以思考一个问题:nonfairTryAcquire已经实现了一个囊括所有可能情况的尝试获取锁的方式,为何在刚进入lock方法时还要通过compareAndSetState(0, 1)去获取锁,毕竟后者只有在锁未被任何线程持有时才能执行成功,我们完全可以把compareAndSetState(0, 1)去掉,对最后的结果不会有任何影响。这种在进行通用逻辑处理之前针对某些特殊情况提前进行处理的方式在后面还会看到,一个直观的想法就是它能提升性能,而代价是牺牲一定的代码简洁性
5. 退回到上层的acquire方法
在这里插入图片描述
tryAcquire(arg)返回成功,则说明当前线程成功获取了锁(第一次获取或者重入),由取反和&&可知,整个流程到这结束,只有当前线程获取锁失败才会执行后面的判断。先来看addWaiter(Node.EXCLUSIVE)部分,这部分代码描述了当线程获取锁失败时如何安全的加入同步等待队列
这部分代码可以说是整个加锁流程源码的精华,充分体现了并发编程的艺术性
6. 获取锁失败的线程如何安全的加入同步队列:addWaiter()
在这里插入图片描述
首先创建了一个新节点,并将当前线程实例封装在其内部,之后我们直接看enq(node)方法就可以了,中间这部分逻辑在enq(node)中都有,之所以加上这部分“重复代码”和尝试获取锁时的“重复代码”一样,对某些特殊情况进行提前处理,牺牲一定的代码可读性换取性能提升
在这里插入图片描述
这里有两个CAS操作:
• compareAndSetHead(new Node()),CAS方式更新head指针,仅当原值为null时更新成功
在这里插入图片描述
• compareAndSetTail(t, node),CAS方式更新tail指针,仅当原值为t时更新成功
在这里插入图片描述
外层的for循环保证了所有获取锁失败的线程经过失败重试后最后都能加入同步队列队列为空时要进行特殊处理,这部分在if分句中。注意当前线程所在的结点不能直接插入空队列,因为阻塞的线程是由前驱结点进行唤醒的故先要插入一个结点作为队列首元素,当锁释放时由它来唤醒后面被阻塞的线程从逻辑上这个队列首元素也可以表示当前正获取锁的线程,虽然并不一定真实持有其线程实例(空实例)

首先通过new Node()创建一个空结点,然后以CAS方式让头指针指向该结点(该结点并非当前线程所在的结点),若该操作成功,则将尾指针也指向该结点。这部分的操作流程可以用下图表示
在这里插入图片描述
当队列不为空,则执行通用的入队逻辑,这部分在else分句中
在这里插入图片描述
首先当前线程所在的结点的前向指针pre指向当前线程认为的尾结点,源码中用t表示。然后以CAS的方式将尾指针指向当前结点,该操作仅当tail=t,即尾指针在进行CAS前未改变时成功。若CAS执行成功,则将原尾结点的后向指针next指向新的尾结点。整个过程如下图所示
在这里插入图片描述
7. 线程加入同步队列后acquireQueued
在这里插入图片描述
这段代码主要的内容都在for循环中,这是一个死循环,主要有两个if分句构成。
第一个if分句中,当前线程首先会判断前驱结点是否是头结点,如果是则尝试获取锁,获取锁成功则会设置当前结点为头结点(更新头指针)。为什么必须前驱结点为头结点才尝试去获取锁?因为头结点表示当前正占有锁的线程,正常情况下该线程释放锁后会通知后面结点中阻塞的线程,阻塞线程被唤醒后去获取锁,这是我们希望看到的然而还有一种情况,就是前驱结点取消了等待,此时当前线程也会被唤醒,这时候就不应该去获取锁,而是往前回溯一直找到一个没有取消等待的结点,然后将自身连接在它后面。一旦我们成功获取了锁并成功将自身设置为头结点,就会跳出for循环否则就会执行第二个if分句:确保前驱结点的状态为SIGNAL,然后阻塞当前线程
8. shouldParkAfterFailedAcquire方法
在这里插入图片描述
可以看到针对前驱结点pred的状态会进行不同的处理
• 1.pred状态为SIGNAL,则返回true,表示要阻塞当前线程
• 2.pred状态为CANCELLED,则一直往队列头部回溯直到找到一个状态不为CANCELLED的结点,将当前节点node挂在这个结点的后面
• 3.pred的状态为初始化状态,此时通过compareAndSetWaitStatus(pred, ws, Node.SIGNAL)方法将pred的状态改为SIGNAL。

其实这个方法的含义很简单,就是确保当前结点的前驱结点的状态为SIGNAL,SIGNAL意味着线程释放锁后会唤醒后面阻塞的线程。毕竟,只有确保能够被唤醒,当前线程才能放心的阻塞。
9. parkAndCheckInterrupt
shouldParkAfterFailedAcquire返回true表示应该阻塞当前线程,则会执行parkAndCheckInterrupt方法,这个方法比较简单,底层调用了LockSupport来阻塞当前线程。
在这里插入图片描述
该方法内部通过调用LockSupport的park方法来阻塞当前线程
在这里插入图片描述
概括的说,线程在同步队列中会尝试获取锁,失败则被阻塞,被唤醒后会不停的重复这个过程,直到线程真正持有了锁,并将自身结点置于队列头部

最后ReentrantLock非公平模式下的加锁流程如下
在这里插入图片描述

7.6 AQS解锁

解锁的源码相对简单
在这里插入图片描述
正确找到sync的实现类,找到真正的入口方法,主要内容都在一个if语句中,先看下判断条件tryRelease方法
在这里插入图片描述
tryRelease其实只是将线程持有锁的次数减1,即将state值减1,若减少后线程将完全释放锁(state值为0),则该方法将返回true,否则返回false由于执行该方法的线程必然持有锁,故该方法不需要任何同步操作
若当前线程已经完全释放锁,即锁可被其他线程使用,则还应该唤醒后续等待线程执行。不过在此之前需要进行两个条件的判断
• h!=null是为了防止队列为空,即没有任何线程处于等待队列中,那么也就不需要进行唤醒的操作
• h.waitStatus != 0是为了防止队列中虽有线程,但该线程还未阻塞,由前面的分析知,线程在阻塞自己前必须设置前驱结点的状态为SIGNAL,否则它不会阻塞自己。

接下来就是唤醒线程的操作,unparkSuccessor(h)源码如下:
在这里插入图片描述
一般情况下只要唤醒后继结点的线程就行了,但是后继结点可能已经取消等待,所以从队列尾部往前回溯,找到离头结点最近的正常结点,并唤醒其线程。
在这里插入图片描述

八、总结

AQS内部是使用了双向链表将等待线程链接起来,当发生并发竞争的时候,就会初始化该队列并让线程进入睡眠等待唤醒。
由FIFO队列的特性知,先加入同步队列等待的线程会比后加入的线程更靠近队列的头部,那么它将比后者更早的被唤醒,它也就能更早的得到锁从这个意义上,对于在同步队列中等待的线程而言,它们获得锁的顺序和加入同步队列的顺序一致,这显然是一种公平模式。然而,线程并非只有在加入队列后才有机会获得锁,哪怕同步队列中已有线程在等待,非公平锁的不公平之处就在于此。回看下非公平锁的加锁流程,线程在进入同步队列等待之前有两次抢占锁的机会:
• 第一次是非重入式的获取锁,只有在当前锁未被任何线程占有(包括自身)时才能成功;
• 第二次是在进入同步队列前,包含所有情况的获取锁的方式。

只有这两次获取锁都失败后,线程才会构造结点并加入同步队列等待而线程释放锁时是先释放锁(state -1),然后才唤醒后继结点的线程的

可以试想下这种情况,线程A已经释放锁,但还没来得及唤醒后继线程C,而这时另一个线程B刚好尝试获取锁,此时锁恰好不被任何线程持有,它将成功获取锁而不用加入队列等待。线程C被唤醒尝试获取锁,而此时锁已经被线程B抢占,故而其获取失败并继续在队列中等待。整个过程如下图所示:
在这里插入图片描述
如果以线程第一次尝试获取锁到最后成功获取锁的次序来看,非公平锁确实很不公平。因为在队列中等待很久的线程相比还未进入队列等待的线程并没有优先权,甚至竞争也处于劣势:在队列中的线程要等待其他线程唤醒,在获取锁之前还要检查前驱结点是否为头结点。在锁竞争激烈的情况下,在队列中等待的线程可能迟迟竞争不到锁。这也就非公平在高并发情况下会出现的饥饿问题

那我们在开发中为什么大多使用会导致饥饿的非公平锁?很简单,因为它性能好啊!

内容很多,各位看官慢慢看,也欢迎各位大佬纠正。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/77202.html

(0)

相关推荐

半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!