-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 35.4 KB
/
content.json
File metadata and controls
1 lines (1 loc) · 35.4 KB
1
{"posts":[{"title":"道普云面试","text":"自我介绍 java基础: List了解,ArrayList底层实现,延伸出扩容机制;Hashmap底层实现;答出了ArrayList动态数组实现,扩容机制(Arrays.copyOf创建新数组并迁移)。问到具体的容量和扩容倍数没有答上来。Hashmap底层什么实现的提示后说了数组加链表;实际上是数组+链表,链表长度≥8且数组长度≥64时,链表转红黑树。没有问Hashmap扩容,看来是看这个样子我也答不上来。 HashMap扩容机制 触发条件:当 HashMap 中的元素数量(size)超过 capacity * loadFactor 时触发扩容。 capacity:当前数组(Node<K,V>[] table)的长度(默认初始 16) loadFactor:负载因子(默认 0.75) 扩容过程 计算新容量:新容量 = 旧容量 << 1(最大容量1 << 30) 数据迁移: 遍历旧数组,重新计算每个键值对的哈希值,并分配到新数组的对应位置 。由于新容量是 2 倍,元素在新数组的位置要么是 原位置,要么是 原位置 + 旧容量。 链表拆分: 如果某个桶是链表,会拆分成两个链表(low 和 high),分别放在新数组的 原位置 和 原位置 + 旧容量 树形优化: 如果某个桶已经是红黑树,会检查是否需要拆分成两棵树或退化为链表。 商城项目为什么要用redis缓存商品数据和营业状态数据,被纠正不能把Redis当作DB,因为持久化。讲了一些RDBAOF持久化的机制,询问有没有了解过mysql和Redis如何保持数据一致性; 为什么用Redis 商品数据:高频读取,缓存减轻MySQL压力,响应速度提升。 营业状态:需要快速切换(如打烊状态),Redis的原子操作(如SETNX)更高效。 持久化问题 RDB:定时快照,适合备份但可能丢失最近数据。 AOF:记录写命令,可配置为everysec平衡性能与安全。 数据一致性方案 双写:先更新DB再删缓存(存在短暂不一致)。 监听Binlog:通过Canal将MySQL变更同步到Redis(最终一致性) 问mysql索引底层实现的数据结构,有哪些情况会导致索引失效,mysql调优相关mysql索引答描述性的了B+树,突然想不起来链接叶子节点的叫双向链表,描述了很久。索引失效、调优都说了一点《数据库原理》课上讲的内容。参考:mysql 索引失效及其解决办法 - 凡人半睁眼 - 博客园一文彻底弄懂MySQL的优化 - lgx211 - 博客园 问Spring常用注解,配置类用哪个注解答得很磕绊,所以被直接问配置类注解是什么,答了@Bean,提示着说出了@Configuration。 Spring常用注解 常用注解: @Controller/@RestController:定义Web层。 @Service:业务逻辑层。 @Autowired:依赖注入。 @Transactional:声明事务。 配置类注解:@Configuration + @Bean。@Configuration 是一个类级别的注解,用于标识一个类作为 Spring 应用上下文的配置源。@Bean 是一个方法级别的注解,用于指示方法返回一个由 Spring 容器管理的 bean。 询问rvos是微内核还是宏内核;具体实现了哪些功能;有没有设计虚拟内存还是直接映射的物理内存;进程调度有没有优先级;实际上根本不知道微内核和宏内核的区别,被教了很久。其他就是照常交代情况。 微内核与宏内核 宏内核: 简单来说,就是把很多东西都集成进内核,例如linux内核,除了最基本的进程、线程管理、内存管理外,文件系统,驱动,网络协议等等都在内核里面。优点是效率高。缺点是稳定性差,开发过程中的bug经常会导致整个系统挂掉。 微内核: 内核中只有最基本的调度、内存管理。驱动、文件系统等都是用户态的守护进程去实现的。优点是超级稳定,驱动等的错误只会导致相应进程死掉,不会导致整个系统都崩溃,做驱动开发时,发现错误,只需要kill掉进程,修正后重启进程就行了,比较方便。缺点是效率低。典型代表QNX,QNX的文件系统是跑在用户态的进程,称为resmgr的东西,是订阅发布机制,文件系统的错误只会导致这个守护进程挂掉。不过数据吞吐量就比较不乐观了。 OpenEuler适配的时候是否需要修改Doris源码;实习的时候TPC-H数据集怎么生成的;如何对比在x86和RISC-V上的表现;base64优化具体做了什么;照着简历答了些东西,似乎感觉有点浅了。 有没有了解过HTTPS加密算法没有了解过。 HTTPS加密原理【网络编程】九、详解 HTTPS 加密原理-腾讯云开发者社区-腾讯云 对shell了解如何说偶尔会用到shell脚本配置环境。 AI专业作业一般做的是什么相关的。没多讲,就说了下一些课堂任务。 有没有用过matlab说自己数学建模比赛用过。","link":"/2025/06/20/%E9%81%93%E6%99%AE%E4%BA%91%E9%9D%A2%E8%AF%95/"},{"title":"曲速科技笔试","text":"A 电话系统设计Q:设计一个简单电话,0-9数字键,拨号/挂机键;拨号键接通电话后会变成挂机键。考虑到扩展性,如追加*按键,添加按键音(类图或伪代码)A:以下是AI给出的答案。把电话的不同状态抽象成独立类确实比起设置按键状态更易拓展。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232import java.util.HashMap; import java.util.Map; // 状态接口 interface PhoneState { void handleDigit(char digit); void handleDial(); void handleStar(); // 扩展特殊键 } // 具体状态实现 class ReadyState implements PhoneState { private Phone phone; public ReadyState(Phone phone) { this.phone = phone; } @Override public void handleDigit(char digit) { phone.setCurrentNumber("" + digit); phone.getSoundService().playTone(digit); phone.setState(new DialingState(phone)); } @Override public void handleDial() { // 待机状态按拨号键无响应 } @Override public void handleStar() { phone.getSoundService().playTone('*'); // 可扩展*键功能 } } class DialingState implements PhoneState { private Phone phone; public DialingState(Phone phone) { this.phone = phone; } @Override public void handleDigit(char digit) { phone.setCurrentNumber(phone.getCurrentNumber() + digit); phone.getSoundService().playTone(digit); } @Override public void handleDial() { System.out.println("Dialing: " + phone.getCurrentNumber()); phone.setState(new ConnectedState(phone)); } @Override public void handleStar() { phone.getSoundService().playTone('*'); // 可扩展*键功能 } } class ConnectedState implements PhoneState { private Phone phone; public ConnectedState(Phone phone) { this.phone = phone; } @Override public void handleDigit(char digit) { // 通话中输入数字(如DTMF) phone.getSoundService().playTone(digit); } @Override public void handleDial() { System.out.println("Hanging up"); phone.setCurrentNumber(""); phone.setState(new ReadyState(phone)); } @Override public void handleStar() { phone.getSoundService().playTone('*'); // 可扩展*键功能 } } // 按键抽象 abstract class Button { public abstract void press(); } class DigitButton extends Button { private char digit; private Phone phone; public DigitButton(char digit, Phone phone) { this.digit = digit; this.phone = phone; } @Override public void press() { phone.getState().handleDigit(digit); } } class StarButton extends Button { private Phone phone; public StarButton(Phone phone) { this.phone = phone; } @Override public void press() { phone.getState().handleStar(); } } class DialButton extends Button { private Phone phone; public DialButton(Phone phone) { this.phone = phone; } @Override public void press() { phone.getState().handleDial(); } } // 声音服务 class SoundService { private Map<Character, String> tones = new HashMap<>(); public SoundService() { // 初始化音调映射 for (char c = '0'; c <= '9'; c++) { tones.put(c, "DTMF-" + c); } tones.put('*', "DTMF-star"); } public void playTone(char digit) { String tone = tones.getOrDefault(digit, "default-tone"); System.out.println("Playing: " + tone); } } // 电话主体 class Phone { private PhoneState state; private SoundService soundService; private String currentNumber = ""; private Map<String, Button> buttons = new HashMap<>(); public Phone() { this.soundService = new SoundService(); this.state = new ReadyState(this); initButtons(); } private void initButtons() { // 数字键 for (int i = 0; i <= 9; i++) { char digit = (char)('0' + i); buttons.put("DIGIT_" + digit, new DigitButton(digit, this)); } // 特殊键 buttons.put("STAR", new StarButton(this)); buttons.put("DIAL", new DialButton(this)); } // 状态管理方法 public void setState(PhoneState state) { this.state = state; } public PhoneState getState() { return state; } public SoundService getSoundService() { return soundService; } public String getCurrentNumber() { return currentNumber; } public void setCurrentNumber(String number) { this.currentNumber = number; } // 按键触发 public void pressButton(String buttonId) { Button button = buttons.get(buttonId); if (button != null) { button.press(); } } // 扩展新按钮 public void addButton(String id, Button button) { buttons.put(id, button); } } // 使用示例 public class Main { public static void main(String[] args) { Phone phone = new Phone(); // 拨号示例 phone.pressButton("DIGIT_1"); phone.pressButton("DIGIT_2"); phone.pressButton("DIGIT_3"); phone.pressButton("DIAL"); // 拨号 // 特殊键使用 phone.pressButton("STAR"); // 挂断 phone.pressButton("DIAL"); // 此时变为挂机键 } } B 字符串处理算法题Q:算法题,找出字符串中出现次数最多的字符 A:比较简单,时间复杂度$O(n)$。 1234567891011121314151617181920212223242526272829303132333435public class MostFrequentChar { public static List<Character> findMostFrequentChar(String str) { if (str == null || str.isEmpty()) { return new ArrayList<>(); } Map<Character, Integer> charCount = new HashMap<>(); int maxCount = 0; // 统计字符频率并记录最大出现次数 for (char c : str.toCharArray()) { int count = charCount.getOrDefault(c, 0) + 1; charCount.put(c, count); if (count > maxCount) { maxCount = count; } } // 找出所有出现次数等于maxCount的字符 List<Character> result = new ArrayList<>(); for (Map.Entry<Character, Integer> entry : charCount.entrySet()) { if (entry.getValue() == maxCount) { result.add(entry.getKey()); } } return result; } public static void main(String[] args) { String str = "aabbc"; List<Character> result = findMostFrequentChar(str); System.out.println("Most frequent characters: " + result); }} C SQL查询语句Q: sql查询语句,查询2022年入职的所有用户信息并按照公司ID排序,包含用户表、用户关系表(用户和上级)、公司表A: 作答的时候好像都没有好好看要求,回来后才想起来没排序。 D 多线程文件下载实现Q:给了String[] url, String dir下载N个文件到某个目录,文件名截取url后面一部分,完全下载完成后返回true;用不超过3个线程处理。下载文件的方法不需要实现publci byte[] getBytesFromUrl(String url); A:Java多线程部分需要进一步学习。查看Java多线程笔记 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859public class FileDownloader { private static final int MAX_THREADS = 3; // 最大线程数 public boolean downloadFiles(String[] urls, String dir) { // 检查目录是否存在,不存在则创建 File directory = new File(dir); if (!directory.exists() && !directory.mkdirs()) { System.err.println("无法创建目录: " + dir); return false; } // 创建线程池(固定大小为3) ExecutorService executor = Executors.newFixedThreadPool(MAX_THREADS); // 创建异步任务列表 List<CompletableFuture<Boolean>> futures = Arrays.stream(urls) .map(url -> downloadAsync(url, dir, executor)) .collect(Collectors.toList()); // 等待所有任务完成并收集结果 boolean allSuccess = futures.stream() .map(CompletableFuture::join) .allMatch(success -> success); // 关闭线程池 executor.shutdown(); return allSuccess; } private CompletableFuture<Boolean> downloadAsync(String url, String dir, ExecutorService executor) { return CompletableFuture.supplyAsync(() -> { try { // 获取文件名(URL最后一个'/'后的部分) String fileName = url.substring(url.lastIndexOf('/') + 1); File outputFile = new File(dir, fileName); // 获取文件内容(题目提供的方法) byte[] data = getBytesFromUrl(url); // 写入文件 try (FileOutputStream fos = new FileOutputStream(outputFile)) { fos.write(data); } return true; } catch (Exception e) { System.err.println("下载失败: " + url + " | 错误: " + e.getMessage()); return false; } }, executor); } // 题目提供的下载方法(实际实现应处理网络请求) private byte[] getBytesFromUrl(String url) throws Exception { // 这里应包含实际的下载逻辑 // 示例中我们返回空数组 return new byte[0]; }} E 服务器性能问题排查Q:你开发的功能模块,发布到生产后,服务器压力激增,怎么排查是什么原因造成的?A:参考以下文章:服务器压力大的解决方案 - yanggb - 博客园服务器压力过大?CPU打满?我来帮你快速检查Linux服务器性能-腾讯云开发者社区-腾讯云","link":"/2025/06/08/%E6%9B%B2%E9%80%9F%E7%A7%91%E6%8A%80%E7%AC%94%E8%AF%95/"},{"title":"java多线程笔记","text":"创建新线程 线程的状态 中断线程 守护线程 线程同步 同步方法 死锁 wait()和notify() ReentrantLock() Condition ReadWriteLock StampedLock Semphore java.util.concurrent包提供的线程安全集合 java.util.concurrent.atomic包提供原子操作 使用线程池 Future CompletableFuture ForkJoin ThreadLocal 虚拟线程 Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。 创建新线程方法1:Java用Thread对象表示一个线程,通过调用start()启动一个新线程;一个线程对象只能调用一次start()方法;线程的执行代码写在run()方法中; 123456789101112131415 // 多线程public class Main { public static void main(String[] args) { Thread t = new MyThread(); t.start(); // 启动新线程 }}class MyThread extends Thread { @Override public void run() { System.out.println("start new thread!"); }} 123456789101112131415161718public class Main { public static void main(String[] args) { Thread t1 = new Thread(new MyRunnable()); Thread t2 = new Thread(() -> { System.out.println("start new thread with lambda."); }); t1.start(); t2.start(); } } class MyRunnable implements Runnable { @Override public void run() { System.out.println("start new thread."); } } 线程优先级 1Thread.setPriority(int n) // 1~10, 默认值5 多线程经常需要读写共享数据,并且需要同步。 线程的状态123456789101112131415161718 ┌─────────────┐ │ New │ └─────────────┘ │ ▼┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌─────────────┐ ┌─────────────┐││ Runnable │ │ Blocked ││ └─────────────┘ └─────────────┘│┌─────────────┐ ┌─────────────┐│ │ Waiting │ │Timed Waiting││└─────────────┘ └─────────────┘│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ▼ ┌─────────────┐ │ Terminated │ └─────────────┘ 线程状态转移图线程状态包含: New:新创建的线程,尚未执行; Runnable:运行中的线程,正在执行run()方法的Java代码; Blocked:运行中的线程,因为某些操作被阻塞而挂起; Waiting:运行中的线程,因为某些操作在等待中; Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待; Terminated:线程已终止,因为run()方法执行完毕。 状态切换 通过对另一个线程对象调用join()方法可以等待其执行结束; 可以指定等待时间,超过等待时间线程仍然没有结束就不再等待; 对已经运行结束的线程调用join()方法会立刻返回。 中断线程123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051// 中断线程public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1); // 暂停1毫秒 t.interrupt(); // 中断t线程 t.join(); // 等待t线程结束 System.out.println("end"); }}class MyThread extends Thread { public void run() { int n = 0; while (! isInterrupted()) { // 必要 n ++; System.out.println(n + " hello!"); } }}// 捕捉Exceptionclass MyThread extends Thread { public void run() { Thread hello = new HelloThread(); hello.start(); // 启动hello线程 try { hello.join(); // 等待hello线程结束 } catch (InterruptedException e) { System.out.println("interrupted!"); } hello.interrupt(); }}class HelloThread extends Thread { public void run() { int n = 0; while (!isInterrupted()) { n++; System.out.println(n + " hello!"); try { Thread.sleep(100); } catch (InterruptedException e) { break; } } }} interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。而t线程的while循环会检测isInterrupted(),所以上述代码能正确响应interrupt()请求,使得自身立刻结束运行run()方法。目标线程检测到isInterrupted()为true或者捕获了InterruptedException都应该立刻结束自身线程; 设置标志位 12345678910111213141516171819202122// 中断线程public class Main { public static void main(String[] args) throws InterruptedException { HelloThread t = new HelloThread(); t.start(); Thread.sleep(1); t.running = false; // 标志位置为false }}class HelloThread extends Thread { public volatile boolean running = true; public void run() { int n = 0; while (running) { n ++; System.out.println(n + " hello!"); } System.out.println("end!"); }} 注意到HelloThread的标志位boolean running是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。[[为什么要对线程间共享的变量用关键字volatile声明?]]volatile关键字的目的是告诉虚拟机: 每次访问变量时,总是获取主内存的最新值; 每次修改变量后,立刻回写到主内存。volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。 守护线程守护线程(Daemon Thread)是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。创建守护线程 123Thread t = new MyThread();t.setDaemon(true); // 标记t.start(); 守护线程不能持有需要关闭的资源(如打开文件等)。 线程同步使用synchronized解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。 我们来概括一下如何使用synchronized: 找出修改共享变量的线程代码块; 选择一个共享实例作为锁; 使用synchronized(lockObject) { ... }。 1234567891011121314151617181920212223242526272829303132333435363738// 多线程public class Main { public static void main(String[] args) throws Exception { var add = new AddThread(); var dec = new DecThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println(Counter.count); }}class Counter { public static final Object lock = new Object(); public static int count = 0;}class AddThread extends Thread { public void run() { for (int i=0; i<10000; i++) { synchronized(Counter.lock) { Counter.count += 1; } } }}class DecThread extends Thread { public void run() { for (int i=0; i<10000; i++) { synchronized(Counter.lock) { Counter.count -= 1; } } }} 在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁: 不需要synchronized JVM规范定义了几种原子操作: 基本类型(long和double除外)赋值,例如:int n = m; 引用类型赋值,例如:List<String> list = anotherList。 单条原子操作不需要同步不可变对象无需同步 同步方法如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe)Java标准库的java.lang.StringBuffer也是线程安全的。还有一些不变类,例如String,Integer,LocalDate,它们的所有成员变量都是final,多线程同时访问时只能读不能写,这些不变类也是线程安全的。类似Math这些只提供静态方法,没有成员变量的类,也是线程安全的。无状态的方法天然线程安全,因为没有共享数据可被竞争。 [[synchronized static 方法的锁机制]] 死锁JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。 两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。**解决方式:**线程获取锁的顺序要一致。 wait()和notify()wait()JVM 源码中的定义:wait() 的本地方法实现在 HotSpot JVM 的 C++ 代码中(如 objectMonitor.cpp),核心逻辑围绕 对象监视器(ObjectMonitor) 展开。 关键步骤: 检查锁状态:确认当前线程持有对象的锁(通过 ObjectMonitor::owner 字段)。 释放锁:通过 ObjectMonitor::exit() 释放锁,允许其他线程进入同步块。 线程挂起:调用操作系统原语(如 pthread_cond_wait 或 WaitForSingleObject)将线程挂起,进入等待队列。 被唤醒后重新竞争锁:当其他线程调用 notify()/notifyAll() 时,线程被移入竞争队列,重新尝试获取锁(通过 ObjectMonitor::enter())因此,只能在锁对象上调用wait()方法。 notify() 唤醒一个等待线程: 当调用 object.notify() 时,会随机唤醒一个正在该对象上通过 object.wait() 阻塞的线程(如果有多个线程在等待)。 不释放锁: notify() 本身不会释放锁,被唤醒的线程需要等到当前线程退出 synchronized 块后,才能重新竞争锁并继续执行。 方法 行为 适用场景 notify() 唤醒一个等待线程 仅需唤醒一个线程(如单生产者-单消费者) notifyAll() 唤醒所有等待线程 需唤醒所有线程(多线程竞争同一资源时) wait() 必须放在 while 循环中,确保被唤醒后条件仍然成立。 notifyAll() 比 notify() 更安全,避免某些线程永远不被唤醒。 ReentrantLock() synchronized关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。 java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁 1234567891011121314public class Counter { private final Lock lock = new ReentrantLock(); private int count; public void add(int n) { lock.lock(); try { count += n; } finally { lock.unlock(); } }} ReentrantLock获取锁更安全; ReentrantLock可以尝试获取锁: 12345678if (lock.tryLock(1, TimeUnit.SECONDS)) { try { ... } finally { lock.unlock(); }} ConditionCondition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,并且其行为也是一样的: await()会释放当前锁,进入等待状态; signal()会唤醒某个等待线程; signalAll()会唤醒所有等待线程; 唤醒线程从await()返回后需要重新获得锁。 此外,和tryLock()类似,await()可以在等待指定时间后,如果还没有被其他线程通过signal()或signalAll()唤醒,可以自己醒来: ReadWriteLock使用ReadWriteLock可以解决这个问题,它保证: 只允许一个线程写入(其他线程既不能写入也不能读取); 没有写入时,多个线程允许同时读(提高性能)。 1234567891011121314151617181920212223242526public class Counter { private final ReadWriteLock rwlock = new ReentrantReadWriteLock(); // 注意: 一对读锁和写锁必须从同一个rwlock获取: private final Lock rlock = rwlock.readLock(); private final Lock wlock = rwlock.writeLock(); private int[] counts = new int[10]; public void inc(int index) { wlock.lock(); // 加写锁 try { counts[index] += 1; } finally { wlock.unlock(); // 释放写锁 } } public int[] get() { rlock.lock(); // 加读锁 try { return Arrays.copyOf(counts, counts.length); } finally { rlock.unlock(); // 释放读锁 } }} StampedLock[[乐观锁与悲观锁]] 乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。 1234567891011121314151617181920212223242526272829303132333435363738public class Point { private final StampedLock stampedLock = new StampedLock(); private double x; private double y; public void move(double deltaX, double deltaY) { long stamp = stampedLock.writeLock(); // 获取写锁 try { x += deltaX; y += deltaY; } finally { stampedLock.unlockWrite(stamp); // 释放写锁 } } public double distanceFromOrigin() { long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁 // 注意下面两行代码不是原子操作 // 假设x,y = (100,200) double currentX = x; // 此处已读取到x=100,但x,y可能被写线程修改为(300,400) double currentY = y; // 此处已读取到y,如果没有写入,读取是正确的(100,200) // 如果有写入,读取是错误的(100,400) if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生 stamp = stampedLock.readLock(); // 获取一个悲观读锁 try { currentX = x; currentY = y; } finally { stampedLock.unlockRead(stamp); // 释放悲观读锁 } } return Math.sqrt(currentX * currentX + currentY * currentY); }} Semphore 如果要对某一受限资源进行限流访问,可以使用Semaphore,保证同一时间最多N个线程访问受限资源。 1234567891011121314151617181920212223242526272829303132333435363738public class Point { private final StampedLock stampedLock = new StampedLock(); private double x; private double y; public void move(double deltaX, double deltaY) { long stamp = stampedLock.writeLock(); // 获取写锁 try { x += deltaX; y += deltaY; } finally { stampedLock.unlockWrite(stamp); // 释放写锁 } } public double distanceFromOrigin() { long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁 // 注意下面两行代码不是原子操作 // 假设x,y = (100,200) double currentX = x; // 此处已读取到x=100,但x,y可能被写线程修改为(300,400) double currentY = y; // 此处已读取到y,如果没有写入,读取是正确的(100,200) // 如果有写入,读取是错误的(100,400) if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生 stamp = stampedLock.readLock(); // 获取一个悲观读锁 try { currentX = x; currentY = y; } finally { stampedLock.unlockRead(stamp); // 释放悲观读锁 } } return Math.sqrt(currentX * currentX + currentY * currentY); }} java.util.concurrent包提供的线程安全集合针对List、Map、Set、Deque等,java.util.concurrent包也提供了对应的并发集合类。我们归纳一下: interface non-thread-safe thread-safe List ArrayList CopyOnWriteArrayList Map HashMap ConcurrentHashMap Set HashSet / TreeSet CopyOnWriteArraySet Queue ArrayDeque / LinkedList ArrayBlockingQueue / LinkedBlockingQueue Deque ArrayDeque / LinkedList LinkedBlockingDeque 还提供了新旧集合转换器 12Map unsafeMap = new HashMap();Map threadSafeMap = Collections.synchronizedMap(unsafeMap); java.util.concurrent.atomic包提供原子操作我们以AtomicInteger为例,它提供的主要操作有: 增加值并返回新值:int addAndGet(int delta) 加1后返回新值:int incrementAndGet() 获取当前值:int get() 用CAS方式设置:int compareAndSet(int expect, int update) Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set。 [[CAS]]是指,在这个操作中,如果AtomicInteger的当前值是prev,那么就更新为next,返回true。如果AtomicInteger的当前值不是prev,就什么也不干,返回false。通过CAS操作并配合do ... while循环,即使其他线程修改了AtomicInteger的值,最终的结果也是正确的。 使用线程池12345678910// 创建固定大小的线程池:ExecutorService executor = Executors.newFixedThreadPool(3);// 提交任务:executor.submit(task1);executor.submit(task2);executor.submit(task3);executor.submit(task4);executor.submit(task5);// 关闭线程池;es.shutdown(); 因为ExecutorService只是接口,Java标准库提供的几个常用实现类有: FixedThreadPool:线程数固定的线程池; CachedThreadPool:线程数根据任务动态调整的线程池; SingleThreadExecutor:仅单线程执行的线程池。 ScheduledThreadPool:(任务定期触发) FutureRunnable接口有个问题,它的方法没有返回值。如果任务需要一个返回结果,那么只能保存到变量。 12345class Task implements Callable<String> { public String call() throws Exception { return longTimeCalculation(); }} 获取结果: 12345678ExecutorService executor = Executors.newFixedThreadPool(4); // 定义任务:Callable<String> task = new Task();// 提交任务并获得Future:Future<String> future = executor.submit(task);// 从Future获取异步执行返回的结果:String result = future.get(); // 可能阻塞 当我们提交一个Callable任务后,我们会同时获得一个Future对象,然后,我们在主线程某个时刻调用Future对象的get()方法,就可以获得异步执行的结果。在调用get()时,如果异步任务已经完成,我们就直接获得结果。如果异步任务还没有完成,那么get()会阻塞,直到任务完成后才返回结果。 一个Future<V>接口表示一个未来可能会返回的结果,它定义的方法有: get():获取结果(可能会等待) get(long timeout, TimeUnit unit):获取结果,但只等待指定的时间; cancel(boolean mayInterruptIfRunning):取消当前任务; isDone():判断任务是否已完成。 CompletableFuture从Java 8开始引入了CompletableFuture,它针对Future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。 Future 和 CompletableFuture 都支持异步计算,但 CompletableFuture 提供了更灵活、更强大的 链式调用(串行执行) 能力,而 Future 在这方面的能力非常有限 Future 无法直接依赖另一个 Future 的结果,必须手动等待前一个 Future 完成,再启动下一个任务。 代码复杂:需要嵌套 get() 调用,容易导致阻塞或回调地狱。 CompletableFuture 提供了 thenApply(), thenCompose(), thenAccept() 等方法,可以 直接在前一个任务完成后触发下一个任务,无需手动等待。 ForkJoinFork/Join是一种基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果。 ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTask或RecursiveAction。 使用Fork/Join模式可以进行并行计算以提高效率。 ThreadLocalThread.currentThread()获取当前线程。 如何在一个线程中传递状态? 这种在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User对象就传不进去了。 Java标准库提供了一个特殊的ThreadLocal,它可以在一个线程中传递同一个对象。 ThreadLocal表示线程的“局部变量”,它确保每个线程的ThreadLocal变量都是各自独立的; ThreadLocal适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递); 使用ThreadLocal要用try ... finally结构,并在finally中清除。why? ThreadLocal 使用注意事项 存储机制 ThreadLocal.set() 将数据存储到当前线程的 ThreadLocalMap中,而非 ThreadLocal 本身。 线程死亡时,ThreadLocalMap 自动销毁,无需手动 remove()。 线程池场景 线程会被复用(如 Tomcat、Spring 线程池),导致 ThreadLocal 数据残留,引发内存泄漏或数据错乱。 必须清理:在 finally 中调用 ThreadLocal.remove(),或使用 AutoCloseable 自动清除。 最佳实践 123456try { threadLocal.set(value); // 业务逻辑...} finally { threadLocal.remove(); // 强制清除,避免线程池复用污染} 或(推荐) 123try (AutoCloseable cleanup = threadLocal.withInitial(value)) { // 业务逻辑...} // 自动调用 remove() 核心原则:只要用线程池,就必须清理 ThreadLocal! 虚拟线程Java 19引入的虚拟线程是为了解决IO密集型任务的吞吐量,它可以高效通过少数线程去调度大量虚拟线程; 虚拟线程在执行到IO操作或Blocking操作时,会自动切换到其他虚拟线程执行,从而避免当前线程等待,能最大化线程的执行效率; 虚拟线程使用普通线程相同的接口,最大的好处是无需修改任何代码,就可以将现有的IO操作异步化获得更大的吞吐能力。 计算密集型任务不应使用虚拟线程,只能通过增加CPU核心解决,或者利用分布式计算资源。","link":"/2025/06/08/java%E5%A4%9A%E7%BA%BF%E7%A8%8B/"}],"tags":[{"name":"面试","slug":"面试","link":"/tags/%E9%9D%A2%E8%AF%95/"},{"name":"java","slug":"java","link":"/tags/java/"}],"categories":[],"pages":[]}