JAVA面试

基础

一、算法

(一)二分查找

在这里插入图片描述
源码:

    public static int binarySearch(int[] array, int num) {
        //1. 首先排序
        int temp;
        for (int i = array.length - 1; i >= 0; i--) {
            for (int j = 0; j < i; j++) {
                if (array[j] > array[j + 1]) {
                    temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                }
            }
        }

        //2. 进行二分查找
        int l = 0, r = array.length - 1, m;
        while (l <= r) {
            m = (l + r) / 2;
            if (array[m] == num) {
                return m;
            }
            if (array[m] > num) {
                r = m - 1;
            } else {
                l = m + 1;
            }
        }
        return -1;
    }

当L和R的值过大时,相加可能超过int能存储的最大范围,导致整数溢出问题
方法一:(l+r)/2>>l+(r-l)/2
方法二:移位运算(l+r)>>>1
补充:java移位运算
在这里插入图片描述

(二)冒泡排序

(1)冒泡排序改进版1.0
    public static void bubbleSort(int[] array) {
        int temp;
        for (int i = 0; i < array.length - 1; i++) {
            //设置flag,只要某一次冒泡没有发生交换,就表明数组已经排好序,直接退出循环
            boolean flag = false;
            for (int j = 0; j < array.length - i - 1; j++) {
                if (array[j] > array[j + 1]) {
                    temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                    flag = true;
                }
            }
            if (!flag)
                break;
            System.out.println("第" + i + "轮冒泡排序:" + Arrays.toString(array));
        }
    }
(2)冒泡排序改进版2.0
    public static void bobbleSort02(int[] array) {
        int n = array.length - 1;
        while (true) {
            //记录最后一次交换的索引位置,初始值0表示假设现有数组全部有序
            int last = 0, temp;
            for (int i = 0; i < n; i++) {
                if (array[i] > array[i + 1]) {
                    temp = array[i];
                    array[i] = array[i + 1];
                    array[i + 1] = temp;
                    //如果发生了交换,就记下交换的索引位置
                    last = i;
                }
            }
            //下一次冒泡的结束索引位置
            n = last;
            if (n == 0)
                break;
        }
    }

(三)选择排序

选择排序: 首先在未排序的数组中找到最小(or最大)元素,然后将其存放到数组的起始位置;接着,再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到未排序数组的起始位置。以此类推,直到所有元素均排序完毕。

    public static void selectionSort(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            int min = i;
            for (int j = i + 1; j < array.length; j++) {
                if (array[min] > array[j]) {
                    min = j;
                }
            }
            if (min != i) {
                //最小元素和第一个元素换位置
                int temp = array[i];
                array[i] = array[min];
                array[min] = temp;
                System.out.println("第" + i + "轮排序:" + Arrays.toString(array));
            }
        }
    }

在这里插入图片描述
稳定性:

  • 在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
  • 简单来说,就是两个元素相同时,冒泡不会交换其顺序,而选择会交换两者顺序。

(三)插入排序

二、集合

(一)ArrayList

  • new一个无参arrayList时,初始容量是0;当有第一个内容添加时,初始容量变为10。
  • ArrayList底层是数组,扩容后新数组会替换掉旧数组,旧数组没引用就会被垃圾回收掉。
  • ArrayList的扩容不是直接*1.5,而是通过移位实现,如10+10>>1。
  • 对于.addAll(Collection<E> c)方法的扩容机制:
    A. 无参构造时:下一次默认的扩容容量大小和当前添加元素个数之间进行比较,取较大值来扩容。
    如添加11个元素,下一次默认扩容大小10,而元素长度11,这时就会取元素长度作为集合扩容后的大小。
    在这里插入图片描述
(1)FailFast&FailSafe

这两个是对于集合迭代器(iterator)而言。
failFast:一旦发现遍历的同时有其他人来修改,则立刻抛异常。
failSafe:发现遍历的同时有其他人来修改,应当能有应对策略,如牺牲一致性来让整个遍历运行完成。

ArrayList属于failFast
CopyOnWriteArrayList属于failSafe

(2)ArrayList对比LinkedList

在这里插入图片描述
局部性原理:CPU缓存读取一个数据时,会附带读取该数据相邻的数据(内存地址必须相邻,所以ArrayList能利用缓存,而LinkedList不能)。

(二)HashMap(重点)

HashMap默认容量16,当元素超过3/4时(不包含)会扩容,每次扩容2倍。

不同的哈希码算出相同的下标Index,就会导致哈希碰撞,一旦发生哈希碰撞,HashMap的查找效率就会从O(1)退化成O(n)或者O(logn)

链表树化为红黑树条件(同时满足):

  • 链表长度>8(如果长度没超过64,首先会尝试扩容来减少链表长度)
  • 数组长度>=64
    红黑树特点 :父节点左边都是比父节点小的数,右边都是比父节点大的数。

在这里插入图片描述
↑删除树中某个key时,才进行是否退化检查(删除前检查,而不是删除后做检查)。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Q:介绍下HashMap的数据结构
A:

  • HashMap 的底层实现基于一个数组,这个数组的每个元素是一个桶(bucket)。每个桶中存储的是链表或红黑树组成的节点(没有hash冲突时桶直接存Node对象)。如果发生哈希冲突(多个键映射到同一个桶),这些节点会以链表或红黑树的形式存储在同一个桶中。
// Node结构
static class Node<K, V> {
    final int hash;	// 键的哈希值
    final K key;	// 键
    volatile V val;	// 值
    volatile Node<K, V> next;	// 指向下一节点的指针
}
// 红黑树结构
static final class TreeNode<K, V> extends Node<K, V> {
    TreeNode<K, V> parent;  // 父节点
    TreeNode<K, V> left;    // 左子节点
    TreeNode<K, V> right;   // 右子节点
    TreeNode<K, V> prev;    // 前驱节点
    boolean red;            // 节点颜色(红/黑)
}
+-------------------+
|   HashMap         |
+-------------------+
|                   |
|   +---------------+       +---------------+       +---------------+
|   |   Bucket 0    |  -->  |   Node (k1,v1)|  -->  |   Node (k3,v3)|  ...
|   +---------------+       +---------------+       +---------------+
|                   |
|   +---------------+       +---------------+
|   |   Bucket 1    |  -->  |   Node (k2,v2)|  ...
|   +---------------+       +---------------+
|                   |
|   +---------------+
|   |   Bucket 2    |  -->  null
|   +---------------+
|                   |
|       ...         |
|                   |
|   +---------------+
|   |   Bucket n-1  |  -->  +---------------+       +---------------+
|   +---------------+       |   Node (k4,v4)|  -->  |   Node (k5,v5)|  ...
|                           +---------------+       +---------------+
+---------------------------+
  • HashMap使用键的hash值来确定它应该存储在哪个桶中,计算过程:
    • 调用键的hashCode()方法,得到一个整数
    • 使用该整数与数组长度进行取模运算,得到桶索引

Q:HashMap是线程安全的吗,为什么?
A:
因为HashMap里的方法没有使用任何同步机制(比如synchronized或volatile)。
在多线程环境下,多个线程同时操作HashMap可能导致数据不一致或扩容死链的问题。

  • 比如在put()时,多个线程同时插入数据,可能导致数据覆盖或丢失。
  • 在扩容时,如果多个线程同时触发resize()方法,可能导致链表形成环,从而引发扩容死链。

Q:介绍下安全的HashMap
A:

  • 通过Collections.synchronized()将HashMap包装成线程安全的Map。
  • 使用HashTable,但性能较差(因为所有方法都用synchronized修饰)。
  • 使用ConcurrentHashMap,专为高并发场景设计的线程安全Map,性能优于HashTable
    通过分段锁(JDK 7)或CAS+synchronized(JDK 8)实现线程安全。

Q:介绍下Collections.synchronized()是如何包装HashMap线程安全的。
A:

注:仅适合低并发环境

  • 互斥锁(mutex)
    • 默认使用当前对象本身作为互斥锁(mutex),但可以通过构造方法传入自定义锁对象。
  • 方法级别的同步
    • 每个方法都被synchronized修饰,确保同一时间只有一个线程可以访问Map。
    • 存在的问题:方法级别的锁,在高并发场景下性能下降。
    • 复合操作仍然是非线程安全的,如if (!synchronizedMap.containsKey("key")) synchronizedMap.put("key", "value");
    • 使用iterator遍历Map时仍然是非线程安全的,需要给遍历加锁。
  • 使用装饰器模式(Decorator Pattern)
    • Synchronized是对原始Map的包装,所有操作都委托给原始Map执行。

Collections.synchronizedMap的核心实现

public static <K, V> Map<K, V> synchronizedMap(Map<K, V> m) {
    return new SynchronizedMap<>(m);
}

// 内部静态类 SynchronizedMap
private static class SynchronizedMap<K, V> implements Map<K, V>, Serializable {
    private final Map<K, V> m; // 被包装的 Map
    final Object mutex; // 互斥锁对象

    SynchronizedMap(Map<K, V> m) {
        this.m = Objects.requireNonNull(m);
        this.mutex = this; // 使用当前对象作为锁
    }

    // 所有方法都使用 synchronized 修饰
    public int size() {
        synchronized (mutex) { return m.size(); }
    }
    public boolean isEmpty() {
        synchronized (mutex) { return m.isEmpty(); }
    }
    public boolean containsKey(Object key) {
        synchronized (mutex) { return m.containsKey(key); }
    }
    public boolean containsValue(Object value) {
        synchronized (mutex) { return m.containsValue(value); }
    }
    public V get(Object key) {
        synchronized (mutex) { return m.get(key); }
    }
    public V put(K key, V value) {
        synchronized (mutex) { return m.put(key, value); }
    }
    public V remove(Object key) {
        synchronized (mutex) { return m.remove(key); }
    }
    public void putAll(Map<? extends K, ? extends V> map) {
        synchronized (mutex) { m.putAll(map); }
    }
    public void clear() {
        synchronized (mutex) { m.clear(); }
    }
    // 其他方法省略...
}

Q:介绍下ConcurrentHashMap是如何利用CAS+synchronized保证线程安全的
A:

(三)单例模式

单例模式常见五种实现方式?

  1. 饿汉式
public class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}
  1. 枚举饿汉式
public enum Singleton {
    INSTANCE;

    // 自定义成员变量
    private String field01;

    public String getField01() {
        return field01;
    }

    public void setField01(String field01) {
        this.field01 = field01;
    }

    // 其他需要的方法
    public void doSomething() {
        System.out.println(field01);
    }
}
  1. 懒汉式(问题:当有多线程环境时,可能会被创建多个对象,导致单例模式实际上是失效的,所以在方法上加锁)
public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

在这里插入图片描述

  1. DCL(Double Check Lock双检锁)懒汉式(首次使用对象时加入同步锁,二次使用后不加锁)
    这里有两个if语句,也是DCL名称的来源(如果不加第二个if,实际上还是会创建多个对象,因为等待解锁的线程在synchronized上等待完成后,进入不再次判断的话就会创建多个对象)
public class Singleton {
    private static volatile Singleton singleton;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

在这里插入图片描述
6. 内部懒汉式(推荐)

public class Singleton {
    private Singleton() {
    }

    private static final class SingletonInner {
        private static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonInner.instance;
    }
}

在这里插入图片描述

jdk中有哪些地方体现了单例模式?

  • Runtime(饿汉式)
  • System的Console对象(DCL懒汉式)

单例模式的五种实现方式及优缺点

并发

临界区

  1. 临界区是指一个程序片段或代码块,其中包含了对临界资源的访问或修改操作。

临界资源

  1. 临界资源是指一次只能被一个进程访问的资源。
  2. 临界资源和临界区的区别:
  • 临界资源:只允许一个进程进行访问的资源,比如打印机、消息缓冲队列。
  • 临界区:使用临界资源的代码区。
  1. 共享资源包含临界资源,临界资源是共享资源的子集。
    在这里插入图片描述
  • 如何判断共享资源和临界资源(前提首先这个资源是共享资源):在一段时间内能否允许被多个进程访问(并发使用),不能则为临界资源,能则为共享资源。
  • 共享资源且不可并发:临界资源。
    共享资源且可并发:共享资源。

共享资源

  1. 共享资源是指多个进程或线程可以同时访问的资源,它通常存储着数据或状态信息。
  2. 共享资源可以是临界资源,也可以是非临界资源。临界资源需要特别的同步机制来保护,而非临界资源可能不需要同步。

参考链接:
临界区、临界资源、共享资源、临界调度原则
临界资源和共享资源
OS 共享变量、临界区、临界资源之间的联系
并发编程三要素:共享数据、互斥访问和同步机制
[多线程问题需要满足哪些条件]https://www.cnblogs.com/bangiao/p/13195689.html#%E5%A4%9A%E7%BA%BF%E7%A8%8B%E9%97%AE%E9%A2%98%E9%9C%80%E8%A6%81%E6%BB%A1%E8%B6%B3%E5%93%AA%E4%BA%9B%E6%9D%A1%E4%BB%B6?#()

互斥访问

同步机制

(一)线程状态

(1)线程有哪些状态?

Java线程有哪几个状态?
Java的线程分成六种状态。视频讲解
操作系统的线程分成五种状态(笔记上有)
在这里插入图片描述
在这里插入图片描述

Java线程状态之间的转换?
如上图

六种状态和五种状态
六种是Java层面的线程状态;五种是操作系统层面的线程状态,如上图

(2)线程池的核心参数?(重点)

执行流程:提交任务后,如果有空闲核心线程就直接执行,不进任务队列;如果核心线程全部在忙,就进入任务队列排队等待(先进先出)。
救急线程:当核心线程全部正在执行任务,同时任务队列也已经满了,这时又添加了一个任务就会启动救急线程,来执行这个溢出的任务。
在这里插入图片描述
handler的四种拒绝策略:

  • AbortPolicy:直接抛出异常,告知新任务不能被执行。
  • CallerRunsPolicy:让调用者自己去执行这个新任务(谁提交的任务谁去执行)。
  • DiscardPolicy:默默丢弃新任务,不报异常和通知。
  • DiscardOldestPolicy:将任务队列的第一个任务丢弃掉(也就是最先加入任务队列的),然后将新任务加入到队列尾中。

代码示例:
在这里插入图片描述

(3)sleep() VS wait()

在这里插入图片描述

(4)lock VS synchronized

在这里插入图片描述

(5)悲观锁和乐观锁

在这里插入图片描述

(6)ConcurrentHashMap

1.8之前
1.8之前是由多个Segment组成,每个Segment就是一把分段锁,通过局部的上锁实现了整体的线程安全问题,理论上有多少个Segment就支持多少线程并发。
默认Segment容量是16,可以在初始化时指定Segment数量,但是一旦初始化完成后,Segment不可以再扩容;每个Segment下的数据结构和HashMap类似。
在这里插入图片描述
1.8之后
抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性,也引入了红黑树。
在这里插入图片描述
HashTable VS ConcurrentHashMap
对比
在这里插入图片描述

(7)ThreadLocal

线程并发:在多线程并发的场景下
传递数据:我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
线程隔离:每个线程的变量都是独立的,不会互相影响

ThreadLocal的数据结构类似于HashMap,可以保存键值对,但是一个ThreadLocal只能保存一个键值对;
在ThreadLocalMap中,也是初始化一个大小为16的数组,但是数组下没有链表的情况,每一个数组只保存一个键值对,ThreadLocal把自己当作key,存储的值为value,放入ThreadLocalMap;
如果在存入时发生hash碰撞的情况,那么ThreadLocal会去找下一个相邻的数组,看是否为空,如果为空就放入,否则继续找下一个。
在这里插入图片描述

  • 强引用:就算内存不足,JVM也不会回收该对象;
  • 软引用:只有在内存不足时,JVM才会回收该对象;
  • 弱引用:一旦被JVM发现弱引用对象,不管内存足够与否,JVM都会回收该对象;
    在这里插入图片描述

虚拟机

一、JVM内存结构

JVM的内存结构有哪几部分?
在这里插入图片描述

  • 方法区里,存放类加载的信息,比如类名,类继承关系,类成员变量等信息。
  • 堆里,存放对象信息。
  • 程序计数器里,用于指明当前程序运行到哪一行了。
  • 虚拟机栈,存放局部变量或者对象实例的引用地址。
  • 本地方法栈,存放引用的系统库。

哪些部分会出现内存溢出?
在这里插入图片描述

二、垃圾回收

JVM的垃圾回收算法?
标记:不能被垃圾回收的对象,将打上标记

框架

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值