研发工程师面试题整理

TCP, UDP区别和各自优缺

  • TCP提供一种面向连接的、可靠的字节流服务 在一个 TCP 连接中,仅有两方进行彼此通信。广播和多播不能用于 TCP
  • TCP使用校验和,确认和重传机制来保证可靠传输
  • TCP 给数据分节进行排序,并使用累积确认保证数据的顺序不变和非重复
  • TCP使用滑动窗口机制来实现流量控制,通过动态改变窗口的大小进行拥塞控制

注意:TCP 并不能保证数据一定会被对方接收到,因为这是不可能的。TCP 能够做到的是,如果有可能,就把数据递送到接收方,否则就(通过放弃重传并且中断连接这一手段)通知用户。因此准确说 TCP 也不是 100% 可靠的协议,它所能提供的是数据的可靠递送或故障的可靠通知。

UDP 是一个简单的传输层协议。和 TCP 相比,UDP 有下面几个显著特性:

  • UDP 缺乏可靠性。UDP 本身不提供确认,序列号,超时重传等机制。UDP 数据报可能在网络中被复制,被重新排序。即 UDP 不保证数据报会到达其最终目的地,也不保证各个数据报的先后顺序,也不保证每个数据报只到达一次
  • UDP 数据报是有长度的。每个 UDP 数据报都有长度,如果一个数据报正确地到达目的地,那么该数据报的长度将随数据一起传递给接收方。而 TCP 是一个字节流协议,没有任何(协议上的)记录边界。
  • UDP 是无连接的。UDP 客户和服务器之前不必存在长期的关系。UDP 发送数据报之前也不需要经过握手创建连接的过程。

总结: 基于连接vs无连接,可靠性不同,有序性,数据边界,速度,重量级vs轻量级

什么情况下用UDP?

UDP更适用于对速度比较敏感的应用,例如:在线视频媒体,电视广播和多人在线游戏。

基于TCP协议的最好例子是HTTP协议和HTTPS协议,他们几乎存在于互联网的任何地方,实际上,绝大多数你所熟悉的通常协议,都是基于TCP的,例如:Telnet,FTP以及SMTP协议。UDP协议没有TCP协议那么受欢迎,但是也被广泛应用,比如DHCP以及DNS协议,其他还有一些基于UDP的协议如SNMP,TFTP,BOOTP以及NFS(早期版本)

多线程和多进程的区别,优缺点

多进程

  • 数据共享复杂,需要IPC(进程间通讯,如管道,文件,Socket),数据是分开的,同步简单
  • 占用内存多,CPU切换复杂,创建销毁简单,利用率低
  • 进程间不会互相影响

多线程

  • 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂
  • 占用内存少,切换简单,CPU利用率高,创建销毁,速度很快
  • 一个线程挂掉将导致整个进程挂掉

同步与异步:在计算机领域,同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。举个例子,打电话时就是同步通信,发短息时就是异步通信。

进程和线程的关系:

进程和线程都是一个时间段的描述,是CPU工作时间段的描述,不过是颗粒大小不同。进程是cpu资源分配的最小单位,线程是cpu调度的最小单位

  1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
  2. 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
  3. CPU分给线程,即真正在CPU上运行的是线程。

并行和并发:并行处理(Parallel Processing)是计算机系统中能同时执行两个或更多个处理的一种计算方法。并行处理可同时工作于同一程序的不同方面。并行处理的主要目的是节省大型和复杂问题的解决时间。并发处理(concurrency Processing):指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机(CPU)上运行,但任一个时刻点上只有一个程序在处理机(CPU)上运行。并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以说,并行是并发的子集

Thread和Runnable的区别

Thread是类,Runnable是接口,继承Thread类和实现Runnable接口都能实现多线程。由于Java不支持多继承,因此继承Thread类就代表这个子类就不能继承其他类,而实现Runnable接口没有这个限制。其实Thread类本身也是实现了Runnable接口。

当使用Runnable接口时,我们还是需要实例化一个Thread类并传入Runnable的实例来调用start()方法

public class RunnableExample implements Runnable{
    public void run(){
    ...
    }
}

public class ThreadExample extends Thread{
    public void run(){
    ...
    }
}

public static void main(String[] args){
    // 实现Runnable接口
    RunnableExample instance = new RunnableExample();
    Thread thread = new Thread(instance);
    thread.start();

    // 继承Thread类
    ThreadExample thread2 = new ThreadExample();
    thread2.start();
}

sleep和wait的区别

sleep()方法正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),在sleep指定时间后CPU再回到该线程继续往下执行(注意:sleep方法只让出了CPU,而并不会释放同步资源锁);

wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。(注意:notify的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度);

sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用;

Java并发CAS (Compare and swap)

CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。如下:

public static class MyLock {
    private AtomicBoolean locked = new AtomicBoolean(false);

    public boolean lock() {
        return locked.compareAndSet(false, true);
    }
}

注意这个locked是同步的,在某一时刻只能有一个线程在同一个MyLock实例上改变它的值。AtomicBoolean类中有一个compareAndSet()方法,它使用一个期望值和AtomicBoolean实例的值比较,和两者相等,则使用一个新值替换原来的值。在这个例子中,它比较locked的值和false,如果locked的值为false,则把修改为true。如果值被替换了,compareAndSet()返回true,否则,返回false。

使用Java5+提供的CAS特性而不是使用自己实现的的好处是Java5+中内置的CAS特性可以让你利用底层的你的程序所运行机器的CPU的CAS特性。这会使还有CAS的代码运行更快。

描述读写锁

如果一个线程上了一个读锁,那么下一个线程是不是必须等待这个读锁释放才可以读?不是, 读与读可以并行, 只有遇到写才会等待, 如: 现在读着, 如果要写,则等待; 现在已经写着, 如果另外一个线程要写, 则等着; 现在写着, 如果要读, 则等着.

用过哪些Linux命令

uname -r 显示正在使用的内核版本
cat,tail [file] 显示文件的信息
nano,vim [file] 编辑文件
ls, cd, rm(-rf),cp, pwd,mkdir 目录和文件管理
find / -name [file] 查找文件
useradd, userdel, passwd 管理用户
chmod [-cfvR] [666/777] [file] 管理文件权限
tar [-cvfz/-xvfz] [file] 压缩/解压文件
apt-get/yum install/update/upgrade/remove/clean 安装管理linux包
grep keyword [file] 在文件中查找关键词keyword
ifconfig 查看网卡信息
lsof -i:[pid] 查看pid的进程
ps ax | grep ruby 查看ruby相关进程
kill -9 [pid] 根据pid结束进程

单例设计模式

单例对象的类必须保证只有一个实例存在, 永远返回同一个对象引用和一个获得该实例的方法getInstance(必须是静态方法), 两种实现方法, 饿汉式和懒汉式, 饿汉式单例实例在类装载时就构建, 线程安全, 但是资源效率不高, 即使不调用getInstance, 该单例实例也会被创建. 而懒汉式资源利用率高,不执行getInstance()就不会实例单例, 但是线程不安全, 多个线程同时访问的时候就可能创建多个实例, 一般使用synchonized同步和双重检测机制解决,但是仍然会有JVM编译器的指令重排导致的问题, 进一步使用修饰符volatile修饰实例对象. 另外还有静态内部类实现方式

应用场景:

单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用:

  1. 需要频繁实例化然后销毁的对象。
  2. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象(访问数据库对象)。
  3. 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
  4. 控制资源的情况下,方便资源之间的互相通信。如线程池等。

应用场景举例:

  1. 操作系统的任务管理器只有一个
  2. 操作系统的回收站也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
  3. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  4. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
  5. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
  6. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。

参考1, 参考2

工厂模式

工厂模式抽象了对象创建的具体细节, 把创建对象过程封装了起来。简单工厂在构造方法中用if else语句来执行选择逻辑,通过传参来决定最后需要实例化的对象。但是,如果新增一个对象,那么我们还是要修改构造器,增加一个case,这样做并不好,特别是在多人协同实现这些对象的时候,这个构造器就要被同时修改,造成同步困难。

对简单工厂进行抽象就得到了工厂方法,每种产品由一种工厂来创建,一个工厂保存一个new,完全遵循“不改代码”的原则。而抽象工厂是工厂方法的复杂化,保存了多个new,可以把有一些有联系或者相近的产品,放到一个工厂去生产,没有必要单独再开一个工厂了。

死锁的条件?如何预防和避免?

必须满足以下四个必要条件,才会发生死锁:

  1. 互斥条件:一个资源只能被一个进程使用,若有其他进程申请该资源,必须等待直至占有资源的进程释放
  2. 请求与保持条件:进程至少已经保持一个资源,但又提出了新的资源请求,而新的资源已经被别的线程占有,此时请求阻塞,但又不会释放自己持有的资源
  3. 不剥夺条件:进程保持的资源只能由自己使用完之后释放,不能被其他进程强制夺取
  4. 循环等待条件:发生死锁时,必有若干进程首尾相连循环等待着对方的资源

避免出现死锁,只需要破坏四个中任意一个:

  1. 破坏互斥条件:对资源进行拷贝,也就是不同的进程请求不同的资源,而资源的内容一致。只适用于进程不对资源进行修改的情况,不然会造成资源同步困难,而且进程增多,导致复制成本增加
  2. 破坏请求与保持条件:进程太“贪心“,自己拥有了某资源还想去请求别的资源,而且对自己的资源保持,因此我们可以强制规定,要请求下一个资源,必须要先释放自己持有的资源。
  3. 破坏不剥夺条件:当请求时间超过一个阈值而且仍然“卡“在同一个位置时,无论是否使用完,主动释放自己的资源
  4. 破坏循环等待条件:对资源的使用顺序进行规定,任何进程都需要遵循一定的顺序取资源,如先取资源1,再取资源2等

Hash解决冲突的方法

  1. 开放寻址法:若p=Hash(key)冲突的话,再用p作为key寻在下一个哈希地址p1=Hash(p), p2=Hash(p1), … , 直到找到一个不冲突的地址 p n 插入元素。

  2. 链地址法:对于冲突的元素,用一个单向链表储存,Hash表中的value储存的是链表的头节点。

比较:开放寻址为减少冲突,通常要求较小的装填因子 α < 1 ,不然随着数据增多,冲突会变多,因此尽量让数据稀疏;而链地址法可以让 α > 1 ,可以动态增加节点,它的删除操作比较简单,只需要删除相应的链表节点即可;但是开放寻址法比较复杂。

语言


Python

python如何打印一个类的所有成员变量:

for property, value in vars(theObject).iteritems():

python的dict底层实现 :
哈希表,最低能在O(1)时间内完成搜索, 采用开放寻址法解决hash冲突

python的dict和list占用内存谁大,大多少,为什么:
list的空间占用为 基本空间 36 + (对象引用 4 + 对象大小) * 元素个数, dict最小140, 因为需要缓存散列表, 所以占用内存多

python的list的查找元素时间复杂度: O(n)

python的list查找时间复杂读高, list占用内存大, 哪种数据结构能取得平衡:
查找二叉树, 插入数据和删除数据时间复杂度O(depth) 树的深度

mysql的索引为什么使得查找操作变快:
索引有Hash索引和B-Tree索引两种, 都能使查找变快

python反射
反射就是通过字符串的形式,导入模块,执行函数。利用字符串的形式去对象(模块)中操作(查找/获取/删除/添加)成员,一种基于字符串的事件驱动, 四个内置函数,getattr, setattr, getattr, delattr,如下profile为google.protobuf的对象,通过DESCRIPTOR.fields找到了所有的fields,每一个feild看作一个方法,因此可以通过getattr来调用:

for field in profile.DESCRIPTOR.fields:
   print getattr(profile, field.name, 'not found')

线程池的实现

线程池有两个元素,一个是queue, 一个是thread_list,队列里装的是(func, args)元组,func表示要调用的方法而args为方法参数;thread_list里装的是一些thread子类(个数上限max_nums),每个线程的run方法就是起一个死循环,循环内部执行:func,args=queue.get(block=True, time_out=5),然后调用方法func(args),这里queue是线程安全的,而且get方法是阻塞的,若等待时间超过time_out还未取到func和args,则抛出异常并break出循环,此时这个线程结束。

线程池在初始化的时候,会实例化固定数量的线程,每个线程都争着在queue对象中拿执行的对象和参数然后执行。现在假设我们有个task任务需要执行,那么可以直接把task和相关参数入队queue,然后某个线程便得到了task然后执行,但是,有可能现在所有的线程都已结束了(因为等待时间过长,“饿“死了),因此我们还需要在入队queue后立即调用一个refresh方法:移除当前thread_list中的已结束线程的线程,新增一些线程并保持活着的线程数为max_num

但是thread_list中的线程都是守护线程,thread.setDaemon(True),若main函数(主线程,且非守护线程)执行完毕,此时进程中已经没有非守护线程了,那么计算机会强制结束所有守护线程,因此我们一般在线程跑起来后,调用所有活着线程的join()方法来让main线程等待他们执行完毕后,再执行后面的代码。当然,若现在线程都是非守护线程的话,那么即使主线程结束了,子线程仍然会继续执行。

线程间通讯:Event

主线程能通过线程的事件Event来控制其他线程的执行,提供了三个方法,wait和clear,set,Event中有个全局flag(默认false),当flag为false时,程序执行到event.wait()时就会阻塞,此时若用event.set()方法能让flag为true,然后wait的地方继续执行;若用event.clear(),那么flag被重置为false,程序执行到wait后会阻塞。

根据以上,我们可以用以下方法手动阻塞线程:当程序运行到wait时,由于前面的clear方法阻塞,因此等待thread运行结束后,由thread调用set()方法通知程序继续往下运行

event.clear()
thread.start() # thread在结束时调用event.set()
event.wait()

猜你喜欢

转载自blog.csdn.net/ppp8300885/article/details/79331424