
多线程之八股文一、锁策略加黄部分就是 synchronized锁 的性质1、乐观锁vs悲观锁悲观锁先锁后操作适合高冲突场景乐观锁先操作后检查适合低冲突场景2、重量级锁vs轻量级锁重量级锁是“挂起等待”让出CPU省资源但慢轻量级锁是“自旋”占着CPU死等费资源但快重量级锁 挂起等待轻量级锁 自旋两组概念讲的是同一件事只是角度不同一组强调开销一组强调行为。3、挂起等待锁vs自旋锁挂起等待是“睡觉等”不占CPU但响应慢自旋是“站着等”占CPU但响应快4、公平锁 vs非公平锁公平锁 排队追女神先来后到非公平锁 半路截胡谁抢到是谁的公平锁严格按等待顺序获取锁先来后到非公平锁允许插队后请求的线程可能先获得锁5、可重入锁vs 不可重入锁可重入锁同一线程能反复拿锁不可重入锁会自己锁自己6、普通互斥锁vs 读写锁互斥锁读写都互斥读写锁读不互斥、写才互斥二、JVM 的优化机制这三个概念是 JVM 对synchronized的优化机制目的是在不影响正确性的前提下减少锁带来的性能开销锁升级锁只能升级不能降级无锁 → 偏向锁 → 轻量级锁 → 重量级锁锁升级竞争越激烈锁越重锁消除单线程用的锁直接去掉锁粗化反复开关太费事合并成一次三、CAS操作CAS 是 CPU 级别的原子操作比较并交换成功就改失败就重试优点:不挂起线程指让一个正在运行的线程主动或被动地放弃 CPU 使用权进入“等待”状态、快缺点:ABA 问题、自旋耗 CPU一个值从 A 变成 B 又变回 ACAS 会认为没变过解决方法是加版本号四、Runnable 与 Callable“Runnable 和 Callable 都是任务接口主要区别有两点返回值Callable 有泛型返回值Runnable 是 void异常Callable 可以抛出受检异常Runnable 只能在内部处理。FutureTask是两者的桥梁因为它既实现了 Runnable可被 Thread 或线程池执行又实现了 Future可保存结果使用时把 Callable 包装进 FutureTask交给线程执行最后调用futureTask.get()获取结果五、synchronized 与 ReentrantLocksynchronized是 JVM 层面的关键字用法简单自动加锁解锁但只能非公平、死等ReentrantLock是 Java 类库提供的需要手动lock/unlock优点是支持tryLock可以超时或立即返回支持公平锁还能用Condition实现多个等待队列比如生产者-消费者场景用两个 Condition 分别控制满和空ReentrantLock 不会自动释放锁必须手动调用unlock()而finally能保证无论是否发生异常锁都能被释放总结两者都是可重入锁简单场景用synchronized需要公平锁或超时等待时用ReentrantLock六、SemaphoreSemaphore 是信号量用于控制同时访问资源的线程数。它的acquire()在拿不到许可时会让线程阻塞挂起release()用于归还许可并唤醒等待线程。两者与ReentrantLock的lock()/unlock()类似都是基于 AQS 实现的。和 synchronized 不同的是Semaphore 没有 JVM 层面的偏向锁、自旋锁优化它是直接通过 AQS 进行阻塞唤醒。不过在实际开发中大部分简单同步场景用 synchronized 就够了代码简洁且 JVM 会自动优化。除非需要限流比如数据库连接池控制并发数才会用 Semaphore七、CountDownLatchCountDownLatch 是 JUC 包下的同步工具让一个或多个线程等待直到一组操作完成。核心方法是await()和countDown()await()线程在这里挂起阻塞直到计数器归零countDown()计数器减 1归零时唤醒所有等待的线程常见用法有两个主线程等待子任务主线程await()每个子线程做完调用countDown()多个线程同时启动所有线程先await()由另一个线程一次性countDown()触发和join()的区别join()只能等一个线程死掉CountDownLatch可以等一组操作不一定是线程结束只要调了countDown()就行八、Hashtable 和 ConcurrentHashMapHashtable铺垫痛点:Hashtable 是一把大锁锁整个 Map用synchronized修饰方法同一时间只有一个线程操作性能很差ConcurrentHashMap 的并发优化给每个链表都安排一把锁—— 不同链表的操作互不影响可以并发执行。JDK 1.7 是分段锁16个 Segment相当于 16 把锁JDK 1.8 之后进一步细化为锁单个桶链表头或红黑树根并发度更高。size 使用 CAS 进行更新—— 不需要加锁就能原子更新计数器减少锁竞争。扩容化整为零—— 不是一次性迁移所有数据而是把整个扩容任务拆分成多个小任务每次迁移一部分允许其他线程边读边写边帮忙扩容避免卡顿。九、文件三面试题1、遍历目录基础版package javaee; import java.io.File; //基础版遍历目录 public class Demo23 { public static void main(String[] args) { //绝对路径 File dir new File(E:/my-first-web); //先判断是不是目录 if(!dir.isDirectory()){ System.out.println(目录不存在或不是文件夹 dir.getAbsolutePath()); return; } //1、当前目录下所有文件和目录的名字 String[] names dir.list(); System.out.println(所有文件和目录名字); for(String name : names){ System.out.println(name); } //2、当前目录下所有文件和目录的 File 对象 File[] files dir.listFiles(); System.out.println(所有对象); for (File file : files){ if(file.isFile()){ System.out.println([文件] file.getName()); } if (file.isDirectory()){ System.out.println([目录] file.getName()); } } } }这是一个基础版本的文件遍历若想更装杯可以使用递归来写2、复制文件package javaee; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.Scanner; public class Demo24 { public static void main(String[] args) throws IOException { Scanner scanner new Scanner(System.in); System.out.print(请输入要复制的文件绝对路径 OR 相对路径); String sourcePath scanner.next(); //创建原文件对象 File sourceFile new File(sourcePath); if (!sourceFile.exists()) { System.out.println(文件不存在请确认路径是否正确); return; } if (!sourceFile.isFile()) { System.out.println(文件不是普通文件请确认路径是否正确); return; } System.out.print(请输入要复制的目标路径绝对路径 OR 相对路径); String destPath scanner.next(); //创建复制完成的文件对象 File destFile new File(destPath); if (destFile.exists()) { if (destFile.isDirectory()) { System.out.println(目标路径已经存在并且是一个目录请确认路径是否正确); return; } if (destFile.isFile()) { System.out.print(目标文件已经存在是否要进行覆盖(y/n)); String ans scanner.next(); if (ans.equals(y)) { System.out.println(开始覆盖复制...); } else { System.out.println(停止复制); return; } } } // 真正的复制代码 try (FileInputStream fis new FileInputStream(sourceFile); FileOutputStream fos new FileOutputStream(destFile)) { byte[] buffer new byte[8192]; int len; while ((len fis.read(buffer)) ! -1) { fos.write(buffer, 0, len); } } System.out.println(复制完成目标文件位置 destFile.getAbsolutePath()); } }3、复制整个目录package javaee; import java.io.*; import java.util.*; public class Demo25 { public static void main(String[] args) throws IOException { Scanner scanner new Scanner(System.in); System.out.print(请输入要复制的源目录); String srcPath scanner.next(); File srcDir new File(srcPath); if (!srcDir.isDirectory()) { System.out.println(源目录不存在或不是文件夹); return; } System.out.print(请输入目标目录); String destPath scanner.next(); File destDir new File(destPath); // 调用复制目录的方法遍历 复制 copyDirectory(srcDir, destDir); System.out.println(目录复制完成); } // 复制目录结合了遍历 复制 public static void copyDirectory(File srcDir, File destDir) throws IOException { // 1. 创建目标目录遍历的第一步 if (!destDir.exists()) { destDir.mkdirs(); } // 2. 遍历源目录来自 Demo23 的思想 File[] children srcDir.listFiles(); if (children ! null) { for (File child : children) { File destChild new File(destDir, child.getName()); if (child.isFile()) { // 3. 复制文件来自 Demo24 的核心代码 copyFile(child, destChild); } else if (child.isDirectory()) { // 4. 遇到子目录递归调用自己继续遍历 复制 copyDirectory(child, destChild); } } } } // 复制单个文件来自 Demo24 的核心代码 private static void copyFile(File src, File dest) throws IOException { try (FileInputStream fis new FileInputStream(src); FileOutputStream fos new FileOutputStream(dest)) { byte[] buffer new byte[8192]; int len; while ((len fis.read(buffer)) ! -1) { fos.write(buffer, 0, len); } } System.out.println(复制文件 src.getName()); } }相当于前两者的结合十、网络基本盘1、OSI 七层 / TCP/IP 四层网络一般看TCP/IP 四层模型应用层、传输层、网络层、网络接口层。数据发送时从上往下封装每层加报头接收时从下往上分用每层拆报头2、Socket编程Socket 是应用层与传输层之间的桥梁让你不用关心下面两层网络层、网络接口层的细节TCP/IP 四层负责什么Socket 的角色应用层你的程序浏览器、微信等你写代码的地方↑↑↑↑↑↑↑↑↑↑↑↑↑Socket 接口在这里↑↑↑↑↑↑↑↑↑↑↑↑↑传输层TCP/UDP端口Socket 帮你调用网络层IP路由Socket 帮你调用网络接口层MAC、网卡物理传输Socket 帮你调用3、TCP 和 UDP 有什么区别主要有三点区别连接TCP 有连接通信前要先保存对方的 IP 和端口三次握手UDP 无连接不保存对方信息直接发可靠性TCP 可靠传输牺牲效率有确认和重传UDP 不可靠丢了不管数据方式TCP 面向字节流没有边界UDP 面向数据报有边界另外两者都是全双工可以同时收发即同一时刻双向通信4、TCP 和 UDP 在“数据传输方式”上的解释TCP面向字节流想发送 100 个字节可以 1 次全发也可以分 10 次每次发 10 字节还可以分 100 次每次发 1 字节本质TCP 不关心你分几次发只关心你发了多少字节。接收方也按字节流读不知道你原来分了几次类比像水管里的水你倒一桶水进去出来也是一桶水但中间是不分“块”的UDP面向数据报一次必须是发送/接收一个完整的 UDP 数据报不能是半个本质UDP 有边界发的时候是一个完整的包收的时候也必须一个完整的包收类比像寄快递你发一个包裹对方收到一个包裹不会收到半个5、TCP和UDP的Socket实现区别代码实现对比维度TCP SocketUDP Socket核心类ServerSocket/SocketDatagramSocket数据传输基于流Stream数据无边界基于数据报Datagram每个包独立连接方式需要accept()建立连接阻塞无需连接直接发收发方法getInputStream()/getOutputStream()send(DatagramPacket)/receive(DatagramPacket)丢包处理底层自动重传保证到达不保证可能丢包/乱序服务端标识每个客户端有独立Socket只有一个DatagramSocket靠包内地址区分追问1那TCP服务端怎么同时处理多个客户端①多线程优点简单直观每个客户端逻辑独立缺点1个客户端 1个线程1000个客户端 1000个线程线程切换开销大内存占用高每个线程约1MB栈空间C10K问题1万并发就扛不住了再问线程池能解决吗能缓解但不能根治。线程池限制了最大线程数超出就得排队或拒绝本质还是一个连接一个线程的模式②IO多路复用优点一个线程能管理成千上万个连接没有线程切换开销内存占用低一个线程同时监控多个socket哪个有数据就读哪个追问2epoll的ET vs LT?边缘触发 vs 水平触发模式一句话解释行为LT水平触发有数据就一直通知你缓冲区有数据每次调用epoll_wait都会通知ET边缘触发新数据来了只通知一次只在状态从无数据→有数据时通知一次回答LT是默认模式更安全但可能重复通知ET性能更高但要求用户一次性把数据读完否则数据会丢。Redis、Nginx用ET追求极致性能本章完