笔记
Java并发篇
相关演示代码地址:hello-github-ui/interview: 面试知识点汇总 (github.com)
一、Java如何开启线程?怎么保证线程安全?
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
method1和method2都是synchronized修饰的方法,在method1里面调用method2的时候,不需要重新申请锁,可以直接调用就行了(其实可以反过来想一想,如果synchronized不具有重入性,当我调用了method1的时候,得申请锁,申请好了之后那么method1就拥有了这个锁,那么调用method2的时候,又要重新申请锁,而锁在method1的手上,这时候又要重新申请锁,显然是不可能得到的,这不科学。所以,synchronize和lock都是具有可重入性的)
具体锁的内容可以参考:https://zhuanlan.zhihu.com/p/85511613
二、Volatile和Synchronized有什么区别?Volatile能不能保证线程安全?DCL(Double Check Lock)单例为什么要加 Volatile?
三、Java线程锁机制是怎样的?偏向锁、轻量级锁、重量级锁有什么区别?锁机制是如何升级的?
四、谈谈你对AQS的理解。AQS=如何实现可重入锁?
五、有A,B,C三个线程,如何保证三个线程同时执行?如何在并发情况下保证三个线程依次执行?如何保证三个线程有序交错运行?
六、如何对一个字符串快速进行排序?
Java网络通信篇
一、TCP和UDP有什么区别?TCP为什么是三次握手,而不是两次?
二、Java有哪几种IO模型?有什么区别?
BIO 同步阻塞IO,传统型 web 服务就是这种模型:
特点:可靠性差,吞吐量低,适用于连接比较少且比较固定的场景。JDK1.4之前唯一的选择。
NIO 同步非阻塞IO
在 BIO 的基础上,增加一个 selector ,减少 Thread 对 Server 的压力,就是 NIO:
特点:可靠性比较好,吞吐量较高,适用于连接比较多并且连接比较短(因为对于客户端来说还是一个轮询,查看是否服务端响应了没),适用于轻操作,例如聊天室
AIO 异步非阻塞IO
在 NIO 的基础上,让 Server 处理完成后,主动推送响应到 客户端就是 AIO 了:
特点:可靠性是最好的,吞吐量也是非常高。适用于连接比较多,并且连接也比较长(重操作)。例如:相册服务器。JDK7版本才开始支持
三、Java NIO的几个核心组件是什么?分别有什么用?
四、select, poll 和 epoll 有什么区别?
五、描述下 Http 和 Https 的区别
JVM调优篇
一、说一说JVM的内存模型?
二、Java类加载的全过程是怎样的?什么是双亲委派机制?有什么作用?一个对象从加载到JVM,再到被GC清除,都经历了什么过程?
三、怎么确定一个对象到底是不是垃圾?什么是GC Root?
四、JVM有哪些垃圾回收算法?
MarkSweep 标记清除算法
这个算法分为两个阶段,标记阶段:把垃圾内存标记出来,清除阶段:直接将垃圾内存回收。
这种算法是比较简单的,但是有个很严重的问题,会产生很多内存碎片。
Copying 拷贝算法
为了解决标记清除算法的内存碎片问题,就产生了拷贝算法。
拷贝算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。
这种算法没有内存碎片,但是它的问题就在于浪费空间。而且,它的效率跟存活对象的个数有关。
MarkCompack 标记压缩算法
为了解决拷贝算法的空间利用缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将端边界以外的所有内存直接清除。
这三种算法各有利弊,各自有各自的适合场景。
五、JVM有哪些垃圾回收器?他们都是怎么工作的?什么是STW?它都发生在哪些阶段?什么是三色标记?如何解决错标记和漏标记的问题?为什么要设计这么多的垃圾回收器?
STW:Stop-The-World,是在垃圾回收算法执行过程当中,需要将JVM内存冻结的一种状态。
在STW状态下,Java的所有线程都是停止执行的-GC线程除外,native方法可以执行,但是,不能与JVM交互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。
核心思想,就是将STW打散,让一部分GC线程与用户线程并发执行。整个GC过程分为四个阶段。
1、初始标记阶段:STW只标记出根对象直接引用的对象。
2、并发标记:继续标记其它对象,与应用程序是并发执行。
3、重新标记:STW对并发执行阶段的对象进行重新标记。
4、并发清除:并行。将产生的垃圾清除。清除过程中,应用程序又会不断的产生新的垃圾,叫做浮动垃圾。这些垃圾就要留到下一次GC过程中清除。
六、 如何进行JVM调优?JVM参数有哪些?怎么查看一个Java进程的JVM参数?谈谈你了解的JVM参数。如果一个Java程序每次运行一段时间后,就变得非常卡顿,你准备如何对他进行优化?
JVM调优主要就是通过定制JVM运行参数来提高Java应用程序的运行数据
JVM参数大致可以分为三类:
1、标准指令:-开头,这些是所有的HotSpot(虚拟机)都支持的参数。可以用java -help打印出来。
2、非标准指令:-X开头,这些指令通常是跟特定的HotSpot版本对应的。可以用java -X打印出来。
3、不稳定参数:-XX开头,这一类参数是跟特定HotSpot版本对应的,并且变化非常大。详细的文档资料非常少。在JDK1.8版本下,有几个常用的不稳定指令:
java -XX:+PrintCommandLineFlags:查看当前命令的不稳定指令
java -XX:+PrintFlagsInitial:查看所有不稳定指令的默认值
消息队列篇
1、MQ有什么用?有哪些具体的使用场景?
2、如何进行产品选型?
3、如何保证消息不丢失?
1、哪些环节会造成消息丢失?
2、怎么去防止消息丢失?
2.1 生产者发送消息不丢失
Kafka:消息发送+回调
RocketMQ:1、消息发送+回调。2、事务消息
RabbitMQ:1、消息发送+回调 2、手动事务:channel.txSelect()开启事务,channeltxCommit()提交事务,channel.txRollback()回滚事务。这种方式对channel是会产生阻塞的,造成吞吐量下降。3、Publisher Confirm,整个处理流程跟RocketMQ的事务消息,基本是一样的。
2.2 MQ主从消息同步不丢失
RocketMQ:1、普通集群中,同步同步、异步同步。异步同步效率更高,但是有丢失消息的风险。同步同步就不会丢消息。2、Dledger集群-两阶段提交
RabbitMQ:
- 普通集群:消息是分散存储的,节点之间不会主动进行消息同步,是有可能丢失消息的。
- 镜像集群:镜像集群会在节点之间主动进行数据同步,这样数据安全性得到提高。
Kafka:允许消息丢失的,通常都是用在允许消息少量丢失的场景下。
2.3 MQ消息存盘不丢失
RocketMQ:同步刷盘,异步刷盘:异步刷盘效率更高,但是有可能丢失消息。同步刷盘消息安全性更高,但是效率会降低。
RabbitMQ:将队列配置成持久化队列。新增的Quorum类型的队列,会采用Raft协议来进行消息同步。
Kafka:
2.4 MQ消费者消费消息不丢失
RocketMQ:使用默认的方式消费就行,不要采用异步方式。
RabbitMQ:autoCommit -> 手动提交 offset
Kafka:手动提交 offset
4、如何保证消息消费的幂等性?
其实就是要防止消费者重复消费消息的问题。
所有MQ产品并没有提供主动解决幂等性的机制,需要由消费者自行控制。
RocketMQ:给每个消息分配了一个MessageID,这个MessageID就可以作为消费者判断幂等的依据。这种方式不太建议(因为当消息量过多之后,这个MessageID并不能保证全局唯一)。最好的方式就是自己带一个有业务标识的ID,来进行幂等性判断。比如OrderID。
统一分配ID方式。
5、如何保证消息的顺序?
全局有序和局部有序:MQ只需要保证局部有序,不需要保证全局有序。
生产者把一组有序的消息放到同一个队列当中,而消费者一次消费整个队列(先锁住该队列)当中的消息。
RocketMQ中有完整的设计(已经定义好的api来实现上述要求),但是在RabbitMQ和Kafka中并没有完整的设计,需要自行设计。
RabbitMQ:要保证目标exchange只有一个队列。并且一个队列只对应一个消费者。
Kafka:生产者通过定制partition分配规则,将消息分配到同一个partition 。Topic下只对应一个消费者。
6、如何保证消息的高效读写?
零拷贝:Kafka和RocketMQ都是通过零拷贝技术来优化文件读写。
传统拷贝是需要在内核空间和用户空间之间进行四次拷贝。
零拷贝:有两种方式,mmap 和 transfile
Java当中对零拷贝进行了封装,Mmap方式通过MappedByteBuffer对象进行操作,而transfile通过FileChannel来进行操作。
Mmap适合比较小的文件,通常文件大小不要超过1.5G~2G之间。
Transfile没有文件大小限制。
RocketMQ当中使用Mmap方式来对它的文件进行读写。commitlog大小1G。
在Kafka当中,它的index日志文件也是通过mmap的方式来读写的。在其它的日志文件中,并没有使用零拷贝的方式。Kafka使用transfile的方式将硬盘数据加载到网卡
7、使用MQ如何保证分布式事务的最终一致性?
分布式事务:业务相关的多个操作(比如,电商订单中,用户下单后,一边需要等待支付,另一边需要生成物流单,这两个操作是分布式的,但必须同时成功或失败),保证他们同时成功或同时失败。
事务的最终一致性:与之对应的就是强一致性,就是只需要保证多个事务的最终时候是一致性即可(言外之意,中间某个过程中可能不一致)。
MQ中要保证事务的最终一致性,就需要做到两点:
1、生产者要保证100%的消息投递。
2、消费者这一端要保证幂等消费。唯一ID+业务自己实现幂等。
8、让你设计一个MQ,你会如何设计?
两个误区:1、放飞自我,漫无边际。2、纠结技术细节。
好的方式:1、从整体到细节,从业务场景到技术实现。2、以现有产品为基础。RocketMQ(本身就吸收了Kafka和RabbitMQ的精华)为模型。
答题思路:MQ的作用、项目大概的样子。
1、实现一个单机的队列数据结构。高效、可扩展。
2、将单机队列扩展成为分布式队列。分布式集群管理。
3、基于Topic定制消息路由策略。-发送者路由策略,消费者与队列对应关系,消费者路由策略。
4、实现高效的网络通信。-Netty Http
5、规划日志文件,实现文件高效读写。-零拷贝,顺序写。服务重启后,快速还原运行现场。
缓存篇
一、为什么使用缓存?
1、高性能:数据库的性能不是那么理想。
2、高可用:防止高并发下瞬间大量的访问造成数据库崩溃。
二、什么是缓存穿透?缓存击穿?缓存雪崩?怎么解决?
1、缓存穿透:缓存中查不到,数据库中也查不到。
解决方案:①对参数进行合法性校验。②将数据库中没有查到结果的数据也写入缓存中。这时要注意防止Redis被无用的key占满,这一类缓存的有效期要设置的短一点。③引入布隆过滤器,在访问Redis之前判断数据是否存在。要注意布隆过滤器存在一定的误判率,并且,布隆过滤器只能加数据不能减数据。
2、缓存击穿:缓存中没有,数据库中有。
一般是出现在缓存数据初始化以及缓存Key过期了的情况下。他的问题在于,重新写入缓存需要一定的时间,如果是在高并发的场景下,过多的请求就会瞬间写到DB上,给DB造成很大的压力。
解决方案:①设置这个热点缓存永不过期(永不过期意味着数据可能不是最新的,所以需要解决更新问题)。这时要注意在value当中包含一个逻辑上的过期时间,然后另起一个线程,定期重建这些缓存。②加载DB的时候,要防止并发。
3、缓存雪崩:缓存大面积过期,导致请求都被转发到DB。
解决方案:①把缓存的失效时间分散开。例如,在原有的统一失效时间基础上,增加一个随机值。②对热点数据设置永不过期。③把redis的key分散到不同的集群上等方案。
三、 如何保证Redis与数据库的数据一致?
当我们对数据进行修改的时候,到底是先删除缓存,还是先写数据库?
1、如果先删除缓存,再写数据库:在高并发场景下,当第一个线程删除了缓存,还没有来得及写数据库,第二个线程来读取数据,会发现缓存中的数据为空,那就会去读数据库中的数据(旧值,脏数据),读完之后,把读到的数据写入缓存(此时,第一个线程已经将新的值写到缓存里面了),这样缓存中的值就会被覆盖为修改前的脏数据。
总结:在这种方式,通常要求写操作不会太频繁。
解决方案:①先操作缓存,但是不删除缓存。将缓存修改为一个特殊值(-999)。客户端读缓存时,发现是默认值,就休眠一小会,再去查一次Redis。(缺点是特殊值对业务有侵入,休眠时间,可能会多次重复,对性能有影响)②延时双删。先删除缓存,然后再写数据库,休眠一小会,再次删除缓存。如果写操作很频繁,同样还是会有脏数据的问题。
2、先写数据库,再删缓存:如果数据库写完了之后,缓存删除失败,数据就会不一致。
解决方案:①给缓存设置一个过期时间。问题:过期时间内,缓存数据不会更新。
②引入MQ,保证原子操作。
四、 如何设计一个分布式锁?如何对锁性能进行优化?
分布式锁的本质:就是在所有进程都能访问到的一个地方,设置一个锁资源,让这些进程都来竞争锁资源。数据库、zookeeper、redis。通常对于分布式锁,会要求响应快、性能高、与业务无关。
Redis实现分布式锁:
SETNX key value:当key不存在时,就将key设置为value,并返回1。如果key存在,就返回0。
EXPIRE key locktime:设置key的有效时长。
DEL key:删除。
GETSET key value:先GET,再SET,先返回key对应的值,如果没有就返回空。然后再将key设置成value。
1、最简单的分布式锁:SETNX加锁,DEL解锁。问题:如果获取到锁的进程执行失败,他就永远不会主动解锁,那这个锁就被锁死了。
2、给锁设置过期时长:问题:SETNX和EXPIRE并不是原子性的,所以获取到锁的进程有可能还没有执行EXPIRE指令就挂了,这时锁还是会被锁死。
3、将锁的内容设置为过期时间(客户端时间+过期时长),SETNX获取锁失败时,拿这个时间跟当前时间对比,如果是过期的锁,就先删除锁,再重新上锁。
5、分析一下,上面各种优化的根本问题在于SETNX和EXPIRE两个指令无法保证原子性。Redis2.6提供了直接执行lua脚本的方式,通过lua脚本来保证原子性。
伪代码:
public boolean tryLock(RedisConnection connn){
if(connn.SETNX("mykey", "1") == 1){
conn.EXPIRE("mykey", 1000); // 给锁设置过期时长
return true; // 表明获取到锁
}else{
return false;
}
}
DEL
五、Redis如何配置key的过期时间?他的实现原理是什么?
redis设置key的过期时间方式:1、EXPIRE,2、SETEX
实现原理:
1、定期删除:每隔一段时间,执行一次删除过期key的操作。
2、懒汉式删除:当使用get,getset等指令去获取数据时,判断key是否过期。过期后,就先把key删除,再执行后面的操作。
Redis是将两种方式结合来使用。
懒汉式删除
定期删除:平衡执行频率和执行时长。
定期删除时会遍历每个database(默认16个),检查当前库中指定个数的key(默认是20个)。随机抽查这些key,如果有过期的,就删除。
程序中有一个全局变量记录删除到了哪个数据库。
注意redis中的这种删除策略,可以拓展自己的思维,将这种方式延伸到其它数据库中。
六、海量数据下,如何快速查找一条记录?
1、使用布隆过滤器,快速过滤不存在的记录。
使用redis的bitmap结构来实现布隆过滤器
2、在redis中建立数据缓存。将我们对redis使用场景的理解尽量表达出来。
以普通字符串的形式来存储,(userId->user.json)。以一个hash来存储一条记录(userId key -> username field -> ,userAge -> )。但是这样会存储海量的userId,有点浪费空间,所以可以使用一个整的hash来存储所有的数据,UserInfo -> field 就用userId,value就用user.json 。一个hash最多能支持2^32-1(40多个亿)个键值对。
缓存击穿:对不存在的数据也建立key,这些key都是经过布隆过滤器过滤的,所以一般不会太多。
缓存过期:将热点数据设置成永不过期,定期重建缓存。使用分布式锁重建缓存。
3、查询优化
redis是按槽位分配数据的。
自己实现槽位计算,找到记录应该分配到哪台机器上,然后直接去目标机器上找。
微服务篇
一、谈谈你对微服务的理解,微服务有哪些优缺点?
微服务是由Martin Fowler大师提出的。微服务是一种架构风格,通过将大型的单体应用拆分为比较小的服务单元,从而降低整个系统的复杂度。
优点:
1、服务部署更灵活:每个应用都可以是一个独立的项目,可以独立部署,不依赖于其它服务,耦合性降低。
2、技术更新灵活:在大型单体应用中,技术要进行更新,往往是困难的。而微服务可以根据业务特点,灵活选择技术栈。
3、应用的性能得到提高:大型单体应用中,往往启动就会成为一个很大的难关。而采用微服务之后,整个系统的性能是能够提高的。
4、更容易组合专门的团队:在单体应用时,团队成员往往需要对系统的各个部分都要有深入的了解,门槛是很高的。
5、代码复用:很多底层服务可以以Rest API的方式对外提供统一的服务,所有基础服务可以在整个微服务系统中调用。
缺点:
1、服务之间的调用的复杂性提高了:网络问题,容错问题,负载问题,高并发问题。。。
2、分布式事务:尽量不要使用微服务事务。
3、测试的难度提升了。
4、运维难度提升了:单体架构只要维护一个环境,而到了微服务是很多个环境,并且运维方式都还不一样。所以对部署,监控,告警等要求就会变得很困难。
二、SpringCloud和SpringCloudAlibaba都有哪些组件?都解决了什么问题?
SpringCloud:提供了构建微服务系统所需要的一组通用开发模式以及一系列快速实现这些开发模式的工具。
通常所说的SpringCloud是指SpringCloud NetFlix,他和SpringCloud Alibaba都是SpringCloud这一系列开发模式的具体实现。
SpringCloud NetFlix:
SpringCloud Alibaba:
三、分布式事务如何处理?怎么保证事务一致性?
误区:分布式事务 = Seata
分布式事务:就是要将不同节点上的事务操作,提供操作原子性保证。同时成功或同时失败。
分布式事务第一个要点就是要在原本没有直接关联的事务之间建立联系。
1、HTTP连接:最大努力通知。 -- 事后补偿。
2、MQ:事务消息机制。
3、Redis:也可以定制出分布式事务机制。
4、Seata:是通过TC来在多个事务之间建立联系。
两阶段:AT XA 就在于要锁资源。
三阶段:TCC 在两阶段的基础上增加一个准备阶段。在准备阶段是不锁资源的。
SAGA模式:类似于熔断,业务自己实现正向操作和补偿操作的逻辑。
四、怎么拆分微服务?怎样设计出高内聚、低耦合的微服务?有没有了解过DDD领域驱动设计?什么是中台?中台和微服务有什么关系?
拆分微服务的时候,为了尽量保证微服务的稳定,会有一些基本的准则:
1、微服务之间尽量不要有业务交叉。
2、微服务之间只能通过接口进行服务调用,而不能绕过接口直接访问对方的数据。
3、高内聚、低耦合。
什么是DDD,在2004年,由Eric Evans提出的,DDD是面向软件复杂之道。Domain-Driven-Design。
中台这个概念是由阿里在2015年提出的“小前台,大中台”战略思想。
所谓中台就是将各个业务线中可以复用的一些功能抽取出来,剥离个性,提取共性,形成一些可复用的组件。
五、你的项目中是怎么保证微服务敏捷开发的?微服务的链路追踪、持续集成、AB发布要怎么做?
开发运维一体化。
敏捷开发:目的就是为了提高团队的交付效率,快速迭代,快速试错。
每个月固定发布新的版本,以分支的形式保存到代码仓库中。快速入职,任务面板,站立会议。团队人员灵活流动,同时形成各个专家代表。测试环境-生产环境-开发测试环境SIT,集成测试环境、压测环境STR、预投产环境、生产环境PRD。文档优先。晨会,周会,需求拆分会。
链路追踪:1、基于日志。形成全局事务ID,落地到日志文件。filebeat-logstash-Elasticsearch 形成大型报表。
2、基于MQ,往往需要架构支持。经过流式计算形成一些可视化的结果。
持续集成:springboot maven pom -> build -> shell ; Jenkins。
AB发布:1、蓝绿发布、红黑发布。老版本和新版本是同时存在的。2、灰度发布,金丝雀发布。
Spring 篇
三、Spring框架中Bean的创建过程是怎样的?
先得说明一下Spring中的Bean是什么?在spring中,构成应用程序主干并由spring IoC容器管理的对象就被成为bean,bean是一个由spring ioc容器实例化、组装和管理的对象。(关于spring ioc参考文章:spring bean是什么 - 歪麦博客 (awaimai.com))
spring中的bean的三种定义方式:①使用构造型@Component注释你的类;②编写在自定义Java配置类中使用@Bean注释的bean工厂方法;③在xml配置文件中声明bean的定义。
首先简单来说,Spring框架中的Bean经过四个阶段:实例化 - 属性赋值 - 初始化 - 销毁。
然后具体来说,Spring中Bean经过了以下几个步骤:
1、实例化:new xxx();两个时机:① 当客户端向容器申请一个Bean时,② 当容器在初始化一个Bean时发现还需要依赖另一个Bean。BeanDefinition 对象保存。到底是new一个对象还是创建一个动态代理?
2、设置对象属性(依赖注入):spring通过BeanDefinition找到对象依赖的其它对象,并将这些对象赋予当前对象。
3、处理Aware接口:spring会检测对象是否实现了xxAware接口,如果实现了,就会调用对应的方法。
BeanNameAware、BeanClassLoaderAware、BeanFactoryAware、ApplicationContextAware
4、BeanPostProcessor前置处理:调用BeanPostProcessor的postProcessBeforeInitialization方法
5、BeanPostProcessor后置处理。
四、Spring框架中的Bean是线程安全的吗?如果线程不安全,要如何处理?
Spring容器本身没有提供Bean的线程安全策略,因此也可以说Spring容器中的Bean不是线程安全的。
要如何处理线程安全问题,就要分情况来分析。
Spring中的作用域:
1、singleton 单例:spring中默认是单例的,所以是线程不安全的。
2、prototype:为每个Bean请求创建一个实例。
3、request:为每个request请求创建一个实例,请求完成后失效。
4、session:与request是类似的。
5、global-session:全局作用域。
对于线程安全问题:
1、对于prototype作用域,每次都是生成一个新的对象,所以不存在线程安全问题。
2、singleton作用域,默认就是线程不安全的。但是对于开发中大部分的bean,其实是无状态的,不需要保证线程安全。所以在平常的MVC开发中,是不会有线程安全问题的。
无状态表示这个实例没有属性对象,是不能保存数据的,是不变的类。比如:controller、service、dao
有状态表示实例是有属性对象,可以保存数据,是线程不安全的,比如pojo
但是如果要保证线程安全,可以将bean的作用域改为 prototype,比如像Model,View等(因为这类对象每次返回时都是new出来的)。
另外还可以采用ThreadLocal来解决线程安全问题。ThreadLocal为每个线程保存一个副本变量,每个线程只操作自己的副本变量。
五、Spring如何处理循环依赖问题?
循环依赖:多个对象之间存在循环引用的关系,在初始化过程当中,就会出现“先有蛋还是先有鸡”的问题。
一种是使用@Lazy注解:解决构造方法造成的循环依赖问题。
另一种是使用三级缓存:如果引用的对象配置了AOP,那在单例池中最终就会需要注入动态代理对象,而不是原对象。而生成动态代理是要在对象初始化完成之后才开始的。于是Spring增加了三级缓存,保存所有对象的动态代理配置信息。在发现有循环依赖时,将这个对象的动态代理信息获取出来,提前进行AOP,生成动态代理。
六、Spring如何处理事务?
Spring当中支持编程式事务管理和声明式事务管理两种方式:
1、编程式事务管理可以使用TransactionTemplate。
2、声明式事务:是Spring在AOP基础上提供的事务实现机制(在需要回滚的方法上使用@Transactional注解)。他的最大优点就是不需要在业务代码中添加事务管理的代码,只需要在配置文件中做相关的事务规则声明就可以了。但是声明式事务只能作用于方法级别,无法控制代码级别的事务管理。
七、SpringMVC中的控制器是不是单例模式?如果是,如何保证线程安全?
本文来自博客园,作者:LoremMoon,转载请注明原文链接:https://www.cnblogs.com/hello-cnblogs/p/15430819.html