抖音一面24/7/12
-
介绍项目(oj)
-
做这个项目时遇到的印象比较深刻的问题
在开发多种代码沙箱的实现方式(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 -
session在redis中怎么存的
(sessionId,用户信息) 引入依赖,修改session存储类型
-
redis有哪些数据结构
-
zset底层实现
-
还有什么遇到的问题
Java安全管理器 限制用户对文件、内存、CPU、网络等资源的操作和访问。
docker容器安全性 超时控制 内存资源 网络资源 权限管理
-
操作系统中进程和线程的区别
- 资源分配:
- 进程是资源分配的基本单位,拥有独立的地址空间,包括代码段、数据段、堆、栈等。
- 线程是进程中的执行单元,共享所属进程的资源,如代码段、数据段等,但每个线程有自己的栈。
- 系统开销:
- 创建和撤销进程时,系统需要分配和回收资源,开销较大。
- 线程的创建和撤销开销相对较小。
- 并发性:
- 进程之间的并发性较低,切换时需要切换上下文。
- 线程之间的并发性较高,切换线程的开销较小。
- 通信方式:
- 进程间通信方式较为复杂,如管道、消息队列、共享内存等。
- 线程间通信相对简单,可以直接读写进程内的共享变量。
- 调度:
- 进程是操作系统调度的基本单位。
- 线程是 CPU 调度的基本单位。
- 资源分配:
-
为什么不用进程做并发,而是设计出了线程这样一个概念
- 资源开销:进程的创建、切换和销毁需要较大的系统资源开销。创建一个新进程需要为其分配独立的地址空间、内存资源、文件描述符等。而线程的创建和切换开销相对较小,因为线程共享所属进程的资源,不需要为每个线程单独分配完整的资源。
- 通信效率:进程间通信的方式相对复杂且效率较低,如管道、消息队列、共享内存等方式都需要额外的系统调用和数据拷贝操作。而线程间由于共享进程的内存空间,通信可以通过直接访问共享变量来实现,通信效率更高。
- 并发粒度:进程的并发粒度较粗,每个进程都是一个独立的执行单元,切换进程意味着较大的上下文切换成本。而线程可以提供更细粒度的并发控制,使得多个任务在同一个进程内并发执行,更灵活地分配系统资源和处理任务。
- 系统性能:在需要大量并发执行任务的场景下,如果使用进程来实现,由于进程的资源开销和切换成本,会导致系统性能下降。而线程能够在较小的资源消耗下实现更高的并发度,提高系统的整体性能和响应能力。
-
在项目中使用到多线程的场景
- 智能BI项目

自定义线程池
▼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));
-
volatile关键字的作用
- 保证可见性:当一个变量被声明为
volatile时,线程对该变量的修改对于其他线程是立即可见的。也就是说,当一个线程修改了一个volatile变量的值,其他线程能够立即感知到这个变化,从而读取到最新的值。 - 禁止指令重排序:编译器和处理器为了优化性能,可能会对代码的执行顺序进行重排序。但对于被
volatile修饰的变量,在其读写操作前后,相关的指令不会被重排序,从而保证了程序执行的顺序和逻辑的正确性。
需要注意的是,
volatile不能保证原子性,即多个线程同时对volatile变量进行操作时,可能会出现数据不一致的问题。例如,当一个线程对volatile变量进行自增操作(++)时,这个操作不是原子性的,可能会出现并发问题。在这种情况下,如果需要保证原子性和线程安全,需要使用锁或者其他并发控制机制。 - 保证可见性:当一个变量被声明为
-
为什么会出现一个线程把数据改了,其他线程读不到的这种情况
- 缓存问题:现代计算机系统中,为了提高性能,CPU 都有自己的高速缓存(L1、L2、L3 缓存等)。当一个线程对共享变量进行修改时,这个修改可能首先被写入到该线程所在 CPU 的缓存中,还没有及时刷新到主内存(计算机的物理内存)。而其他线程可能从自己的 CPU 缓存或者尚未更新的主内存中读取数据,导致无法获取到最新的值。
- 编译器优化:编译器在编译代码时,可能会对代码进行优化重排序。例如,在不影响单线程执行结果的前提下,编译器可能会对指令的执行顺序进行调整。这可能导致在多线程环境下,不同线程对变量的读写顺序与我们在代码中编写的顺序不一致,从而导致数据不一致的问题。
- 处理器优化:处理器为了提高执行效率,也可能会对指令进行乱序执行,使得指令的实际执行顺序与程序代码中的顺序不一致。这在多线程场景下也可能导致数据可见性问题。
-
ThreadLocal的基本原理(map的key是什么)
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上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 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
ThreadLocal的key是弱引用,那么在ThreadLocal.get()的时候,发生GC之后,key是否是null?这个问题刚开始看,如果没有过多思考,弱引用,还有垃圾回收,那么肯定会觉得是
null。其实是不对的,因为题目说的是在做
ThreadLocal.get()操作,证明其实还是有强引用存在的,所以key并不为null,如下图所示,ThreadLocal的强引用仍然是存在的。如果我们的强引用不存在的话,那么
key就会被回收,也就是会出现我们value没被回收,key被回收,导致value永远存在,出现内存泄漏。 -
算法题 数组中滑动窗口的滑动过程中,每一个窗口的最大值(时间复杂度)
窗口中的每一个值存入有序map,key为元素值,value为元素值的出现次数,最大值就是最后一个元素。滑动的过程中将左边移除窗口的元素减减,右边的元素加加,如果移除的元素移除后次数为0,就删除这个元素。
-
一个系统中,记录处理用户提交的订单的耗时,得到实时流式输入的耗时数据的中位数,
排序
优先队列
维护一个有序数组,插入到有序数组
正解
使用两个堆(大顶堆和小顶堆)
维护一个大顶堆和一个小顶堆。
大顶堆存储较小的一半数据,小顶堆存储较大的一半数据。
当新数据到来时:
如果大顶堆为空或者新数据小于等于大顶堆的堆顶元素,将新数据插入大顶堆;否则,将新数据插入小顶堆。
然后进行调整,使两个堆的元素数量差不超过 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()); } }

