面试总结1
面试求职,不止要求的是本职工作,而且还需要有比较扎实的基础知识。
虽然在实践中加深理解最好,但是实际工作中相当一部分知识点基本都遇不到,所以汇总总结一下。
温故知新。
题目和答案是问答形式,相当于一个完成的面试流程。
1.Q: 多线程下同步的方法?
1.操作系统–线程同步
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。
2. 临界区:
每个进程中访问临界资源的那段代码称为临界区(临界资源是一次仅允许一个进程使用的共享资源)。
3. 线程同步的几种方式:
信号量,互斥量,事件(信号,各个线程对中断事件的响应)
一、互斥量:(锁)
互斥量有两种状态--解锁和加锁。当一个线程(或进程)需要访问临界区时,它调用互斥锁。如果该互斥量当前是解锁的(即临界区可用),此调用成功,调用线程可以自由进入该临界区。另一方面,如果该互斥量已经加锁,调用线程被阻塞,直到在临界区中的线程完成并调用互斥锁。如果多个线程被阻塞在该互斥量上,将随机选择一个线程并允许它获得锁。
二、信号量:(OnlyChild)
它允许同一时刻多个线程访问同一资源,但是需要一个计数器来控制可以使用某共享资源的线程数目。
三、事件(信号):(notify/notifyAll)
通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
2.Q: 线程通信方式?
线程通信本身这个问题就是错误的,线程本就是cpu随机切换的,一个进程包含多个线程,同一进程中的线程因属同一地址空间,可直接通信。
总结1就是线程通信的方式。
进程通信方式?
1-4 很好理解,其中1、3、4和线程大同小异,消息队列可以专门作为一个知识点了解。
-
信号量(semophore ) :
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
-
消息队列( messagequeue ) :
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
-
信号 (sinal ) :
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
-
共享内存(shared memory ) :
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
-
管道( pipe ):
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
-
有名管道 (namedpipe) :
有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
-
套接字(socket ) :
套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
3.消息队列( message queue )
4.见过一些线程拥有自己的资源么?
问的是线程的公有资源和私有资源。
线程是进程内部运行的执行流.linux下没有真正的线程.一般使用进程来模拟线程.Linux下的进程叫做轻量级进程.
线程是CPU调度的基本单位.
1.线程共享
线程共享的数据一般都是 进程维度的代码和数据
线程共享的环境包括:(进程维度的代码和数据)
进程代码段、
进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、
进程打开的文件描述符、
信号的处理器、
进程的当前目录
进程用户ID与进程组ID。
2.线程私有(很好理解,都是为了找到线程本身或者保证不被其他线程干扰)
linux是有线程创建和终止函数的pthread_create 和 pthread_exit。
进程拥有这许多共性的同时,还拥有自己的个性。有了这些个性,线程才能实现并发性。这些个性包括:
1.线程ID (唯一确定线程)
每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标识线程。
2.寄存器组的值 (切回时恢复线程自身)
由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上时,
必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。
3.线程的堆栈(函数堆栈,每个线程对应一组函数调用顺序,互不影响)
堆栈是保证线程独立运行所必须的。
线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈,使得函数调用可以正常执行,不受其他线程的影响。
4.错误返回码(每个线程产生的错误不能被覆盖)
由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了error值,
而在该线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。
所以,不同的线程应该拥有自己的错误返回码变量。
5.线程的信号屏蔽码(线程只对的处理逻辑有针对性)
由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。
6.线程的优先级
由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。
3. 举例说明
IPC (进程间通信)
fork()函数: fork()允许创建多个进程,但是进程间存在通信问题。 因为每个进程都有自己的独立的内存空间。
5.那怎么实现线程安全呢?
java的三种方式
1. 同步代码块 synchronized(this) {}
synchronized (this) {
System.out.println(this.getClass().getName().toString());
if (tickets <= 0) {
return;
}
System.out.println(Thread.currentThread().getName()+"--->售出第: "+tickets+" 票");
tickets--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
2. 同步方法 synchronized void synMethod() {}
直接调用同步方法。
synchronized void synMethod() {
synchronized (this) {
if (tickets <=0) {
return;
}
System.out.println(Thread.currentThread().getName()+"---->售出第 "+tickets+" 票 ");
tickets-- ;
}
}
3. Lock锁机制
Lock锁机制, 通过创建Lock对象,采用lock()加锁,unlock()解锁,来保护指定的代码块
Lock lock = new ReentrantLock();
@Override
public void run() {
// Lock锁机制
while(tickets > 0) {
try {
lock.lock();
if (tickets <= 0) {
return;
}
System.out.println(Thread.currentThread().getName()+"--->售出第: "+tickets+" 票");
tickets--;
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}finally {
lock.unlock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
if (tickets <= 0) {
System.out.println(Thread.currentThread().getName()+"--->售票结束!");
}
}
6.Q:那你说下ThreadLocal吧。
通过类ThreadLocal实现每一个线程都有自己的共享变量,解决的就是每个线程绑定自己的值。
【就是这么一个变量a,各个线程都能访问它,而且每个线程获取a时都是该线程上次自己所赋的值或者是初始值】
一句话:每个线程都能访问a,而a对于每个线程都是该线程特有的值。
public class Tools {
public static ThreadLocal tl = new ThreadLocal();
}
public class Run {
public static void main(String[] args) {
try {
ThreadA a = new ThreadA();
ThreadB b = new ThreadB();
a.start();
b.start();
for (int i = 0; i < 100; i++) {
if (Tools.tl.get() == null) {
Tools.tl.set("Main" + (i + 1));
} else {
System.out.println("Main get Value=" + Tools.tl.get());
}
Thread.sleep(200);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ThreadLocal的实现原理
每个Thread的对象都有一个ThreadLocalMap,当创建一个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类型。
threadlocal是线程独有的,在Thread类下有一个map作为成员,key为当前线程thread,value则是多个threadLocald的变量。当要使用时,以当前线程作为key,获取对应的变量。
get 和 set 方法:
7.Q: 那线程池怎么办呢?一个线程一直存在?
这个问题是承接上面的问题,一个线程池中假如使用了threadLocal, 因为线程池会复用线程,被复用的线程去执行新的任务时会使用被上一个线程操作过的 value 对象, 从而产生不符合预期的结果。
线程池中的线程在任务执行完成后会被复用,所以在线程执行完成时,要对 ThreadLocal 进行清理(清除掉与本线程相关联的 value 对象)。不然,被复用的线程去执行新的任务时会使用被上一个线程操作过的 value 对象,从而产生不符合预期的结果。
在每个线程执行完成时,应该清理 ThreadLocal。 从这个角度来说,ThreadLocal是为了那些使用完就销毁的线程设计的。
ThreadLocal的remove方法,finally
8.Q:为什么要用线程池,为什么不自己写一个?线程池专题
为什么要用线程池
1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
为什么不手写一个
缓冲队列
健壮性、测试;人力物力
A:(你为什么要问我这么感觉有坑的问题)线程池提供了各种参数,我们可以根据业务场景来设置对应的参数。比如,当更多的线程被资源限制时,可以使用线程池的队列在存储任务,自己单纯写线程启动的话,可能这些状况下会有问题。
8.Q: 为什么要用spring。
个人感觉就是优点列举
- 解耦和分层架构: 降低了组件之间的耦合性 ,实现了软件各层之间的解耦
- AOP: 容器提供了AOP技术,利用它很容易实现如权限拦截,运行期监控等功能
- 声明式事务的支持
- 方便集成各种优秀框架
总之,效率提高了。
9.Q:spring基础的知道么,IOC,AOP之类的?
IOC(DI)
控制反转模式(也称作依赖性介入)的基本概念是:不创建对象,但是描述创建它们的方式。在代码中不直接与对象和服务连接,但在配置文件中描述哪一个组件需要哪一项服务。容器 (在 Spring 框架中是 IOC 容器) 负责将这些联系在一起。
依赖注入(Dependecy Injection)和控制反转(Inversion of Control)是同一个概念,具体的讲:当某个角色需要另外一个角色协助的时候,在传统的程序设计过程中,通常由调用者来创建被调用者的实例。但在spring中创建被调用者的工作不再由调用者来完成,因此称为控制反转。创建被调用者的工作由spring来完成,然后注入调用者, 因此也称为依赖注入。
spring以动态灵活的方式来管理对象 , 注入的两种方式,设置注入和构造注入。
- 设置注入的优点:直观,自然
- 构造注入的优点:可以在构造器中决定依赖关系的顺序。
AOP
Aop则是面向切点的编程。可以定义一个切点,在它的前后做一些共同的操作。比如打个日志什么的。 联想rails,before_action等等;
10.Q: 为啥不直接打?
- 实现代码复用,提高使用效率。
- 解耦:实现低耦合高内聚;
A: (喵喵喵?)可能有一些操作是通用的,AOP通过定义切点,可以是我们不需要反复写相同的代码。比如打印个方法开始时间,结束时间之类的。
放弃spring cloud,可以补充一下概念
11.Q: 你说你们有微服务是吧?那服务GG了怎么办?
A: 您是想问熔断那方面么?这个我目前的工作只涉及了服务发现和网关配置。熔断那部分我理解是在整个服务GG时,给出一个处理。我还没有碰到过这种情况,基本每个服务都有多个实例。服务发现和注册会通过健康检查检查各个实例的心跳。只要有一个实例存活,服务注册和服务发现会给出相应的策略,还没有接触到熔断那一块的知识。
12.Q: 那你说一下spring cloud都有哪些模块?
A: 服务发现,网关gateway,熔断,配置中心等。
13.Q: 那你说一些ZK,EUREKA之类的区别之类的?
A: (这里已经有点丧了)都是分布式的框架。分布式需要面临CAP的问题。
zookeeper
eureka
14.Q: 说下CAP。
CAP定理:C:数据一致性。A:服务可用性。P:分区容错性(服务对网络分区故障的容错性)
CAP原则(C:强一致性。A:可用性。P:分区容错性)
15.Q: 说下HashMap的实现。
链地址法,根据key的hashcode选择数组
-
采用数组+单链表形式存储元素,从jdk1.8开始,增加了红黑树的结构,当单链表中元素个数超过指定阈值(8个),会转化为红黑树结构存储,目的就是为了解决单链表元素过多时查询慢的问题。
-
和HashTable不同的是,HashMap是线程不安全的,方法都未使用synchronized关键字。因为内部实现不同,允许key和value值为null。
-
构建HashMap实例时有两个重要的参数,会影响其性能:初始大小和加载因子。初始大小用来规定哈希表数组的长度,即桶的个数。加载因子用来表示哈希表元素的填满程度,越大则表示允许填满的元素就越多,哈希表的空间利用率就越高,但是冲突的机会也就增加了。反之,越小则冲突的机会就会越少,但是空间很多就浪费了。
1、 jdk8 是尾插法, jdk6是头插法
2、 jdk7/8 初始大小16(2的次幂,ArrayList是10), 扩容是 x 2, 负载因子 0.75
3、 jdk8 是转树还是扩容?
在jdk1.8中当链表长度大于8是会被转化成红黑树,
jdk7: 创建一个新的数组newTable,容量是oldTable的一倍;遍历oldTable,拿到每个链表;遍历链表,头插法插入newTable
jdk8:
JDK8里面HashMap没有采用头插法转移链表数据,而是保留了元素的顺序位置,新的代码里面采用:
JDK7里面是先判断table的存储元素的数量是否超过当前的threshold=table.length*loadFactor(默认0.75),如果超过就先扩容,在JDK8里面是先插入数据,插入之后在判断下一次++size的大小是否会超过当前的阈值,如果超过就扩容。
1、链表:
如果是链表处理,那么就将oldTable[i]分成高低链表,生成后插入到newTable中.
2.红黑树
TREEIFY_THRESHOLD: 树化阈值 8。当单个segment的容量超过阈值时,将链表转化为红黑树。
MIN_TREEIFY_CAPACITY :最小树化容量 64。当桶中的bin被树化时最小的hash表容量,低于该容量时不会树化。
如果这个桶中bin的数量大于了 TREEIFY_THRESHOLD ,但是capacity(16)小于MIN_TREEIFY_CAPACITY 则依然使用链表结构进行存储,此时会对HashMap进行扩容;
如果capacity大于了MIN_TREEIFY_CAPACITY ,才有资格进行树化(当bin的个数大于8时)。
A:(我爱基础,然并卵)一个object的数组,采用链地址法,被分进同一个桶内的元素会形成链表。Jdk8后,链表长度超过一个常量(记得好像是8)是,会变成一棵树。
16.Q: 扩容会发生什么?
A:首先扩容数组会变成原来的两倍。
17.Q: 为什么是两倍?
A: 数组大小采用位运算,一定是2的x次幂。然后分为两部分,原来的低位部分和新增的高位部分。逐个看各个桶的哈希值,在新通仍处于低位的桶可以直接移动,高位则需要再次put。
18.Q: 扩容会出现什么性能问题?
JDK 1.7 HashMap扩容导致死循环的主要原因(环形链)
HashMap扩容导致死循环的主要原因在于扩容后链表中的节点在新的hash桶使用头插法插入。
新的hash桶会倒置原hash桶中的单链表,那么在多个线程同时扩容的情况下就可能导致产生一个存在闭环的单链表,从而导致死循环。
JDK 1.8 HashMap扩容不会造成死循环的原因
JDK 1.7中HashMap扩容发生死循环的主要原因在于扩容后链表倒置以及链表过长。
jdk8是尾插法。
这里虽然JDK 1.8 中HashMap扩容的时候不会造成死循环,但是如果多个线程同时执行put操作,可能会导致同时向一个单链表中插入数据,从而导致数据丢失的。
A:高位的元素还是需要重新put,这个过程中有性能消耗。hashMap有一个影响因子,可以通过调整这个值,来调整resize的频率。(这个题我有点没get到面试官想问什么。 和学长讨论了一下:OOM)
19.Q:知道sql注入么?
A:知道。一般会根据业务场景拼接sql语句。有些该填写参数的地方,填写了sql语句,使得最终生成sql语句执行了预期以外的语义。
sql注入
根据业务场景拼接sql语句。有些该填写参数的地方,填写了sql语句或者特殊字符,使得最终生成sql语句执行了预期以外的语义。
预防sql注入
- 对用户输入的合法性进行判断
- 永远不要使用动态拼装sql,可以使用参数化的sql或者直接使用存储过程进行数据查询存取。
- 不要使用管理员权限的数据库连接
20.Q: 为什么orm框架可以防止sql注入?
不管输入什么参数,打印出的SQL都是这样的。
这是因为MyBatis启用了预编译功能,在SQL执行前,会先将上面的SQL发送给数据库进行编译;
执行时,直接使用编译好的SQL,替换占位符“?”就可以了。因为SQL注入只能对编译过程起作用,所以这样的方式就很好地避免了SQL注入的问题。
A:mybatis中使用#{arg},将被包含在‘‘字符串内,从而保证该部分即使包含sql关键字,也不会作为sql执行。
21.Q: 问点数据库相关的吧。Mysql哪些存储引擎?
A: InnoDb, myisam, memory。
InnoDB
InnoDB是mysql目前的默认存储引擎,聚簇索引,有行级锁。
InnoDB 因为是存的是数据文件,索引也都放在一起,而且又拥有事务,所以它几乎用于增删改操作,当然,如果数据量小的话,也可以存于innodb引擎的,比如10W内;
Myisam
Myisam不支持行级锁,查找较快,然而增改致命, 不支持事务。
MyISAM 因为它的文件是索引文件 和 数据文件存的,而且索引文件存的是地址,所以基本上是用于频繁的查询的;
22.Q: 如何高性能使用mysql
1.优化数据类型
1-2是面试回答
- 尽量使用可以正确存储数据的最小数据类型。原因:占用更少磁盘,内存,cup。
- 使用简单的。整型比字符操作代价低,原因:字符集和校队规则。
- 避免null。原因:可为null的列索引统计更复杂,更多存储空间,如果确实需要才使用。
- 时间类型。int 可以记录大范围的时间,datetime类型(范围1001-9999)适合用来记录数据的原始的创建时间,timestamp(范围1970-2038)类型适合用来记录数据的最后修改时间,只要修改记录,timestamp字段的值都会被自动更新。
- 小数类型。float和double近似小数,decimal精确小数,尽量只在对小数精确计算时使用,数据量大时使用bigint替代。
- 字符型。varchar可变长字符串适合长的字符串(需要额外的1或2个字节记录长度),char定长的,长度不够用空格填充,适合短的字符串及定值的如MD5值,或者经常变更的。
- 存储很大的字符串数据。使用blob(二进制方式)和text(字符方式,有排序规则和字符集),性能低下,尽量避免。
- 存储IPv4使用整型而不是varchar(15),因为它实际就是32位无符号整数,加小数点只是方便阅读。
2.数据库设计注意
- 设计表非常宽,如果只有一小部分用到,转换代价就非常高,尽量避免。
- 太多关联。
3.索引优化
一般情况都是指B-Tree索引.
1.哪些字段需要建索引
- 经常使用的字段 如 users.user_name
- 关联表所用到的字段
2.哪些字段不需要建索引
- 区分度很低的字段 假设有一个 users.delete_status字段, 表示该用户是否被删除, 该字段的值只有两个0, 1. 该字段就不应该建立索引, 因为区分度太低, 索引的效率很差
- 类型为 text 的字段不应该建索引
- 类型为 varchar, char 的字段的索引长度不要超过10
23.Q: innoDB索引底层什么样?
一般情况都是指B-Tree索引,索引的优点:减少服务器需要扫描的数据量;帮助服务器避免排序和临时表;将随机I/O变为顺序I/O。
A:B+树和类哈希索引的集合。B+树同时有父子节点和前后续节点,查找起来更快。
Tree索引
索引的值都是按顺序存储的,之所以加快访问数据的速度,因为存储引擎不再需要全表扫描,而是从索引的根节点开始搜索。
哈希索引
基于哈希表实现,只有精确匹配索引所有列的查询才有效。冲突越多代价越大,只适用于特定场合,如url等。
24.Q: 那节点的值是什么?
InnoDB使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上;
非叶子节点只是为了方便算法寻找叶子节点。
(高性能Mysql。P166, 包含主键值、事务ID、用于事务和MVCC的回归指针以及剩余列)
25.Q:那如何简历多列索引?(最左匹配)
A: 符合左侧优先原则。
26.Q: 那假设有性别和课程号,谁在前谁在后?
课程号在前,因为性别辨识度太低,意义不大。
手写代码:
1.1-n个数,找出按照字典序排序的第K个数。
思路1:最坏直接当成字符串处理,排序,取K 思路2:重写compare方法,快排,找到K, 思路3:(玩脱)找到小于等于n的下一个字典序数。
2.一个超大的文件,每一行都是Timestamp:content,时间有序,找指定时间范围的content。 1、让我写个MR。 2、行号中序