面试

数据库

三大范式

  1. 第一范式1NF:数据库表中所有字段不可再分
  2. 第一范式2NF:在第一范式基础上,表中所有字段都完全依赖于主键,不存在部分依赖
  3. 第一范式2NF:在第二范式基础上,表中所有字段都直接依赖于主键,不存在间接依赖(传递依赖)

索引类型

  • 按底层数据结构划分:
    • B-Tree索引
    • 哈希索引
    • R-Tree索引
    • 全文索引
  • 按数据存储方式划分
    • 聚簇索引
    • 非聚簇索引
  • 按应用类型划分
    • 主键索引
    • 普通索引
    • 唯一索引:用于保证列值唯一
    • 联合索引:多个列组成一个索引
    • 覆盖索引:组成索引的字段覆盖了需要查询的字段
    • 全文索引

事务中出现的问题

  • 脏读:事务A先修改再回滚,事务B在事务A回滚之前读,读到的就是脏数据
  • 丢失修改:事务A和事务B同时读取并修改一个数据(例如A=A-1),其中一个事务的修改会被覆盖,从而丢失
  • 不可重复读:事务A多次读一个数据,事务B在中间修改删除数据导致事务A前后读取的数据不一致
  • 幻读:事务A多次读一个数据,事务B在中间插入数据导致事务A读到的数据变多

隔离级别

  • 读取未提交:可能导致脏读、幻读、不可重复读
  • 读取已提交:可能导致幻读、不可重复读
  • 可重复读:可能导致幻读
  • 串行化:

面试

Java并发

创建线程的方式

  1. 继承Thread
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 使用线程池

sleep()和wait()的区别

  • sleep()是Thread类的静态本地方法,而wait()是Object类的本地方法
  • sleep()没有释放锁,wait()会释放锁
  • sleep()重点在于暂停当前线程,wait()重点在于线程间的交互
  • sleep()必须指定一个时间,时间到了后线程恢复运行,wait()可以选择指定时间,若不指定时间则需要其他线程使用notify()或notifyAll()唤醒

volatile关键字

在Java中将变量声明为volatile,表示此变量是多线程共享的,每次读取时都到主存中读取。

乐观锁和悲观锁

悲观锁总是假设最坏的情况,每次获取共享资源时先上锁,如果其他线程需要资源就会阻塞。例如Synchronized关键字ReentrantLock独占锁

乐观锁总是假设最好的情况,线程获取资源时不上锁,而是在提交修改的时候去验证对应的资源是否被其他线程修改了,例如CAS算法

悲观锁适合于写操作较多,乐观锁适合于读操作较多。

线程池

创建方法
  1. 通过ThreadPoolExecutor构造函数传入参数创建
  2. 通过Executor框架的工具类Executors创建
线程池参数
1
2
3
4
5
6
7
8
public ThreadPoolExecutor(
int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务)
拒绝策略
  • ThreadPoolExecutor.AbortPolicy(默认):直接拒绝任务并抛出异常
  • ThreadPoolExecutor.CallerRunsPolicy:调用自己的线程来执行任务
  • ThreadPoolExecutor.CallerRunsPolicy:直接拒绝任务,不报异常
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃掉最早的未处理任务,把新的任务加入队列

面试

操作系统

进程和线程

  • 进程是一个程序的实例
  • 线程是轻量级进程,一个进程可以包含多个线程,同一个进程的所有线程可以共享进程的资源(内存空间、文件句柄、网络连接等),每个线程也拥有自己的资源(程序计数器、虚拟机栈、本地方法栈)
  • 线程执行开销小,不利于资源的管理和保护,进程相反

线程间同步方式

  1. 互斥锁
  2. 读写锁
  3. 信号量
  4. 屏障
  5. 事件:wait/notify

进程的状态

  • 创建状态:进程刚被创建,还未就绪
  • 就绪状态:进程处于可以运行并准备运行的状态
  • 运行状态:进程在就绪状态时被分配到cpu就会在cpu上运行,即为运行状态
  • 阻塞状态:进程因等待别的资源或IO操作而暂停运行
  • 结束状态:进程从系统中消失

进程间通信方式

  1. 管道/匿名管道:父子进程间或兄弟进程间的通信
  2. 有名管道:匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循 先进先出(First In First Out) 。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
  3. 信号:信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生
  4. 消息队列:消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。
  5. 信号量:信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
  6. 共享内存
  7. 套接字

僵尸进程和孤儿进程

  • 僵尸进程:子进程已经终止,但是其父进程仍在运行,且父进程没有调用 wait()或 waitpid()等系统调用来获取子进程的状态信息,释放子进程占用的资源,导致子进程的 PCB 依然存在于系统中,但无法被进一步使用。这种情况下,子进程被称为“僵尸进程”。避免僵尸进程的产生,父进程需要及时调用 wait()或 waitpid()系统调用来回收子进程。
  • 孤儿进程:一个进程的父进程已经终止或者不存在,但是该进程仍在运行。这种情况下,该进程就是孤儿进程。孤儿进程通常是由于父进程意外终止或未及时调用 wait()或 waitpid()等系统调用来回收子进程导致的。为了避免孤儿进程占用系统资源,操作系统会将孤儿进程的父进程设置为 init 进程(进程号为 1),由 init 进程来回收孤儿进程的资源。

死锁

  • 死锁:多个进程或线程同时阻塞,都在等待资源释放。由于进程/线程被无限阻塞,因此程序不可能正常终止。

死锁的必要条件

  1. 互斥:资源不能被共享,同一时间只能有一个进程或线程使用。
  2. 非抢占:资源不能被抢占,当前正在使用的进程或线程用完后才会被释放。
  3. 占有并等待
  4. 循环等待

如何解决死锁

从四个条件入手:

  1. 解决互斥:使得资源可以同时被多个线程或进程使用,缺点是很多资源是必须独享的
  2. 解决非抢占:采用剥夺式调度算法,缺点是会导致资源利用率下降。

这两种都不太适合

  1. 解决占有并等待循环等待
    • 静态分配:让进程或线程执行前先获取到所有需要的资源,缺点是会降低资源利用率
    • 层次分配:
    • 使用算法(例如银行家算法)判断线程是否安全,若能够进入到安全的状态,则就 真的分配资源给该进程