Java面试笔记

1. ThreadPoolExecutor浅析

参考Executor框架之ThreadPoolExcutor&ScheduledThreadPoolExecutor浅析
ThreadPoolExecutor构造函数

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
  • corePoolSize
    线程池中核心线程数,默认情况下,核心线程会一直在线程中存活,即使没有任务执行,也会处于IDLE状态。但如果ThreadPoolExecutor将allowCoreThreadTimeOut属性置为true,则核心线程会在等待任务的时执行超时策略,时间间隔为keepAliveTime,超过时间后,核心线程将终止。
  • maximumPoolSize
    线程池能容纳的最大线程数,当活动线程数达到这个阈值后,后续提交的任务将阻塞,即调用RejectedExecutionHandler执行阻塞策略。
  • keepAliveTime和unit
    非核心线程闲置的最大时长,超过该阈值,线程会被回收。另外,当ThreadPoolExecutor将allowCoreThreadTimeOut属性置为true时,核心线程也将执行超时策略。
  • workQueue
    线程池中的等待队列,当提交的任务超过阈值corePoolSize时,会先将任务加入workQueue中等待。
  • threadFactory
    创建线程的工厂方法
  • RejectedExecutionHandler
    拒绝策略,当ThreadPoolExecutor关闭或者达到饱和的时候,会执行该策略,默认情况下是抛出RejectExecutionException异常。

任务提交流程
ThreadPoolExecutor提交任务,有两个方法,submit()和execute(),其中submit方法是带返回结果的,只是将入参封装成RunnableFuture,再交给execute,执行execute的流程如下图
在这里插入图片描述

  1. 如果线程池里线程的数目还未达到阈值corePoolSize,则将提交的Runnable封装成Work,然后直接执行;
  2. 如果线程池里线程的数目已经达到或者超过阈值corePoolSize,则将任务插入到workQueue里排队等待,核心线程会从workQueue里获取任务;
  3. 如果workQueue队列已满,则查看线程池里的线程数是否达到阈值maximumPoolSize,如果未达到,则创建非核心线程处理任务;
  4. 如果已达到阈值maximumPoolSize,则执行拒绝策略,调用RejectExecutionHandler的rejectExecution方法来通知调用者

常见线程池

  • FixedThreadPool
    构造函数:
   public static ExecutorService newFixedThreadPool(int nThreads) {
       return new ThreadPoolExecutor(nThreads, nThreads,
                                     0L, TimeUnit.MILLISECONDS,
                                     new LinkedBlockingQueue<Runnable>());
   }

从构造过程中看,FixedThreadPool的corePoolSize和maximumPoolSize的值均是nThreads,并且keepAliveTime的值为0,则说明该线程池只有核心线程,固定数目为nThreads。还有一点是,线程池FixedThreadPool以无边界的LinkedBlockingQueue作为等待队列,所以会一直向等待队列里添加任务,而不会执行拒绝策略。
在这里插入图片描述

  • CachedThreadPool
    构造函数:
   public static ExecutorService newCachedThreadPool() {
       return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                     60L, TimeUnit.SECONDS,
                                     new SynchronousQueue<Runnable>());
   }

从构造过程中看,CachedThreadPool的corePoolSize设置为0,maximumPoolSize设置为Integer.MAX_VALUE,超时时长设置为60s,而等待队列使用的是无容量限制的SynchronousQueue,也就是说,当提交一个任务时,会先添加到等待队列中进行等待,然后线程池会从等待队列中取任务执行。这也意味着,如果任务的提交速度一直快于任务完成速度的话,则线程池会一直创建线程,而消耗过多的CPU和资源。
在这里插入图片描述

  • SingleThreadExecutor
    构造函数:
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

SingleThreadExecutor和FixedThreadPool的构造类似,只不过唯一不同的是,SingleThreadExecutor的corePoolSize和maximumPoolSize都固定设置为1,也就是说,SingleThreadExecutor每次只能执行单个任务,任务完成后就去等待队列中取任务。

各自的使用场景

  1. FixedThreadPool:适用于为了满足资源管理需求,而需要限制当前线程的数量的应用场景,它适用于负载比较重的服务器。
  2. SingleThreadExecutor:适用于需要保证执行顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的场景。
  3. CachedThreadPool:大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者负载较轻的服务器

2. synchronized和reentrantlock

参考synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比
相同点
两者均是可重入锁
不同点

  1. synchronized是Java关键字,实现原理依赖于JVM的实现,同步代码块是采用monitor对象锁实现,而同步方法则使用ACC_SYNCHRONIZED标识。而Reentrantlock是Java的工具类,通过CAS和AQS实现。
  2. synchronized执行完成或者遇到异常会自动释放锁,而Reentrantlock必须主动调用unLock方法才可以释放锁,所以一般在finally里执行unLock
  3. synchronized不能被中断,而Reentrantlock可以被中断lockInterruptibly
  4. synchronized只能是非公平锁,而Reentrantlock可选择支持公平锁和非公平锁
  5. synchronized与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,而Reentrantlock更灵活,可借助Condition接口和newCondition方法
  6. synchronized可以修饰实例方法、静态方法和代码块,而Reentrantlock则需要显示调用lock和unlock方法
  7. Reentrantlock提供了可尝试获取锁trylock(),可设置超时时间

3. synchronized和volatile的区别

参考
面试官最爱的volatile关键字
Synchronized解析——如果你愿意一层一层剥开我的心

  1. volatile仅能作用在变量,而synchronized可作用在方法和代码块
  2. volatile不能保证原子性,而由于synchronized具有排他机制,被修饰的同步代码块无法被中途打断,所以能保证代码的原子性
  3. 两者均可以保证可见性,但是实现机制不同,synchronized采用JVM指令monitor enter和monitor exit使得代码串行化,在monitor exit时,将共享资源刷新到主内存;而volatile则通过内存屏障的方式。
  4. volatile可以禁止指令重排序,所以能保证有序性,而synchronized可以保证其修改的同步方法具有有序性,但是同步代码块内部代码的执行顺序无法保证
  5. volatile不会导致线程的阻塞,而synchronized则会导致阻塞

4. sleep()/yield()和wait()三者的区别

  • sleep
    Thread类的方法,让当前正在执行的线程休眠,线程会转为阻塞状态,并且不会考虑其他线程的优先级,所以会给低优先级线程机会,但是sleep方法不会释放锁。可被interrupt打断,会抛出InterruptedException
  • yield
    Thread类的方法,暂停当前正在执行的线程,线程会转为就绪状态,只会给同优先级或者更高优先级的线程机会。yield也不会放弃锁。yield方法没有声明任何异常。
  • wait
    Object类的方法,该对象调用wait方法后,会导致所在线程放弃对象锁,从而重新等待获取锁,只有针对此对象发出notify方法或者notifyAll方法,才可以竞争锁。另外,wait需要配合synchronized使用,wait时会释放点拿到的synchronized锁,和sleep一样也可被interrupt打断。

5. CAS以及AQS原理

参考
深入理解AbstractQueuedSynchronizer(AQS)
CAS、原子操作类的应用与浅析及Java8对其的优化
AQS 原理以及AQS 同步组件总结
一行一行源码分析清楚 AbstractQueuedSynchronizer

6. Threadlocal使用场景及问题

参考
Java面试必问,ThreadLocal终极篇
ThreadLocal内存泄漏分析与解决方案

ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

  • 内部数据结构
    ThreadLocal是一种数据结构,可以保存"key : value"键值对,其中以ThreadLocal本身作为key键,变量作为value。每个线程中都有一个ThreadLocalMap数据结构,当执行set方法时,其值是保存在当前线程的threadLocals变量中,当执行set方法中,是从当前线程的threadLocals变量获取。
  • Hash冲突的解决
    ThreadLocalMap是通过向后环形查找的方式来解决Hash冲突,所以如果冲突严重的话,效率很低
  • 内存泄漏
    实线代表强引用,虚线代表的是弱引用,如果threadLocal外部强引用被置为null(threadLocalInstance=null)的话,threadLocal实例就没有一条引用链路可达,很显然在gc(垃圾回收)的时候势必会被回收,因此entry就存在key为null的情况,无法通过一个Key为null去访问到该entry的value。同时,就存在了这样一条引用链:threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory,导致在垃圾回收的时候进行可达性分析的时候,value可达从而不会被回收掉,但是该value永远不能被访问到,这样就存在了内存泄漏。所以每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
    ThreadLocal结构图

7. 深拷贝和浅拷贝的区别

参考
一看就懂的,java深拷贝浅拷贝

  • 值类型字段
    深拷贝和浅拷贝均是复制一份
  • 引用类型字段
    浅拷贝只是拷贝引用地址,原始对象及其副本引用同一个对象
    深拷贝则是创建一个新对象,无论字段是值类型的还是引用类型,均复制到新对象中

8. Java中的四种引用类型

参考
Java四种引用类型

  • 强引用
    只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了
  • 软引用
    在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常
  • 弱引用
    无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
  • 虚引用
    虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,虚引用必须要和 ReferenceQueue 引用队列一起使用。
  • 引用队列(ReferenceQueue)
    引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。

9. 重载和重写的区别

参考
Java静态分派与动态分派
JVM学习笔记——节码执行引擎
多态是指允许不同子类型的对象可以对同一方法做出不同的响应。

  • 静态分派
    所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用就是方法重载。使用静态分派的主要是静态方法、私有方法、实例构造器和父类方法,这些方法被称为非虚方法。
  • 动态分派
    在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。动态分派的典型应用就是方法重写。使用动态分派的主要是虚方法,即除去非虚方法的其他方法。实现方式是为类在方法区中建立虚方法表,虚方法表中存放着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那子类的虚方法表中的地址入口和父类相同方法的地址入口一致,都指向父类的实现入口。

动态分派通过invokevirtual指令来方法的调用,invokevirtual指令的运行时解析过程大致分为以下几个步骤:

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
  2. 如果在类型C中找到与常量中的描述符和简单名称相符合的方法,然后进行访问权限验证,如果验证通过则返回这个方法的直接引用,查找过程结束;如果验证不通过,则抛出java.lang.IllegalAccessError异常。
  3. 否则未找到,就按照继承关系从下往上依次对类型C的各个父类进行第2步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,则跑出java.lang.AbstractMethodError异常。

由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质。

  • 虚方法表
    虚方法表中存放着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的入口地址是一致的,都指向父类的实现入口

10. 泛型擦除

参考
深入理解Java泛型
泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

11. HashMap的实现原理

参考
面试必备:HashMap源码解析(JDK8)
HashMap? ConcurrentHashMap? 相信看完这篇没人能难住你!

12. JVM内存区域

参考
JVM系列(二) - JVM内存区域
在这里插入图片描述

13. 对象创建和销毁

参考
深入理解Java虚拟机】Java内存区域模型、对象创建过程、常见OOM
Java 对象创建过程。init 方法和 clinit方法
JVM:Java对象的创建、内存布局 & 访问定位 全过程解析
对象的创建大概分为以下几步:

  1. 检查类是否已经被加载;
  2. 为对象分配内存空间;
  3. 为对象字段设置零值;
  4. 设置对象头;
  5. 执行构造方法。

对象的初始化顺序
静态变量/静态代码块 -> 普通代码块 -> 构造函数

  1. 父类静态变量和静态代码块(先声明的先执行)
  2. 子类静态变量和静态代码块(先声明的先执行)
  3. 父类普通成员变量和普通代码块(先声明的先执行)
  4. 父类的构造函数
  5. 子类普通成员变量和普通代码块(先声明的先执行)
  6. 子类的构造函数。

14. 垃圾收集算法

参考
深入理解Java虚拟机-垃圾回收器与内存分配策略

当执行一次Minor gc时,Eden空间的存活对象会被复制到To Survivor空间,并且之前经过一次Minor gc并在From Survivor空间存活的年轻对象也会复制到To Survivor空间。
有两种情况Eden空间和From Survivor空间存活的对象不会复制到To Survivor空间,而是晋升到老年代。一种是存活的对象的分代年龄超过-XX:MaxTenuringThreshold(用于控制对象经历多少次Minor gc才晋升到老年代)所指定的阈值,默认是15。另一种是To Survivor空间容量达到阈值。
当所有存活的对象被复制到To Survivor空间,或者晋升到老年代,也就意味着Eden空间和From Survivor空间剩下的都是可回收对象
在这里插入图片描述

15. 类加载过程

参考
JVM系列(四) - JVM类加载机制详解
在这里插入图片描述

  • 静态连接
    在加载阶段将符号引用解析成直接引用的过程
  • 动态连接
    在运行阶段将符号引用解析成直接引用的过程

16. 双亲委派模型

参考
JVM系列(四) - JVM类加载机制详解
在这里插入图片描述
loadClass()本身是一个递归向上调用的过程。

  1. 自底向上检查类是否已加载
    先通过findLoadedClass()方法从最底端类加载器开始检查类是否已经加载。如果已经加载,则根据resolve参数决定是否要执行连接过程,并返回Class对象。如果没有加载,则通过parent.loadClass()委托其父类加载器执行相同的检查操作(默认不做连接处理)。直到顶级类加载器,即parent为空时,由findBootstrapClassOrNull()方法尝试到Bootstrap ClassLoader中检查目标类。
  2. 自顶向下尝试加载类
    如果仍然没有找到目标类,则从Bootstrap ClassLoader开始,通过findClass()方法尝试到对应的类目录下去加载目标类。如果加载成功,则根据resolve参数决定是否要执行连接过程,并返回Class对象。如果加载失败,则由其子类加载器尝试加载,直到最底端类加载器也加载失败,最终抛出ClassNotFoundException。

17. 并发工具类详解

参考
CountDownLatch、Semaphore等4大并发工具类详解
CountDownLatch

  • 功能
    允许一个或多个线程,等待其他一组线程完成操作,再继续执行。
  • 原理
    使用AQS实现,await方法会一直尝试申请锁,但是state状态不为0,所以一直返回-1,调用LockSuppor的park方法阻塞该线程,其他线程调用countDown方法会将state状态减1,当state到达0时,会再次调用LockSuppor的unpark唤醒被阻塞线程。

关键代码:

    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);//将state初始化成值count
    }
    public void countDown() {
        sync.releaseShared(1); //线程每次调用,会做减1操作
    }
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1); //尝试申请锁
    }
    private static final class Sync extends AbstractQueuedSynchronizer {
        Sync(int count) {
            setState(count);
        }
        int getCount() {
            return getState();
        }
        //由于state状态不为0,所以一直返回-1,线程被阻塞
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1; //减1操作
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

CyclicBarrier

  • 功能
    CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。该工具类可重置,反复使用。
  • 原理
    使用ReentrantLock和Condition实现,可以看到线程调用await方法,会让count减1,如果没有达到0,则自旋阻塞,当count个线程执行了await方法,则count就会减为0,此时会先调用barrierAction这个Runnable,然后再唤醒其他的所有线程。

关键代码:

    private final ReentrantLock lock = new ReentrantLock();
    private final Condition trip = lock.newCondition();
    private int count; //线程个数
    private final Runnable barrierCommand;
    
    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }
    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }
    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            int index = --count;
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run(); //先执行barrierCommand
                    ranAction = true;
                    nextGeneration();//通过Condition唤醒其他线程
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }
            // loop until tripped, broken, interrupted, or timed out
            for (;;) {
                try {
                    if (!timed)
                        trip.await();//线程阻塞
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                }
            }
        } finally {
            lock.unlock();
        }
    }
    private void nextGeneration() {
        // signal completion of last generation
        trip.signalAll(); //唤醒线程
        // set up next generation
        count = parties; //再次初始化count和generation,供下次使用
        generation = new Generation();
    }

Semaphore

  • 功能
    类似计数器,控制线程执行的数量,即过控制一定数量的许可(permit)的方式,来达到限制通用资源访问的目的。
  • 原理
    Semaphore实现原理很简单,使用AQS,将state的数量初始化为permits个,线程调用acquire方法则申请一个许可,申请成功才可以继续执行,否则阻塞等待。任务执行完成后,调用release方法,释放许可并唤醒其他线程,竞争获取许可。
    Semaphore分为公平和非公平。

Exchanger

  • 功能
    Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

18. 重写equals的时候需要重写hashCode方法

参考
为什么重写了equals()也要重写hashCode()

19. HashMap和HashTable

  1. 初始容量不同
    HashMap默认容量是16,而HashTable则是11,利用构造函数设置初始容量时,HashMap会调用tableSizeFor函数,将容量调整为2的幂次方,而HashTable则直接使用initialCapacity
  2. hash方式不同
    HashMap是将key键的hash值的高16位和低16位进行异或处理,这样更散列。而HashTable直接使用key键的hashCode
  3. table索引计算方式不同
    HashTable使用取余的方式,而HashMap使用hash值和length-1进行&操作
  4. 链表的插入方式不同
    HashTable采用头插法,没有使用树结构。而HashMap在1.8版本使用尾插法,链表长度超过8且哈希表长度大于64时,会转化成红黑树
  5. HashMap是线程不安全的,而HashTable是线程安全的,通过synchronized实现

20. ConcurrentHashMap原理

21. fail-fast和fail-safe的介绍和区别

参考
了解fail-fast 和 fail-safe

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值