JAVA面试题总结【深入问题,拓展广度

2023年 9月 25日 50.0k 0

容器:

用过什么容器,hashmap底层原理、优化

用过HashMap、ArrayList、LinkedList、Deque、HashSet、ConcurrentHashMap等

  • ArrayList底层由Object[]实现,初始化时赋值的是一个空数组,当放入第一元素时初始容量变为10,每次扩容为原数组的1.5倍,可以存储null值,但是不建议。

  • LinkedList是由双向链表实现,但是因为每次在列表中间插入时需要先定位到指定位置,所以时间复杂度也为O(n),所以相对来说平时使用ArrayList多一些。

  • HashMap/HashTable/ConcurrentHashMap:

  • 1)HashMap底层是数组和链表结合在一起使用的,也就是链表散列,通过对key的hashcode进行扰动得到hash值,经过(length - 1) & hash进行判断当前元素存放的位置,如果当前位置存在数据的话,则判断是否一致,一致则覆盖,如果不一致则使用拉链法进行解决冲突。在jdk1.8之后引入了红黑树,在链表超过了阈值(默认为8)后,链表自动转化为红黑树,但是转化前会判断如果数组长度小于64,则会优先考虑扩容

    2)对于HashMap在多线程场景下不安全的问题,jdk1.7之前在扩容时会出现不安全问题,当一个桶位【Entry 数组】有多个元素需要扩容时,多线线程会同时操作链表,在头插法的情况下可能会形成死循环。在jdk1.8之后采用了尾插法避免,但是不建议使用,会有数据覆盖的风险,建议使用ConcurrentHashMap。;

    3)使用ConcurrentHashMap:ConcurrentHashMap在jdk1.7前使用segment[默认16个]+entry数组+链表,原理是使用了分割分段,只对segment进行加锁,降低了线程之间的加锁冲突,提高了并发率。jdk1.8之后使用node/treeNode数组+链表/红黑树,并发控制使用synchronized+CAS操作,看起来就像是线程安全的hashMap。而HashTable只用了Synchronized加锁,效率非常低下。;

    4)对于ConcurrentHashMap不能放入null值是因为有二义性,无法判断是get的是没有值还是值就为null,所以对多线程操作会有影响,而单线程不会。可以使用putIfAbsent、compute、computeIfAbsent、merge等进行实现原子操作

  • Deque是一个双端队列,在队列的两段都可以插入删除,根据失败返回结果的不同分为两类,一类是抛出异常add/remove/get,一类是抛出特殊值offet/poll/peek。也有push/pop用于模拟栈
  • 针对容器使用时需要注意的地方:

  • 集合判断空用isEmpty(),不是size == 0的方式,这样会保证复杂度为O(n),因为size对于ConcurrentHashMap会产生复杂度不是O(n);
  • 使用toMap转换map时需要主要value为null的情况,因为toMap会调用merge方法,merge会首先对值进行判断非null
  • 对Map进行remove操作,不要再forEach中进行才做,最好使用Iterator,并发情况是对其进行加锁
  • 集合去重,不要用contains,直接转化为set,因为两者的contains实现方式不同,set为O(1),list为O(n)
  • 集合转数组,必须使用toArray,传入类型一致长度为0的空数组,因为JVM优化,new String[0]作为参数效果最好,只是起到一个模板的效果
  • 数组转集合,使用Array.asList不能使用集合相关方法,add/remove/clear会出现UnsupportedOperationException异常,因为只是返回一个内部类,没有重写add这些方法。所以需要new ArrayList(Array.asList(xxxx)),或者使用Stream的Arrays.stream(xxxx).collect(Collectors.toList());
  • 线程

    线程安全了解吗,线程安全的容器原理,创建线程的方式

    线程安全是一份数据,指在多个线程访问下,可以保持一致性和正确性

    Concurrent容器线程安全原理

    创建线程的方式:

  • 继承Thread类,重写run方法
  • 实现Runnable接口,重写run方法 Thread thread = new Thread(myRunnable);
  • 使用匿名内部类重写run方法,创建Thread子类对象
  • 使用匿名内部类重写run方法,实现Runnable方法
  • lambda代替内部类
  • 实现Callable接口,重写call方法,带有返回值,创建FutureTask【传入callable实例,两个构造方法,一个只需要实例,另一个需要泛型的result】,将其传入new thread
  • 使用线程池创建线程,
    • 创建方式:
      • 使用Executors工具类中有可创建多类型的FixedThreadpoll、SingleThreadPool、CachedThreadPool、ScheduleThreadPool。其中fixed和Single用的都是无界LinkedBlockedQueue,堆积大量任务,会产生OOM问题,Cached使用的同步队列,任务数量多之后速度较慢产生OOM问题。Shcedule用的无界延迟队列问题,堆积请求,OOM问题。总之,都是使用的队列最大长度为INTEGER.MAX_VALUE,会堆积请求,发生OOM问题
      • 使用ThreadPoolExecutor创建,有3个核心参数corePoolSize\maximumPoolSize\workQueue。
    • 饱和策略:AbortPolicy直接返回拒绝异常,DiscardPolicy直接丢弃,DiscardOldestPolicy丢弃最早未处理的任务,CacheRunsPolicy是调用执行自己的线程运行任务,若执行程序已经被关闭,则丢弃该任务
    • 常用的阻塞队列:SingleThreadExecutorl\FixedThreadPool使用LinkedBlokingQueue无界队列CachedThreadPool\使用SynchronousQueue同步队列ScheduleThreadPool\SingleThreadScheduledExecutor使用DelayedWorkQueueDelayedWorkQueue 添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 Integer.MAX_VALUE,所以最多只能创建核心线程数的线程。
    多线程,线程池参数以及作用

    多线程指的是在一个程序中执行多个独立的任务和操作,每个任务都是由一个线程来完成的。多线程共享堆、方法区,每个线程有自己的程序计数器、本地方法栈、虚拟机栈。进程往往由多个线程组成,这样更适合于我们当下多CPU的处理逻辑,提高了并发量。

    线程参数主要有3个重要corePoolSize、maximumPoolSize、workQueue,其中corePoolSize指的是核心线程数,未达到任务队列容量时可以运行的线程数,maximumPoolSize指的是最大线程数,指的是线程达到了任务队列最大容量时,当前可以同时运行的线城市,workQueue指的是来新的线程后,判断线程数是否达到了最大线程数,如果达到了则放入任务队列中。还有其他4个参数,分别是keepAliveTime、unit、ThreadFactory、handler,其中keepAliveTime为线程在大于核心线程数时,多余的空闲线程存活时间,unit为时间单位、ThreadFactory为线程工程、handler为饱和策略。

    synchronized底层原理、和lock、volatile的区别

    synchronized用于解决多线程之间同步的问题,在java的早期版本之中,synchronized是比较重的,在后续的自旋锁、锁消除、锁粗化、偏向锁、轻量锁的一系列优化减少系统开销,让synchronized的效率提升了许多。

    对于synchronized用于代码块的情况,底层是用moniter实现的,其中monitorenter指令指向代码开始的位置,monitorexit指向代码块结束的位置,并且有两个monitorexit从而保证出现异常了也可以释放锁。monitorenter在每次执行后首先判断锁是否为0,若是则+1获锁成功,若不是则获取锁失败。执行monitorexit后首先判断锁是否锁所有者如果是则-1释放锁,若不是则结束

    对于synchronized用于方法的情况,底层是用ACC_SYNCHRONIZED标识进而标注是否是一个同步方法lock。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。

    volatile:volatile可让变量共享可见,让变量从本地内存进入内主存中,指示JVM这个变量是共享不稳定的,每次获取都需要去主内存获取。但是volatile是无法保证原子性的,synchronized既可以保证可见性又可以保证原子性。并且volatile可以防止指令重排序的问题,比如对于使用双重校验锁实现的单例模式中,instance = new Singleton() 在编译后会被分解为 3 个指令,1分配内存空间,2初始化,3赋内存地址,需要对uniqueInstance加上volatile关键字才可以保证顺序,否则线程1执行了分配内存空间和内存地址赋值,线程2调用getInstance发现不为空,但是未被初始化就会出现问题嘞。

    public calss Singleton{
    
        private vlotile static Singleton instance = null;
        
        public Singleton(){
        }
        
        public static Singleton getInstance(){
            if(instance == null){
                synchronized(Singleton.class){
                    if(instance == null){
                        return new Singleton();
                    }
                }
            }
        }
    
    }
    

    但是volatile不能保证原子性,比如i++实际上是三个操作,读取、加一、写回,所以可以使用Synchronized、AtomicInteger、ReetrantLock进行优化

    Lock: Synchronized

    AQS原理:全名为抽象队列同步器,他是基于CLH实现的,CLH的全称是由3个人的人名组成,他是一个抽象的双向队列,其中每个节点包含有线程的引用,同步状态,后继节点,前驱节点。AQS使用被volatile修饰的int state作为同步状态,使用内置的FIFO线程等待获取资源。

    比如可重入的ReentrantLock就是维护了一个state,他每次占有这个state的线程,如果没有占有到那么就会进入CLH中,如果占用了,而且后面如果需要再次需要,那么就直接累加,这就是可重入锁的提现,也意味着每次释放也要减一。比如CountDownLatch计数器中,对state初始化为子线程的个数n,多个子线程会使用CAS对state减一,直到0后会启动uppark(),主线程从await()中返回,继续执行后续操作。

    AQS共有两种共享方式,一种是独占比如reentrantLock一种是共享semaphonre\CountDownLatch。自定义的同步器中,可以同时实现独占+共享两种方式,比如ReentrantReadWriteLock。

    Semaphore:Syncrhonized和ReentrantLock都是一次只允许一个线程访问某个资源,Semaphore可以控制同时获得资源的线程数量。若假设有5个线程来获取Semaphore中的资源那么直接final Semaphore semaphore = new Semaphore(5);初始共享数量,每次获得时候semaphore.acquire,每次释放的时候semaphore.release;,剩1的时候就会退化成排他锁。它有两种模式一个是遵循FIFO的公平模式,一个是抢占模式。他的原理是基于AQS,当state>0时,通过CAS获得许可,state--,如果State

    相关文章

    服务器端口转发,带你了解服务器端口转发
    服务器开放端口,服务器开放端口的步骤
    产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
    如何使用 WinGet 下载 Microsoft Store 应用
    百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
    百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

    发布评论