超详细 | 21张图带你领略集合的线程不安全

来源 | 悟空聊架构

本篇主要内容如下:

本篇主要内容

本篇所有示例代码已更新到 我的Github

本篇文章已收纳到我的Java在线文档


线程不安全之ArrayList

集合框架有Map和Collection两大类,Collection下面有List、Set、Queue。List 下面有 ArrayList、Vector、LinkedList。如下图所示:

集合框架思维导图

JUC并发包下的集合类Collections有Queue、CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentMap

JUC包下的Collections

我们先来看看 ArrayList。

1.1、ArrayList 的底层初始化操作

首先我们来复习下ArrayList的使用,下面是初始化一个ArrayList,数组存放的是 Integer 类型的值。

new ArrayList<Integer>();

那么底层做了什么操作呢?

1.2、ArrayList 的底层原理

1.2.1 初始化数组

/*** Constructs an empty list with an initial capacity of ten.*/
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

创建了一个空数组,容量为0,根据官方的英文注释,这里容量应该为10,但其实是0,后续会讲到为什么不是10。

1.2.2 ArrayList 的 add 操作

public boolean add(E e) {ensureCapacityInternal(size + 1);  // Increments modCount!!elementData[size++] = e;return true;
}

重点是这一步:elementData[size++] = e; size++ 和 elementData[xx]=e,这两个操作都不是原子操作(不可分割的一个或一系列操作,要么都成功执行,要么都不执行)。

1.2.3 ArrayList 扩容源码解析

(1)执行 add 操作时,会先确认是否超过数组大小。

ensureCapacityInternal(size + 1);

ensureCapacityInternal方法

(2)计算数组的当前容量 calculateCapacity。

private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

minCapacity : 值为1

elementData:代表当前数组

我们先看 ensureCapacityInternal 调用的 ensureCapacityInternal 方法:

calculateCapacity(elementData, minCapacity)

calculateCapacity方法如下:

private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;
}

elementData:代表当前数组,添加第一个元素时,elementData 等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA(空数组)。

minCapacity:等于1

DEFAULT_CAPACITY:等于10

返回 Math.max(DEFAULT_CAPACITY, minCapacity) = 10。

小结:所以第一次添加元素时,计算数组的大小为10。

(3)确定当前容量 ensureExplicitCapacity。

ensureExplicitCapacity方法

minCapacity = 10

elementData.length=0

小结:因minCapacity > elementData.length,所以进行第一次扩容,调用grow()方法从0扩大到10。

(4)调用 grow 方法。

grow方法

oldCapacity=0,newCapacity=10。

然后执行 elementData = Arrays.copyOf(elementData, newCapacity);

将当前数组和容量大小进行数组拷贝操作,赋值给elementData。数组的容量设置为10。

elementData的值和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的值将会不一样。

(5)然后将元素赋值给数组第一个元素,且size自增1。

elementData[size++] = e;

(6)添加第二个元素时,传给ensureCapacityInternal的是2。

ensureCapacityInternal(size + 1)

size=1,size+1=2

(7)第二次添加元素时,执行 calculateCapacity。

mark

elementData的值和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的值不相等,所以直接返回2。

(8)第二次添加元素时,执行 ensureExplicitCapacity。

因minCapacity等于2,小于当前数组的长度10,所以不进行扩容,不执行grow方法。

mark

(9)将第二个元素添加到数组中,size自增1。

elementData[size++] = e

(10)当添加第11个元素时调用grow方法进行扩容。

mark

minCapacity=11, elementData.length=10,调用grow方法。

(11)扩容1.5倍。

int newCapacity = oldCapacity + (oldCapacity >> 1);

oldCapacity=10,先换算成二级制1010,然后右移一位,变成0101,对应十进制5,所以newCapacity=10+5=15,扩容1.5倍后是15。

扩容1.5倍

(12)小结

  • 1.ArrayList初始化为一个空数组。

  • 2.ArrayList的Add操作不是线程安全的。

  • 3.ArrayList添加第一个元素时,数组的容量设置为10。

  • 4.当ArrayList数组超过当前容量时,扩容至1.5倍(遇到计算结果为小数的,向下取整),第一次扩容后,容量为15,第二次扩容至22...

  • 5.ArrayList在第一次和扩容后都会对数组进行拷贝,调用Arrays.copyOf方法。

1.3、ArrayList单线程环境是否安全?

场景:

我们通过一个添加积木的例子来说明单线程下ArrayList是线程安全的。

将 积木 三角形A、四边形B、五边形C、六边形D、五角星E依次添加到一个盒子中,盒子中共有5个方格,每一个方格可以放一个积木。

ArrayList单线程下添加元素

代码实现:

(1)这次我们用新的积木类 BuildingBlockWithName

这个积木类可以传形状shape和名字name:

/*** 积木类* @author: 悟空聊架构* @create: 2020-08-27*/
class BuildingBlockWithName {String shape;String name;public BuildingBlockWithName(String shape, String name) {this.shape = shape;this.name = name;}@Overridepublic String toString() {return "BuildingBlockWithName{" + "shape='" + shape + ",name=" + name +'}';}
}

(2)初始化一个ArrayList

ArrayList<BuildingBlock> arrayList = new ArrayList<>();

(3)依次添加三角形A、四边形B、五边形C、六边形D、五角星E。

arrayList.add(new BuildingBlockWithName("三角形", "A"));
arrayList.add(new BuildingBlockWithName("四边形", "B"));
arrayList.add(new BuildingBlockWithName("五边形", "C"));
arrayList.add(new BuildingBlockWithName("六边形", "D"));
arrayList.add(new BuildingBlockWithName("五角星", "E"));

(4)验证arrayList中元素的内容和顺序是否和添加的一致:

BuildingBlockWithName{shape='三角形,name=A}
BuildingBlockWithName{shape='四边形,name=B}
BuildingBlockWithName{shape='五边形,name=C}
BuildingBlockWithName{shape='六边形,name=D}
BuildingBlockWithName{shape='五角星,name=E}

我们看到结果确实是一致的。

小结:单线程环境中,ArrayList是线程安全的。

1.4、多线程下ArrayList是不安全的

场景如下:20个线程随机往ArrayList添加一个任意形状的积木。

多线程场景往数组存放元素

(1)代码实现:20个线程往数组中随机存放一个积木。

多线程下ArrayList是不安全的

(2)打印结果:程序开始运行后,每个线程只存放一个随机的积木。

打印结果

数组中会不断存放积木,多个线程会争抢数组的存放资格,在存放过程中,会抛出一个异常: ConcurrentModificationException(并行修改异常)。

Exception in thread "10" Exception in thread "13" java.util.ConcurrentModificationException

mark

这个就是常见的并发异常:java.util.ConcurrentModificationException

1.5 那如何解决 ArrayList 线程不安全问题呢?

有如下方案:

  • 用Vector代替ArrayList

  • 用Collections.synchronized(new ArrayList<>())

  • CopyOnWriteArrayList

1.6 Vector 是保证线程安全的?

下面就来分析vector的源码。

1.6.1 初始化 Vector

初始化容量为10

public Vector() {this(10);
}

1.6.2 Add 操作是线程安全的

Add方法加了synchronized,来保证add操作是线程安全的(保证可见性、原子性、有序性),对这几个概念有不懂的可以看下之前的写的文章-》 反制面试官 | 14张原理图 | 再也不怕被问 volatile!

Add方法加了synchronized

1.6.3 Vector 扩容至2倍

int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);

容量扩容至2倍

注意:capacityIncrement 在初始化的时候可以传值,不传则默认为0。如果传了,则第一次扩容时为设置的oldCapacity+capacityIncrement,第二次扩容时扩大1倍。

缺点:虽然保证了线程安全,但因为加了排斥锁synchronized,会造成阻塞,所以性能降低。

1.6.4 用积木模拟Vector的add操作

vector的add操作

当往vector存放元素时,给盒子加了一个锁,只有一个人可以存放积木,放完后,释放锁,放第二元素时,再进行加锁,依次往复进行。

1.7 使用 Collections.synchronizedList 保证线程安全

我们可以使用Collections.synchronizedList方法来封装一个ArrayList。

List<Object> arrayList = Collections.synchronizedList(new ArrayList<>());

为什么这样封装后,就是线程安全的?

源码解析:因为Collections.synchronizedList封装后的list,list的所有操作方法都是带synchronized关键字的(除iterator()之外),相当于所有操作都会进行加锁,所以使用它是线程安全的(除迭代数组之外)。

加锁

mark

注意:当迭代数组时,需要手动做同步。官方示例如下:

synchronized (list) {Iterator i = list.iterator(); // Must be in synchronized blockwhile (i.hasNext())foo(i.next());
}

1.8 使用 CopyOnWriteArrayList 保证线程安全

1.8.1 CopyOnWriteArrayList思想

  • Copy on write:写时复制,一种读写分离的思想。

  • 写操作:添加元素时,不直接往当前容器添加,而是先拷贝一份数组,在新的数组中添加元素后,在将原容器的引用指向新的容器。因为数组时用volatile关键字修饰的,所以当array重新赋值后,其他线程可以立即知道(volatile的可见性)。

  • 读操作:读取数组时,读老的数组,不需要加锁。

  • 读写分离:写操作是copy了一份新的数组进行写,读操作是读老的数组,所以是读写分离。

1.8.2 使用方式

CopyOnWriteArrayList<BuildingBlockWithName> arrayList = new CopyOnWriteArrayList<>();

1.8.3 底层源码分析

CopyOnWriteArrayList的add方法分析

add的流程:

  • 先定义了一个可重入锁 ReentrantLock。

  • 添加元素前,先获取锁lock.lock()。

  • 添加元素时,先拷贝当前数组 Arrays.copyOf。

  • 添加元素时,扩容+1(len + 1)。

  • 添加元素后,将数组引用指向新加了元素后的数组setArray(newElements)。

为什么数组重新赋值后,其他线程可以立即知道?

因为这里的数组是用volatile修饰的,哇,又是volatile,这个关键字真妙^_^

 private transient volatile Object[] array;

1.8.4 ReentrantLock 和synchronized的区别

划重点

相同点:

  • 1.都是用来协调多线程对共享对象、变量的访问。

  • 2.都是可重入锁,同一线程可以多次获得同一个锁。

  • 3.都保证了可见性和互斥性。

不同点:

  • 1.ReentrantLock 显示的获得、释放锁, synchronized 隐式获得释放锁。

  • 2.ReentrantLock 可响应中断, synchronized 是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性。

  • 3.ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的。

  • 4.ReentrantLock 可以实现公平锁、非公平锁。

  • 5.ReentrantLock 通过 Condition 可以绑定多个条件。

  • 6.底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略, lock 是同步非阻塞,采用的是乐观并发策略。

1.8.5 Lock和synchronized的区别

  • 1.Lock需要手动获取锁和释放锁。就好比自动挡和手动挡的区别。

  • 2.Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。

  • 3.synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。

  • 4.Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。

  • 5.通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

  • 6.Lock 可以通过实现读写锁提高多个线程进行读操作的效率。


线程不安全之 HashSet

有了前面大篇幅的讲解 ArrayList 的线程不安全,以及如何使用其他方式来保证线程安全,现在讲HashSet应该更容易理解一些。

2.1 HashSet的用法

用法如下:

 Set<BuildingBlockWithName> Set = new HashSet<>();
set.add("a");

初始容量=10,负载因子=0.75(当元素个数达到容量的75%,启动扩容)

2.2 HashSet的底层原理

 public HashSet() {map = new HashMap<>();
}

底层用的还是HashMap()。

考点:为什么HashSet的add操作只用传一个参数(value),而HashMap需要传两个参数(key和value)?

2.3 HashSet的add操作

private static final Object PRESENT = new Object();public boolean add(E e) {return map.put(e, PRESENT)==null;
}

考点回答: 因为HashSet的add操作中,key等于传的value值,而value是PRESENT,PRESENT是new Object();,所以传给map的是 key=e,  value=new Object。Hash只关心key,不考虑value。

为什么HashSet不安全:底层add操作不保证可见性、原子性。所以不是线程安全的。

2.4 如何保证线程安全

1.使用 Collections.synchronizedSet

Set<BuildingBlockWithName> set = Collections.synchronizedSet(new HashSet<>());

2.使用 CopyOnWriteArraySet

CopyOnWriteArraySet<BuildingBlockWithName> set = new CopyOnWriteArraySet<>();

2.5 CopyOnWriteArraySet 的底层还是使用的是 CopyOnWriteArrayList

public CopyOnWriteArraySet() {al = new CopyOnWriteArrayList<E>();
}

线程不安全之HashMap


3.1 HashMap 的使用

同理,HashMap和HashSet一样,在多线程环境下也是线程不安全的。

Map<String, BuildingBlockWithName> map = new HashMap<>();
map.put("A", new BuildingBlockWithName("三角形", "A"));

3.2 HashMap线程不安全解决方案:

  • 1.Collections.synchronizedMap

Map<String, BuildingBlockWithName> map2 = Collections.synchronizedMap(new HashMap<>()); 
  • 2.ConcurrentHashMap

ConcurrentHashMap<String, BuildingBlockWithName> set3 = new ConcurrentHashMap<>();

3.3 ConcurrentHashMap原理

ConcurrentHashMap,它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 中添加一个新的表项,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该表项应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。


其他的集合类

LinkedList: 线程不安全,同ArrayListTreeSet:线程不安全,同HashSetLinkedHashSet:线程不安全,同HashSetTreeMap:同HashMap,线程不安全;HashTable:线程安全。


总结

本篇第一个部分详细讲述了ArrayList集合的底层扩容原理,演示了ArrayList的线程不安全会导致抛出并发修改异常。然后通过源码解析的方式讲解了三种方式来保证线程安全:

  • Vector是通过在add等方法前加synchronized来保证线程安全。

  • Collections.synchronized()是通过包装数组,在数组的操作方法前加synchronized来保证线程安全。

  • CopyOnWriteArrayList通过写时复制来保证线程安全的。

第二部分讲解了HashSet的线程不安全性,通过两种方式保证线程安全:

  • Collections.synchronizedSet

  • CopyOnWriteArraySet

第三部分讲解了HashMap的线程不安全性,通过两种方式保证线程安全:

  • Collections.synchronizedMap

  • ConcurrentHashMap

另外在讲解的过程中,也详细对比了ReentrantLock和synchronized及Lock和synchronized的区别。

彩蛋:聪明的你,一定发现集合里面还漏掉了一个重要的东西:那就是Queue。期待后续么?

更多阅读推荐

  • 云起云涌:PaaS 体系架构与运维系统上云实践

  • 该买哪家二手手机呢?程序员爬取京东告诉你

  • 17 年安全界老兵,专注打造容器安全能行吗?

  • 字节跳动斩获支付牌照欲建金融帝国,技术实力配得上野心吗?

  • 腾讯微博即将关停,十年了,你用过吗?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/516583.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

快速迁移Next.js应用到函数计算

首先介绍下在本文出现的几个比较重要的概念&#xff1a; 函数计算&#xff08;Function Compute&#xff09;: 函数计算是一个事件驱动的服务&#xff0c;通过函数计算&#xff0c;用户无需管理服务器等运行情况&#xff0c;只需编写代码并上传。函数计算准备计算资源&#xff…

《我想进大厂》之 MYSQL 夺命连环13问

来源 | 科技缪缪想进大厂&#xff0c;mysql不会那可不行&#xff0c;来接受mysql面试挑战吧&#xff0c;看看你能坚持到哪里&#xff1f;能说下 myisam 和 innodb 的区别吗&#xff1f;myisam引擎是5.1版本之前的默认引擎&#xff0c;支持全文检索、压缩、空间函数等&#xff0…

心动网络:PolarDB助力心动网络打造爆款手游

公司介绍 心动网络&#xff0c;国内极具知名度的游戏公司&#xff0c;中国互联网百强企业。旗下业务涉及游戏研发运营、动画制作、偶像娱乐等多个产业。公司创立于2002年&#xff0c;前身为中国最早的互联网分享网站之一的VeryCD。2009年起&#xff0c;公司开始打造心动网络的…

Vue文件在VsCode工具中红色波浪线的问题解决方法

在setting.json种添加 "vetur.validation.template": false, //vue文件取消波浪线

数云:PolarDB助力数云轻松应对双十一

公司介绍 我们杭州数云信息技术有限公司成立于2011年&#xff0c;伴随着电子商务、大数据应用和零售企业互联网化的趋势快速发展&#xff0c;目前已成为国内领先的数据化营销软件产品和服务提供商。我们致力于为消费品牌和零售品牌商提供整合软件产品、数据模型和专业服务的一…

点触科技:构建实时计算和数据仓库解决方案

公司介绍 厦门点触科技股份有限公司&#xff0c;新三板挂牌企业&#xff08;股票代码&#xff1a;870702&#xff09;&#xff0c;成立于2013年&#xff0c;是一家以历史养成类游戏研发与发行为主&#xff0c;专业从事手机游戏的策划、研发制作、商业化运营的创新型发展公司。…

定位云原生数据中台,「智领云」获数千万元A轮融资

来源 | 智领云科技据消息&#xff0c;「智领云」获金沙江联合资本领投&#xff0c;线性资本跟投的数千万元A轮融资。本轮融资将主要用于市场拓展和产品线完善。此前&#xff0c;智领云在2019年5月获得线性资本千万级人民币Pre-A轮融资。智领云成立于2016年&#xff0c;是一家数…

写给大家看的“不负责任” K8s 入门文档

作者 | 邓青琳&#xff08;轻零&#xff09; 阿里巴巴技术专家 导读&#xff1a;本文转载自阿里巴巴技术专家邓青琳(轻零)在内部的分享&#xff0c;他从阿里云控制台团队转岗到 ECI 研发团队&#xff08;Serverless Kubernetes 背后的实现基石&#xff09;&#xff0c;从零开…

腾讯智慧交通战略重磅升级 打造以人为中心的未来交通

在新基建加速布局下&#xff0c;智慧交通正在成为新基建的主力军&#xff0c;不仅可以助力新基建与传统基建融合&#xff0c;还将推动智慧城市建设&#xff0c;推动我国实现“交通大国”向“交通强国”的升级。9月10日&#xff0c;腾讯全球数字生态大会智慧交通分论坛云上召开&…

GitHub 标星 11000+,阿里开源微服务如何连续 10 年扛住双十一大促

云栖号资讯&#xff1a;【点击查看更多行业资讯】 在这里您可以找到不同行业的第一手的上云资讯&#xff0c;还在等什么&#xff0c;快来&#xff01; 作者 | 宿何&#xff0c;阿里云高级开发工程师 责编 | 唐小引 封图 | CSDN 下载自东方 IC 出品 | CSDN&#xff08;ID&#x…

轻松构建基于 Serverless 架构的弹性高可用音视频处理系统

作者 | 罗松(西流) 阿里巴巴技术专家 本文整理自架构师成长系列 2 月 12 日直播课程。 关注“阿里巴巴云原生”公众号&#xff0c;回复 “212”&#xff0c;即可获取对应直播回放链接及 PPT 下载链接。 前言 随着计算机技术和 Internet 的日新月异&#xff0c;视频点播技…

学不动?Apache Member 教你评估实用技术的思路

导读&#xff1a;笔者从 2008 年开始工作到现在也有 11 个年头了&#xff0c;一路走来都在和数据打交道&#xff0c;做过很多大数据底层框架内核的开发 ( Hadoop&#xff0c;Pig&#xff0c;Tez&#xff0c;Spark&#xff0c;Livy )&#xff0c;现在是多个 Apache 项目的 PMC。…

linux异步IO的几种方法及重点案例

异步IO的方法 在Linux下&#xff0c;有几种常见的异步I/O&#xff08;Asynchronous I/O&#xff09;机制可供选择。以下是其中一些主要的异步I/O机制&#xff1a; POSIX AIO&#xff08;Asynchronous I/O&#xff09;&#xff1a;POSIX AIO是一种标准的异步I/O机制&#xff0c…

AI赋能案例—阿里云身份证OCR识别助力实现“无接触”式政务服务!

2020年初的这场疫情&#xff0c;是一场对突发性公共卫生事件应急处置的大考&#xff0c;也是对数字政务体系能力的检验。在保证不影响办事效率的情况&#xff0c;如何减少人员的接触是政务场景下的“防疫”关键&#xff01;日前由一窗&#xff08;北京&#xff09;互联网科技研…

Hive 终于等来了 Flink

Apache Spark 什么时候开始支持集成 Hive 功能&#xff1f;笔者相信只要使用过 Spark 的读者&#xff0c;应该都会说这是很久以前的事情了。 那 Apache Flink 什么时候支持与 Hive 的集成呢&#xff1f;读者可能有些疑惑&#xff0c;还没有支持吧&#xff0c;没用过&#xff1…

AWS拓展中国合作伙伴生态 加速企业数字化转型进程

在2020年9月9日举办的AWS合作伙伴峰会2020上&#xff0c;亚马逊云服务&#xff08;AWS&#xff09;宣布将携手APN合作伙伴进一步拓展中国合作伙伴生态&#xff0c;以更好地服务客户的数字化转型和数字创新需求。AWS宣布与毕马威、神州数码分别达成战略合作关系&#xff0c;结合…

如何在 Flink 中规划 RocksDB 内存容量?

本文描述了一些配置选项&#xff0c;这些选项将帮助您有效地管理规划 Apache Flink 中 RocksDB state backend 的内存大小。在前面的文章[1]中&#xff0c;我们描述了 Flink 中支持的可选 state backend 选项&#xff0c;本文将介绍跟 Flink 相关的一些 RocksDB 操作&#xff0…

能力差的程序员90%输在这点上!CTO:其实都是瞎努力!

在大数据浪潮当中&#xff0c;数据分析是这个时代的不二“掘金技能”。我们每一个人&#xff0c;每天无时无刻都在生产数据&#xff0c;一分钟内&#xff0c;微博上新发的数据量超过10万&#xff0c;b站的视频播放量超过600万......这些庞大的数字&#xff0c;意味着什么&#…

DNS高可用设计--软件高可用

DNS是网络的基础服务&#xff0c;网络上的各种应用对DNS的依赖性很高。DNS的稳定&#xff0c;直接决定了上层应用服务的稳定。那如何保障DNS服务的高可用呢&#xff1f; 我们先来看下高可用的概念&#xff1a; 高可用 高可用&#xff08;High availability&#xff09;&#…

十年磨一剑!支付宝自研数据库OceanBase通过阿里云向全球开放

近日&#xff0c;由支付宝自研的金融级分布式数据库OceanBase正式通过阿里云向全球开放&#xff0c;提供高可用、高性能、低成本的计算服务&#xff0c;企业可在云上获得“支付宝同款”的世界顶级数据库处理能力。 数据库和操作系统一样&#xff0c;是IT行业的重要基础软件&am…