线程面试:高级篇

本节复习点

1、 java内存模型

2、 线程的任务调度&java线程的实现

3、何为线程安全

4、线程安全的实现

5、 CAS机制

6、锁优化技术

7、 volitile原理:文中工作内存与主存交互过程中结合理解。

8、syncronized原理:虚拟机自动完成那个,通过monitorenter、monitorexit指令来完成。

9、AQS机制(结合几种锁来探究)

一、java内存模型

问题引申1:让计算机的同时做几件事的根本原因?

让计算机同时做几件事的原因不仅仅是因为计算机的运算能力强大,还有一个重要的原因就是计算机的运算速度太快,然而他的数据存储、数据访问相对速度太慢(磁盘io/数据库访问等速度相对来说很慢)。

如果不希望处理器大部分时间都处于等待状态,这时可以压榨计算机的处理器,让它同时处理多项任务。

问题引申2:让计算机处理器并发处理多个任务可以更充分的利用计算机处理器的效能吗?

这个看起来是一个因果关系,实际上并没有那么简单。绝大多数的任务不仅仅是靠计算机运算器的运算来完成的。处理器至少要与内存交互(如读取运算数据、存储计算结果),

这个io操作不可避免。由于计算机的存储设备与运算器的运算存速度在较大的差距,现在计算机系统在cpu与内存之间加入了读写速度尽可能接近cpu速度的高速缓冲区。

在这里插入图片描述
在这里插入图片描述

高速缓冲区的作用?

将运算所需要的数据从内存复制到缓存中,让运算能快速进行,当运算结束后吧缓存中的数据同步到内存。这样处理器就无需等待较慢内存读写了。

高速缓冲区带来的问题?

解决了处理器与内存速度的矛盾但是带来了一个新的问题,即缓存一致性问题

在多处理器操作系统中,每个处理器都有自己的高速缓存,然而这些处理器共用一块主存。当多个处理器的缓存任务都涉及到主存的同一区域时,这时就会出现数据问题,同步主存的数据使用哪个缓冲区的呢?

为了解决一致性问题,处理器访问缓存时需要遵循一些缓存协议

处理器做了哪些优化?

代码进行乱序执行优化。(java的jit编译器也有类似的指令重排)

处理器在计算之后会将乱序执行的结果进行重组,保证结果与顺序执行的结果一致。但不保证程序中各个语句计算的先后顺序与代码中的顺序一致。

如下:处理器可以保证最终 c的结果,但不能保证上述三步的执行顺序。

                    int a = getA();

                    int b = getB();

                    int c = a+b;
1、 Java内存模型的由来

主流的语言如c,c++ 直接使用物理硬件个操作系统的内存模型,这可能导致同一套并发程序到会不同平台上运行时可能会出现错误。因此Java 虚拟机规范试图定义一种 Java 内存模型(JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各个平台下都能达到一致的内存访问效果。所以参考着计算机的处理器、高速缓存、主存之间的交互关系设计了JMM。

在这里插入图片描述

在缓存一致性上计算机定义了缓存一致性协议,而JMM定义了工作内存与之内存之间的原子交互操作~

2、主内存与工作内存

Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与 Java 编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的,不会被共享,自然就不存在竞争问题。后者只在于工作内存中处理

Java 内存模型规定了所有的变量都是存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保留了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

3、内存间的交互操作(类似于计算机内存模型cpu与主存间的交互模型)

(1)java 内存模型中定义了以下八种操作来实现主内存与工作内存之间交互的细节。虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于 double 和 long 类型的变量来说有例外)

  • read (读取): 作用于主内存的变量,把一个变量的值从主内存传输到线程工作内存,以便于随后的load动作使用。

  • load (载入): 作用于工作内存的变量,把read操作从主内存传递过来变量值放入到工作内存的变量副本中

  • use (使用): 作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎(执行引擎可以看做运行时栈结构,也即吧堆上的信息放到栈中处理)。

  • assign (赋值): 作用于工作内存的变量,把一个从执行引擎接收到的值,赋值给工作内存的变量。

  • store (存储) : 作用于工作内存的变量,把工作内存中一个变量的值传递到主内存中,以便于以后的write操作使用。

  • write (写入 ) : 作用于主内存的变量,把store操作从工作内存得到的值放到主内存的变量中。

  • lock (锁定): 作用于主内存的变量,把一个变量标记为一条线程独占状态。

  • unlock (解锁): 作用于主内存的变量,把一个处于锁定状态的变量释放出来。释放后的变量才能被其他线程锁定。

在这里插入图片描述

ps:read load 操作 必须是顺序执行的。但不保证二者操作必须连续。因此read、load之前可以穿插其他指令。同理store、write操作必须顺序执行。

(2)Java 内存模型还规定了在执行上诉八种基本操作时必须满足以下规则:

  • 不允许 read、load、store 和 write 操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起了回写但主内存不接受的情况。

  • 不允许一个线程丢弃它最近的 assign 操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

  • 不允许一个线程无原因的把数据从工作内存同步回主内存中

  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign操作)的变量。也就是说对变量进行use、store操作前必须执行了assgin和load操作。

  • 一个变量在同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁

  • 如果对一个变量执行 lock 操作,那么将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。

  • 如果一个变量事先没有被 lock 操作锁定,那就不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定住的变量。

  • 对一个变量执行 unlock 操作之前,必须把此变量同步回主内存中

(3)对volatile变量的特殊规则。当一个变量定义为 volatile 之后,它将具备两种特性:

a、保证此变量对所有线程的可见性

1、这里的可见性是指当一个线程修改了这个变量的值,新值对于其它线程来说是可以立即得知的,而普通变量的值在线程间传递均需要通过主内存来完成。
2、其实volatile修饰的变量也是依靠工作内存与主内存交互才使变量其他线程可见的,只是这个变量修饰后jvm多了些处理,使主内存快速与工作内存交互。volatile 变量的特殊规则是保证了新值能立即同步到主内存,以及每次使用变量前立即从主内存刷新。如下添加volatile后相当于在主内存与所有工作内存间添加总线嗅探器监听,这个总线嗅探器其实就是缓存一致性协议(MESI)

在这里插入图片描述

b、禁止指令重新排序优化(这里可结合单例DCL分析,面试时也可以举这个例子来探究下使用场景。有兴趣可以看下有无这个关键字时的汇编代码 )

1、普通的变量仅仅会保证在该方法执行过程中 所有依赖赋值结果 的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的方法执行过程中无法感知到这一点(依赖赋值结果的代码就算乱序执行,对结果也无影响),这也就是 Java 内存模型中描述的所谓的 “线程内表现为串行的语义”(As - if - Serial)。
2、 volatile 变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢一些,因为它需要在本地代码中插入很多内存屏障指令来保证处理器不会发生乱序执行。

问题:volatile变量在各线程中都是一致的,所基于volatile修饰的变量在并发下是安全的吗?

答:volatile修饰的变量,指当一个线程修改了这个变量的值,新值对于其它线程来说是可以立即得知的。但这个并不能得出“volatile修饰的变量在并发下是安全的”。

虽然volatile修饰的变量在各线程的工作内存中都是可见的、一致的(各线程的工作内存中volatile修饰的变量也可以存在不一致的情况,但每次执行之前都要刷新,执行引擎看不到不一致问题,所以认为不存在一致性问题)。但是java里面的运算并非原子性操作(如变量依赖的赋值结果相关语句一个方法中存在非原子性操作)。仅仅使用volatile是不行的还需要使用synchronized来保证整体操作的原子性。因此volatile变量在并发下一样是不安全的。

由于 volatile 变量只能保证可见性、一致性,在不符合以下两条规则的运算场景下,我们仍然要通过加锁来保证原子性:

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一线程修改了变量的值

  • 变量不需要与其他的状态变量共同参与不变约束

(4)总结

java8种内存访问操作、8中操作要求、以及volatile变量的特殊规则就已经完全确定了java中哪些内存访问操作在并发下是安全的。

(5)感悟

volatile的引入保证了 变量相关赋值结果语句的一致性,这样结合synchronized就可以保证整体操作的 多线程安全。这里需要注意static的区别:

  • volatile修饰的变量线程共享。

  • static修饰的变量类的所有对象共享。

  • 静态变量也称为类变量,属于类对象所有,位于方法区,为所有对象共享,共享一份内存,一旦值被修改,则其他对象均对修改可见。

二、原子性、可见性、有序性

1、原子性

我们大致可以认为基本数据类型的访问读写是具备原子性的。如果应用场景需要一个更大范围的原子性保证,Java 内存模型还提供了 lock 和 unlock 操作来满足这种需求。

尽管虚拟机未把 lock 和 unlock 操作直接开放给用户使用,但是却提供了更高层次的字节码指令 monitorenter 和 monitorexit 来隐式的使用这两种操作,这两个字节码指令就对应于 Java 中的 synchronized 关键字。

2、可见性

可见性是指当一个变量修改了共享变量的值,其他线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性。

无论是普通变量还是 volatile 变量都是如此,volatile 变量的特殊规则是保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说 volatile 变量保证了多线程操作时的可见性,而普通变量则不能保证这一点。

除了 volatile 之外,Java 还有两个关键字能 实现可见性,即 synchronizedfinal。同步块的可见性是由 “对变量执行 unlock 操作之前,必须先把此变量同步回主内存中” 这条规则获得。

而 final 关键字的可见性是指:被 final 修饰的字段在构造器中一旦初始化完成,并且构造器没有把 this 引用传递出去,那么其他线程就能看见 final 字段的值。

3、有序性

如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指 “线程内表现为串行的语义(As - if - Serial)”,后半句是指 “指令重排序” 现象和 “工作内存和主内存同步延迟” 现象。

Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 关键字本身就包含了禁止指令重排序的语义,而 synchronized 则是由 “一个变量在同一个时刻只允许一条线程对其进行 lock 操作” 这条规则获得的,这条规则决定了持有同一个锁的同步块只能串行的进入。这样结合起来就保证了并发下的线程安全。

三、先行发生原则

先行发生是指 Java 内存模型中定义的 两项操作之间的偏序关系。如果是操作 A 先行发生与B,那么操作 A 产生的影响能够被B观察到。

下面是 Java 内存模型中天然的先行发生关系,可以在编码中使用,如果两个操作不在此列,或者无法用下列规则推导出,则jvm可对代码 进行随意排序。

1、程序次序规则

线程内按照代码次序执行。

2、管程锁定规则

一个 unlock 操作先行发生与后面(时间上的先后)对同一个锁的 lock 操作。

3、volatile 变量规则

对一个 volatile 变量的写操作先行发生于后面((时间上的先后))对这个变量的读操作。

4、线程启动规则

Thread.start 方法先行发生于此线程的每一个动作。

5、线程终止规则

线程中所有的操作都优先于对此线程的终止操作。

四、线程的任务调度&java线程实现

线程是比进程更轻量级的调度执行单位,线程的引入可以吧一个进程的资源分配和执行调度分开。 各个线程既可以共享资源(内存地址/文件io),又可以独立的调度(线程是cpu调度的基本单位)。

1、主流的操作系统对线程的实现

java语言提供了在不同硬件和操作系统平台下对线程的线程操作的统一处理。每个执行了start方法,但是还未结束的Thread类就代表一个线程。主流的操作系统都支持了对线程的实现,主要有三种方式:

a、使用内核线程实现

b、使用用户线程实现

c、使用用户线程+轻量级进程实现

jdk1.2之前java线程采用用户线程实现,jdk1.2中线程模型替换为操作系统原生线程模型(也即操作系统采用哪种方式实现很大决定jvm上线程是怎样映射的)这点在不同平台上存在差异,虚拟机规范也未规定jvm必须使用哪种线程模型。

线程模型只对线程的并发规模、和操作成本产生影响,对java程序的编码运行来说这些差异都是透明化的

2、java线程的调度

线程调度:指操作系统为线程分配处理器使用权的过程。主要有两种调度方式:

a、协同式线程调度(window3.X上使用)

线程的执行时间由线程本身来控制,线程把自己的工作执行完后会主动通知操作系统切换到另一个线程上。

优点:实现简单,线程切换操作对线程本身可知,无线程同步问题。

缺点:线程执行时间不可控。(如果线程执行遇到问题可能会一直阻塞,不告知操作系统进行线程切换)

b、抢占式线程调度(window9x/NT内核使用)

每个线程将由系统来分配执行时间,线程的切换不由线程本身决定。(java可通过Thread#yield()让出执行时间,但是要获取执行时间线程本身是无任何办法的。只能由操作系统调度)

优点:线程执行时间操作系统可控,不会由一个线程阻塞而导致整个操作系统崩溃。

ps:虽然java线程的调度是由系统自动完成,但是我们可以建议系统给某些线程多些执行时间。Thread.setPriority[1-10]。注意不要以优先级为基准优先级并不一定靠谱。优先级可能会被系统改变如windows的“优先级推进器”

五、线程安全

非常严谨的定义:当多个线程访问同一个对象时,如果不考虑这些线程运行环境下的调度和交替执行,也不需要进行额外的同步。或者在调用方进行其他的协助操作。调用这个对象的行为都可以获取正确的结果。那这个对象是线程安全的。

按照线程安全的程度,java中的操作可以分为5类:

1、不可变(final修饰)

final 关键字的可见性回顾:被 final 修饰的字段在构造器中一旦初始化完成,并且构造器没有把 this 引用传递出去,那么其他线程就能看见 final 字段的值。

jdk1.5之后(java内存模型被修正后的语言)不可变的对象一定是线程安全的。无论是对象的方法实现还是对象的调用者都不需要采取任何线程安全的措施。“不可变”带来的安全性是最简单最纯粹的。

java语言中如果共享数据是基本数据类型那么只需要使用final修饰就可保证他是不可变的。如果共享数据是一个对象,那么需要保证对象的行为不会对其行为产生影响才行。

这里可以参考下String类的对象,他是个典型的final类,其对象都是不可变的。它的一些方法如replace()、subString()的操作都不会对当前对象产生影响,而是重新new Sting,构建了新的对象。

保证对象的行为不影响自己状态的途径有很多种,其中最简单的就是把带有状态的变量都使用final修饰。这样在构造方法执行完毕后他就是不可变的。

(1)java中不可变类型例子:

  • String
  • 枚举
  • Number部分子类:如Long、Double包装类型、BigInteger类型但Number子类AtomicInteger、AtomicLong原子类是可变的。
2、绝对线程安全

绝对线程安全的定义是非常严格的。如上述的线程安全定义。java api中标记自己是线程安全的类大多数不是线程安全的,如Vector。写代码时我们需要做额外的同步。

3、相对线程安全

相对线程安全就是我们平时所讲的线程安全。他需要保证对这个对象的单独操作是线程安全的。我们在调用时不需要做额外的操作。但是如果我们在其他地方
连续多次调用时在其他地方需要单独写保证安全的操作。

如Vector集合,StringBuffer等这些类,方法上都加了锁,假如多线程下我们在方法A中多次调用Vector的某些方法。最好也要把方法A同步下。

4、线程兼容

指对象本身不是线程安全的,可以在调用端使用同步等手段保证某些操作安全。我们写的普通代码,java api中的普通集合就是线程兼容的。

5、线程对立

无论调用端是否采取同步都无法在多线程下并发使用代码。

如两个线程同时持有一个线程对象,并发执行时一个去suppend,一个去resume。那线程就会死锁。这两个对立的操作已被jvm屏蔽。

六、线程安全的实现

1、互斥同步:synchronized

1.6之前性能存在问题,1.6之后增加了优化性能几乎与ReentrantLock 差不多。无果非满足ReentrantLock的场景建议使用synchronized。

特点:互斥同步一种悲观的并发处理策略,认为只要不去做正确的同步处理那么肯定会出现问题。无论共享数据是否真出现竞争他都要进行加锁、用户态核心态转换、维护锁计数器、检查是否有被阻塞的线程是否需要被唤醒。十分消耗性能。

2、非阻塞同步

互斥带来的主要问题就是进行线程阻塞和唤醒带来的性能问题,因此互斥同步也被称为阻塞同步。随着“硬件指令集”的发展。基于冲突检测的乐观并发策略就产生了:

通俗的讲就是先进行操作,如果没有线程竞争资源那么就成功了,当发生竞争时采取其他的补偿措施。最常见的就是不断重试直到成功为止。

这种乐观并发策略的许多实现都不需要吧线程挂起,因此这种同步操作被称为非阻塞式同步。

不借助于互斥操作,那么如何能够保证多步操作、检测的这一些列步骤是原子性呢?这时就要靠硬件指令集了,典型的就是“比较交换”CAS指令。这个指令可把多次操作通过这一条指令来完成。

(1)常见的指令集:前三条执行在20世纪就已经存在大多数处理器指令集中。后两条现代处理器新增。

  • 测试并设置(Test-and-set)
  • 获取并增加(Fetch-and-increment)
  • 交换(swap)
  • 对比交换(compare-and-swap)简称CAS
  • 加载链接/条件存储(load-linked/store-conditional)简称LL/SC
3、无同步方案

要想保证线程安全并不一定需要资源同步。为啥需要资源同步呢?资源同步触发的条件是:多个线程环境、多线程共享同一份资源、资源非原子性(可以简单理解条线程竞争多条代码,这多条代码是一份资源)

如果某个方法本来就不涉及共享数据,那么就无需同步方案来保证安全性。这些代码天生就是安全的。常见的方案有两类:

(1)可重入代码:在代码执行的任何时刻中断他,进而执行其他代码(包括递归调用本身),在控制权返回后原来的代码不会产生任何问题。有点理论化,可以这样理解:

可重入代码不依赖于存储在堆上的数据和共用的系统资源。用到的状态量都是由参数传入。不使用非可重入的代码。例如一个简单的方法调用后就打印句hello world。

这个方法就是单一的提供个打印语句,结果就是hello world。无论哪个线程访问执行完这个方法他都是hello world。

(2)线程本地存储(Thread local Storage):如果一段代码中的数据必须要与其他代码进行共享,那么我们可以看下数据是否能够保证在同一个线程中执行,如果可以能够保证数据在同一个线程中执行我们就把数据的可见限制在一个线程之内。线程间可见就不需要同步问题了。

java提供了ThreadLocal类来实现变量的线程可见。每个线程的Thread对象中都有一个ThreadLocalMap对象。这个对象中存储了以ThreadLocal 的hashCode为键,本地变量为值的 key-value 键值对。

ThreadLocal对象就是当前线程的ThreadLocalMap对象访问入口。通过ThreadLocal 操作ThreadLocalMap进行读取或者存储。

七、CAS机制

1、 CAS:即 CompareAndSwap 比较并替换的意思,是现代处理器新增的处理器指令。这个操作是原子性的,因为对比、替换这些多项操作可以通过这一指令直接完成。
2、CAS实现思路:不断循环对比当前工作内存中的值,和主内存中的值。
3、原理:CAS 机制当中使用了三个基本操作数:内存位置 V(可理简单解为变量内存地址),旧的预期值 A,要修改的新值 B。更新一个变量的时候,只有当内存位置V的值和旧的预期值 A相同时,处理器才会将内存地址V对应的值修改为新值B。否则就一直循环对比直到相等。
4、java中的引入

jdk1.5之后提供了一个类Unsafe,这个类提供了几个方法如compareAndSwapInt()、compareAndSwapInt()等等。jvm对这些方法进行了特殊处理,每个方法及时编译后就会生成对应操作系统平台的CAS指令。

Unsafe类我们不能直接使用,只有启动类加载器才能访问他的实例。如果不使用反射我们只能使用并发包下的原子类提供的api来使用。

5、 CAS弊端:

(1)ABA 问题: 如果一个变量的值初始时是A,期间他的值被改为了B,后来又被改为了A。那么CAS就会认为他从来没有改变过。

(栗子:变量a初始值为100,ThreadA 修改a=200,a=100,ThreadB 此时操作a时发现自己存储的值与主存相同,这时ThreadB认为a没变化过)

参考

(2) 不能保证代码块的原子性(只能保证你操作的某一变量相关特定操作的原子性)

(3) CPU 开销大(在多次尝试更新一个值时不成功时)

6、CAS的解决方案

给变量值增加一个版本号来保证CAS的准确性。

java提供了AtmicStampedReference 来解决ABA问题,保证CAS的正确性。但是这个类比较鸡肋,大部分ABA问题不会影响程序的并发结果。若解决ABA问题使用同步锁方式比CAS更高效。

八、锁优化

JDK 1.6 对锁的实现引入了大量的优化技术,如用自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁来减少锁操作的开销。

1、锁的主要四种状态

(1)无锁状态

(2)偏向锁状态

(3)轻量级锁状态

(4)重量级锁状态。

ps:锁可以升级但是不能降级,这种策略是为了提高获取锁和释放锁的效率。

2、各种锁优化技术

(1)重量级锁

重量级锁就是采用互斥量 Mutex 来控制对互斥资源的访问,在 Java 中被抽象为监视器锁 Monitor,这种同步方式成本非常高,主要体现在阻塞的实现,线程的挂起恢复需要内核态到用户态切换,性能差。

(2)自旋锁与自适应自旋

在许多应用中,锁定状态只会持续很短的时间,为了这么一点时间就去挂起、恢复线程,不值得。如果物理机器上有一个以上的处理器,能让两个或者以上的线程并行执行,我们可以让等待线程执行一定次数的循环,在循环中去获取锁,这项技术称为自旋锁,它可以节省系统切换线程的消耗, 但仍然占有处理器。

在之后又引入自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

(3)锁消除

虚拟机在运行时,如果发现一段被锁住的代码中不可能存在共享数据,就会将这个锁清除。

(4)锁粗化

当虚拟机检测到有一串零碎的操作都对同一个对象加锁时,会把锁扩展到整个操作序列外部。如 StringBuffer 的 append 操作。

(5)轻量级锁

提升性能的依据是对绝大部分锁来说,整个同步周期内不存在竞争,如果没有竞争,轻量级锁可以使用 CAS 操作来避免使用互斥量的开销。但是如果存在竞争条件轻量级锁会比普通锁更慢。因为涉及到同步互斥问题,额外的CAS操作。

注意轻量级锁并不是带起重量级锁的,他的本意是在没有多线程竞争的前提下,减少传统锁使操作系统产生的性能消耗。

(6)偏向锁

轻量级锁是在无竞争的条件下使用cas消除同步使用的互斥量,那偏向锁就是在无竞争的条件下把整个同步也消除。

偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值