Android知识点整理5:并发和锁

目录

 

一、线程并发的问题

1、死锁

(1)简单描述:

(2)死锁的必要条件:

 (3) 死锁打破必要条件:

(4)死锁的危害:

(5)死锁的解决方案:

2、活锁

(1)简介:

(2)解决方案:

3、线程饥饿

(1)简介:

(2)解决方案:

二、java提供的几种解决线程安全问题的方案

1、CAS

(1)原子操作

(2)CAS 实现原子操作的三大问题

(3)Jdk中相关原子操作类的使用

2、AQS

3、voliatile

4、sychronized


一、线程并发的问题

1、死锁

(1)简单描述:

两个或两个以上的线程或进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。所以死锁是必然发生在多操作者(M>=2个)情况下,争夺多个资源(N>=2个,且N<=M)才会发生这种情况

(2)死锁的必要条件:

2).1 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

2).2 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

2).3 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

2).4 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

 (3) 死锁打破必要条件:

根据上面的死锁必要条件,其实打破一个就行了

3).1 打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。

3).2 打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。

3).3 打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。

3).4 打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。常见的算法有:有序资源分配法、银行家算法。银行加算法感兴趣的可以看 https://www.jianshu.com/p/c7658a81fb21

总结:关键是保证拿锁顺序一致,解决方案一:内部通过顺序比较,确定拿锁的顺序; 解决方案二:采用尝试拿锁机制

(4)死锁的危害:

4).1 线程不工作了,但是整个程序还是活着的

4).2 没有任何的异常信息可以供我们检查。

4).3 一旦程序发生了发生了死锁,是没有任何的办法恢复的,只能重启程序,对正式已发布程序来说,这是个很严重的问题。

(5)死锁的解决方案:

关键是保证拿锁的顺序一致

5).1 内部通过顺序比较,确定拿锁的顺序;

4).2 采用尝试拿锁的机制。

2、活锁

(1)简介:

两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。

(2)解决方案:

每个线程休眠随机数,错开拿锁的时间。

3、线程饥饿

(1)简介:

低优先级的线程,总是拿不到执行时间

(2)解决方案:

使用比较好的 “公平策略”

详细的可以看这篇翻译的文章:

https://segmentfault.com/a/1190000021910655

二、java提供的几种解决线程安全问题的方案

1、CAS

简介:Compare and Swap,即比较再交换。是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术

(1)原子操作

说原子操作之前我们可以了解下 Java内存模型(JMM)、可见性、原子性和有序性的基本概念

比较详细内容,大家可以网上找找例如下面两篇文章

https://blog.csdn.net/u011080472/article/details/51337422

https://www.jianshu.com/p/8420ade6ff76

我这里只是简单介绍下

  1).1 java 内存模型(JMM)

     从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

可以用下面图来展示

 

 1).2 可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

由于线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,那么对于共享变量V,它们首先是在自己的工作内存,之后再同步到主内存。可是并不会及时的刷到主存中,而是会有一定时间差。很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了 。

要解决共享对象可见性这个问题,我们可以使用volatile关键字或者是加锁。

 1).3 原子性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

我们都知道CPU资源的分配都是以线程为单位的,并且是分时调用,操作系统允许某个进程执行一小段时间,例如 50 毫秒,过了 50 毫秒操作系统就会重新选择一个进程来执行(我们称为“任务切换”),这个 50 毫秒称为“时间片”。而任务的切换大多数是在时间片段结束以后,

那么线程切换为什么会带来bug呢?因为操作系统做任务切换,可以发生在任何一条CPU 指令执行完!注意,是 CPU 指令,CPU 指令,CPU 指令,而不是高级语言里的一条语句。比如count++,在java里就是一句话,但高级语言里一条语句往往需要多条 CPU 指令完成。其实count++包含了三个CPU指令!

 1).4 有序性

JMM的有序性表现为:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句指“线程内表现为串行的语义”(as-if-serial),后半句值“指令重排序”和普通变量的”工作内存与主内存同步延迟“的现象。 

 在执行程序时为了提高性能,编译器和处理器经常会对指令进行重排序。对单个volatile操作有不允许指令重排操作

1).5 什么是原子性

假定有两个操作A和B(A和B可能都很复杂),如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。

实现原子操作可以使用锁,锁机制,满足基本的需求是没有问题的了,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁,

这里会有些问题:首先,如果被阻塞的线程优先级很高很重要怎么办?其次,如果获得锁的线程一直不释放锁怎么办?(这种情况是非常糟糕的)。还有一种情况,如果有大量的线程来竞争资源,那CPU将会花费大量的时间和资源来处理这些竞争,同时,还有可能出现一些例如死锁之类的情况,最后,其实锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿过于笨重。

实现原子操作还可以使用当前的处理器基本都支持CAS()的指令,只不过每个厂家所实现的算法并不一样

CAS算法:每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。

CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作,直到成功为止。

(2)CAS 实现原子操作的三大问题

(2).1  ABA问题

 因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。举个通俗点的例子,你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA问题。

如果你是一个讲卫生讲文明的小伙子,不但关心水在不在,还要在你离开的时候水被人动过没有,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值0,别人喝水前麻烦先做个累加才能喝水。

(2).2 循环时间开销大问题

 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

(2).3 只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

(3)Jdk中相关原子操作类的使用

     3).1 AtomicInteger

•int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。

•boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。

•int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。

•int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。

线程池中ctl变量就用到了这个类: 文章链接:https://blog.csdn.net/wuqiqi1992/article/details/107880764

     3).2 AtomicIntegerArray

主要是提供原子的方式更新数组里的整型,其常用方法如下。

•int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。

•boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。

需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。

     3).3 更新引用类型

原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类。

AtomicReference

原子更新引用类型。

AtomicStampedReference

利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在ABA问题了。这就是AtomicStampedReference的解决方案。AtomicMarkableReference跟AtomicStampedReference差不多, AtomicStampedReference是使用pair的int stamp作为计数器使用,AtomicMarkableReference的pair使用的是boolean mark。 还是那个水的例子,AtomicStampedReference可能关心的是动过几次,AtomicMarkableReference关心的是有没有被人动过,方法都比较简单。

AtomicMarkableReference:

原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。

2、AQS

简介

队列同步器AbstractQueuedSynchronizer(以下简称同步器或AQS),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。并发包的大师(Doug Lea)期望它能够成为实现大部分同步需求的基础。

(1)AQS使用方式和其中的设计模式

AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,在AQS里由一个int型的state来代表这个状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。

 /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     */
    private volatile int state;

在实现上,子类推荐被定义为自定义同步组件的静态内部类,AQS自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器。可以这样理解二者之间的关系:

锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;

同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

实现者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

(2)AQS中的方法

模板方法模式

同步器的设计基于模板方法模式。模板方法模式的意图是,定义一个操作中的算法的骨架,而将一些步骤的实现延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。我们最常见的就是Spring框架里的各种Template。

实际例子

我们开了个蛋糕店,蛋糕店不能只卖一种蛋糕呀,于是我们决定先卖奶油蛋糕,芝士蛋糕和慕斯蛋糕。三种蛋糕在制作方式上一样,都包括造型,烘焙和涂抹蛋糕上的东西。所以可以定义一个抽象蛋糕模型

然后就可以批量生产三种蛋糕

这样一来,不但可以批量生产三种蛋糕,而且如果日后有扩展,只需要继承抽象蛋糕方法就可以了,十分方便,我们天天生意做得越来越赚钱。突然有一天,我们发现市面有一种最简单的小蛋糕销量很好,这种蛋糕就是简单烘烤成型就可以卖,并不需要涂抹什么食材,由于制作简单销售量大,这个品种也很赚钱,于是我们也想要生产这种蛋糕。但是我们发现了一个问题,抽象蛋糕是定义了抽象的涂抹方法的,也就是说扩展的这种蛋糕是必须要实现涂抹方法,这就很鸡儿蛋疼了。怎么办?我们可以将原来的模板修改为带钩子的模板。

做小蛋糕的时候通过flag来控制是否涂抹,其余已有的蛋糕制作不需要任何修改可以照常进行。

AQS中的方法

模板方法

实现自定义同步组件时,将会调用同步器提供的模板方法,

这些模板方法同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放、同步状态和查询同步队列中的等待线程情况。

可重写的方法

 

访问或修改同步状态的方法

重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。

•getState():获取当前同步状态。

•setState(int newState):设置当前同步状态。

•compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。 

(3)CLH队列锁

CLH队列锁即Craig, Landin, and Hagersten (CLH) locks。

CLH队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。

当一个线程需要获取锁时:

  1. 创建一个的QNode,将其中的locked设置为true表示需要获取锁,myPred表示对其前驱结点的引用

  1. 线程A对tail域调用getAndSet方法,使自己成为队列的尾部,同时获取一个指向其前驱结点的引用myPred

线程B需要获得锁,同样的流程再来一遍

3.线程就在前驱结点的locked字段上旋转,直到前驱结点释放锁(前驱节点的锁值 locked == false)

4.当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前驱结点

如上图所示,前驱结点释放锁,线程A的myPred所指向的前驱结点的locked字段变为false,线程A就可以获取到锁。

CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail)。CLH队列锁常用在SMP体系结构下。

Java中的AQS是CLH队列锁的一种变体实现。

(4)ReentrantLock的实现

锁的可重入

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。

1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。

2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

nonfairTryAcquire方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。同步状态表示锁被一个线程重复获取的次数。

如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。

公平和非公平锁

ReentrantLock的构造函数中,默认的无参构造函数将会把Sync对象创建为NonfairSync对象,这是一个“非公平锁”;而另一个构造函数ReentrantLock(boolean fair)传入参数为true时将会把Sync对象创建为“公平锁”FairSync。

nonfairTryAcquire(int acquires)方法,对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同。tryAcquire方法,该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

3、voliatile

4、sychronized

有时间再完善

三、各种角度锁对比

1、乐观锁和悲观锁

(1)乐观锁:

乐观锁是相对悲观锁而言,也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。

相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。

CAS就是典型的乐观锁

(2)悲观锁

它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

之所以叫做悲观锁,是因为这是一种对数据的修改抱有悲观态度的并发控制方式。我们一般认为数据被并发修改的概率比较大,所以需要在修改之前先加锁。

悲观锁主要分为 共享锁和 其他锁(又叫排它锁)
 

2、独享锁和共享锁

都属于悲观锁下面

(1)共享锁【Shared lock】又称为读锁,简称S锁。

顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

(2)排他锁【Exclusive lock】又称为写锁,简称X锁。

顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。

 

3、互斥锁和读写锁

(1)互斥锁

一次只能一个线程拥有互斥锁,其他线程只有等待

(2)读写锁

多个读者可以同时进行读

写者必须互斥(只允许一个写者写,也不能读者写者同时进行)

写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

 

4、可重入锁和不可重入锁

(1)可重入锁

不仅判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界资源,并把加锁次数加一。

(2)不可重入锁

只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待。实现简单

详细的可以看这篇博客:https://blog.csdn.net/qq_29519041/article/details/86583945

 

5、公平锁和非公平锁 

 

(1)公平锁

老的线程排队使用锁,新线程仍然排队使用锁。

 

(2)非公平锁

非公平锁保证:老的线程排队使用锁;但是无法保证新线程抢占已经在排队的线程的锁。

(3)ReentrantLock 中相关公平锁和非公平锁实现,直接看源码

构造方法,默认使用非公平锁

  /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * 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();
    }

非公平锁实现:

   /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        // Android-removed: @ReservedStackAccess from OpenJDK 9, not available on Android.
        // @ReservedStackAccess
        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;
        }

公平锁实现

  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;
        }

 非公平锁的实现在刚开始获取锁的时候,直接使用了一次CAS去尝试获取锁,不成功才会构建node节点。而在公平锁中会首先判断是否已经有等待中的线程。这就是公平锁与非公平锁的差异所在。

1.非公平锁在调用lock后,会首先进行CAS抢锁,如果这个时候抢占到了,就直接返回true了
2.相对来说,非公平所性能会比较好一点,吞吐量比较大。

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值