抖音一面24/7/12

  1. 介绍项目(oj)

  2. 做这个项目时遇到的印象比较深刻的问题

    在开发多种代码沙箱的实现方式(Java 原生和 Docker)发现有大量的重复代码,不利于修改和维护,想到使用模板方法来解决;

    改造项目为微服务后发现无法在题目服务中获取到用户信息,想到把项目登录方式由本地 Session 改造为基于 Redis 的分布式 Session 等。(解决 cookie 跨路径问题)

    yml
    复制代码
    server: address: 0.0.0.0 port: 8104 servlet: context-path: /api/judge # cookie 30 天过期 session: cookie: max-age: 2592000 path: /api
  3. session在redis中怎么存的

    (sessionId,用户信息) 引入依赖,修改session存储类型

  4. redis有哪些数据结构

  5. zset底层实现

  6. 还有什么遇到的问题

    Java安全管理器 限制用户对文件、内存、CPU、网络等资源的操作和访问。

    docker容器安全性 超时控制 内存资源 网络资源 权限管理

  7. 操作系统中进程和线程的区别

    1. 资源分配:
      • 进程是资源分配的基本单位,拥有独立的地址空间,包括代码段、数据段、堆、栈等。
      • 线程是进程中的执行单元,共享所属进程的资源,如代码段、数据段等,但每个线程有自己的栈。
    2. 系统开销:
      • 创建和撤销进程时,系统需要分配和回收资源,开销较大。
      • 线程的创建和撤销开销相对较小。
    3. 并发性:
      • 进程之间的并发性较低,切换时需要切换上下文。
      • 线程之间的并发性较高,切换线程的开销较小。
    4. 通信方式:
      • 进程间通信方式较为复杂,如管道、消息队列、共享内存等。
      • 线程间通信相对简单,可以直接读写进程内的共享变量。
    5. 调度:
      • 进程是操作系统调度的基本单位。
      • 线程是 CPU 调度的基本单位。
  8. 为什么不用进程做并发,而是设计出了线程这样一个概念

    1. 资源开销:进程的创建、切换和销毁需要较大的系统资源开销。创建一个新进程需要为其分配独立的地址空间、内存资源、文件描述符等。而线程的创建和切换开销相对较小,因为线程共享所属进程的资源,不需要为每个线程单独分配完整的资源。
    2. 通信效率:进程间通信的方式相对复杂且效率较低,如管道、消息队列、共享内存等方式都需要额外的系统调用和数据拷贝操作。而线程间由于共享进程的内存空间,通信可以通过直接访问共享变量来实现,通信效率更高。
    3. 并发粒度:进程的并发粒度较粗,每个进程都是一个独立的执行单元,切换进程意味着较大的上下文切换成本。而线程可以提供更细粒度的并发控制,使得多个任务在同一个进程内并发执行,更灵活地分配系统资源和处理任务。
    4. 系统性能:在需要大量并发执行任务的场景下,如果使用进程来实现,由于进程的资源开销和切换成本,会导致系统性能下降。而线程能够在较小的资源消耗下实现更高的并发度,提高系统的整体性能和响应能力。
  9. 在项目中使用到多线程的场景

    • 智能BI项目

    image-20240712161335537

    自定义线程池

    java
    复制代码
    @Configuration public class ThreadPoolExecutorConfig { @Bean public ThreadPoolExecutor threadPoolExecutor() { ThreadFactory threadFactory = new ThreadFactory() { private int count = 1; @Override public Thread newThread(@NotNull Runnable r) { Thread thread = new Thread(r); thread.setName("线程" + count); count++; return thread; } }; ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), threadFactory); return threadPoolExecutor; } }

    提交任务到线程池:

    java
    复制代码
    CompletableFuture.runAsync(() -> { System.out.println("任务执行中:" + name + ",执行人:" + Thread.currentThread().getName()); try { Thread.sleep(60000); } catch (InterruptedException e) { e.printStackTrace(); } }, threadPoolExecutor);
    • 导入数据

      java
      复制代码
      private ExecutorService executorService = new ThreadPoolExecutor(16, 1000, 10000, TimeUnit.MINUTES, new ArrayBlockingQueue<>(10000));
  10. volatile关键字的作用

    1. 保证可见性:当一个变量被声明为volatile时,线程对该变量的修改对于其他线程是立即可见的。也就是说,当一个线程修改了一个volatile变量的值,其他线程能够立即感知到这个变化,从而读取到最新的值。
    2. 禁止指令重排序:编译器和处理器为了优化性能,可能会对代码的执行顺序进行重排序。但对于被volatile修饰的变量,在其读写操作前后,相关的指令不会被重排序,从而保证了程序执行的顺序和逻辑的正确性。

    需要注意的是,volatile不能保证原子性,即多个线程同时对volatile变量进行操作时,可能会出现数据不一致的问题。例如,当一个线程对volatile变量进行自增操作(++)时,这个操作不是原子性的,可能会出现并发问题。在这种情况下,如果需要保证原子性和线程安全,需要使用锁或者其他并发控制机制。

  11. 为什么会出现一个线程把数据改了,其他线程读不到的这种情况

    1. 缓存问题:现代计算机系统中,为了提高性能,CPU 都有自己的高速缓存(L1、L2、L3 缓存等)。当一个线程对共享变量进行修改时,这个修改可能首先被写入到该线程所在 CPU 的缓存中,还没有及时刷新到主内存(计算机的物理内存)。而其他线程可能从自己的 CPU 缓存或者尚未更新的主内存中读取数据,导致无法获取到最新的值。
    2. 编译器优化:编译器在编译代码时,可能会对代码进行优化重排序。例如,在不影响单线程执行结果的前提下,编译器可能会对指令的执行顺序进行调整。这可能导致在多线程环境下,不同线程对变量的读写顺序与我们在代码中编写的顺序不一致,从而导致数据不一致的问题。
    3. 处理器优化:处理器为了提高执行效率,也可能会对指令进行乱序执行,使得指令的实际执行顺序与程序代码中的顺序不一致。这在多线程场景下也可能导致数据可见性问题。
  12. ThreadLocal的基本原理(map的key是什么)

    ThreadLocal 详解 | JavaGuide

    Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap

    ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocalvalue为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。

    每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离

    ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构

    ThreadLocal内部维护了一个ThreadLocalMap,这个ThreadLocalMap以当前线程作为键,要存储的值作为值。

    当在一个线程中通过ThreadLocal对象进行数据存储时,ThreadLocal会获取当前线程,然后将数据存储在当前线程的ThreadLocalMap中。每个线程都有自己独立的ThreadLocalMap,所以不同线程通过同一个ThreadLocal对象存储的数据不会相互干扰和影响。

    当需要获取数据时,ThreadLocal同样根据当前线程从对应的ThreadLocalMap中获取数据。

    当线程结束时,如果没有对ThreadLocal中的数据进行正确的清理,可能会导致内存泄漏,因为线程结束后,对应的ThreadLocalMap以及其中存储的数据可能无法被正常回收。

    Java四种引用类型

    • 强引用:我们常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
    • 软引用:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
    • 弱引用:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
    • 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知

    ThreadLocalkey是弱引用,那么在ThreadLocal.get()的时候,发生GC之后,key是否是null

    这个问题刚开始看,如果没有过多思考,弱引用,还有垃圾回收,那么肯定会觉得是null

    其实是不对的,因为题目说的是在做 ThreadLocal.get() 操作,证明其实还是有强引用存在的,所以 key 并不为 null,如下图所示,ThreadLocal强引用仍然是存在的。

    如果我们的强引用不存在的话,那么 key 就会被回收,也就是会出现我们 value 没被回收,key 被回收,导致 value 永远存在,出现内存泄漏。

  13. 算法题 数组中滑动窗口的滑动过程中,每一个窗口的最大值(时间复杂度)

    窗口中的每一个值存入有序map,key为元素值,value为元素值的出现次数,最大值就是最后一个元素。滑动的过程中将左边移除窗口的元素减减,右边的元素加加,如果移除的元素移除后次数为0,就删除这个元素。

  14. 一个系统中,记录处理用户提交的订单的耗时,得到实时流式输入的耗时数据的中位数,

    排序

    优先队列

    维护一个有序数组,插入到有序数组

    正解

    使用两个堆(大顶堆和小顶堆)

    维护一个大顶堆和一个小顶堆。

    大顶堆存储较小的一半数据,小顶堆存储较大的一半数据。

    当新数据到来时:

    如果大顶堆为空或者新数据小于等于大顶堆的堆顶元素,将新数据插入大顶堆;否则,将新数据插入小顶堆。

    然后进行调整,使两个堆的元素数量差不超过 1。

    要获取中位数:

    如果两个堆的元素数量相同,中位数为两个堆顶元素的平均值;如果大顶堆的元素数量比小顶堆多 1,大顶堆的堆顶元素为中位数;反之,小顶堆的堆顶元素为中位数。

    插入数据和获取中位数的时间复杂度都为 O(log n),效率较高。

    java
    复制代码
    import java.util.PriorityQueue; class MedianCalculator { private PriorityQueue<Integer> maxHeap; // 存储较小的一半数据 private PriorityQueue<Integer> minHeap; // 存储较大的一半数据 public MedianCalculator() { maxHeap = new PriorityQueue<>((a, b) -> b - a); minHeap = new PriorityQueue<>(); } public void insertData(int data) { if (maxHeap.isEmpty() || data <= maxHeap.peek()) { maxHeap.offer(data); } else { minHeap.offer(data); } // 调整两个堆的大小 if (maxHeap.size() > minHeap.size() + 1) { minHeap.offer(maxHeap.poll()); } else if (minHeap.size() > maxHeap.size()) { maxHeap.offer(minHeap.poll()); } } public double getMedian() { if (maxHeap.size() == minHeap.size()) { return (maxHeap.peek() + minHeap.peek()) / 2.0; } else if (maxHeap.size() > minHeap.size()) { return maxHeap.peek(); } else { return minHeap.peek(); } } public static void main(String[] args) { MedianCalculator calculator = new MedianCalculator(); calculator.insertData(5); calculator.insertData(3); calculator.insertData(7); calculator.insertData(2); calculator.insertData(6); System.out.println("中位数: " + calculator.getMedian()); } }
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP