20180512下午深圳vivo面试题目

5月11日在上班时间,VIVO那边对我进行一轮电话面试,面试的主要内容如下:

1.简单的自我介绍

2.kafka的实现原理?

分区+按顺序写+按位移读

3.JVM垃圾回收的算法?

(1)标记-清除

标记出要回收的对象,清除掉要回收的对象

(2)标记-整理

标记出要回收的对象,清除掉要回收的对象,存活对象整理到连续的内存空间。

(3)标记-复制

标记出要回收的对象,复制存活的对象,缺点是内存使用率只有50%。

(3)分代收集

年轻代使用:标记-复制

老年代使用:

4.如何判断一个对象要被垃圾回收?GC Roots是怎么判断的?

根搜索方法是通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链(Reference Chain),当一个对象没有被GC Roots的引用链连接的时候,说明这个对象是不可用的。

GC Roots对象包括:

a) 虚拟机栈(栈帧中的本地变量表)中的引用的对象。

b) 方法区域中的类静态属性引用的对象。

c) 方法区域中常量引用的对象。

d) 本地方法栈中JNI(Native方法)的引用的对象。

引用类型:强引用、软引用、弱引用和虚引用

5.垃圾回收器有哪些?有啥区别?

(1)Serial垃圾收集器

是一个单线程收集器,但它在执行垃圾收集的时候,其他线程得全部暂停工作。

(2)ParNew垃圾收集器

ParNew收集器是Serial收集器的多线程版本,它是多个线程在执行回收工作,但在执行回收工作时,其他所有线程也是要全部暂停工作的。以追求最短垃圾回收为主要目的。

(3)Parallel Scanvenge收集器

Parallel Scanvenge是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。这个垃圾收集器更关注吞吐量,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。它是多个线程在执行回收工作,但在执行回收工作时,其他所有线程也是要全部暂停工作的。

(4)CMS(Concurrent Mark Sweep)

CMS(Concurrent Mark Sweep)并发标记清除收集器是一种以获取最短回收停顿时间为目标的收集器,在JDK1.5中发布。会产生内存碎片。

整个过程分四个步骤。

1) 初始标记

2) 并发标记

3) 重新标记

4) 并发清除

其中初始标记和重新标记都是要停止其他所有线程的工作执行的,并发标记和并发清除可以与其他工作线程一起执行。

(5)G1收集器
G1(Garbage-First)收集器是整体上基于标记-整理,从局部看是基于复制算法实现。不会产生太多的内存碎片。利用多CPU、多核环境,通过并发的方式让JAVA程序在执行垃圾回收的时候也能继续执行。
G1收集器的过程如下。
(1) 初始标记
(2) 并发标记
(3) 最终标记
(4) 筛选回收
其中初始标记、最终标记和筛选回收是要停止其他所有线程的工作执行的,并发标记可以与其他工作线程一起执行。

6.垃圾回收器CMS的原理?

CMS(Concurrent Mark Sweep)并发标记清除收集器是一种以获取最短回收停顿时间为目标的收集器,在JDK1.5中发布。会产生内存碎片。

整个过程分四个步骤。

1) 初始标记

2) 并发标记

3) 重新标记

4) 并发清除

其中初始标记和重新标记都是要停止其他所有线程的工作执行的,并发标记和并发清除可以与其他工作线程一起执行。

https://www.sohu.com/a/214780788_753508

7.说下Synchronized和ReentrantLock的区别和实现原理?

(1)Synchronized在虚拟机实现,是阻塞的

1)同步方法

添加ACC_SYNCHRONIZED标识

2)同步代码块

反编译后得到monitor enter和monitor exit指令。

(2)ReentrantLock在代码层通过volatile+CAS实现,是非阻塞的

CAS 是通过调用循环调用compareAndSet来判断值是有修改,通过调用unsafe.compareAndSwapInt来实现,内部调用C++代码并且加锁(多处理器),通过offset读取内存中指定位置的数值,再跟当前高速缓存中的值比较是否一致。

8.说下AQS的实现原理?如何解决并发获取锁的问题?

aqs全称为AbstractQueuedSynchronizer,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件。

AQS的实现依赖内部的同步队列,也就是FIFO的双向队列,如果当前线程竞争锁失败,那么AQS会把当前线程以及等待状态信息构造成一个Node加入到同步队列中,同时再阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。

在获得同步锁时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态state。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

调用ReentrantLock中的lock()方法,源码的调用过程我使用了时序图来展现从图上可以看出来,当锁获取失败时,会调用addWaiter()方法将当前线程封装成Node节点加入到AQS队列,基于这个思路,我们来分析AQS的源码实现

https://segmentfault.com/a/1190000017372067

回答的还可以,后面就叫我12日(周六)过去深圳步步高大厦的VIVO总部面试了。

5月12日(周六)到了VIVO,发现VIVO都有上班,后面一问才知道,VIVO那边是大小周的,也就是每隔一周的周六是要上班的。

来了一个技术面试官,面试的主要内容如下:

1.简单的自我介绍

2.KAFKA的实现原理?

分区+按顺序写+按位移读

3.常用消息中间件的区别?

(1)ActiveMQ
使用JAVA语言编写,开源消息中间件。与Spring结合比较好,管理界面比较友好。持久化方式支持AMQ(基于文件)、KahaDB(基于文件数据库,基于B树索引)、JDBC(关系型数据库,比如ORACLE、MYSQL等)、LevelDB(基于文件数据库),支持的集群方式包括主从、多主等。

(2)KAFKA

Kafka是一种高吞吐量的分布式发布订阅消息系统,有如下特性:
1)    以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间复杂度的访问性能。
2)    高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条以上消息的传输。原因在于kafka使用集群和分区来分散保存消息,这样可以利用多个机器的IO。Leader节点负责某一topic(可以分成多个partition)的某一partition的消息的读写,任何发布到此partition的消息都会被直接追加到log文件的尾部,因为每条消息都被append到该partition中,是顺序写磁盘,因此效率非常高(经验证,顺序写磁盘效率比随机写内存还要高,这是Kafka高吞吐率的一个很重要的保证),同时通过合理的partition,消息可以均匀的分布在不同的partition里面。Kafka基于时间或者partition的大小来删除消息,同时broker是无状态的,consumer的消费状态(offset)是由consumer自己控制的(每一个consumer实例只会消费某一个或多个特定partition的数据,而某个partition的数据只会被某一个特定的consumer实例所消费),也不需要broker通过锁机制去控制消息的消费,所以吞吐量惊人,这也是Kafka吸引人的地方。
3)    支持Kafka Server间的消息分区,及分布式消费,同时保证每个Partition内的消息顺序传输。
4)    同时支持离线数据处理和实时数据处理。
5)    Scale out:支持在线水平扩展。
6)    KAFKA的持久化时间可以指定,比如一个星期、一个月等,客户端可以通过offset来读取KAFKA中的数据。

4.KAFKA是否可以作为订单等特别重要业务场景的中间件?

可以

5.DUBBO的原理、集群方式、负载均衡方式、服务发现方式?

Dubbo缺省协议采用单一长连接和NIO异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。集群方式包括Mutilcast、Zookeeper、Redis和Simple,负载均衡方式包括随机、按权重、最少活跃优先和一致性Hash。

缺点:只支持JAVA、不适合传数据量大的服务调用,比如传文件。

6.常用垃圾收集方法?

标记清除、标记整理、 标记-复制、分代收集

7.常用的垃圾收集器?

(1)Serial

是一个单线程收集器,但它在执行垃圾收集的时候,其他线程得全部暂停工作。

(2)ParNew

ParNew收集器是Serial收集器的多线程版本,它是多个线程在执行回收工作,但在执行回收工作时,其他所有线程也是要全部暂停工作的。以追求最短垃圾回收为主要目的。

(3)ParallelScanvenge

Parallel Scanvenge是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。这个垃圾收集器更关注吞吐量,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。它是多个线程在执行回收工作,但在执行回收工作时,其他所有线程也是要全部暂停工作的。

(4)CMS

CMS(Concurrent Mark Sweep)并发标记清除收集器是一种以获取最短回收停顿时间为目标的收集器,在JDK1.5中发布。会产生内存碎片。

整个过程分四个步骤。

(1) 初始标记

(2) 并发标记

(3) 重新标记

(4) 并发清除

其中初始标记和重新标记都是要停止其他所有线程的工作执行的。并发标记和并发清除可以与其他工作线程一起执行。

(5)G1

G1(Garbage-First)收集器是整体上基于标记-整理,从局部看是基于复制算法实现。不会产生太多的内存碎片。利用多CPU、多核环境,通过并发的方式让JAVA程序在执行垃圾回收的时候也能继续执行。

G1收集器的过程如下。

(1) 初始标记

(2) 并发标记

(3) 最终标记

(4) 筛选回收

其中初始标记、最终标记和筛选回收是要停止其他所有线程的工作执行的,并发标记可以与其他工作线程一起执行。

8.CMS的缺点?

(1)浮动垃圾

由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然会有新垃圾产生,这部分垃圾得标记过程之后,所以CMS无法在当收集中处理掉他们,只好留待下一次GC清理掉,这一部分垃圾称为浮动垃圾。在jdk1.5默认设置下,CMS收集器当老年代使用了68%的空间就会被激活,可以通过-XX:CMSInitialOccupancyFraction的值来提高触发百分比,在jdk1.6中CMS启动阈值提升到了92%,要是CMS运行期间预留的内存无法满足程序的需要,就会出现”Concurrent Mode Failure“,然后降级临时启用Serial Old收集器进行老年代的垃圾收集,这样停顿时间就很长了,所以-XX:CMSInitialOccupancyFraction设置太高容易导致大量”Concurrent Mode Failure“。

(2)有空间碎片

CMS是一款基于“标记-清除”算法实现的,所以会产生空间碎片。为了解决这个问题,CMS提供了-XX:UseCMSCompactAtFullCollection开发参数用于开启内存碎片的合并整理,由于内存整理是无法并行的,所以停顿时间会变长。还有-XX:CMSFullGCBeforeCompaction,这个参数用于设置多少次不压缩Full GC后,跟着来一次带压缩的(默认为0)。

(3)对CPU资源敏感

CMS默认启动的回收线程数是(cpu数量+3)/4。所以CPU数量少会导致用户程序执行速度降低较多。

9.G1的特点

G1(Garbage-First)收集器是整体上基于标记-整理,从局部看是基于复制算法实现。不会产生太多的内存碎片。利用多CPU、多核环境,通过并发的方式让JAVA程序在执行垃圾回收的时候也能继续执行。

G1收集器的过程如下。

(1) 初始标记

(2) 并发标记

(3) 最终标记

(4) 筛选回收

其中初始标记、最终标记和筛选回收是要停止其他所有线程的工作执行的,并发标记可以与其他工作线程一起执行。

G1是一款面向服务端应用的垃圾收集器。G1具备如下特点:

(1)并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

(2)分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。

(3)空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

(4)可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内

10.Spring使用到的设计模式?

(1)单例模式:通过 BeanFactory 或 ApplicationContext 创建 bean 对象

(2)工厂模式:BeanFactory

(3)动态代理模式:事务

动态代理有两种

目标方法有接口时候自动选用 JDK 动态代理

目标方法没有接口时候选择 CGLib 动态代理

(4)反射模式:事务

(5)策略模式:加载资源文件的方式,使用了不同的方法,比如:ClassPathResource,FileSystemResource,ServletContextResource,UrlResource但他们都有共同的借口Resource

(6)模板模式:JdbcTemplate

模板方法模式是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。

(7)观察者模式:ContextLoaderListener

(8)IOC模式:由容器控制对象的生成

(9)AOP模式:分离出日志、权限和事务

(10)适配器模式:在Spring MVC中,DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由HandlerAdapter 适配器处理。HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。
 

11.有没阅读常用框架的源码?

12.Spring注解的实现原理?

Spring注解通过反射实现,具体请看

(1)处理set方法上的注解

1)获取所有属性描述

2)获取属性描述中的所有set方法

3)判断是否有定义注解,判断是否有指定name类型

4)通过Method.invoke(bean,value)赋值

(2)处理field属性上的注解

1)获取所有field

2)获取field中的所有带注解的

3)判断是否有定义注解,判断是否有指定name类型

4)通过Field.set(bean,value)赋值

https://zxf-noimp.iteye.com/blog/1071765

13.虚拟机堆栈的主要参数有哪些?

XSS、PrintGCDetails

  • -Xmx(最大堆的空间)
  • -Xms(最小堆的空间)
  • -Xmn (设置新生代的大小)
  • -XX:NewRatio(设置新生代和老年代的比值,如果设置为4则表示(eden+from(或者叫s0)+to(或者叫s1)): 老年代 =1:4),即年轻代占堆的五分之一
  • -XX:SurvivorRatio(设置两个Survivor(幸存区from和to或者叫s0或者s1区)和eden区的比),8表示两个Survivor:eden=2:8,即Survivor区占年轻代的五分之一
  • -XX:PermSize(设置永久代的初始空间大小)
  • -XX:MaxParmSize(设置永久代的最大空间)
  •  年老代里存放的都是存活时间较久的,大小较大的对象,因此年老代使用标记整理算法。当年老代容量满的时候,会触发一次Major GC(full GC),回收年老代和年轻代中不再被使用的对象资源。

14.哪些情况会出现OutOfMemory?

(1)JAVA堆溢出

年老代满了,原因可能是以下几种

1)短时间出现大量对象放入堆中,比如从表中读出几百万的数据

2)出现大对象,会直接在年老代中分配空间,导致年老代空间不够

(2)虚拟机栈和本地方法栈溢出

(3)方法区和运行时常量池溢出

15.线程池的种类、实现原理、主要参数、队列的类型?

线程池的各类

1)Executor.newSingleThreadExecutor

corePoolSize=1

maximumPoolSize=1

BlockingQueue=LinkedBlockingQueue

创建一个单线程的线程池,这个线程池最多只有一个线程在执行,也就是相当于单线程串行执行

2)Executor newFixedThreadPool

corePoolSize=n

maximumPoolSize=n

BlockingQueue=LinkedBlockingQueue

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

3)newCachedThreadPool

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

队列的种类

(1)SynchronousQueue

SynchronousQueue是一个内部只能包含一个元素的队列。插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。

(2)LinkedBlockingQueue

LinkedBlockingQueue是先进先出的阻塞队列,通过链路实现,队列最大长度是Integer.MAX_VALUE=2^31-1,入队列采用ReentrantLock,底层是使用CAS使用。

(3)有界队列ArrayBlockingQueue

通过ReentrantLock实现同步,通过数组存储。

4)Executor.newScheduledThreadPool

定时执行的线程池

16.使用什么连接池?连接池的实现原理?

减少连接频繁的开启和关闭,减少网络开销。

HikariCP。

HikariCP使用threadlocal缓存连接及大量使用CAS的机制,最大限度的避免lock。单可能带来cpu使用率的上升。

HikariCP的优势:
- 字节码精简:优化代码,直到编译后的字节码最少,这样,CPU缓存可以加载更多的程序代码;
- 优化代理和拦截器:减少代码,例如HikariCP的Statement proxy只有100行代码,只有BoneCP的十分之一;
- 自定义数组类型(FastList)代替ArrayList:避免每次get()调用都要进行range check(校验数组大小是否越界,如果越界要进行扩充),避免调用remove()时的从头到尾的扫描;
- 自定义集合类型(ConcurrentBag):提高并发读写的效率;
- 其他针对BoneCP缺陷的优化,比如对于耗时超过一个CPU时间片的方法调用的研究
https://download.csdn.net/download/taisenki/10037606

17.如何在LINUX中查询虚拟机的堆栈信息、垃圾回收情况?

-XX:+PrintGCDetails 打印详细的GC日志

-XX:+PrintHeapAtGC 每次GC前后分别打印堆的信息

查看JVM堆栈信息   jstack 11497 >> /home/zhaoxiao/jstack11497.txt

每隔1S打印一次GC信息,打印3次

jstat -gc 19870 1000 3

18.如何使用kill命令干掉进程时,可以让程序先执行完。

kill -15 pid   相当 于kill pid

kill -9 pid  强制删除,有风险

19.虚拟机的内存模型是啥?

JVM将内存为主内存和工作内存两个部分。

主内存: 主要包括本地方法区和 堆

Java 内存模型规定了所有变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件的主内存名字一样,两者可以互相类比,但此处仅是虚拟机内存的一部分)。
工作内存: 每个线程都有一个工作内存,工作内存中主要包括两个部分,一个是属于该线程私有的栈和 对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)。

每条线程都有自己的工作内存(Working Memory,又称本地内存.),线程的工作内存中保存了该线程使用到的变量,该变量是主内存中的共享变量的副本拷贝。
(工作内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。)
————————————————
版权声明:本文为CSDN博主「oollXianluo」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq877728715/article/details/101547608

20.说下变量的原子性、可见性?

https://www.jianshu.com/p/cf57726e77f2

(1)原子性

原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。及时在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。我们先来看看哪些是原子操作,哪些不是原子操作,有一个直观的印象:

int a = 10; //1 a++; //2 int b=a; //3 a = a+1; //4

上面这四个语句中只有第1个语句是原子操作,将10赋值给线程工作内存的变量a,而语句2(a++),实际上包含了三个操作:1. 读取变量a的值;2:对a进行加一的操作;3.将计算后的值再赋值给变量a,而这三个操作无法构成原子操作。对语句3,4的分析同理可得这两条语句不具备原子性。当然,java内存模型中定义了8中操作都是原子的,不可再分的。

上面的这些指令操作是相当底层的,可以作为扩展知识面掌握下。那么如何理解这些指令了?比如,把一个变量从主内存中复制到工作内存中就需要执行read,load操作,将工作内存同步到主内存中就需要执行store,write操作。注意的是:java内存模型只是要求上述两个操作是顺序执行的并不是连续执行的。也就是说read和load之间可以插入其他指令,store和writer可以插入其他指令。比如对主内存中的a,b进行访问就可以出现这样的操作顺序:read a,read b, load b,load a。

由原子性变量操作read,load,use,assign,store,write,可以大致认为基本数据类型的访问读写具备原子性(例外就是long和double的非原子性协定)

(2)可见性

当一个线程对共享变量做了修改之后,其他线程能够立即感知这个变量的变动

volatile可以保证可见性,但并不能保证原子性。

volatile包含禁止指令重排序的语义,其具有有序性。

synchronized: 具有原子性,有序性和可见性; volatile:具有有序性和可见性

21.说下happen before的原理?

https://blog.csdn.net/ma_chen_qq/article/details/82990603

happen before 并不是字面的英文的意思,想要理解它必须知道 happen before 的含义,它真正的意思是前面的操作对后续的操作都是可见的,比如 A happen before B 的意思并不是说 A 操作发生在 B 操作之前,而是说 A 操作对于 B 操作一定是可见的。

具体的规则:

1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。

2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。

3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

5)start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。

6)Join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

7)程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。

8)对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

22.如果接口的并发量比较大,导致性能跟不上,如何预热接口。

23.volatile变量的作用

volatile可以保证可见性,但并不能保证原子性。

volatile包含禁止指令重排序的语义,其具有有序性。

24.你有什么想问的吗?

第一轮技术面试面了一个多小时,感觉被虐了,很多东西答不上来,但面试官叫我等会,说会有另外一个人过来面试。

另外一个是架构组的负责人,面试的主要内容如下

1.DUBBO的实现原理、集群方式、负载集群方式?

2.KAFKA高并发吞吐量的原理?

3.个人的职业规划?

4.为什么选择来深圳?(我现在在广州工作)

5.你有什么想问的吗?

我就问了公司用户量、并发量、数据量,以及所做了业务,公司的上班时间,公司的技术级别,刚刚面试的人的技术级别。

其他面试问题我也忘记,暂时想到这些。面试官面完就叫我回去了,估计挂了^..^,感觉被虐了.

通过面试能够查漏补缺,面而知不足,再通过学习被短板。

目前手上已经拿到两家上市公司的offer,月底就要去新公司报道了。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页