面试必备——synchronized底层原理的基础知识

在这里插入图片描述

1 问题背景

利用下班时间花了半个月研究完volatile关键字,详情见面试必备——说说你对volatile关键字的理解,因为其不保证原子性,可以用synchronized保证。因此来研究synchronized的底层原理。

参考自:子路系列:java高并发编程原理、源码分析

2 前言

本篇博文参考自b站的子路老师,他讲的视频都挺好(无论是spring源码、nacos服务注册、还是并发),与网上普罗大众互相抄袭的讲解思路不一样,如果有时间推荐大家看看视频,本篇博文大多是基于视频复述出来。文章有很多知识可能需要某些基础知识,这种情况需要大家自行去百度谷歌搜索。需要反复阅读,细细品味。

synchronized底层锁相关的知识与jdk版本有关系,因此在此处先声明本篇博客都是基于 jdk1.8Java8研究的。

3 研究synchronized底层原理为什么要了解Java对象头?

网上的csdn博客、博客园、知乎、简书,只要搜索synchronized底层原理,都会讲解monitorentermonitorexit指令,然后就直接讲解Java对象头!!!。笔者就非常地疑惑非常地纠结,我研究的是synchronized,你无理由地直接讲解Java对象头干嘛?脑子里思考了很久,为什么涉及到Java对象头。很多人就在博客里面直接讲述Java对象头里面存储了锁表记,00、01、10、11,偏向锁标志位等等。给我的一种感觉就很突兀,我很纠结于为什么,研究原理不应该是直接灌输知识,而是从其背景、问题、缺点等循序渐进引入文中慢慢讲解,这样会有一条很清晰的思路把整个研究过程串起来。

基于上述的原因,笔者引入本小节阐述为什么要了解就Java对象头。

首先看下面的一个synchronized的小例子,伪代码如下:

public class L {

}

public class A {
  private int num = 0;
  
  public void incr() {
    L l = new L();
    synchronized(l) {
      int tmp = num;
      tmp = tmp + 1;
      num = tmp;
    }
  }
}

如上伪代码所示,synchronized作用在了一个代码块。加上synchronized关键字就是加锁了。我们想点进去看synchronized关键字源码都点不了。synchronized到底是怎么加锁的?它把锁加在哪里了?或者说synchronized锁住的是哪里?

网上的博客都会说到synchronized关键字加在静态方法上是锁住类对象、加在普通方法上是锁住实例对象、synchronized同步块锁住的是括号中的对象(括号中如果是this是锁住实例对象)。从前面这么多情况看出,都说是锁住对象,并没有出现锁住代码。那么我们先认为synchronized是对对象加锁。

锁的本质是一个对象,怎么理解锁?怎么理解加锁?

《深入理解Java虚拟机》中提到 Java中任何对象都可以充当锁。举生活中上厕所的一个例子,我要上厕所,进入里面锁上门。此时厕所外面的人不能进来使用厕所。我用完厕所后,解锁打开门,别人可以用厕所了。别人进去厕所后,也是锁上门。对对象加锁的这一个动作,可以理解成锁上厕所门这个动作。如下图所示:

在这里插入图片描述
上图就是厕所门加锁解锁的图。加锁时,我把它顺时针扭动到图中红色的状态,从竖着变成横着。解锁时,我逆时针扭动到图中绿色的状态,从横着变成竖着。那么加锁解锁可以理解成把某个东西的某种属性改变了。比如上图中就是把图中的按钮的位置改变了,本来是竖着的,加锁后变成横着了。

类比到Java中的锁。既然Java中任何对象都能充当锁,那么加锁就是把Java对象的某些东西改变了,解锁也是把Java对象的某些东西改变了。那改变的到底是什么东西呢?因此我们就要研究Java对象

Java对象是由对象头、实例数据、填充值组成的,详情可见Java的对象组成简介。上面伪代码中的L实例对象并没有属性却仍能作为锁,那么可以说明加锁并不是改变实例数据。当Java对象的内存占用是8byte的倍数时,也不需填充值,那么剩下的就只有对象头了。说明加锁是改变了Java对象头里面的某些东西。因此我们要研究Java对象头

4 打印Java对象的组成布局

上面完成了一些列的推敲让我们知道为什么要研究就Java对象头,因此我们将Java对象的组成布局打印出来来研究对象头。

4.1 引入依赖

要将Java对象的组成布局打印出来,得使用下面的依赖提供的方法

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.8</version>
</dependency>

4.2 例子

/**
 * 充当锁
 */
public class L {
    /**
     * booelan类型占用1个byte
     */
    boolean i;
}
public class Layout1 {
    static L l = new L();
  
    public static void main(String[] args) {
        System.out.println("start");

        // 打印Java对象的组成布局
        System.out.println(ClassLayout.parseInstance(l).toPrintable());

        /*
         * synchronized锁住的是对象不是代码
         */
         synchronized(l) {
             System.out.println("locking");
         }
         System.out.println("end");
    }
}

4.3 运行结果

解释见下图中的文字注解:

在这里插入图片描述

5 研究Java对象头的组成

5.1 如何找最官方的介绍

网上很多博客都有如下的介绍:对象头由 mark wordKlass Pointer组成,甚至有些博客还给出了 mark wordKlass Pointer各占了多少位。如果直接这样给出来就很突兀,并且并不能知道是否正确,毕竟网上很多东西都是互抄的,或者很多资料对于新时代都过时了。笔者都很想从最原始的官方文档看是否真如此。

基于以上的疑惑,笔者去官方文档找简介。

JVM规范与JVM实现的关系(必读):首先要了解要知道一回事,JVM规范里面定义了Java对象头由什么组成。而JVM规范是一套定义Java的标准,只是做了定义,具体实现交由各个厂商去做。(这一点与spring cloud定义服务注册接口很想,具体实现交由厂商决定,阿里的nacos就是实现了它定义的服务注册接口。) JVM的实现有很多,有Java原生团队做的HotSpot(有一个开源的jdk叫openjdk),淘宝的淘宝VM,JRocket等。这些商用JVM是由具体的代码实现JVM规范定下的标准。因此我们可以去开源的openjdk官网上找hotspot的文档。

访问openjdk官网,选择HotSpot:

在这里插入图片描述

找到glossary of terms,如下图所示:

在这里插入图片描述

搜索mark word,如下图所示:

在这里插入图片描述

5.2 官方介绍

mark word:
The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.

笔者翻译: mark word是每个对象头的第一个word,word指的是操作系统的一个(操作系统的一个专业术语),可以理解为是一个属性。即mark word是每个对象头的一个属性。mark word通常包含有 同步状态、唯一标识hash code。也会有一个指向同步相关信息的指针。当GC时,也会包含GC状态位。

总结:

  1. mark word根据对象处于的状态 (这个状态有无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,后面小节会详细阐述),会包含一些信息,比如有同步状态、唯一标识hash code、指向同步相关信息的指针、GC状态。
  2. 可以看到 mark word里面包含了同步状态,再次证明 synchronized 是修改了对象头里面 mark word 的某些值,从而实现加锁的。

klass pointer:
The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. For Java objects, the “klass” contains a C++ style “vtable”.

笔者翻译: Klass pointer是每个对象头的第二个属性。它指向另外一个对象(元数据对象),这个元数据对象描述了原始对象的布局和行为。对于Java对象,“klass”包含了一个C++风格的“vtable”。

总结: Klass pointer指向类对象

注意:以上两个概念的官方介绍仅仅是给出定义,具体实现仍然要根据具体的商用虚拟机是怎么做的。因此接下来找HotSpot虚拟机的源码看看。

5.3 找关于 mark word 的HotSpot源码

前往 openjdk github,找jdk8版本的源码,如下所示,选中右击在新标签页打开:

在这里插入图片描述

根据下面的路径找,如下图所示:

在这里插入图片描述

在页面ctrl + f搜索 mark,找到 markOop.hpp,如下所示:

在这里插入图片描述

找到 64bit ,如下图所示:

在这里插入图片描述

5.4 官方源码注释

上面5.3节讲述了如何找对象头的官方源码,下面贴出官方源码对对象头的注释:

//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)

6 Java对象有多少种状态

Java对象的状态,其实并不算是很官方的说法,但为了方便理解概念,我们先了解大家普遍认可的Java对象的各种状态:

  1. 无锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态
  5. GC状态

以上5种状态,但在hotspot源码的注释这里只有4行,为什么?,如下所示:

//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)

从上面对各个bit介绍,笔者理解是JVM用了几个bit位来表示5种状态。有 biased_lock占用了1bit,lock占用了2bit。

7 无锁状态的对象头

7.1 指针压缩

//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)

64bit表示 mark word占用了64bit,具体的bit上存储了什么都给出了解释。

前面第4.3小节打印出的结果是对象头整个长度有 12byte = 12 * 8bit = 96bit,对象头由 mark word 和 klass pointer 组成,mark word 长度有 64bit,那么 klass pointer 的长度有 96bit - 64bit = 32bit

实际上 klass pointer的长度是64bit,只不过jjvm开启了 指针压缩,压缩了对象头的长度,未开启指针压缩,对象头的长度是 16byte = 16 * 8bit = 128bit,那么 klass pointer的长度有 128bit - 64bit = 64bit

也就是说不开启指针压缩,klass pointer 指针用64bit来存储,开启指针压缩后,就只用32bit 来存储。

为什么要指针压缩?详情可以看JVM的指针压缩

可以简单的理解为压缩后可以 节省存储空间,在 内存 硬件的费用上 节约成本

7.2 GC分代年龄

在这里插入图片描述
age 字段占用4bit,是记录GC的分代年龄。

此处引入GC 年轻代垃圾回收的次数为什么第16次就进入老年代?(GC年轻代垃圾回收,存活的对象在 from区和to区辗转生存,第16次进入老年代区域)

解答:4bit的二进制位能记录的最大值是1111,转换成十进制数是15,即age字段能记录GC分代年龄的最大值是15,存活的对象在 from区和to区辗转生存,每生存下来都会用age记录一下,即age加1,加到最大值15,age加1后无法再存储了,那么第16次会进入老年代。(当然这可能是仅仅其中一方面导致第16次进入老年代,它可以用3位存储age,那么为什么用4位,肯定还有有其他原因,此处不做深究)

7.3 HashCode

网上很多博客都说在无锁状态下,Java头的HashCode是占56bit,那么为什么是占56bit,我们总不可能背下来,面试官问到也答不上来啊,看下面:

在这里插入图片描述

如上图所示,hash实际上是占用了 31bit = 3byte + 7bit 而已,但是计算机拿值是以字节为单位的,它拿不了31bit,要么它就拿 32bit = 4byte,因此hashunused1bit,形成 32bit = 4byte。而unused又是未被使用的,可以借给hash。因此市面上常说的 56bit就是 25bitunused + 31bithash


下面记录一些hashcode的扩展,仅当了解即可,不必背诵。

既然是56bit存储hashcode,那么打印出来为什么是下图这些呢?看起来hashcode应该会有若干个1啊。

在这里插入图片描述
如上图所示,很多位上都是0,一个字节的位全是0,无论怎么转换进制(10进制、16进制等)得出来最终的结果都是0。

官方注释写着是56bit存储hashcode的,但是打印出来又是不太符合现实的,那是官方文档说错了?官方文档不可能说错,那么大概率就是我们在某些方面有知识盲区。

问题一:为什么大部分位上全是0?
答:可能是还没有计算过hashcode,那么这个对象就没有存储hashcode。我们计算一下hashcode,代码如下:

在这里插入图片描述

运行结果:

在这里插入图片描述
可以看到不再是大部分位上都是0了。hashcode是使用Java通过算法计算Java对象的内存所在地址得出来的一个值,存储到对象头上面。没经过计算是不存在对象头里面。

问题二:官方文档说前25bit是未被使用的,但是为什么上图中的前25bit确实有很多1,而后面的字节基本为0呢?
答:因为一般的操作系统(window)是 小端存储

什么是大小端存储?

在这里插入图片描述

因此对象的hashcode应该是这样读:

在这里插入图片描述
所以可以看到从最右边读起,连续24个bit是0表示未被使用。

我们自己计算的hashcode是否与对象头打印出来的一致,看下图:

在这里插入图片描述
一致的。

7.4 偏向锁标志

在这里插入图片描述
如上所示,使用1bit存储 偏向锁标志

7.5 锁状态

在这里插入图片描述
如上所示,使用2bit存储 锁状态。2bit可以表示4种状态,分别是00、01、10、11。加上偏向锁标志,总共3bit,可以表示 2 ^ 3 = 8种状态,足够表示无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态、GC状态。

7.6 总结无锁状态

前面几个小节研究了hashcode、GC分代年龄,让我们对无锁状态下的对象头有更官方更清晰的理解,下面对无锁状态的情况做一些小总结:

unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)

如上所示,无锁状态下,对象头由hashcodeGC分代年龄偏向锁标志锁状态标志组成。

8 为什么会有偏向锁标志和锁状态位呢?

前面的小节已经阐述了对象头中会有1bit是偏向锁标志和2bit锁状态位。抛开偏向锁、轻量级锁、重量级锁,为什么给对象加锁要用这两个字段呢?用一个字段不就行了?给对象加锁就设置某位为1,解锁就设置为0。说白了就是为什么要有偏向锁、轻量级锁、重量级锁这几种锁状态,仅仅用1位是无法满足表达这几种状态的。

这就得了解锁发展的历史背景了。

8.1 jdk1.6之前

Java中的Thread对象底层实际上是调用操作系统级别的线程,可以粗略理解为是与操作系统级别的线程一一对应的。而synchronized对线程实现同步互斥是底层调用操作系统级别的互斥(linux的是用 mutex)。通过操作系统级别的互斥,性能比较低。因为互斥涉及到线程的阻塞和唤醒,需要切换状态(用户态与内核态切换)。切换的过程性能很低。

8.2 jdk1.6以后

由于操作系统级别的互斥太重,因此hotspot对锁做了优化,给出了从Java级别做互斥的解决方案。

8.2.1 只有单个线程执行——引入偏向锁

设计者们考虑各种场景的加锁解决方案,比如有下面这个方法:

public void test() {

}

很多时候我们并不能100%保证test()同一时间只会被一个线程调用,因此常常会给该方法加 synchronized,如下:

public synchronized void test() {

}

但是很多时候其实又基本不会有多个线程在同一时间执行test(),jdk1.6之前的synchronized是操作系统级别的互斥(Linux的是用mutex),这会很重。

设计者针对这种没有资源竞争的同步代码块,用 偏向锁 来实现(引入偏向锁),偏向锁就是一个线程调用。

如下代码所示:

在这里插入图片描述
L对象被new 出来后,是处于 匿名偏向的状态 (对象头存储了 hashcode,偏向锁的bit为1,锁状态为01,此处暂时不纠结或背诵这些标志位是0还是1,仅当陈述句阐述)。而当第一次用snychronized加锁后,对L对象加偏向锁,通过CAS把自己的线程唯一标识设置到L对象的对象头里面。L对象是处于 偏向锁状态 (对象头存储偏向的 线程唯一标识、epoch、偏向锁的bit为1,锁状态为01)。如下图所示:

在这里插入图片描述

在这里插入图片描述

如下图所示:

在这里插入图片描述
main线程再次对L对象加锁,通过CAS发现自己线程的唯一标识在L对象的对象头里面,证明已经拿到该锁了,不需要再次加锁。不需要执行任何操作系统级别的东西。

8.2.2 多个线程交替执行(无竞争)——引入轻量级锁

如下图所示:

在这里插入图片描述
当main主线程执行完synchronized,又有thread对象来锁住L对象,此时不存在线程竞争,都是交替执行。此时锁会进行升级,从偏向锁升级为轻量级锁。如下图所示:

在这里插入图片描述

从上图看到,升级到轻量级锁,会保留锁状态的2个bit,其余都是存储一个指针 ptr_to_lock_record,该指针指向一个叫 lock record 数据结构

8.2.3 多个线程同时执行(有竞争)

如下图所示:

在这里插入图片描述

从上图看,线程1加完锁后就休眠了,仍然持有锁。而线程2来竞争锁了,那么此时就升级为 重量级锁 。这就回到了jdk1.6以前的做法,调用操作系统层级的互斥来实现锁。

8.3 用3bit表示所有锁状态

上面提到jdk1.6之后引入了有偏向锁状态、轻量级锁状态、重量级锁状态,加上无锁状态和GC标记状态,总共有5种状态。仅用2bit的锁状态字段是无法表示5种状态的。2bit仅能表示00、01、10、11共4种状态。

因此使用1bit的偏向锁字段和2bit的锁状态字段一起来表达这5种状态。

9 各种锁状态对应的二进制值

上面分析了为什么要用3个bit表示所有锁状态,本小节讲述各种锁状态具体的二进制是怎么样的

官方源码的注释中有如下介绍:

//    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread
//    [0           | epoch | age | 1 | 01]       lock is anonymously biased
//
//  - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
//    [ptr             | 00]  locked             ptr points to real header on stack
//    [header      | 0 | 01]  unlocked           regular object header
//    [ptr             | 10]  monitor            inflated lock (header is wapped out)
//    [ptr             | 11]  marked             used by markSweep to mark an object
//                                               not valid at any other time

上面出现了 unlockedbiasedlockedmonitormarked,综合分析他们分别对应的是无锁偏向锁轻量级锁重量级锁GC标记

那么可以得出如下图的结论:

在这里插入图片描述

其余位偏向锁位锁状态位锁状态
hashcode、GC分代年龄001无锁状态
线程唯一标识、epoch、GC分代年龄101偏向锁(此时有线程占有)
hashcode、GC分代年龄101偏向锁(此时无线程占有)
ptr指针000轻量级锁
ptr指针010重量级锁
ptr指针011GC标记

10 验证

10.1 无锁状态

执行以下代码,对象new出来后直接打印:

在这里插入图片描述
new出来后是无锁状态,如上图所示,为001

10.2 偏向锁状态

new 出来后,用synchronized尝试加锁:

在这里插入图片描述
如上图所示,用synchronized加锁后,是00,即已经是轻量级锁了。为什么呢?对无锁状态的对象加锁,并且没有其他线程竞争,加锁成功后应该是偏向锁101

为什么会这样呢?是我们理论错误还是我们验证的方法错误?

因为jvm把偏向锁延迟了。为什么会延迟?因为jvm启动的时候会不单单只有main线程,还有其他很多线程。而且jvm本身自己也会有很多代码并且加了synchronized。jvm百分百确定他自己的代码肯定会被其他线程使用。如下所示:


sychronized (aa) {

}

sychronized (bb) {

}

当线程1给aa加锁,加锁成功,则aa称为偏向锁状态。线程1执行完同步代码块后,线程2给aa加锁。这个过程是首先aa从偏向锁状态撤销,而这个撤销过程是很复杂的过程,会耗费很大的性能。撤销后再升级为轻量级锁。这个过程很复杂,所以jvm一上来就给aa加轻量级锁,而不是偏向锁。偏向锁是一种优化,jvm的开发者做了大量的研究发现——很多代码只有一个线程执行,所以采用偏向锁。而jvm确定他自己代码不可能只有一个线程在执行这些代码(比如GC线程、守护线程),所以他把偏向锁延迟了。

我们把延迟偏向锁关闭,再验证下:

在这里插入图片描述
关闭偏向锁延迟 后,synchronized加锁后的状态确实是偏向锁101,但是为什么未加synchronized之前也是101呢?

当关闭偏向锁延迟后,或者说jvm已经允许偏向锁后,为什么一个对象new出来后,就已经是偏向锁101呢?

官方文档有说,当jvm允许偏向锁后,一个对象被new出来后,是处于一个 匿名偏向 锁的状态,但对象头并没有存储任何线程的唯一标识,即对象并没有偏向谁。而当有线程偏向时,对象头就会存储偏向的那个线程的唯一标识。如下图所示:

在这里插入图片描述

官方文档对于 匿名偏向 状态的注释:

//    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread
//    [0           | epoch | age | 1 | 01]       lock is anonymously biased

当第二次加synchronized时,由于都是main线程,不存在锁竞争,所以锁仍然是偏向锁状态:

在这里插入图片描述

10.3 轻量级锁

两个线程交替执行,不存在竞争,则升级为轻量级锁,代码如下:

public class Layout1 {

    static L l = new L();

    public static void main(String[] args) {
        System.out.println("start");
        System.out.println();

        System.out.println(ClassLayout.parseInstance(l).toPrintable());
        /**
         * synchronized锁住的是对象,不是代码
         */
        synchronized (l) {
            System.out.println("locking 1");
            System.out.println(ClassLayout.parseInstance(l).toPrintable());

        }

        synchronized (l) {
            System.out.println("locking 2");
            System.out.println(ClassLayout.parseInstance(l).toPrintable());
        }

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                test1();
            }
        });
        thread1.start();

        System.out.println("end");
    }

    private static void test1() {
        synchronized (l) {
            System.out.println("xx");
            System.out.println(ClassLayout.parseInstance(l).toPrintable());
        }
    }


}

在这里插入图片描述
如上图结果所示,main线程与thread1线程交替执行,无竞争,锁升级为轻量级锁。

10.4 小结

如下图所示:

在这里插入图片描述

new一个对象出来是无锁状态001,用synchronized对它加锁是轻量级锁状态000。为什么不是偏向锁状态而是轻量级锁状态?因为jdk1.8版本,jvm默认开启偏向锁延迟4000ms。为什么开启?因为jvm设计者们能确定在jvm启动时,jvm里面有很多代码肯定会被多个线程执行,存在竞争时,处于偏向锁状态的对象需要经历偏向锁的撤销过程并升级为轻量级锁。而这个过程是很复杂很耗性能的。jvm启动,当处于未开启偏向锁的阶段,用synchronized对无锁状态的对象加锁则会直接升级为轻量级锁,减少了偏向锁的撤销过程和锁升级过程的性能消耗。

当jvm开启了偏向锁后,new出来的对象是匿名偏向状态101(对象头的线程ID为空)。如果没有线程交替执行且没有竞争,给这个对象加锁,对象会进入偏向锁状态101(线程ID不为空)。如果有线程交替执行且没有竞争,给这个对象加锁,则这个对象会进入是轻量级锁状态000。多个线程非交替执行且存在竞争,给这个对象加锁,则这个对象会膨胀为重量级锁010。

引入轻量级锁是为了避免操作系统级别的重量级锁带来的开销,引入偏向锁是为了减少锁撤销和升级到轻量级锁的开销。(这句是笔者大概的理解,不存在官方权威认证)

11 总结

本篇博客重在对synchronized有从0到1的一个超级官方超级新的整体认知,了解synchronized的基础知识。关于面试常问的锁升级过程还需要再写一篇博客总结。

关于下一篇锁升级的博客预热: synchronized锁升级的设计思想本质上是性能和安全性的一种平衡,即如何在一个不加锁的情况下保证线程的安全性。这种思想在编程领域是非常常见的,比如说MySQL里面的MVCC,使用版本链的方式来解决多个并行事务的竞争问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值