4/16 又一个公司要倒下了:从洗脑广告到裁员风波
- 作者
- Name
- 青玉白露
- Github
- @white0dew
- Modified on
- Reading time
- 12 分钟
阅读:.. 评论:..
从 这一次开始,我们引入更有趣的开篇
曾几何时,“找工作,就上58同城!”这句话仿佛成了无数求职者的心声。杨幂代言时期的“58同城,一个神奇的网站”广告语,更是在电视上、网络上洗脑式地传播。但今日的58同城,似乎正经历着自己的内部风波——面对盈利压力,不得不对业务进行大刀阔斧的裁剪。
近期,一则让58同城员工或许要重新面对求职市场的消息震动了业界。季度管理会上传来的声音不容乐观,CEO姚劲波的内部信透露着紧迫感:年内无法盈利的业务将被无情砍掉。组织架构调整、人员优化、考核目标……一系列变动表明,58同城或将面临一场自我革新的变革。
回望过去,58同城曾在美股退市后,业务版图扩张至房产、汽车、招聘等多个领域,但仅有的几个亮点如快狗打车上市后的股价下跌,也反映出了公司发展的困境。裁员、高管职务变动,似乎预示着58同城的“神奇旅程”已经到达了必须减速或甚至停步思考的时刻。
2015年,58同城与赶集网的合并及之后的连番收购,虽然为它带来一时的辉煌,但在高速发展的道路上,铺得太广的摊子终究难以持续焕发光彩。如今,面对市场的残酷现实,姚劲波也可能不得不选择“断臂求生”,以确保企业的长远生存。
虽然58这个公司目前遇到一些问题,但是目前招聘市场仍然有很大的竞争力,甚至就连实习生的公司都很高:
因此不管面试公司是哪一个,都需要做好准备。我们今天就来看一下58同城的面试内容吧。面试官: 小明,你好呀!欢迎来到58同城的面试。咱们先从JUC说起吧,我注意到你的简历上有提到你熟悉HashMap和ConcurrentHashMap,那你能先给我说说它们的区别和用法吗?
求职者: 嗯,HashMap是非线程安全的,在多线程环境下使用可能会出现并发问题。而ConcurrentHashMap是线程安全的,它采用了分段锁的技术,提高了并发访问率。
面试官: 对头,既然提到了ConcurrentHashMap的线程安全,你能解释一下它是如何利用CAS(Compare-And-Swap)和synchronized来保证线程安全的吗?
求职者: 当然。在ConcurrentHashMap中,它使用CAS操作来保证基本的插入、更新操作的原子性。CAS是一种无锁的非阻塞算法,它通过对比内存中的值和预期值,只有在相同的情况下,才会将内存值更新为新的值。而在结构上发生变化,比如resize时,则会使用synchronized来保证只有一个线程在进行。
面试官: 这块你说的挺清楚的。那现在我们聊聊volatile和synchronized的区别和原理吧,你能详细解释一下么?
求职者: 当然可以,volatile主要用于变量的可见性,当一个变量被volatile修饰之后,它保证了不同线程对这个变量操作的可见性,即一个线程修改了某个volatile变量的值,这新值对其他线程来说是立即可见的。而synchronized则是一种同步机制,它不仅保证了操作的原子性,也保证了变量的可见性。synchronized会锁定对象或者方法,使得每次只有一个线程可以进入代码块进行操作。 面试官: 很好,那你能基于volatile和synchronized,分别给我举个实现单例模式的例子吗?
求职者: 好的。首先是使用volatile的单例模式,俗称双重检查锁定(Double-Check)单例模式。代码如下:
public class Singleton { private volatile static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
这里使用volatile是为了防止instance实例化时产生的指令重排问题,确保在instance被初始化之后,其他线程读取到的instance对象一定是初始化完成的。
接下来是使用synchronized的方式,这个比较简单,就是在方法上加上synchronized关键字,每次只允许一个线程进入该方法,代码如下:
public class SingletonSync { private static SingletonSync instance; private SingletonSync() {} public static synchronized SingletonSync getInstance() { if (instance == null) { instance = new SingletonSync(); } return instance; } }
面试官: 看来你对单例模式的理解还挺深的。那我们再深入一点,聊聊线程池。你能说说你对线程池的理解,以及你使用过哪些线程池吗?
求职者: 线程池主要是用来管理线程的创建和销毁的,它能够复用已创建的线程,减少线程创建和销毁的开销,提高系统资源的利用率。Java中的线程池主要是通过Executor框架实现的。我使用过的线程池有FixedThreadPool、CachedThreadPool和SingleThreadExecutor。
面试官: 好的,那你能详细解释一下线程池的工作流程吗?
求职者: 当然。线程池的工作流程大致如下:
- 当提交一个新任务到线程池时,线程池会首先判断核心线程是否都在执行任务,如果不是,则创建一个新的核心线程来执行任务;
- 如果核心线程已经达到最大数量且都在执行任务,任务会被加入到工作队列中;
- 如果工作队列已满,且线程数量未达到最大线程数,则创建非核心线程来执行任务;
- 如果线程数已达到最大线程数,采取拒绝策略处理该任务。
面试官: 解释得很清楚。现在,我们转向网络部分。你能解释一下TCP的三次握手和四次挥手吗?
求职者: 好的。三次握手主要是建立可靠的连接过程:
- 客户端发送SYN包到服务器,并进入SYN_SEND状态,等待服务器确认;
- 服务器收到SYN包,必须确认客户的SYN(ACK=1),同时自己也发送一个SYN包(SYN=1),即SYN+ACK包,此时服务器进入SYN_RECV状态;
- 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ACK=1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
求职者: 接下来是四次挥手的过程,它主要是用来断开一个TCP连接:
- 当某一端(假设为客户端)准备断开连接时,会发送一个FIN包给对方,并进入FIN_WAIT_1状态,表示客户端没有数据发送了,但是仍可以接收数据;
- 服务器收到这个FIN包后,会发送一个ACK包作为回应,并进入CLOSE_WAIT状态。此时,客户端收到ACK后,进入FIN_WAIT_2状态,等待服务器端的连接释放;
- 服务器准备好关闭连接时,也会发送一个FIN包给客户端,并进入LAST_ACK状态,表示服务器也没有数据要发送了,只是等待客户端的确认;
- 客户端收到这个FIN包后,会发送一个ACK包作为回应,并进入TIME_WAIT状态。等待足够长的时间以确保服务器收到其ACK包之后,客户端和服务器都进入CLOSED状态,完成连接的断开。
面试官: 很好。那你能解释一下为什么建立连接是三次握手,而断开连接却需要四次挥手呢?
求职者: 当然。在TCP连接建立的过程中,三次握手是为了确保双方的发送和接收能力都是正常的。而在断开连接的时候,由于TCP连接是全双工的,即数据传输是双向的,所以当一方完成数据的发送后,需要发送FIN来关闭自己的数据传输,但是另一方可能还有数据需要发送,所以它需要确认当前方的关闭请求,并且发送自己的关闭请求,这就导致了四次挥手。简而言之,三次握手确保了连接的建立,而四次挥手则是确保双方数据完全传输完毕后再关闭连接。
面试官: 说得很清楚。既然提到了网络,那你知道在Linux环境下,如何查看某个端口被哪个进程占用吗?
求职者: 是的,可以使用lsof命令或者netstat命令来查看。以lsof为例,具体命令如下:
lsof -i:端口号
这个命令会列出所有监听指定端口的进程信息。如果是使用netstat的话,命令如下:
netstat -tulnp | grep 端口号
这里的-tulnp
选项分别表示TCP、UDP、Listening状态的端口,以及显示对应的进程号和程序名。
面试官: 很好,看来你对Linux的命令也很熟悉。那我们接下来聊聊MySQL方面的问题。你能解释一下为什么MySQL选择了B+树作为存储结构,而不是B树吗?
求职者: MySQL选择B+树作为索引结构的主要原因是B+树的查询效率相对较高,且查询速度相对稳定。B+树的所有数据都存储在叶子节点上,并且叶子节点之间有指针相连,这样就使得范围查询变得非常高效。而B树的数据是分布在整个树中的,这就导致了范围查询时可能需要遍历很多不相关的节点。另外,B+树的非叶子节点不存储数据,只存储键值,这样一来,相同大小的页可以存储更多的键值,从而降低了树的高度,进一步提高了查询效率。
面试官: 说得好。那你能解释一下聚集索引和非聚集索引的区别吗?
求职者: 聚集索引的特点是,表中行的物理顺序和键值的逻辑(索引)顺序相同。也就是说,聚集索引决定了数据的物理存储顺序。每个表只能有一个聚集索引,因为你不能以两种不同的顺序来存储同一份数据。对于非聚集索引,它的索引结构和数据本身是分开存储的,非聚集索引包含索引的键值和指向数据行的指针,因此一个表可以有多个非聚集索引。
面试官: 非常详细的解答!接下来,我们来聊聊算法。你能说明一下冒泡排序和快速排序的原理吗?
求职者: 当然可以。冒泡排序是一种简单的排序算法,它重复地遍历待排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
public void bubbleSort(int[] arr) { int n = arr.length; for (int i = 0; i < n-1; i++) for (int j = 0; j < n-i-1; j++) if (arr[j] > arr[j+1]) { // swap arr[j+1] and arr[j] int temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp; } }
而快速排序是由C. A. R. Hoare在1960年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide and Conquer)。在快速排序中,选择一个元素作为"基准"(pivot),分区过程将数组分为两个部分,使得一部分的所有元素都比另一部分的所有元素小,然后对这两部分继续进行排序,以此类推,直到整个序列有序。
public void quickSort(int[] arr, int low, int high) { if (low < high) { int pi = partition(arr, low, high); quickSort(arr, low, pi-1); quickSort(arr, pi+1, high); } } int partition(int[] arr, int low, int high) { int pivot = arr[high]; int i = (low-1); // smaller element index for (int j=low; j<high; j++) { if (arr[j] < pivot) { i++; // swap arr[i] and arr[j] int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } // swap arr[i+1] and arr[high] (or pivot) int temp = arr[i+1]; arr[i+1] = arr[high]; arr[high] = temp; return i+1; }
面试官: 很棒,代码也很规范。那你能告诉我快速排序的最坏时间复杂度是多少吗?以及在什么情况下会达到最坏情况?
求职者: 快速排序的最坏时间复杂度是O(n^2)
。最坏的情况发生在每次分区操作所选的基准元素都是当前数组中的最小或最大元素,这样每次只减少一个元素,导致分区非常不平衡,需要进行n-1次分区操作,因此总的时间复杂度为O(n^2)
。
面试官: 对,这种情况通常在数组已经是有序的或者接近有序的情况下发生。那冒泡排序的最坏和最好情况复杂度又是多少?
求职者: 冒泡排序的最坏时间复杂度也是O(n^2)
,当数组完全逆序时会达到这个复杂度。而最好情况的时间复杂度是O(n)
,当数组已经是有序的,只需要进行一次遍历就可以确定排序已经完成。
面试官: 最后一个问题,能否给我展示一下如何在一个单链表中删除重复元素的方法?
求职者: 当然可以。这里我们可以使用哈希表来记录每个元素是否出现过,遍历链表,如果元素在哈希表中已存在,则删除该节点;否则,将该元素添加到哈希表中。代码如下:
class ListNode { int val; ListNode next; ListNode(int x) { val = x; } } public void deleteDuplicates(ListNode head) { HashSet<Integer> set = new HashSet<>(); ListNode current = head; ListNode prev = null; while (current != null) { if (set.contains(current.val)) { prev.next = current.next; } else { set.add(current.val); prev = current; } current = current.next; } }
面试官: 很好,你的回答非常全面。今天的面试就到这里,感谢你的参与,我们会尽快给你反馈的。再次感谢!
求职者: 非常感谢这次面试机会,期待您的好消息!