---
url: /notes/面试/面试题/index.md
---
## 原文链接：[Java基础面试16问](https://mp.weixin.qq.com/s/-xFSHf7Gz3FUcafTJUIGWQ)

## 说说进程和线程的区别？

进程是**程序的一次执行**，是**系统资源分配与调动的基本单位**，使程序能够**并发执行**

因为进程的创建、销毁、切换产生大量的时间和空间开销，进程的数量不能太多，线程是比**进程更小的能独立运行的基本单位，是进程的一个实体**

线程基本不占用系统资源，只运行必不可少的资源（程序计数器，寄存器，栈）

进程占用堆、栈

## 知道synchronized原理吗？

java提供的**原子性内置锁🔒**，内置的使用者看不到的锁称为**监视器锁**，使用synchronized之后，会在编译之后在同步的代码块前后加上`monitorenter`和

`monitorexit`字节码指令,它依赖操作系统底层互斥锁实现。它的作用主要是实现原子性操作和解决共享变量的内存可见性问题。

执行`monitorenter`指令时会**尝试获取对象锁**，如果**对象没有被锁定或者已经获得了锁**，锁的计数器+1.此时其他竞争锁的线程会**进入等待队列**中。

执行`monitorexit`指令时会把计数器-1，当计数器为0时，锁释放，处于等待队列中的线程再继续竞争锁。

synchronized是排它锁，当一个线程获得锁之后，其他线程必须等待该线程释放锁后才能获得锁。

## 对于加锁，那再说下ReentrantLock原理？他和synchronized有什么区别？

ReentrantLock需要**显式的获取锁和释放锁**

1. **等待可中断**，当持有锁的线程长时间不释放锁的时候，等待中的线程可以选择放弃等待，转而处理其他的任务。

2. **公平锁**：`synchronized`和`ReentrantLock`默认都是非公平锁，但是ReentrantLock可以通过构造函数传参改变。只不过使用公平锁的话会导致性能急剧下降。

   ```java
   public ReentrantLock(boolean fair) {
           sync = fair ? new FairSync() : new NonfairSync();
       }
   ```

3. **绑定多个条件**：ReentrantLock可以同时绑定多个Condition条件对象。

[java并发控制：ReentrantLock Condition使用详解-阿里云开发者社区 (aliyun.com)](https://developer.aliyun.com/article/384548)

```java
package com.Test;

import org.junit.Test;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Description TODO
 * @Date 2021/10/28 22:43
 * @Created by 折腾的小飞
 */
public class ReentrantLockDemo {
    /**
     * description: 我们要打印1到9这9个数字，由A线程先打印1，2，3，然后由B线程打印4,5,6，
     * 然后再由A线程打印7，8，9. 这道题有很多种解法，
     * 现在我们使用Condition来做这道题（使用Object的wait，notify方法的解法在这里）。
     *
     * @since: 1.0.0
     * @author: 涂鏊飞tu_aofei@163.com
     * @date: 2021/10/28 22:49
     * @return: void
     */
    static class NumberWrapper {
        public int value = 1;
    }

    /*
    初始化可重入锁
     */
    final Lock lock = new ReentrantLock();

    @Test
    public void Demo() {
        //第一个条件当屏幕上输出到3
        final Condition reachThreeCondition = lock.newCondition();
        //第二个条件当屏幕上输出到6
        final Condition reachSixCondition = lock.newCondition();

        //NumberWrapper只是为了封装一个数字，一边可以将数字对象共享，并可以设置为final
        //注意这里不要用Integer, Integer 是不可变对象
        final NumberWrapper num = new NumberWrapper();

        //初始化A线程
        Thread threadA= new Thread(new Runnable() {
            public void run() {
                //需要先获得锁
                lock.lock();
                try {
                    System.out.println("threadA start write");
                    //A线程先输出前3个数
                    while (num.value <= 3) {
                        System.out.println(num.value);
                        num.value++;
                    }
                    //输出到3时要signal，告诉B线程可以开始了
                    reachThreeCondition.signal();
                }finally {
                    lock.unlock();
                }
                lock.lock();
                try{
                    //等待输出6的条件
                    reachSixCondition.await();
                    System.out.println("threadA start write");
                    //输出剩余数字
                    while (num.value <= 9) {
                        System.out.println(num.value);
                        num.value++;
                    }
                }catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {

                }
            }
        });

        Thread threadB = new Thread(new Runnable() {
            public void run() {
                try {
                    lock.lock();

                    while (num.value <= 3) {
                        //等待3输出完毕的信号
                        reachThreeCondition.await();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
                try {
                    lock.lock();
                    //已经收到信号，开始输出4，5，6
                    System.out.println("threadB start write");
                    while (num.value <= 6) {
                        System.out.println(num.value);
                        num.value++;
                    }
                    //4，5，6输出完毕，告诉A线程6输出完了
                    reachSixCondition.signal();
                } finally {
                    lock.unlock();
                }
            }

        });
        threadA.start();
        threadB.start();
    }
}
```

![image-20211124223557657](https://note-1259190304.cos.ap-chengdu.myqcloud.com/note/image-20211124223557657.png)

## 好，说说HashMap原理吧？

HashMap主要由**数组和链表**组成，存储键值对，不是线程安全的。

插入数据用`put`，`get`查询数据以及扩容的方式。

jdk1.7和1.8的主要区别在于**头插**和**尾插**方式的修改,头插容易导致HashMap链表死循环，并且1.8后加入红黑树性能有提升。

https://blog.csdn.net/littlehaes/article/details/105241194

### put插入数据流程

向map插入数据时获得**key的hash与数组长度-1进行与运算**【(n-1)\&hash】。找到数组中的位置后，如果数组中**没有元素，则直接存入**，有元素则**判断key是否相同**，**相同则覆盖**，**不相同则插入到链表的尾部**，**如果链表长度超过8并且数据长度超过64，则会转换成红黑树**（CRUD操作的时间复杂度都为O(log n)），最后判断数组长度是否超过**默认长度（16）\* 负载因子（0.75）**，超过则进行扩容。

### get查询数据

首先计算hash值，去数组查询，是红黑树就去红黑树查，链表就遍历链表查询

### resize扩容过程

对key重新计算hash，把数据拷贝到新的数组

## 那多线程环境怎么使用Map呢？ConcurrentHashmap了解过吗？

可使用`Collections.synchronizedMap`同步加锁和`hashTable`，`ConcurrentHashmap`更适合高并发场景。

ConcurrentHashmap在JDK1.7和1.8的版本改动比较大，1.7使用`Segment`+`HashEntry`**分段锁**的方式实现，1.8则抛弃了Segment，改为使用`CAS`+`synchronized`+`Node`实现，同样也**加入了红黑树**，避免链表过长导致性能的问题。

***

### 1.7分段锁

1.7版本的ConcurrentHashMap采用**分段锁机制**，里面包含一个`Segment数组`，Segment**继承**与`ReentrantLock`，Segment则包含`HashEntry的数组`，HashEntry本事是一个**链表**的结构，具有保存**key、value的能力能指向下一个节点的指针**。

实际上每个Segment都是一个HashMap，默认的长度是16，支持16个线程的并发写，Segment之间相互不影响。

#### put流程

定位到具体的Segment，然后通过ReentrantLock去操作

1. 计算hash，定位到segment，segment如果是空就先初始化
2. 使用ReentrantLock加锁，如果获取锁失败则尝试自旋，自旋超过次数就阻塞获取，保证一定获取锁成功
3. 遍历HashEntry，就是和HashMap一样，数组中key和hash一样就直接替换，不存在就再插入链表，链表同样

#### get流程

key通过hash定位到segment，再遍历链表定位到具体的元素上，需要注意的是value是`volatile`的，所以get是不需要加锁的。

### 1.8 CAS+synchronized

#### put流程

1. 首先计算hash，遍历node数组，如果node是空的话，就通过CAS+自旋的方式初始化
2. 如果当前数组位置是空则直接通过CAS自旋写入数据
3. 如果hash==MOVED，说明需要扩容，执行扩容
4. 如果都不满足，就使用synchronized写入数据，写入数据同样判断链表、红黑树，链表写入和HashMap的方式一样，key hash一样就覆盖，反之就尾插法，链表长度超过8就转换成红黑树

#### get查询

通过key计算hash，如果key hash相同就返回，如果是红黑树按照红黑树获取，都不是就遍历链表获取。

### 1.7扩容

单独扩容segment数组，不影响其它的segment

infexFor计算元素的下标

### 1.8扩容

整体扩容，CAS多线程扩容，渐进式扩容

（n-1）& hashcode计算元素的下标

### 1.7 size()

三次获取sgment数组大小，如果3次结果不同，加锁计算数组大小

### 1.8 size()

维护一个数组保存节点数量

## volatile原理知道吗？

相比synchronized的加锁方式来解决共享变量的内存可见性问题，volatile就是更轻量的选择，他没有上下文切换的额外开销成本。使用volatile声明的变量，可以确保值被更新的时候对其他线程立刻可见。volatile使用内存屏障来保证不会发生指令重排，解决了内存可见性的问题。

## 线程池原理知道吗？

1. 最大线程数maximumPoolSize

2. 核心线程数corePoolSize

3. 活跃时间keepAliveTime

4. 阻塞队列workQueue

5. 拒绝策略RejectedExecutionHandler

   当提交一个新任务到线程池时，具体的执行流程如下：

   1. 当我们提交任务，线程池会根据corePoolSize大小创建若干任务数量线程执行任务
   2. 当任务的数量超过corePoolSize数量，后续的任务将会进入阻塞队列阻塞排队
   3. 当阻塞队列也满了之后，那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务，如果任务处理完成，maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁
   4. 如果达到maximumPoolSize，阻塞队列还是满的状态，那么将根据不同的拒绝策略对应处理

### 拒绝策略有哪些？

1. AbortPolicy：直接丢弃任务，抛出异常，这是默认策略
2. CallerRunsPolicy：只用调用者所在的线程来处理任务
3. DiscardOldestPolicy：丢弃等待队列中最旧的任务，并执行当前任务
4. DiscardPolicy：直接丢弃任务，也不抛出异常
