质感企业网站导航用ps怎么做,一级建造师找工作网站,罗湖网站建设价格,人们做网站怎么赚钱干货分享#xff0c;感谢您的阅读#xff01;背景高频面试题基本总结回顾#xff08;含笔试高频算法整理#xff09;
最全文章见#xff1a;Java高频面试基础知识点整理
#xff08;一#xff09;Java基础高频知识考点 针对人员#xff1a; 1.全部人员都…干货分享感谢您的阅读背景高频面试题基本总结回顾含笔试高频算法整理
最全文章见Java高频面试基础知识点整理
一Java基础高频知识考点 针对人员 1.全部人员都适用 2.正式员工针对应届工作1-3年的面试者本部分可部分考察 3.外包员工本部分一般加大比重。 1.基本类型和包装类区别
数据类型
基本类型包括byte、short、int、long、float、double、char和boolean等8种基本数据类型。它们是直接存储数据值的不具有方法和属性。包装类对应于每种基本类型Java提供了相应的包装类例如Byte、Short、Integer、Long、Float、Double、Character和Boolean等。包装类是引用类型它们包装了对应基本类型的值并提供了一些方法和属性来操作这些值。
对象和存储
基本类型基本类型的变量直接存储在栈中它们的值是直接存储的没有指向其他对象的引用。包装类包装类是对象存储在堆中当创建包装类对象时会在堆中分配内存并将基本类型的值封装到包装类中。
自动装箱和拆箱
自动装箱Java提供了自动装箱机制即在需要包装类对象的地方可以直接使用基本类型系统会自动将其转换为对应的包装类对象。拆箱同样当需要基本类型的值时可以直接使用包装类对象系统会自动将其转换为对应的基本类型值。
空值处理
基本类型基本类型不能存储空值null因为它们不是对象没有引用。包装类包装类可以存储空值null可以用于表示缺失数据或特殊情况。
性能
基本类型由于基本类型直接存储值因此处理速度更快占用内存较少。包装类由于包装类是对象需要额外的内存开销并且在自动装箱和拆箱过程中会涉及到一些性能消耗。
在实际开发中通常优先使用基本类型只有在需要对象特性例如泛型或集合中才会使用对应的包装类。自动装箱和拆箱的特性使得基本类型和包装类之间的转换变得更加方便。
2.实例方法和静态方法有什么不一样
实例方法是与类的实例相关联的需要先创建类的实例才能调用而静态方法则不依赖于类的实例可以直接通过类名来调用。两者在访问权限、内存分配、重写和多态性等方面也有所不同。
特点实例方法静态方法调用方式通过类的实例调用直接通过类名调用方法访问权限可以访问实例变量和实例方法不能直接访问实例变量和实例方法内存分配每次调用会分配内存来存储实例变量不会分配内存来存储实例变量重写与隐藏可以被子类重写不能被子类重写但可以被子类隐藏多态性受对象实际类型影响在编译时就确定了调用的方法
3.Java指向的是引用还是地址怎么理解高频考点
在Java中变量存储的是引用而不是直接的内存地址。理解Java中变量指向的是引用而不是地址
内存管理Java的内存管理是由垃圾回收器负责的它通过对引用的追踪和分析来确定对象的生命周期和回收时机。因此Java中的变量存储的是对象的引用而不是直接的内存地址。对象生命周期Java中的对象在堆内存中分配和销毁而引用则用于访问这些对象。变量存储的引用指向对象所在的内存空间可以通过引用来操作和访问对象的成员变量和方法。引用的赋值和传递在Java中通过将引用赋值给变量或将引用传递给方法来操作对象。这意味着变量和方法参数存储的是引用使得我们可以在不直接操作对象内存地址的情况下对对象进行操作。引用的可变性在Java中引用是可变的即可以通过改变引用的指向来指向不同的对象。这使得在程序执行过程中可以改变对象的访问方式而不需要直接操作对象的内存地址。
因此虽然在语义上可以说Java的变量指向对象的内存地址但更准确地说Java的变量存储的是引用用于访问对象。这种引用的使用方式使得Java具有更高层次的内存管理和安全性同时提供了更好的抽象和封装性。
4.Object类内的方法高频考点
Object是所有类的父类任何类都默认继承Object。Object类到底实现了哪些方法
clone方法保护方法实现对象的浅复制只有实现了Cloneable接口才可以调用该方法否则抛出CloneNotSupportedException异常。
getClass方法final方法获得运行时类型。
toString方法该方法用得比较多一般子类都有覆盖。
finalize方法该方法用于释放资源。因为无法确定该方法什么时候被调用很少使用。
equals方法该方法是非常重要的一个方法。一般equals和是不一样的但是在Object中两者是一样的。子类一般都要重写这个方法。
hashCode方法该方法用于哈希查找重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。一般必须满足obj1.equals(obj2)true。可以推出obj1.hashCode()obj2.hashCode()但是hashCode相等不一定就满足equals。不过为了提高效率应该尽量使上面两个条件接近等价。
wait方法wait方法就是使当前线程等待该对象的锁当前线程必须是该对象的拥有者也就是具有该对象的锁。wait()方法一直等待直到获得锁或者被中断。wait(long timeout)设定一个超时间隔如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态直到以下事件发生。
1其他线程调用了该对象的notify方法。
2其他线程调用了该对象的notifyAll方法。
3其他线程调用了interrupt中断该线程。
4时间间隔到了。
此时该线程就可以被调度了如果是被中断的话就抛出一个InterruptedException异常。
notify方法该方法唤醒在该对象上等待的某个线程。
notifyAll方法该方法唤醒在该对象上等待的所有线程。
5.hashCode方法的作用
hashCode() 方法是 Java 中 Object 类的一个方法用于返回对象的哈希码散列码。它的作用是为了提高哈希表如 HashMap、HashSet 等的性能。哈希码是一种用于快速定位对象存储位置的技术。在使用哈希表时对象被存储在哈希桶数组中哈希表会根据对象的哈希码来确定对象存储的位置以便快速查找、插入或删除对象。
hashCode() 方法的具体作用包括 作为哈希表的索引 哈希表使用对象的哈希码来确定对象存储在数组中的位置。每个对象都有一个哈希码哈希表会根据对象的哈希码来计算出存储位置以便快速定位对象。 作为对象在集合中的唯一标识 在使用集合类如 HashSet、HashMap 等时对象的哈希码用于检查对象是否已经存在于集合中。如果两个对象的哈希码相同集合类会进一步调用对象的 equals() 方法来比较对象的内容是否相同。 作为对象在分布式系统中的唯一标识 在分布式系统中对象的哈希码可以用于分片Sharding和路由等操作以便将对象均匀分布在不同的节点上提高系统的扩展性和性能。
因此实现良好的 hashCode() 方法可以提高哈希表的性能和效率并且能够在集合中正确地处理对象的唯一性和相等性。
6.Java中一个字符占多少个字节扩展再问int、 long、double占多少字节
在 Java 中一个字符占用 2 个字节即 16 位。这是因为 Java 使用 Unicode 字符集来表示字符其中每个字符都用 16 位表示因此一个字符占用 2 个字节。
至于其他基本数据类型的大小可以根据 Java 虚拟机规范来确定
int 一个 int 类型占用 4 个字节32 位范围为 -2^31 到 2^31-1。long 一个 long 类型占用 8 个字节64 位范围为 -2^63 到 2^63-1。double 一个 double 类型占用 8 个字节64 位表示双精度浮点数。
需要注意的是Java 虚拟机规范中并没有强制规定各种基本数据类型的大小而是要求实现者根据规范的要求来实现。因此以上大小是 Java 中常见的实现方式但并非所有的 Java 虚拟机都必须按此方式实现。
7.Boolean占几个字节
未精确定义字节。
首先在Java中定义的八种基本数据类型中除了其它七种类型都有明确的内存占用字节数外就boolean类型没有给出具体的占用字节数因为对虚拟机来说根本就不存在 boolean 这个类型boolean类型在编译后会使用其他数据类型来表示。
boolean类型没有给出精确的定义《Java虚拟机规范》给出了4个字节和boolean数组1个字节的定义具体还要看虚拟机实现是否按照规范来所以1个字节、4个字节都是有可能的。这其实是运算效率和存储空间之间的博弈两者都非常的重要。
8.Exception和Error
Exception和Error都是继承了Throwable类在java中只有Throwable类型的实例才可以被抛出throw或者捕获catch他是异常处理机制的基本组成类型。Exception和Error体现了java平台设计者对不同异常情况的分类Exception是程序正常运行中可以预料的意外情况可能并且应该被捕获进行相应的处理。Error是指正常情况下不大可能出现的情况绝大部分的Error都会导致程序比如JVM自身处于非正常状态不可恢复状态。既然是非正常情况所以不便于也不需要捕获常见的比如OutOfMemoryError之类都是Error的子类。Exception又分为可检查checked异常和不检查unchecked异常可检查异常在源码里必须显示的进行捕获处理这里是编译期检查的一部分。前面我们介绍的不可查的Error是Throwable不是Exception。不检查异常就是所谓的运行时异常类似NullPointerException,ArrayIndexOutOfBoundsExceptin之类通常是可以编码避免的逻辑错误具体根据需要来判断是否需要捕获并不会在编译器强制要求。
9.和equals的区别
在 Java 中 运算符和 equals() 方法是用于比较对象之间的差异的两种不同方式。 运算符 运算符用于比较两个对象的引用是否指向同一个内存地址即判断两个对象是否是同一个对象的引用。当 运算符用于比较基本数据类型时它会比较它们的值。在比较对象时 比较的是对象的引用地址如果两个对象的引用地址相同则返回 true表示这两个对象是同一个对象如果引用地址不同则返回 false。
equals() 方法
equals() 方法是 Object 类的一个方法用于比较两个对象的内容是否相等即判断两个对象是否逻辑上相等。在 Object 类中的默认实现中equals() 方法的行为与 运算符相同即比较对象的引用地址。但是许多 Java 类库中的类如 String、Integer 等会重写 equals() 方法以便比较对象的内容而不是引用地址。 运算符用于比较两个对象的引用地址是否相同而 equals() 方法用于比较两个对象的内容是否相同。在实际应用中如果需要比较对象的内容通常应该使用 equals() 方法而不是 运算符。
10.String strhello world和String strnew String(hello world)的区别高频考点
String str“hello world”
通过直接赋值的形式可能创建一个或者不创建对象如果hello world在字符串池中不存在会在java字符串池中创建一个String对象“hello world”,常量池中的值不能有重复的所以当你通过这种方式创建对象的时候java虚拟机会自动的在常量池中搜索有没有这个值如果有的话就直接利用他的值如果没有他会自动创建一个对象所以str指向这个内存地址无论以后用这种方式创建多少个值为”hello world”的字符串对象始终只有一个内存地址被分配。
String strnew String(“hello world”)
通过new 关键字至少会创建一个对象也有可能创建两个。
因为用到new关键字肯定会在堆中创建一个String对象如果字符池中已经存在hello world则不会在字符串池中创建一个String对象如果不存在则会在字符串常量池中也创建一个对象。他是放到堆内存中的这里面可以有重复的所以每一次创建都会new一个新的对象所以他们的地址不同。
String 有一个intern() 方法native用来检测在String pool是否已经有这个String存在。
11.StringBuffer和StringBuilder的区别是什么性能对比如何鉴定线程安全高频考点
基本对比
StringString对象是不可变的。“对String对象的任何改变都不影响到原对象相关的任何change操作都会生成新的对象”。StringBuilderStringBuilder是可变的它不是线程安全的。StringBufferStringBuffer也是可变的它是线程安全的所以它的开销比StringBuilder大
使用时的建议
循环外字符串拼接可以直接使用String的操作没有必要通过StringBuilder进行append.有循环体的话好的做法是在循环外声明StringBuilder对象在循环内进行手动append。不论循环多少层都只有一个StringBuilder对象。当字符串相加操作较多的情况下建议使用StringBuilder如果采用了多线程则使用StringBuffer。
如何鉴定线程安全
查看源代码便一目了然事实上StringBuilder和StringBuffer类拥有的成员属性以及成员方法基本相同区别是StringBuffer类的成员方法前面多了一个关键字synchronized不用多说这个关键字是在多线程访问时起到安全保护作用的,也就是说StringBuffer是线程安全的。
12.StringBuffer 和 StringBuilder 底层怎么实现的高频考点
StringBuffer 和 StringBuilder 都是可变的字符串类它们底层的实现方式略有不同
StringBuffer
StringBuffer 是线程安全的可变字符串类它的底层实现使用字符数组char[]来存储字符串内容。StringBuffer 内部维护了一个字符数组value以及表示字符串长度的变量count。当进行字符串操作时StringBuffer 会根据需要动态调整字符数组的大小以容纳更多的字符。StringBuffer 的操作方法如追加、插入、删除等会对字符数组进行修改并更新字符串的长度。
StringBuilder
StringBuilder 也是可变字符串类它与 StringBuffer 的区别在于线程安全性。StringBuilder 是非线程安全的因此在多线程环境中使用时需要进行额外的同步措施。StringBuilder 的底层实现与 StringBuffer 类似同样使用字符数组来存储字符串内容并根据需要动态调整数组大小。
无论是 StringBuffer 还是 StringBuilder它们的底层实现都是通过字符数组进行字符串的存储和操作。通过动态调整字符数组的大小它们可以高效地进行字符串的修改和拼接操作。使用字符数组存储字符串内容的好处是可以避免频繁创建新的字符串对象从而提高性能和内存利用率。
13.switch支持哪些数据类型支持long么高频考点
在 Java 中switch 语句支持的数据类型有限包括整型数据和枚举类型。具体来说switch 语句支持的数据类型有
整型数据类型byte、short、int 和 char。枚举类型自 Java 5 起switch 语句也支持枚举类型。
对于 long 类型数据switch 语句是不支持的。如果需要在 switch 语句中使用 long 类型数据可以考虑将其转换为 int 或 byte 类型或者使用一系列的 if-else 语句来实现相同的逻辑。
14.创建一个类的实例都有哪些办法高频考点
在 Java 中创建一个类的实例对象有以下几种常见的方式 使用 new 关键字 最常见的创建对象的方式是使用 new 关键字通过调用类的构造方法来创建对象。例如 MyClass obj new MyClass(); 使用反射机制 Java 提供了反射机制可以在运行时动态地创建对象。通过 Class 类的 newInstance() 方法来创建对象。例如 Class? clazz MyClass.class;
MyClass obj (MyClass) clazz.newInstance(); 通过对象克隆 如果一个类实现了 Cloneable 接口并且重写了 clone() 方法那么可以通过对象的克隆来创建新的对象。例如 MyClass obj1 new MyClass();
MyClass obj2 (MyClass) obj1.clone(); 通过反序列化 可以将对象序列化为字节流然后再反序列化为对象。通过 ObjectInputStream 类的 readObject() 方法来创建对象。例如 FileInputStream fileIn new FileInputStream(object.ser);
ObjectInputStream in new ObjectInputStream(fileIn);
MyClass obj (MyClass) in.readObject(); 使用工厂方法或者设计模式 可以使用工厂方法模式、建造者模式等设计模式来创建对象以封装对象的创建过程。例如 MyClass obj MyClassFactory.createInstance(); 使用匿名类 可以通过定义匿名类来创建对象尤其在创建接口实例时较为常见。例如 Runnable runnable new Runnable() { public void run() {// 实现 run 方法 }
};
以上是 Java 中创建对象的常见方式可以根据具体的需求和场景选择合适的方式。
15.final、finnally、finalize的区别是什么
finalfinallyfinalize之间长得像但一点关系都没有仅仅是长的像
final 表示不可修改的可以用来修饰类方法变量。
final修饰class表示该class不可以被继承。inal修饰方法表示方法不可以被overrride重写。final修饰变量表示变量是不可以修改。一般来说推荐将本地变量成员变量固定的静态变量用final修饰明确是不可以被修改的。
finally是Java的异常处理机制中的一部分。finally块的作用就是为了保证无论出现什么情况finally块里的代码一定会被执行。
一般来说在try-catch-finally 来进行类似关闭 JDBC连接释放锁等资源的操作。如果try语句块里有return语句那么finally还会被执行吗答案是肯定的。
finalize是Object类的一个方法是GC进行垃圾回收前要调用的一个方法。
如果实现了非空的这个方法那么会导致相应对象回收呈现数量级上的变慢在新版的JDK中好像是1.9之后的版本这个方法已经逐渐被抛弃了。
16.Jdk1.8/Jdk1.7都分别新增了哪些特性其他版本呢高频考点
Java 8新增特性
Lambda表达式引入了函数式编程的概念使得代码更简洁、可读性更高。Stream API提供了一种更便利的处理集合数据的方式支持并行处理。默认方法Default Methods接口中可以定义默认实现允许在接口中添加新方法而不破坏现有实现类的兼容性。方法引用Method References可以通过方法的名字来引用已存在的方法。Optional类提供了一种更好的处理可能为null的对象的方式避免了空指针异常。新的日期/时间APIjava.time包提供了更好的日期和时间处理方式解决了旧的日期API的一些问题。CompletableFuture类新增的异步编程工具支持更方便地处理异步任务和回调。
Java 7新增特性
switch语句支持字符串类型可以在switch语句中使用字符串进行比较。泛型实例化类型自动推断在创建泛型对象时可以省略泛型类型的重复声明。try-with-resources语句用于自动关闭实现了AutoCloseable接口的资源避免了手动关闭资源的繁琐操作。改进的类型推断在实例化泛型对象时编译器可以根据上下文推断出泛型的类型。数字字面量下划线支持可以在数字字面量中使用下划线分隔以提高可读性。
除了Java 8和Java 7其他版本的Java也都引入了一些新特性和改进其中一些主要的特性包括
Java 9
模块化系统Project JigsawJShell交互式解释器Reactive Streams API改进的Stream API私有接口方法改进的垃圾收集器
Java 10
局部变量类型推断基于时间的版本号Release Versioning并行全垃圾回收器
Java 11
HTTP Client API局部变量语法增强ZGC垃圾回收器Epsilon垃圾收集器
Java 12
Switch表达式增强新的垃圾收集器ShenandoahMicrobenchmark Suite
Java 13
文本块Text Blocks动态CDS归档Dynamic CDS ArchivesZGC并发压缩
Java 14
Switch表达式增强Pattern匹配垃圾回收器改进Records记录类
Java 15
Sealed Classes密封类Text Blocks改进垃圾回收器改进隐藏类
Java 16
隐藏类Hidden ClassesPattern匹配Records改进新的垃圾回收器ZGC并发压缩UNIX套接字通道的改进可见注释
Java 17
Sealed Classes密封类改进Pattern匹配增强垃圾回收器改进向后兼容性保持升级Elasticsearch版本
Java 18计划中
目前尚未发布具体特性尚未确定。
这些是Java 9到18版本的一些重要新增特性和改进。请注意每个版本可能还包含了其他小的改进、修复和性能优化。建议参考官方文档和相关资源以获取更详细和全面的信息。
17.简单说下Lambda表达式其解决了什么相比java7的处理优化了什么
Lambda 表达式是 Java 8 引入的一个重要特性它提供了一种更简洁、更灵活的方式来编写匿名函数。Lambda 表达式的引入主要解决了以下两个问题并在某些情况下优化了代码。 匿名内部类的冗余代码 在 Java 7 及之前的版本中要实现一个简单的功能常常需要编写大量的匿名内部类。这些类会增加代码量并使代码显得冗余。Lambda 表达式通过简化匿名内部类的写法让开发者能够更紧凑地表达逻辑减少冗余代码。 代码可读性和可维护性 Lambda 表达式使代码更具可读性。通过将逻辑放在更接近使用它的地方可以更清晰地传达代码的意图。这使得代码更易于理解和维护。
相比 Java 7 的方式Lambda 表达式的引入在以下几个方面进行了优化 简洁性 使用 Lambda 表达式可以大大减少冗余的语法让代码更加紧凑。特别是在处理集合、流式处理以及函数式编程方面代码的可读性和简洁性得到了明显的提升。 迭代集合的优化 在 Java 7 中迭代集合需要通过 foreach 循环或迭代器来完成而 Lambda 表达式和 Stream API 让集合的处理变得更加优雅同时还能够自动利用多核处理器进行并行处理。 函数式编程 Lambda 表达式为 Java 引入了函数式编程的元素使得在 Java 中更容易表达和使用函数式概念如高阶函数、闭包等。
总之Lambda 表达式的引入使 Java 编程更具现代化和函数式特性使代码更具可读性、简洁性同时提供了更好的性能优化和并行处理能力。这对于简化开发和编写高效代码都具有积极影响。
18.有人说“Lambda能让Java程序慢30倍”你怎么看
在实际运行中基于 Lambda/Stream 的版本lambdaMaxInteger比传统的 for-each 版本forEachLoopMaxInteger慢很多。
// 一个大的ArrayList内部是随机的整形数据
volatile ListInteger integers …// 基准测试1
public int forEachLoopMaxInteger() {int max Integer.MIN_VALUE;for (Integer n : integers) {max Integer.max(max, n);}return max;
}// 基准测试2
public int lambdaMaxInteger() {return integers.stream().reduce(Integer.MIN_VALUE, (a, b) - Integer.max(a, b));
}第一基准测试是一个非常有效的通用手段让我们以直观、量化的方式判断程序在特定条件下的性能表现。
第二基准测试必须明确定义自身的范围和目标否则很有可能产生误导的结果。前面代码片段本身的逻辑就有瑕疵更多的开销是源于自动装箱、拆箱auto-boxing/unboxing而不是源自 Lambda 和 Stream所以得出的初始结论是没有说服力的。
第三虽然 Lambda/Stream 为 Java 提供了强大的函数式编程能力但是也需要正视其局限性
一般来说我们可以认为 Lambda/Stream 提供了与传统方式接近对等的性能但是如果对于性能非常敏感就不能完全忽视它在特定场景的性能差异了例如初始化的开销。 Lambda 并不算是语法糖而是一种新的工作机制在首次调用时JVM 需要为其构建CallSite实例。这意味着如果 Java 应用启动过程引入了很多 Lambda 语句会导致启动过程变慢。其实现特点决定了 JVM 对它的优化可能与传统方式存在差异。增加了程序诊断等方面的复杂性程序栈要复杂很多Fluent 风格本身也不算是对于调试非常友好的结构并且在可检查异常的处理方面也存在着局限性等。
19.SpI和API区别是什么SpI底层实现是什么
APIApplication Programming Interface是一组定义了程序之间如何交互的规则和协议提供了访问和使用某个软件组件、库或服务的接口。API 描述了如何调用和使用已经存在的功能。开发者可以通过调用 API 中的函数、方法等来使用底层的功能而不需要关心具体的实现细节。
SPIService Provider Interface则是一种设计模式它用于在软件中提供可扩展的功能实现。SPI 允许开发者在不修改核心代码的情况下通过插件或扩展点来增加或替换功能的实现。在 SPI 中核心代码定义了一组接口或抽象类而实际的实现则由不同的服务提供者来提供。这种设计方式使得系统的扩展性更好可以更容易地添加新的功能实现。
APIApplication Programming Interface
定义了如何与已经存在的功能或服务进行交互。提供了使用已有功能的方法、函数、类等。关注于如何正确地使用已有功能而不关心实现细节。使用 API 可以调用现有功能但不可以随意添加新的实现。
SPIService Provider Interface
是一种设计模式用于实现插件化的扩展机制。允许在不修改核心代码的情况下通过插件添加或替换功能的实现。定义了一组接口或抽象类具体的实现由不同的服务提供者提供。使用 SPI 可以动态地扩展和替换系统的功能实现。
在 Java 中SPI 的底层实现通常是通过在 META-INF/services/ 目录下创建配置文件其中列出了实现了某个接口的类的全限定名。这些配置文件被加载器读取以实现在运行时发现并加载不同的服务提供者。Java 标准库中的许多功能如日志、数据库驱动、XML 解析器等都使用了 SPI 设计模式来实现可扩展性。
20.深克隆和浅克隆考频不多
深克隆Deep Clone和浅克隆Shallow Clone是针对对象克隆Clone操作的两种方式
浅克隆Shallow Clone
浅克隆是指在克隆过程中只复制对象本身和对象内部的基本数据类型字段而不复制对象内部的引用类型字段。被复制的对象和原始对象中的引用类型字段将指向同一块内存地址。如果原始对象中存在引用类型字段那么浅克隆后的对象中的引用类型字段与原始对象中的引用类型字段指向相同的对象。浅克隆的克隆对象和原始对象共享相同的引用对象因此对克隆对象或原始对象的引用类型字段进行修改会影响到另一个对象。
深克隆Deep Clone
深克隆是指在克隆过程中不仅复制对象本身还会递归复制对象内部的引用类型字段所引用的对象直到所有相关的对象都被复制。深克隆后的对象和原始对象是完全独立的它们的内部引用对象是不同的不会相互影响。深克隆的克隆对象和原始对象之间不存在共享的引用对象因此对克隆对象或原始对象的引用类型字段进行修改不会影响到另一个对象。
浅克隆通常比深克隆操作快速和简单但是在某些情况下可能会导致意外的行为因为克隆对象和原始对象之间共享引用对象。在需要完全独立的对象副本时深克隆是更可靠的选择。
21.伪共享机制简述分析
伪共享False Sharing是一种硬件和软件交互的现象它可能对多线程程序的性能产生负面影响。
下面是对伪共享机制的简要分析
伪共享通常发生在多个线程同时访问不同但位于同一缓存行Cache Line的数据时。缓存行是计算机内存中缓存的最小单位通常是64字节。当多个线程同时修改或读取不同的数据但这些数据位于同一缓存行时就会引发伪共享问题。
当一个线程修改缓存行中的某个数据时该缓存行会被标记为”脏”并且会将整个缓存行的数据刷新到主内存中。这将导致其他线程对于同一缓存行中的数据的缓存失效即其他线程需要从主内存重新加载该缓存行的数据。这种缓存失效的频繁发生会导致性能下降。
伪共享问题的解决方案之一是通过对共享的数据进行填充Padding使得不同线程访问的数据分散到不同的缓存行上从而避免了不必要的缓存行失效。填充可以通过在数据结构中添加额外的空间或使用特定的对齐方式来实现。
另一种解决伪共享问题的方法是使用缓存行对齐Cache Line Alignment技术。这种技术通过将数据结构的每个成员对齐到缓存行的边界确保不同线程访问的数据位于不同的缓存行中减少了缓存行的失效次数。
总而言之伪共享是由于多个线程同时访问同一缓存行中不同数据而导致的性能问题。通过填充和缓存行对齐等技术可以减少伪共享对多线程程序性能的影响提高系统的并发性能。
22.假设引用了一个第三方的jar 有个类和我自己写的代码类一样那么在类加载机制过程中是如何处理的高频考点
当在类加载机制中遇到同名类的情况时Java 虚拟机JVM会根据双亲委派模型来进行处理。这个模型要求除了顶层的启动类加载器Bootstrap ClassLoader外每个类加载器在加载类时首先委托其父类加载器进行加载只有在父类加载器无法加载该类时才由当前类加载器自行加载。
具体来说如果在加载某个类时遇到同名类JVM 会按照以下步骤进行处理 委派给父类加载器 当前类加载器会首先委派给父类加载器进行加载。父类加载器会按照双亲委派模型先尝试从自己的缓存中查找已加载的类如果找到了则直接返回如果没有找到则继续委派给其父类加载器加载。 依次向上委派 类加载请求会依次向上委派直到达到顶层的启动类加载器。如果所有父类加载器都无法加载该类则当前类加载器会尝试自己加载该类。 本地加载 当前类加载器在自己的类路径下查找并加载该类。如果找到了同名类则直接加载如果没有找到则抛出类未找到异常ClassNotFoundException。
综上所述如果第三方的 jar 包中包含了与自己代码中相同名称的类首先会由系统类加载器或者扩展类加载器进行加载只有当这两个类加载器都无法找到该类时才会由自定义的类加载器加载。这样可以确保在运行时不会混淆相同名称的类。
23.Java提供了哪些IO方式 NIO如何实现多路复用
Java IO 方式有很多种基于不同的 IO 抽象模型和交互方式可以进行简单区分。
第一传统的 java.io 包它基于流模型实现提供了我们最熟知的一些 IO 功能比如 File 抽象、输入输出流等。交互方式是同步、阻塞的方式也就是说在读取输入流或者写入输出流时在读、写动作完成之前线程会一直阻塞在那里它们之间的调用是可靠的线性顺序。
java.io 包的好处是代码比较简单、直观缺点则是 IO 效率和扩展性存在局限性容易成为应用性能的瓶颈。
很多时候人们也把 java.net 下面提供的部分网络 API比如 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库因为网络通信同样是 IO 行为。
第二在 Java 1.4 中引入了 NIO 框架java.nio 包提供了 Channel、Selector、Buffer 等新的抽象可以构建多路复用的、同步非阻塞 IO 程序同时提供了更接近操作系统底层的高性能数据操作方式。
第三在 Java 7 中NIO 有了进一步的改进也就是 NIO 2引入了异步非阻塞 IO 方式也有很多人叫它 AIOAsynchronous IO。异步 IO 操作基于事件和回调机制可以简单理解为应用操作直接返回而不会阻塞在那里当后台处理完成操作系统会通知相应线程进行后续工作。
24.谈谈接口和抽象类有什么区别
接口和抽象类是 Java 面向对象设计的两个基础机制。
接口是对行为的抽象它是抽象方法的集合利用接口可以达到 API 定义和实现分离的目的。接口不能实例化不能包含任何非常量成员任何 field 都是隐含着 public static final 的意义同时没有非静态方法实现也就是说要么是抽象方法要么是静态方法。Java 标准类库中定义了非常多的接口比如 java.util.List。
抽象类是不能实例化的类用 abstract 关键字修饰 class其目的主要是代码重用。除了不能实例化形式上和一般的 Java 类并没有太大区别可以有一个或者多个抽象方法也可以没有抽象方法。抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量然后通过继承的方式达到代码复用的目的。Java 标准库中比如 collection 框架很多通用部分就被抽取成为抽象类例如 java.util.AbstractList。
Java 类实现 interface 使用 implements 关键词继承 abstract class 则是使用 extends 关键词我们可以参考 Java 标准库中的 ArrayList。
public class ArrayListE extends AbstractListEimplements ListE, RandomAccess, Cloneable, java.io.Serializable
{
//...
} 二Java集合框架高频知识考点高频考点集中地 针对人员 1.全部人员都适用 2.正式员工针对应届工作1-3年的面试者加大比重3年以上的这部分可适量 3.外包员工本部分比重可减轻但针对1-3年的需要适当增加比重 1.HashMap相关put操作get操作等流程高频考点
以下回答进行简单简述如下。
当调用HashMap的put(key, value)方法时会执行以下步骤
计算键的哈希码通过调用键对象的hashCode()方法来获取键的哈希码。定位桶位置将计算得到的哈希码映射到HashMap的内部数组中的一个桶bucket位置。使用哈希码与桶数量取模的方式来确定桶的位置。查找或创建节点在选定的桶位置上遍历链表或红黑树如果存在以查找是否已存在具有相同键的节点。如果找到相同键的节点则将新值替换旧值。如果未找到相同键的节点则创建一个新节点。插入或添加节点将新节点插入到选定桶位置的链表或红黑树中。判断是否需要调整容量如果插入节点后链表长度或红黑树的节点数量超过一定阈值Java 8中为8则会触发调整容量的操作即扩容HashMap的内部数组。
当调用HashMap的get(key)方法时会执行以下步骤
计算键的哈希码通过调用键对象的hashCode()方法来获取键的哈希码。定位桶位置将计算得到的哈希码映射到HashMap的内部数组中的一个桶位置。查找节点在选定的桶位置上遍历链表或红黑树如果存在通过比较键的相等性调用键对象的equals()方法来查找具有相同键的节点。返回节点值如果找到具有相同键的节点则返回该节点的值否则返回null表示未找到对应的值。
需要注意的是HashMap使用哈希码和相等性比较来确定键值对的存储位置和查找操作。因此在自定义对象作为键时确保正确实现equals()和hashCode()方法非常重要以保证HashMap的正确性和一致性。
我们重点还是分析put的内容下图展开方便更深的理解 2.Hash为啥要扩容
哈希表Hash Table在存储元素时使用哈希函数将元素的键映射到一个固定的数组位置上这个数组被称为桶bucket。扩容是指在哈希表中的桶数量不足以容纳当前元素数量时自动增加桶的数量。
哈希表扩容的主要目的是保持哈希表的负载因子Load Factor在一个合适的范围内。负载因子是指当前哈希表中存储元素的数量与桶的数量之比。
为什么需要控制负载因子呢因为负载因子过高会导致哈希冲突的概率增加即多个元素映射到同一个桶的可能性增大进而降低哈希表的性能。通过扩容可以增加桶的数量从而降低负载因子减少哈希冲突的发生提高哈希表的效率和性能。
扩容的具体过程如下
创建一个更大的桶数组通常是原数组的两倍大小。将原数组中的元素重新计算哈希值并分配到新的桶中。将元素存储到新的桶中。最后将新的桶数组替代原来的桶数组完成扩容操作。
需要注意的是哈希表的扩容是一项开销较大的操作因为需要重新计算哈希值、重新分配桶并且需要移动元素。为了减少频繁的扩容操作通常在设计哈希表时会预估元素的数量并根据预估值初始化合适大小的初始桶数组。此外选择适当的负载因子阈值也是重要的以平衡空间利用率和性能。
总结起来哈希表扩容是为了保持合适的负载因子减少哈希冲突提高哈希表的性能和效率。
3.HashMap如果我想要让自己的Object作为K应该怎么办
重写hashCode()是因为需要计算存储数据的存储位置需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能这样虽然能更快但可能会导致更多的Hash碰撞重写equals()方法需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值xx.equals(null)必须返回false的这几个特性目的是为了保证key在哈希表中的唯一性
4.Hashmap 线程不安全的原因高频考点
HashMap 在多线程环境下可能会出现线程不安全的问题主要原因包括以下几点 非同步操作 HashMap 是非同步的即它不对多线程进行同步控制。在多线程并发操作的情况下可能会导致竞争条件从而引发不确定的行为。 并发扩容问题 在 HashMap 进行扩容的时候即在原有的数组上重新分配空间并重新计算哈希值可能会导致多个线程同时修改 HashMap 结构从而破坏内部数据结构引发异常或导致死循环。 链表成环问题 在 JDK7 中在多线程环境下由于链表的头插法和并发扩容的原因可能导致链表成环。当一个线程正在进行链表的迁移操作另一个线程插入新节点时可能造成链表成环从而导致死循环。 并发操作引发的数据丢失问题 当多个线程同时进行 put 操作时可能会导致部分数据的丢失。例如两个线程同时判断需要进行扩容都计算了新的数组位置然后分别在新位置进行插入操作这样其中一个线程的插入操作会被覆盖导致数据丢失。
为了解决以上问题可以采取以下措施 使用线程安全的集合类 使用 Collections.synchronizedMap 或者 ConcurrentHashMap 来替代普通的 HashMap这两者都提供了一定程度的线程安全性。 手动同步控制 在对 HashMap 进行操作时使用显式的同步控制例如使用 synchronized 关键字确保在多线程环境下对 HashMap 的修改是同步的。 使用并发安全的Map实现 ConcurrentHashMap 是 Java 提供的并发安全的 Map 实现它在设计上避免了一些 HashMap 中的问题提供更好的并发性能。 5.HashMap1.7与HashMap1.8的区别从数据结构上、Hash值的计算上、链表数据的插入方法、内部Entry类的实现上分析
数据结构上
JDK1.7的时候使用的是数组 单链表的数据结构。数组和链表节点的实现类是Entry类。在JDK1.8及之后时使用的是数组链表红黑树的数据结构当链表的深度达到8的时候也就是默认阈值就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从On变成OlogN提高了效率。数组和链表节点的实现类是Node类。
Hash值的计算上
JDK1.7用了9次扰动处理4次位运算5次异或JDK1.8只用了2次扰动处理1次位运算1次异或。直接用了JDK1.7的时候计算的规律相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。
链表数据的插入方法上
JDK1.7用的是头插法用单链表进行的纵向延伸当采用头插法就是能够提高插入的效率但是也会容易出现逆序且环形链表死循环问题。JDK1.8及之后使用的都是尾插法因为加入了红黑树使用尾插法能够避免出现逆序且链表死循环的问题。
内部Entry类的实现上
JDK1.7数组和链表节点的实现类是Entry类实现了Map.entry接口。 static class EntryK,V implements Map.EntryK,V {final K key;V value;EntryK,V next;int hash;
} JDK1.8数组和链表节点的实现类是Node类但是还是实现了Map.entry接口。 static class NodeK,V implements Map.EntryK,V {final int hash;final K key;V value;NodeK,V next;
}
6.HashSet和HashMap区别
HashSet和HashMap是Java集合框架中的两个常用类它们具有一些共同的特点但也有一些区别。以下是对HashSet和HashMap的对比分析和一些建议
共同点
底层数据结构HashSet和HashMap都使用哈希表作为其底层数据结构。它们都通过哈希码来确定元素的存储位置从而实现快速的查找、插入和删除操作。唯一性HashSet和HashMap都保证元素的唯一性。在HashSet中它保证集合中没有重复的元素在HashMap中它保证没有重复的键。
区别
存储机制HashSet是基于HashMap实现的但它只存储元素的键值都被设置为同一个固定值常量PRESENT。因此HashSet实际上是一个无序、不重复的集合而HashMap是键值对的存储结构可以存储键值对并且键是唯一的。APIHashSet提供了Set接口的方法而HashMap提供了Map接口的方法。因此HashSet适合用于需要存储唯一元素的场景而HashMap适合需要键值对映射关系的场景。迭代顺序由于HashSet是无序的迭代元素的顺序是不确定的而HashMap在迭代时可以按照插入顺序或者根据键的哈希码顺序进行迭代通过LinkedHashMap可以实现有序的遍历。
使用建议 如果只需要存储元素且不关心键值对 使用HashSet它提供了唯一性和集合操作。 如果需要存储键值对 使用HashMap它提供了键值对的存储和检索功能。 如果只关心键的唯一性 如果只关心键的唯一性而不需要值也可以使用HashMap并将值设为常量。 性能注意事项 在需要频繁查找、插入、删除元素的场景下HashMap通常更适用。但如果只关心唯一性、集合操作且不需要键值对关系时HashSet可能更简洁。
7.Hash1.7是基于数组和链表实现的为什么不用双链表HashMap1.8中引入红黑树的原因是为什么要用红黑树而不是平衡二叉树高频考点
在 HashMap 的早期版本JDK7 及之前使用的确实是单向链表而非双向链表。这是因为在实际的使用场景中插入和删除节点时只需要修改节点前后的指针即可而不需要访问到当前节点的前一个节点。使用单链表可以降低节点的存储开销因为不需要额外的指针指向前一个节点。为了提高HashMap的性能在解决发生哈希碰撞后链表过长导致索引效率慢的问题同时红黑树解决快速增删改查特点。红黑树的平衡度相比平衡二叉树要低对于删除、插入数据之后重新构造树的开销要比平衡二叉树低查询效率比普通二叉树高所以选择性能相对折中的红黑树。
8.HashMap、HashTable、ConcurrentHashMap的原理与区别
以java7为背景情况下回答如下
HashTable
底层数组链表实现无论key还是value都不能为null线程安全实现线程安全的方式是在修改数据时锁住整个HashTable效率低ConcurrentHashMap做了相关优化初始size为11扩容newsize olesize*21计算index的方法index (hash 0x7FFFFFFF) % tab.length
HashMap
底层数组链表实现可以存储null键和null值线程不安全初始size为16扩容newsize oldsize*2size一定为2的n次幂扩容针对整个Map每次扩容时原来数组中的元素依次重新计算存放位置并重新插入插入元素后才判断该不该扩容有可能无效扩容插入后如果扩容如果没有再次插入就会产生无效扩容当Map中元素总数超过Entry数组的75%触发扩容操作为了减少链表长度元素分配更均匀计算index方法index hash (tab.length – 1)
ConcurrentHashMap简单简述java7的java8见上面的讲解
底层采用分段的数组链表实现线程安全通过把整个Map分为N个Segment可以提供相同的线程安全但是效率提升N倍默认提升16倍。(读操作不加锁由于HashEntry的value变量是 volatile的也能保证读取到最新的值。)Hashtable的synchronized是针对整张Hash表的即每次锁住整张表让线程独占ConcurrentHashMap允许多个修改操作并发进行其关键在于使用了锁分离技术有些方法需要跨段比如size()和containsValue()它们可能需要锁定整个表而而不仅仅是某个段这需要按顺序锁定所有段操作完毕后又按顺序释放所有段的锁扩容段内扩容段内元素超过该段对应Entry数组长度的75%触发扩容不会对整个Map进行扩容插入前检测需不需要扩容有效避免无效扩容
注意在 JDK8 中HashMap 进行了一些优化如引入红黑树、采用尾插法等。但在高并发环境下ConcurrentHashMap在设计上更注重并发性能 能够更好地保持性能并且它提供了更多的灵活性允许部分并发读写操作。如果不需要并发操作而且对性能要求不高可以选择 HashMap如果需要线程安全可以考虑 ConcurrentHashMap 或 HashTable。
9.HashMap和ConcurrentHashMap区别高频考点
HashMap和ConcurrentHashMap是Java中的两种不同类型的映射Map实现。它们有以下几个区别
线程安全性最显著的区别是HashMap是非线程安全的而ConcurrentHashMap是线程安全的。在多线程环境下多个线程可以同时访问和修改ConcurrentHashMap的不同部分而不会导致数据不一致或其他并发问题。相反如果多个线程同时访问和修改HashMap则可能导致数据损坏或抛出ConcurrentModificationException等异常。锁机制在Java 8之前的版本中ConcurrentHashMap采用了分段锁的机制来实现线程安全。每个段Segment都有自己的锁可以独立地进行读写操作从而提高并发性能。但在Java 8及以后的版本中ConcurrentHashMap的内部结构发生了改变采用了更为高效的实现方式。在Java 8及以后的版本中ConcurrentHashMap使用了一种称为分段锁升级Striped Locking的机制。它首先将数据分成一组小的桶buckets每个桶都可以独立地进行读写操作。每个桶中的元素可能对应多个键值对但仍然保持并发安全。当多个线程同时访问ConcurrentHashMap时会根据键的哈希值选择相应的桶并使用Synchronized来锁定该桶。这样可以保证在同一个桶中的操作是互斥的避免并发冲突。而在桶内部的读写操作则使用了CAS操作来保证并发安全。因此结合Synchronized和CAS的机制ConcurrentHashMap能够在并发环境中提供高效的线程安全性允许多个线程同时读取和写入不同的桶而不需要全局的锁。这种实现方式在性能和扩展性方面都有较好的表现。而HashMap没有锁机制所以在多线程环境下需要自行实现同步机制来确保线程安全。迭代器HashMap的迭代器Iterator是快速失败的fail-fast也就是说如果在迭代过程中有其他线程修改了HashMap的结构添加或删除元素则会抛出ConcurrentModificationException异常。而ConcurrentHashMap的迭代器是弱一致性的weakly consistent不会抛出该异常并且保证迭代器遍历期间能够看到最新的数据状态。性能由于ConcurrentHashMap使用了并发控制手段它在高并发环境下能够提供更好的性能表现允许多个线程同时读取和写入不同的段。相比之下HashMap在并发情况下需要进行手动的同步操作性能相对较低。
综上所述如果需要在多线程环境下使用映射数据结构并且需要高并发性能则应该使用ConcurrentHashMap。而在单线程环境下或者不需要并发安全的情况下HashMap是更简单、更高效的选择。
当选择使用HashMap或ConcurrentHashMap时以下是一些建议
使用HashMap
在单线程环境下或者在不需要并发安全性的情况下使用HashMap是简单和高效的选择。当只有一个线程对Map进行读写操作时HashMap通常比ConcurrentHashMap性能更好因为它不需要进行额外的并发控制。
使用ConcurrentHashMap
在多线程环境下特别是需要高并发性能和线程安全性的情况下应使用ConcurrentHashMap。当多个线程需要并发读写Map时ConcurrentHashMap能够提供更好的性能因为它使用了分段锁在Java 7及之前的版本或基于结合Synchronized和CAS的机制算法在Java 8及之后的版本来实现并发控制。当需要在遍历ConcurrentHashMap时它的迭代器提供弱一致性保证不会抛出ConcurrentModificationException异常因此更适合在并发环境中进行遍历操作。
总结
如果在单线程环境下或者不需要并发安全性使用HashMap。如果在多线程环境下特别是需要高并发性能和线程安全性使用ConcurrentHashMap。
10. ConcurrentHashMap的数据结构高频考点
在JDK1.7版本中ConcurrentHashMap维护了一个Segment数组Segment这个类继承了重入锁ReentrantLock并且该类里面维护了一个 HashEntryK,V[] table数组在写操作putremove扩容的时候会对Segment加锁所以仅仅影响这个Segment不同的Segment还是可以并发的所以解决了线程的安全问题同时又采用了分段锁也提升了并发的效率。
在JDK1.8版本中ConcurrentHashMap摒弃了Segment的概念而是直接用Node数组链表红黑树的数据结构来实现并发控制使用Synchronized和CAS来操作整个看起来就像是优化过且线程安全的HashMap。
在JDK1.8版本中对于size的计算在扩容和addCount()时已经在处理了。JDK1.7是在调用时才去计算。
11.高并发HashMap的环是如何产生的高频考点
重点该问题产生于jdk7中jdk8已经解决了“环形链表”其采用了尾插法而非反转链表的方式HashMap成环原因的代码出现在transfer代码中也就是扩容之后的数据迁移部分代码如下
void transfer(Entry[] newTable, boolean rehash) {int newCapacity newTable.length;for (EntryK,V e : table) {while(null ! e) {EntryK,V next e.next;if (rehash) {e.hash null e.key ? 0 : hash(e.key);}int i indexFor(e.hash, newCapacity);e.next newTable[i];newTable[i] e;e next;}}
}
解释一下transfer的过程首先获取新表的长度之后遍历新表的每一个entry然后每个ertry中的链表以反转的形式形成rehash之后的链表。
并发问题若当前线程一此时获得entry节点但是被线程中断无法继续执行此时线程二进入transfer函数并把函数顺利执行此时新表中的某个位置有了节点之后线程一获得执行权继续执行因为并发transfer所以两者都是扩容的同一个链表当线程一执行到e.next new table[i] 的时候由于线程二之前数据迁移的原因导致此时new table[i] 上就有ertry存在所以线程一执行的时候会将next节点设置为自己导致自己互相使用next引用对方因此产生链表导致死循环。
解决问题
使用synchronize使用collection.synchronizeXXX方法使用concurrentHashmap来解决。
12.哪些集合是线程安全的
线程安全的集合类通常在 java.util.concurrent 包下以下是几种常见的线程安全的集合类 ConcurrentHashMap 用于代替 Hashtable它提供了线程安全的键值对存储并且性能比 Hashtable 更好。 CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个类提供了线程安全的动态数组和集合它们在遍历操作频繁而修改操作较少的情况下性能很好。 ConcurrentLinkedQueue 和 ConcurrentLinkedDeque 这两个类提供了线程安全的队列和双端队列的实现。 BlockingQueue 接口的实现类 ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue 等它们提供了阻塞队列的实现可以在多线程环境中安全地进行数据交换。 ConcurrentSkipListMap 和 ConcurrentSkipListSet 这两个类提供了线程安全的有序映射和有序集合的实现基于跳表的数据结构实现。
这些线程安全的集合类可以在多线程环境中安全地使用不需要额外的同步措施。然而需要注意的是虽然这些集合类提供了线程安全的操作但并不意味着它们可以完全替代同步措施有时仍然需要在多线程访问时进行额外的同步操作。
13.Collections.SynchronizedCollection方法实现原理是什么
Collections.synchronizedCollection(CollectionT c) 方法的实现原理主要涉及以下几个方面 包装原始集合 方法会创建一个 SynchronizedCollection 对象作为包装器其中内部持有传入的原始集合 c。 同步化操作 在 SynchronizedCollection 类中对于所有会修改集合状态的操作例如添加、删除元素都会使用 synchronized 关键字修饰以确保在多线程环境下的线程安全性。通过对 mutex 对象进行同步化实现了对原始集合的同步化操作。 锁对象 SynchronizedCollection 类中定义了一个 mutex 对象作为同步锁。这个锁对象通常是 this 或者 SynchronizedCollection 对象本身。使用 synchronized 块来锁定这个锁对象以确保对原始集合的操作是互斥的从而保证了线程安全性。 透明性 返回的 SynchronizedCollection 对象对外部用户来说是透明的用户不需要知道内部实现的细节只需要知道它是一个线程安全的集合。这种透明性使得用户可以像操作普通集合一样操作线程安全集合提高了使用的便捷性。
Collections.synchronizedCollection(CollectionT c) 方法通过创建一个包装器类使用同步化的方式对原始集合的操作进行处理从而实现了对传入集合的线程安全封装。
14.Array和ArrayList有什么区别使用时注意事项有哪些
Array和ArrayList是Java中用于存储和操作多个元素的数据结构它们有一些区别和使用时需要注意的事项。
固定大小 vs 动态大小
Array数组具有固定的大小一旦创建后大小不可变。ArrayList是基于数组实现的动态大小的容器可以根据需要自动调整大小。
类型限制
Array可以存储任何类型的元素包括基本类型和引用类型。ArrayList只能存储引用类型的元素不能直接存储基本类型需要使用其对应的包装类。
增删元素
Array的大小固定无法直接增加或删除元素。可以通过创建新的Array并复制元素来模拟增删操作。ArrayList提供了方便的方法来添加add()和删除remove()元素可以动态调整大小。
遍历
Array可以使用简单的for循环或增强for循环遍历。ArrayList同样可以使用for循环或增强for循环遍历也可以使用迭代器Iterator进行遍历。
性能
Array的访问速度较快因为元素在内存中是连续存储的。ArrayList的访问速度相对较慢因为需要通过索引计算元素位置。
注意事项
Array在创建时需要指定大小并且大小不能改变如果需要动态调整大小需要手动操作。ArrayList在使用时可以根据需要动态调整大小无需手动处理大小问题。使用Array时需要手动处理增删元素和数组大小的维护。ArrayList是线程不安全的如果在多线程环境下使用需要进行适当的同步或使用线程安全的替代类如Vector、CopyOnWriteArrayList等。Array可以直接存储基本类型的元素而ArrayList需要使用对应的包装类作为元素类型。Array在创建时需要明确指定元素类型和大小而ArrayList在创建时无需指定大小可以根据需要动态扩展。
根据具体的需求和场景选择适合的数据结构。如果需要灵活的大小调整和内置操作方法可以使用ArrayList如果需要更高的性能和直接的内存访问可以使用Array。
15.常用的集合类有哪些比如List如何排序最好说下底层上的实现高频考点
List 接口的实现类
ArrayList基于数组实现的动态数组支持快速随机访问元素。LinkedList基于双向链表实现的列表支持快速插入和删除操作。Vector线程安全的动态数组性能较 ArrayList 差不推荐使用。
Set 接口的实现类
HashSet基于哈希表实现的集合不保证元素的顺序。TreeSet基于红黑树实现的有序集合元素按照自然顺序或者指定比较器的顺序进行排序。LinkedHashSet基于哈希表和链表实现的集合元素按照插入顺序排序。
Map 接口的实现类
HashMap基于哈希表实现的键值对映射不保证元素的顺序。TreeMap基于红黑树实现的有序键值对映射键按照自然顺序或者指定比较器的顺序进行排序。LinkedHashMap基于哈希表和链表实现的键值对映射元素按照插入顺序排序。
对于 List 接口的实现类可以使用 Collections 类的静态方法 sort() 来排序。如果需要自定义排序规则可以传入一个 Comparator 对象给 sort() 方法。List 排序的底层原理取决于具体使用的排序算法。在 Java 中Collections.sort() 方法使用了归并排序Merge Sort或者快速排序Quick Sort算法来对 List 进行排序。
归并排序Merge Sort
归并排序是一种稳定的排序算法它将待排序的 List 不断分割为更小的子序列直到每个子序列只有一个元素然后将这些子序列两两合并直到整个 List 排序完成。归并排序的时间复杂度为 O(nlogn)空间复杂度为 O(n)适用于大规模数据和外部排序。
快速排序Quick Sort
快速排序是一种不稳定的排序算法它通过选择一个基准元素将 List 分割为两个子序列其中一个子序列的元素都小于基准元素另一个子序列的元素都大于基准元素然后对这两个子序列分别递归进行快速排序。快速排序的时间复杂度为 O(nlogn)空间复杂度为 O(logn)适用于大规模数据但可能会因为基准选择不当而导致性能下降。
在具体实现中Collections.sort() 方法的具体实现会根据 List 的大小和元素类型选择合适的排序算法。通常情况下对于小规模的 List会使用插入排序Insertion Sort或者二分插入排序Binary Insertion Sort来进行排序因为这些排序算法在小规模数据上有较好的性能表现。而对于大规模的 List则会使用归并排序Merge Sort或者快速排序Quick Sort来进行排序因为这些排序算法在大规模数据上有较好的性能表现。
16.ArrayList和LinkedList内部的实现大致是怎样的他们之间的区别和各自适应的场景是什么
ArrayList 和 LinkedList 是 Java 中常见的两种 List 实现它们的内部实现有所不同适用于不同的场景。
ArrayList 内部实现
ArrayList 基于数组实现内部维护一个 Object 数组作为数据存储。当向 ArrayList 中添加元素时如果数组容量不足则会创建一个新的数组并将原数组中的元素复制到新数组中然后将新元素添加到新数组中。ArrayList 支持随机访问通过索引直接访问元素因为数组支持快速随机访问。由于底层是数组实现ArrayList 在随机访问和修改元素时具有较好的性能但在插入和删除元素时性能相对较差因为需要移动后续元素。
LinkedList 内部实现
LinkedList 基于双向链表实现每个节点包含了元素值以及指向前后节点的引用。当向 LinkedList 中添加元素时只需简单地调整节点的引用关系不需要像 ArrayList 那样复制数组。LinkedList 支持快速插入和删除操作因为只需要调整节点的引用关系而不涉及元素的移动。由于底层是链表实现LinkedList 在随机访问时性能较差因为需要遍历链表来找到指定位置的元素。
区别和适应场景
ArrayList 适用于需要快速随机访问元素的场景例如经常需要根据索引来获取或修改元素的场景。LinkedList 适用于需要频繁插入、删除元素的场景例如经常需要在列表的中间插入或删除元素的场景。如果只需在列表的末尾进行添加或删除操作并且不需要频繁的随机访问元素则两者性能差异不大可以根据具体情况选择合适的实现。
总的来说ArrayList 的优势在于快速随机访问而 LinkedList 的优势在于快速插入和删除。根据实际需求选择合适的实现以获得更好的性能和效率。 三多线程与并发编程高频知识考点高频考点集中地 针对人员 1.全部人员都适用 2.正式员工针对应届工作1-3年的面试者减少比重3年以上的这部分可加大比重 3.外包员工本部分比重可减轻但针对1-3年的需要适当增加比重3年以上的必考 1.进程和线程的区别进程间如何通讯线程间如何通讯
进程和线程是操作系统中的两个重要概念它们有以下区别
定义进程是操作系统分配资源的基本单位是一个正在执行的程序的实例。线程是进程内的一个执行单元是进程的实际运行单位。资源占用每个进程都有独立的内存空间、文件句柄、打开的文件等系统资源进程之间的资源相互隔离。而线程是在进程内共享进程的资源包括内存空间、文件句柄等。多个线程共享同一进程的资源因此线程之间的通信和数据共享更为方便。调度和切换进程拥有自己的执行状态、程序计数器、栈等信息需要操作系统进行进程切换和调度。而线程作为进程内的执行单元由操作系统进行线程切换和调度。线程切换开销较小因为线程共享相同的地址空间切换时只需保存和恢复寄存器状态即可。执行关系一个程序至少包含一个进程而进程可以包含多个线程。进程是多个线程的容器线程是进程的实际执行单位。并发性多个进程之间可以并发执行每个进程都有自己的一组线程。而线程是轻量级的执行单位线程之间可以并发执行一个进程的多个线程可以在不同的处理器上并行执行。影响进程的创建和销毁都需要较大的系统开销包括分配内存空间、建立数据结构等。而线程的创建和销毁开销较小。
总结来说进程和线程是操作系统中的两个基本概念进程是资源分配的单位而线程是执行的单位。进程之间相互独立线程在同一进程内共享资源。线程切换开销小可以实现更高效的并发执行。在设计和开发应用程序时需要根据具体需求和系统架构选择适合的进程和线程模型。
进程间通信Inter-Process CommunicationIPC和线程间通信Inter-Thread CommunicationITC是实现进程或线程之间数据交换和信息共享的方式。在操作系统中进程间通信和线程间通信通常采用以下方式
进程间通信IPC
管道Pipe命名管道Named PipeFIFO信号Signal消息队列Message Queue共享内存Shared Memory套接字Socket
线程间通信ITC
共享内存Shared Memory信号量Semaphore互斥锁Mutex条件变量Condition Variable信号Signal屏障Barrier队列Queue
通过这些通信方式进程间或线程间可以进行数据交换、同步操作等。选择合适的通信方式取决于具体的需求和场景。
2.Java中线程之间如何通信
在Java中线程之间可以通过以下几种方式进行通信
共享变量线程之间可以通过共享变量来进行通信。多个线程可以访问和修改同一个共享变量通过读取和修改共享变量的值来进行信息交换。需要注意的是当多个线程同时访问共享变量时需要保证线程安全可以使用锁或其他同步机制来实现。管道Pipe管道是一种半双工的通信方式其中一个线程通过输出流将数据发送到管道另一个线程通过输入流从管道中读取数据。管道可以用于在两个线程之间传递数据。阻塞队列Blocking Queue阻塞队列是一种线程安全的数据结构它提供了线程之间安全的数据交换。一个线程可以将数据放入阻塞队列的尾部而另一个线程可以从队列的头部获取数据。当队列为空时获取操作会被阻塞直到有数据可用当队列已满时插入操作会被阻塞直到有空间可用。wait/notify机制通过调用对象的wait()方法线程可以进入等待状态释放对象的锁并等待其他线程的通知。另一个线程可以通过调用对象的notify()或notifyAll()方法来唤醒等待的线程。这种方式常用于实现线程之间的协调与同步。Condition条件java.util.concurrent.locks.Condition接口提供了线程间通信的高级方式。可以通过Condition对象与锁例如ReentrantLock结合使用实现更灵活的线程间通信。线程可以通过调用await()方法进入等待状态通过调用signal()或signalAll()方法来唤醒等待的线程。
这些是常见的线程间通信方式具体的选择取决于场景和需求。需要根据具体情况选择合适的通信方式并使用正确的同步机制来保证线程间的安全性和可靠性。
3.并发和并行的区别
并发Concurrency和并行Parallelism是计算机领域中两个相关但不同的概念
并发Concurrency
并发指的是在同一时间段内执行多个任务或处理多个事件。它强调多个任务之间的交替执行和共享资源的竞争。在并发情况下多个任务通过快速的切换使得它们似乎是同时执行的。并发可以提高系统的吞吐量和资源利用率并改善响应时间。
并行Parallelism
并行指的是同时执行多个任务或处理多个事件。在并行情况下多个任务真正地同时执行每个任务占用不同的物理处理器核心或计算资源。并行利用了多核处理器或分布式系统的优势通过同时处理多个任务来提高系统的处理能力和性能。
总结来说
并发是在同一时间段内执行多个任务或处理多个事件强调任务之间的交替执行和资源竞争。并行是真正同时执行多个任务或处理多个事件利用多核处理器或分布式系统的能力提高处理能力和性能。
可以将并发视为一种逻辑上的概念强调任务之间的关系和调度方式而并行则是一种物理上的概念强调任务的同时执行。
在实际应用中通过并发和并行的技术可以提高系统的性能和响应能力。例如通过多线程实现并发处理、利用多核处理器实现并行计算、使用分布式系统实现并行处理等。
4.Java线程的状态细说一下BLOCKED和WAITING有什么区别高频考点
Java 线程的状态可以分为以下几种 新建New 线程对象被创建但还未启动时的状态。此时线程对象已经被创建但是还没有调用 start() 方法启动线程。 就绪Runnable 线程对象调用了 start() 方法后线程处于就绪状态。此时线程已经准备好运行但是还未获得 CPU 执行时间。 运行Running 线程获取到 CPU 执行时间开始执行任务的状态。处于运行状态的线程正在执行任务代码。 阻塞Blocked 线程因为某些原因暂时无法执行任务而被阻塞的状态。常见的情况包括等待某个资源如锁、等待输入输出操作完成、等待其他线程执行完毕等。 等待Waiting 线程调用了 wait() 方法后进入等待状态。在等待状态下线程会等待其他线程调用 notify() 或 notifyAll() 方法来唤醒它。 超时等待Timed Waiting 线程调用了 sleep()、join() 或 LockSupport.parkNanos() 等方法并设置了等待时间线程会进入超时等待状态。在超时等待状态下线程会等待指定的时间如果时间到了仍未被唤醒线程会自动唤醒并进入就绪状态。 终止Terminated 线程执行完任务或者因异常而终止时的状态。处于终止状态的线程不会再执行任务线程对象也会被销毁。
这些状态在 Java 线程的生命周期中是动态变化的线程会根据不同的情况在各个状态之间转换。
BLOCKED阻塞和 WAITING等待是 Java 线程状态中的两种不同状态它们之间有以下区别
BLOCKED阻塞
线程处于 BLOCKED 状态通常是因为等待某个锁的释放而被阻塞。当一个线程在等待进入同步代码块或方法时如果这个同步块或方法已经被其他线程占用则当前线程会被阻塞进入 BLOCKED 状态。线程在 BLOCKED 状态下会等待其他线程释放锁资源以便获取锁并继续执行任务。典型的场景包括线程竞争同步锁等待其他线程释放锁资源。
WAITING等待
线程处于 WAITING 状态通常是因为需要等待特定的条件才能继续执行而不是等待锁的释放。当线程调用 Object.wait()、Thread.join()、LockSupport.park() 等方法时会进入 WAITING 状态。线程在 WAITING 状态下会一直等待特定条件的出现或者其他线程的唤醒。典型的场景包括线程调用 wait() 方法等待其他线程的通知或者调用 join() 方法等待指定线程执行完毕。
总的来说BLOCKED 状态是因为等待锁资源而被阻塞而 WAITING 状态是因为等待特定条件或其他线程的唤醒而被阻塞。两者的区别在于等待的对象不同导致了不同的线程状态。
5.Java实现多线程的方式有哪些高频考点
在 Java 中有多种方式可以实现多线程。以下是一些常见的方法
1.继承 Thread 类
创建一个类继承自 Thread 类。重写 run 方法在 run 方法中定义线程执行的任务。创建该类的对象调用 start 方法启动线程。
2.实现 Runnable 接口
创建一个类实现 Runnable 接口。实现 run 方法定义线程执行的任务。创建该类的对象并将其作为参数传递给 Thread 类的构造方法。调用 start 方法启动线程。
3.使用匿名类
创建一个匿名类继承 Thread 类或实现 Runnable 接口。重写 run 方法定义线程执行的任务。创建匿名类的对象并调用 start 方法。
4.使用 Callable 和 Future
创建一个实现 Callable 接口的类。实现 call 方法定义线程执行的任务并返回结果。使用 ExecutorService 提交 Callable 对象得到 Future 对象通过 Future 可以获取线程的执行结果。
5.使用 Executor 框架
使用 Executor 框架提供的线程池。创建一个实现 Runnable 接口或 Callable 接口的类。将任务提交给 Executor。
这些是 Java 中常用的多线程实现方式。选择合适的方式取决于任务的性质和对线程的管理需求。
6.Java处理多线程的方式有哪些
Java处理多线程的方式有以下几种
同步机制使用关键字synchronized或使用Lock接口及其实现类如ReentrantLock进行线程同步。这可以确保多个线程之间的访问顺序和互斥访问共享资源避免数据竞争和不一致的结果。线程通信使用等待/通知机制实现线程之间的协调和通信。可以使用wait()、notify()和notifyAll()方法在多个线程之间进行信号的发送和接收。线程安全的容器Java提供了一些线程安全的容器类如Vector、ConcurrentHashMap、ConcurrentLinkedQueue等。这些容器类在多线程环境下提供了并发访问的安全性。原子类Java提供了一些原子类Atomic classes如AtomicInteger、AtomicLong、AtomicReference等。这些类提供了原子操作可以保证操作的原子性避免了线程间的竞争条件。线程池使用线程池管理和调度线程的执行。可以通过Executors类创建线程池然后提交任务给线程池执行。线程池提供了线程的复用、线程数量的控制以及任务队列等功能。并发工具类Java提供了一些并发工具类如CountDownLatch、CyclicBarrier、Semaphore等用于多线程间的协作和控制。并发框架Java提供了并发框架如Java 8引入的CompletableFuture和Stream API可以简化多线程编程实现异步和并行操作。
这些方式可以帮助处理多线程编程中的并发和同步问题提高程序的性能和可靠性。具体使用哪种方式取决于具体的需求和场景。
7.Java程序中启动一个线程是用run()还是start()
在 Java 中启动一个线程应该使用 start() 方法而不是直接调用 run() 方法。
使用 start() 方法启动一个线程会创建一个新的线程并在新线程中执行 run() 方法的内容。这样做会实现多线程并发执行的效果。直接调用 run() 方法只会在当前线程中执行 run() 方法的内容并不会创建新的线程。这样做并不会实现多线程的效果只是简单地执行了一个方法而已。
因此如果希望实现多线程并发执行的效果应该调用 start() 方法来启动线程。
8.Thread的start方法调用两次会怎么样Thread是如何保证start方法调用只有一次生效高频考点
Thread 的 start 方法调用两次会怎么样
如果 Thread 的 start() 方法被调用两次第二次调用会导致 IllegalThreadStateException 异常被抛出。这是因为 Thread 类内部维护了一个状态机用来标识线程的状态。在调用 start() 方法时会检查线程状态是否处于新建状态即线程还未启动。如果线程处于新建状态则可以启动线程执行并将状态转换为就绪状态如果线程不处于新建状态例如已经处于就绪状态、运行状态等再次调用 start() 方法就会抛出 IllegalThreadStateException 异常因为线程已经启动或正在执行无法重新启动。
Thread 的 start 方法调用只有一次生效的原因
start() 方法是 Thread 类的一个同步方法内部使用了synchronized来确保线程状态转换的原子性。这样可以防止多个线程同时调用 start() 方法导致的竞态条件问题。当一个线程调用 start() 方法时会获取 Thread 对象的锁执行线程状态转换的过程如果另一个线程尝试再次调用 start() 方法由于该方法被 synchronized 修饰需要等待前一个线程释放锁才能执行因此确保了 start() 方法只能被调用一次生效的特性。
9.什么是守护线程有什么用
在 Java 中守护线程Daemon Thread是一种特殊类型的线程其特点是当所有非守护线程结束时守护线程会自动结束从而随着 JVM 的退出而结束。守护线程与普通线程的区别在于它们的生命周期不会影响 JVM 的终止。守护线程的特点包括 在后台运行 守护线程通常用于执行一些后台任务不需要用户主动控制的工作。它们在后台默默地运行不会干扰到用户主线程的执行。 随着 JVM 的终止而结束 当所有非守护线程结束时JVM 会自动关闭守护线程也会随之结束。这样可以避免守护线程继续运行导致 JVM 无法正常退出的问题。 不影响 JVM 的终止 守护线程的生命周期不会影响 JVM 的终止。当所有非守护线程结束时JVM 会检查是否还有守护线程在运行如果没有则会正常退出如果有则会强制结束所有守护线程并退出。
守护线程通常用于执行一些后台任务例如垃圾回收器Garbage Collector就是一个典型的守护线程。垃圾回收器在后台不断地回收无用的内存资源以便释放内存空间但它并不需要用户主动控制而是由 JVM 自动管理。
总的来说守护线程的作用是在后台执行一些不需要用户主动控制的任务它们的生命周期不会影响 JVM 的终止可以提高系统的稳定性和可靠性。
10.两个线程如何串行执行
要实现两个线程的串行执行可以使用线程间的协调机制来控制它们的执行顺序。以下是几种常见的方法 使用 join() 方法 在一个线程中调用另一个线程的 join() 方法会等待该线程执行完成后再继续执行当前线程。通过这种方式可以实现两个线程的串行执行。例如 Thread thread1 new Thread(new MyTask1());
Thread thread2 new Thread(new MyTask2());
thread1.start();
thread1.join(); // 等待 thread1 执行完成
thread2.start(); // thread2 在 thread1 执行完成后再启动 使用 wait() 和 notify() 方法 可以在一个线程中使用 wait() 方法使其进入等待状态然后在另一个线程中使用 notify() 或 notifyAll() 方法来唤醒等待的线程。通过这种方式可以实现两个线程的串行执行。例如 Object lock new Object();// 线程1
synchronized (lock) {// 执行线程1的任务// 线程1执行完成后唤醒等待的线程lock.notify();
}// 线程2
synchronized (lock) {// 等待线程1执行完成lock.wait();// 执行线程2的任务
} 使用同步方法或同步代码块 可以使用 synchronized 关键字来保证多个线程对共享资源的访问是同步的从而实现线程的串行执行。例如 synchronized void thread1Task() {// 线程1的任务
}synchronized void thread2Task() {// 线程2的任务
}
通过以上方式可以实现两个线程的串行执行确保它们按照指定的顺序执行。选择合适的方法取决于具体的场景和需求。
11.可以运行时kill掉一个线程吗
在 Java 中没有直接的方法可以在运行时“杀死”一个线程也没有提供类似于 Thread.kill() 的方法。这是因为在 Java 中线程的停止是基于协作和共享状态的而不是通过直接终止线程的方式。
然而你可以通过设置一个标志位让线程在下一个合适的时机自行停止。例如你可以使用一个 volatile boolean 类型的标志位在线程执行的过程中定期检查该标志位并在标志位变为 true 时自行停止线程。
下面是一个简单的示例
public class MyThread extends Thread {private volatile boolean running true;public void stopThread() {running false;}Overridepublic void run() {while (running) {// 线程执行的任务System.out.println(Thread is running...);try {Thread.sleep(1000); // 模拟线程执行任务的时间} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread stopped.);}public static void main(String[] args) throws InterruptedException {MyThread thread new MyThread();thread.start();Thread.sleep(5000); // 等待一段时间thread.stopThread(); // 设置标志位停止线程}
}
12.Synchronized的实现原理高频考点
Synchronized 是 Java 中用于实现同步的关键字它可以被用于方法、代码块、以及实例方法和静态方法。Synchronized 的主要目的是控制多个线程访问共享资源时的并发问题确保线程之间的协调执行。在 Java 中Synchronized 的实现原理主要基于对象头的 Mark Word 和 Monitor监视器。
基于对象头的 Mark Word 对象头 在 Java 对象的内存布局中对象头包含了一些用于存储对象自身的运行时数据其中的 Mark Word 就是其中的一部分。 Mark Word Mark Word 存储了对象的 hashCode、分代年龄、锁标志等信息。其中Synchronized 使用了 Mark Word 中的锁标志位来实现同步。
Monitor监视器 每个 Java 对象都与一个 Monitor 关联用于实现对象级别的同步。 Monitor 中有两个队列 EntryList 存储等待获取锁的线程。WaitSet 存储因为某些原因例如调用了Object.wait()方法而被挂起的线程。
Synchronized 实现原理 进入同步块 当一个线程尝试进入一个同步代码块时会首先尝试获取对象的锁。 锁的获取如果对象的 Mark Word 中的锁标志位为 0表示该对象没有被锁定那么线程将尝试获取锁并将锁标志位设置为线程的 ID。如果对象已经被其他线程锁定那么当前线程会进入 EntryList 队列等待。 锁的释放当线程退出同步块时会释放对象的锁将锁标志位清零。如果有其他线程在 EntryList 中等待会选择其中一个线程唤醒并将锁标志位设置为唤醒线程的 ID。 锁的升级
初始时Mark Word 的锁标志位为无锁状态01。当一个线程获取锁时Mark Word 的锁标志位变为偏向锁状态00记录获取偏向锁的线程 ID。如果有其他线程尝试获取锁会升级为轻量级锁状态通过 CAS 操作进行加锁。如果多个线程争用轻量级锁会升级为重量级锁即使用 Monitor。
总体而言Synchronized 通过对对象头的 Mark Word 进行操作以及通过 Monitor 进行锁的获取和释放来实现对共享资源的同步控制。这种同步机制保证了对共享资源的互斥访问。需要注意的是锁的升级过程旨在优化性能避免过多的锁竞争。
13.volatile与synchronized的区别是什么volatile作用高频考点
背景知识了解
Java的线程抽象内存模型
Java的线程抽象内存模型中定义了每个线程都有一份自己的私有内存里面存放自己私有的数据其他线程不能直接访问而一些共享数据则存在主内存中供所有线程进行访问。 上图中如果线程A和线程B要进行通信就要经过主内存比如线程B要获取线程A修改后的共享变量的值要经过下面两步 1、线程A修改自己的共享变量副本并刷新到了主内存中。 2、线程B读取主内存中被A更新过的共享变量的值同步到自己的共享变量副本中。
Java多线程中的原子性、可见性、有序性 1、原子性是指线程的多个操作是一个整体不能被分割要么就不执行要么就全部执行完中间不能被打断。 2、可见性是指线程之间的可见性就是一个线程修改后的结果其他的线程能够立马知道。 3、有序性为了提高执行效率java中的编译器和处理器可以对指令进行重新排序重新排序会影响多线程并发的正确性有序性就是要保证不进行重新排序保证线程操作的执行顺序。
volatile关键字的作用
volatile关键字的作用就是保证了可见性和有序性不保证原子性如果一个共享变量被volatile关键字修饰那么如果一个线程修改了这个共享变量后其他线程是立马可知的。
为什么是这样的呢比如线程A修改了自己的共享变量副本这时如果该共享变量没有被volatile修饰那么本次修改不一定会马上将修改结果刷新到主存中如果此时B去主存中读取共享变量的值那么这个值就是没有被A修改之前的值。如果该共享变量被volatile修饰了那么本次修改结果会强制立刻刷新到主存中如果此时B去主存中读取共享变量的值那么这个值就是被A修改之后的值了。 volatile能禁止指令重新排序在指令重排序优化时在volatile变量之前的指令不能在volatile之后执行在volatile之后的指令也不能在volatile之前执行所以它保证了有序性。
synchronized关键字的作用
synchronized提供了同步锁的概念被synchronized修饰的代码段可以防止被多个线程同时执行必须一个线程把synchronized修饰的代码段都执行完毕了其他的线程才能开始执行这段代码。 因为synchronized保证了在同一时刻只能有一个线程执行同步代码块所以执行同步代码块的时候相当于是单线程操作了那么线程的可见性、原子性、有序性线程之间的执行顺序它都能保证了。
volatile关键字和synchronized关键字的区别 1、volatile只能作用于变量使用范围较小。synchronized可以用在变量、方法、类、同步代码块等使用范围比较广。 2、volatile只能保证可见性和有序性不能保证原子性。而可见性、有序性、原子性synchronized都可以保证。 3、volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。
14.synchronized和Lock的区别高频考点
背景知识了解
synchronized
Java语言的关键字可用来给对象和方法或者代码块加锁当它锁定一个方法或者一个代码块的时候同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而当一个线程访问object的一个加锁代码块时另一个线程仍然可以访问该object中的非加锁代码块。
lock
(1)synchronized的缺陷
synchronized是java中的一个关键字也就是说是Java语言内置的特性。那么为什么会出现Lock呢
如果一个代码块被synchronized修饰了当一个线程获取了对应的锁并执行该代码块时其他线程便只能一直等待等待获取锁的线程释放锁而这里获取锁的线程释放锁只会有两种情况 1获取锁的线程执行完了该代码块然后线程释放对锁的占有 2线程执行发生异常此时JVM会让线程自动释放锁。 那么如果这个获取锁的线程由于要等待IO或者其他原因比如调用sleep方法被阻塞了但是又没有释放锁其他线程便只能等待试想一下这多么影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去比如只等待一定的时间或者能够响应中断通过Lock就可以办到。
再举个例子当有多个线程读写文件时读操作和写操作会发生冲突现象写操作和写操作会发生冲突现象但是读操作和读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话就会导致一个问题如果多个线程都只是进行读操作所以当一个线程在进行读操作时其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程都只是进行读操作时线程之间不会发生冲突通过Lock就可以办到。
另外通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
总结一下也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点 1Lock不是Java语言内置的synchronized是Java语言的关键字因此是内置特性。Lock是一个类通过这个类可以实现同步访问 2Lock和synchronized有一点非常大的不同采用synchronized不需要用户去手动释放锁当synchronized方法或者synchronized代码块执行完之后系统会自动让线程释放对锁的占用而Lock则必须要用户去手动释放锁如果没有主动释放锁就有可能导致出现死锁现象。
(2)java.util.concurrent.locks包下常用的类
public interface Lock {/*获取锁如果锁被其他线程获取则进行等待*/void lock(); /**当通过这个方法去获取锁时如果线程正在等待获取锁则这个线程能够响应中断即中断线程的等待状态。也就使说当两个线程同时通过lock.lockInterruptibly()想获取某个锁时假若此时线程A获取到了锁而线程B只有在等待那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。*/void lockInterruptibly() throws InterruptedException;/**tryLock()方法是有返回值的它表示用来尝试获取锁如果获取成*功则返回true如果获取失败即锁已被其他线程获取则返回*false也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。*/boolean tryLock();/*tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的只不过区别在于这个方法在拿不到锁时会等待一定的时间在时间期限之内如果还拿不到锁就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁则返回true。*/boolean tryLock(long time, TimeUnit unit) throws InterruptedException;void unlock(); //释放锁Condition newCondition();
}
注意: 当一个线程获取了锁之后是不会被interrupt()方法中断的。因为单独调用interrupt()方法不能中断正在运行过程中的线程只能中断阻塞过程中的线程。而用synchronized修饰的话当一个线程处于等待某个锁的状态是无法被中断的只有一直等待下去。
(3)ReentrantLock ReentrantLock意思是“可重入锁”是唯一实现了Lock接口的类并且ReentrantLock提供了更多的方法。
如果锁具备可重入性则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁可重入性在我看来实际上表明了锁的分配机制基于线程的分配而不是基于方法调用的分配。
举个简单的例子当一个线程执行到某个synchronized方法时比如说method1而在method1中会调用另外一个synchronized方法method2此时线程不必重新去申请锁而是可以直接执行方法method2。
class MyClass {public synchronized void method1() {method2();}public synchronized void method2() {}
}
synchronized和lock区别
1Lock是一个接口而synchronized是Java中的关键字synchronized是内置的语言实现
2synchronized在发生异常时会自动释放线程占有的锁因此不会导致死锁现象发生
而Lock在发生异常时如果没有主动通过unLock()去释放锁则很可能造成死锁现象因此使用Lock时需要在finally块中释放锁
3Lock可以让等待锁的线程响应中断而synchronized却不行使用synchronized时等待的线程会一直等待下去不能够响应中断
4通过Lock可以知道有没有成功获取锁而synchronized却无法办到。
5Lock可以提高多个线程进行读操作的效率。
在性能上来说如果竞争资源不激烈两者的性能是差不多的而当竞争资源非常激烈时即有大量线程同时竞争此时Lock的性能要远远优于synchronized。所以说在具体使用时要根据适当情况选择。
15.Atomic类如何保证原子性CAS操作高频考点
前提知识Atomic 内部的value 使用volatile保证内存可见性使用CAS保证原子性
volatile保证内存可见性
打开AtomicInteger的源码可以看到:
private static final Unsafe unsafe Unsafe.getUnsafe();
private volatile int value;
volatile关键字用来保证内存的可见性但不能保证线程安全性线程读的时候直接去主内存读写操作完成的时候立即把数据刷新到主内存当中。
使用CAS保证原子性
/**
* Atomically sets the value to the given updated value
* if the current value {code } the expected value.
*
* param expect the expected value
* param update the new value
* return {code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
从注释就可以看出当线程写数据的时候先对内存中要操作的数据保留一份旧值真正写的时候比较当前的值是否和旧值相同如果相同则进行写操作。如果不同说明在此期间值已经被修改过则重新尝试。
compareAndSet使用Unsafe调用native本地方法CASCompareAndSet递增数值。CAS利用CPU调用底层指令实现。
16.AtomicInteger、AtomicBoolean这些类之所以在高并发时高效共同的原因是
AtomicInteger、AtomicBoolean 等原子类之所以在高并发时高效共同的原因是因为它们使用了硬件级别的原子操作来实现对变量的更新避免了使用锁带来的性能开销和线程阻塞。
具体来说它们的高效性可以归结为以下几个方面 无锁机制原子类通过底层的CASCompare and Swap操作实现对变量的原子更新而不需要使用显式的锁。CAS操作是一种硬件级别的原子操作可以保证在多线程环境下对变量的安全访问和更新避免了使用锁带来的性能开销和线程阻塞。 并发性能原子类的实现通常基于CPU的原子指令比如compareAndSet()方法在多核处理器上能够充分利用硬件并发性能实现高效的并发访问。 无阻塞由于原子类采用了无锁的方式实现对变量的更新因此不存在线程阻塞的问题。即使有大量线程同时访问原子类中的变量也不会导致线程的长时间阻塞等待锁资源释放从而提高了系统的响应性和吞吐量。
综上所述AtomicInteger、AtomicBoolean 等原子类通过利用硬件级别的原子操作来实现对变量的原子更新从而实现了高效的并发访问和线程安全性成为了在高并发场景下常用的并发工具之一。
17.关于 Atomic 类中的主要变量如下其使用了 volatile 关键字进行修饰。你知道它在源码中的主要意义是高频考点
使用了 volatile 关键字的变量每当变量的值有变动的时候都会将更改立即同步到主内存中而如果某个线程想要使用这个变量就先要从主存中刷新到工作内存这样就确保了变量的可见性。有了这个关键字的修饰就能保证每次比较的时候拿到的值总是最新的。
18.CASCAS 有什么缺陷如何解决高频考点
更详细的见文章CAS技术分析 超越并发瓶颈CAS与乐观锁的智慧应用
CASCompare and Swap是一种并发编程中的原子操作用于实现多线程环境下的无锁同步。它基于比较当前值与期望值的方式来更新变量的值只有在当前值与期望值相等的情况下才进行更新否则不进行更新。
CAS的主要缺陷是ABA问题。ABA问题指的是在执行CAS操作期间变量的值经过一系列的修改先变成了A然后又被修改为B最后又被修改回A。在这种情况下CAS操作会错误地认为变量的值没有被其他线程修改过导致操作成功但实际上变量的值已经发生了变化。
为了解决ABA问题可以采取以下两种方法
版本号或标记在变量值的基础上增加一个版本号或标记每次修改时都更新版本号或标记。这样即使变量的值从A变为B再变回A由于版本号或标记的变化CAS操作会正确地判断变量是否被修改过。带有回退的CAS在执行CAS操作时除了比较当前值与期望值外还比较变量的修改历史。如果发现变量的值在修改期间发生了变化即使当前值与期望值相等CAS操作也会失败需要重新尝试。
Java中的Atomic类提供了基于CAS操作的原子类如AtomicInteger、AtomicLong等。这些原子类已经内部处理了ABA问题使用了类似版本号或标记的机制来解决ABA问题从而提供了线程安全的原子操作。
需要注意的是尽管CAS是一种无锁的同步机制但在高并发场景下由于CAS操作可能会多次失败和重试从而导致性能下降。因此在选择使用CAS时需要根据具体场景综合考虑其性能和实现复杂度。
19.比较和替换是两个动作CAS 是如何保证这两个操作的原子性呢
具体理解可见超越并发瓶颈CAS与乐观锁的智慧应用
直接以AtomicInteger 中 CAS 操作的原子性保证来进行理解。
Java 层次
AtomicInteger 类中的 compareAndSet 方法用于执行 CAS 操作其代码如下
public final boolean compareAndSet(int expectedValue, int newValue) {return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}这里的 U 是 Unsafe 类的实例VALUE 是内存偏移量。compareAndSetInt 是 Unsafe 类中的一个本地方法直接调用底层的硬件指令来实现原子操作。
public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);JVM 层次
Unsafe 类中 compareAndSetInt 方法的实现会调用 weakCompareAndSetInt 方法该方法通过自旋重试实现CAS操作
public final int getAndAddInt(Object o, long offset, int delta) {int v;do {v getIntVolatile(o, offset);} while (!weakCompareAndSetInt(o, offset, v, v delta));return v;
}在这个方法中getIntVolatile 获取当前值weakCompareAndSetInt 尝试更新值如果更新失败则重复上述过程直到成功即自旋重试。
硬件层次
在 Linux 系统的 x86 架构上CAS 操作最终会映射到 cmpxchgl 汇编指令这是由 os_cpu/linux_x86/atomic_linux_x86.hpp 文件中的代码实现的
template
templatetypename T
inline T Atomic::PlatformCmpxchg4::operator()(T exchange_value,T volatile* dest,T compare_value,atomic_memory_order /* order */) const {STATIC_ASSERT(4 sizeof(T));__asm__ volatile (lock cmpxchgl %1,(%3): a (exchange_value): r (exchange_value), a (compare_value), r (dest): cc, memory);return exchange_value;
}这里的 cmpxchgl 指令是关键。这条汇编指令的作用是
比较寄存器 EAX 中的值compare_value和内存地址 dest 中的值。如果两者相等则将 exchange_value 存储到 dest 中。如果不相等则将 dest 中的值加载到 EAX 中。
lock 前缀确保了操作的原子性这意味着在多处理器系统中该指令在执行时会锁住总线或使用缓存一致性协议保证其他处理器不能访问内存地址直到操作完成。 在不同的硬件平台上支持CAS操作的指令可能不同但其基本原理是一致的 x86 平台x86处理器提供了 CMPXCHG 指令来实现CAS操作。这个指令是原子的即在执行过程中不会被其他指令中断。PowerPC 平台PowerPC处理器提供了 lwarx 和 stwcx. 指令组合来实现CAS操作这些指令也确保了操作的原子性。ARM 平台ARM处理器提供了 LDREX 和 STREX 指令组合来实现CAS操作。 总结
硬件指令 cmpxchgl 结合 lock 前缀保证了在多处理器环境下的原子性即整个比较和替换操作是不可分割的这就是 CAS 操作能够实现原子性的原因。
20.Java不可重入锁与可重入锁的区别如何理解
更详细的见文章可重入锁 VS 非可重入锁
可重入锁Reentrant Lock和不可重入锁Non-reentrant Lock是锁的两种不同实现方式其主要区别在于是否支持同一个线程多次获取同一把锁。
可重入锁允许同一个线程多次获取同一把锁而不可重入锁不允许同一个线程多次获取同一把锁。具体来说可重入锁会维护一个获取锁的计数器每次成功获取锁时计数器会加1线程释放锁时计数器会减1。只有当计数器归零时其他线程才能获取该锁。这样同一个线程在持有锁的情况下可以再次获取同一把锁而不会被阻塞称为锁的重入性。
不可重入锁则不支持同一个线程多次获取同一把锁。当一个线程已经持有该锁时再次尝试获取同一把锁会导致线程被阻塞直到其他线程释放该锁。
理解可重入锁和不可重入锁的区别有助于避免死锁和实现复杂的同步逻辑。可重入锁能够适应更复杂的同步需求允许在同一线程中递归地调用同步方法或代码块而不可重入锁则需要谨慎使用以防止死锁和逻辑错误。
在Java中synchronized关键字实现的锁是可重入锁即同一个线程在持有锁的情况下可以再次获取同一把锁。而ReentrantLock类也是可重入锁的实现它提供了更多灵活性和扩展性可以用于更复杂的同步场景。
21.无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁解释锁升级高频考点
在Java中锁的升级是指在多线程竞争的情况下从低级别的锁逐渐升级到高级别的锁。Java的锁升级过程包括无锁、偏向锁、轻量级锁和重量级锁每个级别的锁都有不同的开销和适用场景。
无锁在无竞争的情况下线程可以自由地访问共享数据无需任何锁机制。偏向锁Biased Locking当只有一个线程访问共享数据时使用偏向锁可以减少同步的开销。偏向锁会偏向于第一个获取锁的线程将对象头标记为偏向锁并将线程ID记录在对象头中。此后该线程再次访问同步块时无需竞争直接获取锁。偏向锁的目标是提供低延迟的锁操作。轻量级锁Lightweight Locking当多个线程同时访问同一块同步代码时偏向锁会升级为轻量级锁。轻量级锁使用CASCompare and Swap操作来尝试获取锁如果成功获取锁则继续执行同步代码块。如果获取锁失败则表示存在竞争升级为重量级锁。重量级锁Heavyweight Locking当多个线程竞争同步锁时轻量级锁会升级为重量级锁。重量级锁使用操作系统的互斥量Mutex来实现确保同一时间只有一个线程可以访问同步代码块。当线程无法获取重量级锁时会被阻塞挂起直到锁被释放。
锁的升级过程是动态的根据竞争情况和线程访问模式来进行判断和转换。如果竞争激烈锁会很快升级为重量级锁如果竞争较小或仅有一个线程访问锁可能一直保持为偏向锁。锁升级的过程会带来一定的开销因此在设计多线程应用程序时需要综合考虑锁的升级过程和并发性能的平衡。需要注意的是锁的升级是由Java虚拟机自动进行的开发人员无需显式控制。锁升级机制的目标是提供更好的并发性能和适应不同的多线程竞争场景。 更详细的见文章无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
22.乐观锁 VS 悲观锁公平锁 VS 非公平锁独享锁 VS 共享锁 更详细的见文章乐观锁 VS 悲观锁公平锁 VS 非公平锁独享锁 VS 共享锁
23.自旋锁 VS 适应性自旋锁简单介绍 更详细的见文章Java中常用的锁总结与理解
24.为什么读多写少的情况就适合使用乐观锁呢悲观锁在读多写少的情况下不也是有很少的冲突吗高频考点
主要内容和介绍具体可见超越并发瓶颈CAS与乐观锁的智慧应用
乐观锁适用于读多写少的情况的原因
乐观锁不阻塞读操作乐观锁在读取数据时并不会进行加锁操作而是先读取数据然后在更新数据时检查是否被其他线程修改过。因此即使有写操作正在进行读操作也不会被阻塞从而可以实现读操作的并发执行。乐观锁的冲突少在读多写少的情况下写操作的频率较低因此冲突的概率也相对较低。乐观锁的重试操作是在发生冲突时进行的因此在冲突较少的情况下重试的概率也较低从而可以更高效地处理并发冲突。乐观锁的开销低乐观锁的实现通常比较轻量级不需要频繁地进行加锁和解锁操作因此在读多写少的情况下乐观锁的性能通常会更好。
悲观锁在读多写少的情况下也有冲突少的特点为什么不适合呢 尽管悲观锁在读多写少的情况下可能会有较少的冲突但它的主要问题在于加锁这个动作上
悲观锁在读取数据时通常会对共享资源进行加锁这会导致其他线程无法同时进行读操作。即使读操作之间并没有冲突也会由于加锁操作而导致不必要的阻塞。悲观锁在进行加锁和解锁操作时会引入额外的开销尤其是在并发量较大的情况下频繁的加锁和解锁操作会降低系统的性能。
尽管悲观锁在一些情况下也能够处理并发问题但在读多写少的情况下乐观锁更适合因为它更符合读多写少的特点可以更好地实现读操作的并发执行提高系统的性能。
25.死锁发生的原因
死锁是指在并发系统中两个或多个进程或线程因为争夺资源而被永久地阻塞导致系统无法继续执行的状态。以下是导致死锁发生的常见原因
互斥条件Mutual Exclusion某些资源一次只能被一个进程或线程使用如果一个进程占用了资源其他进程必须等待。请求和保持条件Hold and Wait进程占有了至少一个资源并且在等待其他进程的资源时保持对已占有资源的占用。不可剥夺条件No Preemption资源只能由持有者显式地释放其他进程无法抢占已被占用的资源。循环等待条件Circular Wait多个进程形成一种循环等待资源的关系每个进程都在等待下一个进程所占有的资源。
当这四个条件同时满足时就可能发生死锁。当系统进入死锁状态后没有外部干预系统将无法恢复正常。
为了避免死锁的发生可以采取以下策略
破坏互斥条件对于某些资源允许多个进程共享或同时访问。 破坏请求和保持条件进程请求资源时不保持已占有的资源而是先释放已占有的资源再重新请求。破坏不可剥夺条件对于某些资源允许系统剥夺已占有的资源将其分配给其他进程。破坏循环等待条件通过定义资源的线性顺序要求进程按顺序申请资源避免形成循环等待的情况。
死锁是一种复杂的并发问题需要细心的设计和合理的资源管理来避免。在实际开发中可以使用死锁检测、死锁避免、死锁恢复等技术手段来处理死锁问题。
26.用java 代码实现一个死锁用例说说怎么解决死锁问题回到用例代码下如何解决死锁问题呢高频考点
死锁是一个并发编程中常见的问题它发生在两个或更多线程互相持有对方所需要的资源而无法继续执行的情况。下面是用 Java 代码实现一个简单的死锁示例
package org.zyf.javabasic.test.thread;/*** program: zyfboot-javabasic* description: 死锁用例* author: zhangyanfeng* create: 2023-08-13 22:37**/
public class DeadlockExample {private static Object resource1 new Object();private static Object resource2 new Object();public static void main(String[] args) {Thread thread1 new Thread(() - {synchronized (resource1) {System.out.println(Thread 1: Holding resource 1...);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread 1: Waiting for resource 2...);synchronized (resource2) {System.out.println(Thread 1: Acquired resource 2!);}}});Thread thread2 new Thread(() - {synchronized (resource2) {System.out.println(Thread 2: Holding resource 2...);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread 2: Waiting for resource 1...);synchronized (resource1) {System.out.println(Thread 2: Acquired resource 1!);}}});thread1.start();thread2.start();}
}在这个示例中两个线程thread1 和 thread2分别持有 resource1 和 resource2并试图获取对方的资源。由于每个线程都在等待另一个线程释放资源因此这段代码会导致死锁。
解决死锁问题需要采取一些常见的方法和策略以确保线程在并发执行时不会发生死锁。以下是一些解决死锁问题的方法
避免使用多个锁尽量减少在代码中使用多个锁这样可以减少死锁的可能性。如果有多个锁确保线程按照相同的顺序获取锁这样可以避免循环等待导致的死锁。使用超时机制在尝试获取锁时设置一个超时时间如果在超时时间内无法获取到锁则放弃该操作释放已经持有的锁并进行回退操作避免死锁发生。使用Lock对象Java提供了java.util.concurrent.locks.Lock接口它比传统的synchronized块更加灵活可以使用tryLock()方法尝试获取锁并在获取失败时进行后续处理从而避免死锁。按顺序获取锁在使用多个锁的情况下确保线程按照固定的顺序获取锁这样可以避免循环等待。死锁检测有些系统和工具可以进行死锁检测监测程序运行时的锁和资源使用情况如果发现潜在的死锁情况可以采取相应的措施例如中断某个线程解除死锁。避免长时间持有锁在设计并发程序时尽量避免长时间持有锁尽快完成对资源的访问和操作然后释放锁从而减少死锁的可能性。合理的资源分配策略设计合理的资源分配策略避免出现资源竞争的情况从而减少死锁的发生。
请注意死锁问题可能比较复杂解决方法需要根据具体的代码和场景来确定。在设计并发程序时要注意多线程之间的资源竞争和互斥关系合理地选择锁和同步方式并进行充分的测试和验证以确保程序在运行时不会出现死锁问题。
在上面提供的死锁代码示例中可以通过改变锁的获取顺序来解决死锁问题。确保线程在获取锁时按照相同的顺序来避免循环等待。具体来说可以修改线程2的代码将它的锁获取顺序与线程1相同从而避免死锁。
下面是修改后的代码示例
package org.zyf.javabasic.test.thread;/*** program: zyfboot-javabasic* description: 死锁用例解决* author: zhangyanfeng* create: 2023-08-13 22:41**/
public class DeadlockDealExample {private static Object resource1 new Object();private static Object resource2 new Object();public static void main(String[] args) {Thread thread1 new Thread(() - {synchronized (resource1) {System.out.println(Thread 1: Holding resource 1...);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread 1: Waiting for resource 2...);synchronized (resource2) {System.out.println(Thread 1: Acquired resource 2!);}}});Thread thread2 new Thread(() - {synchronized (resource1) {System.out.println(Thread 2: Holding resource 1...);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread 2: Waiting for resource 2...);synchronized (resource2) {System.out.println(Thread 2: Acquired resource 2!);}}});thread1.start();thread2.start();}
}通过将线程2的锁获取顺序调整为先获取resource1再获取resource2就能够避免死锁。当线程1持有resource1时线程2无法获取resource1从而避免了相互等待对方资源的情况解决了死锁问题。
27.请实现让10个任务同时并发启动
使用 ExecutorService 实现让10个任务同时并发启动
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ConcurrentTasks {public static void main(String[] args) {int numberOfTasks 10;ExecutorService executor Executors.newFixedThreadPool(numberOfTasks);for (int i 0; i numberOfTasks; i) {Runnable task new Task(Task (i 1));executor.execute(task);}executor.shutdown();}static class Task implements Runnable {private final String name;public Task(String name) {this.name name;}Overridepublic void run() {System.out.println(Executing name);// 在这里放置任务的逻辑}}
}创建了一个固定大小的线程池大小为10然后循环创建10个任务并使用 executor.execute(task) 方法将任务提交到线程池中执行。由于线程池的大小为10因此这10个任务可以同时并发启动执行。
28.AQS理论的数据结构是什么样的高频考点
更详细的见文章从ReentrantLock理解AQS的原理及应用总结
AQS全称为AbstractQueuedSynchronizer是Java中用于构建锁和同步器的框架性组件它是Java并发包中ReentrantLock、Semaphore、ReentrantReadWriteLock等同步器的基础。AQS的设计思想是在其内部维护了一个双向队列用于管理请求锁的线程。当有线程请求锁时AQS会将其封装成一个Node节点并加入到等待队列中线程则会进入阻塞状态。当持有锁的线程释放锁时AQS会从等待队列中唤醒一个线程来获取锁从而实现线程的同步和互斥。
AQS的主要特点包括
支持独占模式和共享模式。独占模式下只允许一个线程持有锁共享模式下可以允许多个线程同时持有锁。内部维护了一个双向队列用于管理请求锁的线程队列中的节点是线程的封装。通过CASCompare And Swap操作实现状态的改变状态可以是任意int类型的变量。具有可重入性即同一个线程可以多次获取同一把锁而不会出现死锁。
AQS的实现被广泛应用于Java并发包中的各种同步器如ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch等。AQS为这些同步器提供了一个统一的基础框架并且可以让开发人员基于此进行扩展和定制化。
AQS内部有3个对象一个是state用于计数器类似gc的回收计数器一个是线程标记当前线程是谁加锁的一个是阻塞队列。
它内部实现主要是状态变量state和一个FIFO队列来完成同步队列的头结点是当前获取到同步状态的结点获取同步状态state失败的线程会被构造成一个结点或共享式或独占式加入到同步队列尾部采用自旋CAS来保证此操作的线程安全随后线程会阻塞释放时唤醒头结点的后继结点使其加入对同步状态的争夺中。
29.ReentrantLock底层公平锁和非公平锁的原理高频考点
底层公平锁和非公平锁的原理涉及到 ReentrantLock 内部的同步器 Sync 的实现。底层原理
非公平锁默认
在非公平锁模式下ReentrantLock 使用的同步器是 NonfairSync 类。NonfairSync 内部使用了 CAS 操作通过 compareAndSetState 方法来修改锁的状态尝试直接获取锁。如果获取失败线程会进入等待队列并尝试重新获取锁。
公平锁
在公平锁模式下ReentrantLock 使用的同步器是 FairSync 类。FairSync 实现了一种公平的获取锁的机制。当线程尝试获取锁时如果发现队列中已经有等待的线程会将当前线程加入到等待队列中然后进入自旋等待直到获得锁。
同步器的基础
无论是公平锁还是非公平锁它们的底层同步器都基于 AbstractQueuedSynchronizerAQS实现。AQS 提供了一个框架用于构建基于队列的同步器。ReentrantLock 则在 AQS 的基础上实现了重入锁的语义。理解 AbstractQueuedSynchronizerAQS对于理解 ReentrantLock 的底层原理是至关重要的。AQS 是一个用于构建锁和其他同步器的框架它提供了一种基于 FIFO 等待队列的机制用于管理线程的获取和释放资源。 状态管理 AQS 使用一个 int 类型的变量来表示同步状态。这个状态可以被不同的同步器进行修改和检查。比如ReentrantLock 中的状态表示锁的持有次数。 等待队列 AQS 使用一个等待队列CLH 队列来维护等待线程。这个队列是一个虚拟的双向链表每个节点代表一个等待线程按照 FIFO 的顺序进行排队。 原子性操作 AQS 提供了一些原子性的操作比如 getState、setState、compareAndSetState 等这些操作基于 Unsafe 类的 CAS 操作。 独占锁与共享锁 AQS 支持独占锁和共享锁两种模式。ReentrantLock 就是一个独占锁的典型例子而 CountDownLatch 可以用作共享锁的例子。 模板方法 AQS 是一个框架它定义了一些模板方法其中最为重要的是 tryAcquire 和 tryRelease。这两个方法需要被子类重写以实现具体的同步逻辑。在 ReentrantLock 中这两个方法分别对应着获取锁和释放锁的逻辑。
30.多线程中sleep与wait的区别是什么
使用 sleep 主要是为了线程休眠不考虑锁的释放和唤醒的问题。
使用 wait 主要是为了线程等待并通常与锁和条件结合使用需要在同步块或同步方法中调用。
sleep 和 wait 是多线程编程中用于线程等待的两种不同机制它们的主要区别在于使用的上下文、作用对象以及条件触发等方面。 调用的上下文 sleep sleep 是 Thread 类的静态方法直接通过线程对象调用。它不会释放持有的锁即使当前线程持有某个对象的锁调用 sleep 后也不会释放该锁。 wait wait 是 Object 类的实例方法需要在对象的同步块或同步方法中调用。调用 wait 会释放对象的锁并使当前线程进入等待状态直到其他线程调用相同对象的 notify 或 notifyAll 方法唤醒它。
作用对象 sleep sleep 是线程级别的它不依赖于任何对象直接通过线程对象调用。 wait wait 是对象级别的它必须在同步块或同步方法中调用作用于当前对象。线程会等待其他线程调用相同对象的 notify 或 notifyAll 方法来唤醒它。
条件触发 sleep sleep 会在指定的时间内阻塞当前线程不依赖于外部条件的变化。即使指定的时间到达也不会被其他线程主动唤醒。 wait wait 会阻塞当前线程并且需要等待其他线程通过相同对象的 notify 或 notifyAll 方法来唤醒。通常wait 会与某个条件结合使用即在等待之前检查某个条件等待满足条件时才继续执行。
错误使用的情况 sleep 如果在同步块或同步方法中使用 sleep它不会释放锁可能会导致其他线程无法进入同步块。 wait 如果在没有持有锁的情况下调用 wait会抛出 IllegalMonitorStateException 异常。
31.notify和notifyAll区别
在 Java 中notify() 和 notifyAll() 都是用于线程间通信的方法用于唤醒等待在对象监视器上的线程。它们之间的主要区别在于
notify() 方法
notify() 方法用于唤醒在当前对象的监视器上等待的单个线程。如果有多个线程等待在同一个对象的监视器上那么只会唤醒其中一个线程但是具体唤醒哪个线程是不确定的取决于 JVM 的实现。唤醒的线程可以通过竞争重新获取对象的锁并继续执行。
notifyAll() 方法
notifyAll() 方法用于唤醒在当前对象的监视器上等待的所有线程。如果有多个线程等待在同一个对象的监视器上那么所有等待的线程都会被唤醒。唤醒的线程可以通过竞争重新获取对象的锁并继续执行。
因此notify() 方法只唤醒一个线程而 notifyAll() 方法会唤醒所有等待的线程。通常情况下当多个线程等待同一个条件变量时应该使用 notifyAll() 方法来确保所有等待的线程都被唤醒以避免发生死锁或者部分线程被遗漏的情况。
32.除了用Object.wait和Object.notifyAll来实现线程间的交互外你还会常用哪些来实现
除了使用 Object.wait() 和 Object.notifyAll() 方法来实现线程间的交互外还可以使用以下几种方式 使用Lock和Condition通过 java.util.concurrent.locks.Lock 接口和 java.util.concurrent.locks.Condition 接口提供的方法来实现线程间的协调和通信。使用 Condition 的 await() 和 signalAll() 方法可以代替 Object.wait() 和 Object.notifyAll() 方法。 使用CountDownLatchjava.util.concurrent.CountDownLatch 是一种同步工具类它可以使一个或多个线程等待其他线程完成操作后再执行。通过调用 CountDownLatch 的 await() 和 countDown() 方法可以实现线程间的等待和触发。 使用CyclicBarrierjava.util.concurrent.CyclicBarrier 也是一种同步工具类它可以使一组线程相互等待直到所有线程都到达某个屏障点后再继续执行。通过调用 CyclicBarrier 的 await() 方法可以实现线程间的等待和同步。 使用Semaphorejava.util.concurrent.Semaphore 是一种计数信号量它可以限制同时访问某个资源的线程数量。通过调用 Semaphore 的 acquire() 和 release() 方法可以实现线程的互斥和同步。 使用BlockingQueuejava.util.concurrent.BlockingQueue 是一种线程安全的队列它提供了阻塞式的读写操作。通过将 BlockingQueue 作为线程间的共享数据结构可以实现线程间的安全通信。
33.ThreadLocal的原理和实现高频考点
了解ThreadLocal
ThreadLocal主要用来存储当前线程上下文的变量信息它可以保障存储进去的数据只能被当前线程读取到并且线程之间不会相互影响。ThreadLocal提供了set和get函数set函数表示把数据存储到线程的上下文中get函数表示从线程的上下文中读取数据。通过get函数读取数据类似于以当前线程线程为key从map中读取数据。在实际的应用场景中InheritableThreadLocal可能更常用它不仅可以取出当前线程存储的数据还可以在子线程中读取父线程存储的数据。某些业务场景中需要开启子线程InheritableThreadLocal就派上用场了。
典型的应用场景
数据库事务事务的实现原理非常简单只需要在整个请求的处理过程中用同一个connection开启事务、执行sql、提交事务就可以了。按照这个思路实现起来也有两种方案一种就是在第一次执行的时候 获取connection在调用其他函数的时候显示的传递connection对象。这种方案只能存在于学习的demo中无法应用到项目实践。另一种方案就是通过AOP的方式对执行数据库事务的函数进行拦截。函数开始前获取connection开启事务并存储在ThreadLocal中任何用到connection的地方从ThreadLocal中获取函数执行完毕后提交事务释放connection。web项目中的用户登录信息web项目中用户的登录信息通常保存在session中。按照分层的设计理念往往会被分成controller层、service层、dao层等等还约定在service层是不能处理request、session等对象的。一种方案是调用service函数的时候显示的传递用户信息另一种方案则是用到了ThreadLocal做一个拦截器把用户信息放在ThreadLocal中在任何用到用户信息的时候只需要从TreadLocal中读取就可以了。
ThreadLocal实现原理
step1首先看一下ThreadLocalMap它是在ThreadLocal定义的一个内部类看名字就可以知道它用你来存储键值对的。只不过呢它的Key只能是ThreadLocal对象。step2再来看一下Thread它有个ThreadLocalMap类型的属性threadLocals。step3最后看一下get()函数的实现得到当前线程的ThreadLocalMap然后以当前的ThreadLocal对象为key读取数据。这也就解释了为什么线程之间不会相互干扰因为读取数据的时候是从当前线程的ThreadLocalMap中读取的。
34.ThreadLocal为什么要使用弱引用和内存泄露问题高频考点
ThreadLocal 使用弱引用的主要目的是为了防止内存泄漏。在多线程环境中如果没有正确处理 ThreadLocal 的引用关系可能导致线程结束后ThreadLocal 对象及其对应的值无法被垃圾回收从而造成内存泄漏。
内存泄漏场景
强引用时的问题如果 ThreadLocal 使用强引用当一个线程持有 ThreadLocal 对象并且该线程长时间存活那么 ThreadLocal 对象及其对应的值将一直存在于内存中。
即使该线程结束ThreadLocal 对象对应的值在 ThreadLocalMap 中仍然存在因为 ThreadLocalMap 是线程的一个字段会一直存在于内存中。
线程结束时的清理问题如果一个线程结束但没有显式调用 ThreadLocal 的 remove 方法来清理对应的值那么这部分内存将一直被占用。
在长时间运行的服务或应用中可能会创建大量的 ThreadLocal 实例如果不及时清理可能会导致大量内存泄漏。
弱引用的解决方案
使用弱引用可以解决上述内存泄漏问题
弱引用特点弱引用在垃圾回收时会被更容易地回收。如果一个对象只被弱引用引用而没有被强引用引用那么在下一次垃圾回收时这个对象就会被回收。
ThreadLocalMap.Entry 使用弱引用ThreadLocalMap.Entry 是 ThreadLocalMap 中的元素其中的 ThreadLocal 使用弱引用。当 ThreadLocal 对象被垃圾回收时对应的 Entry 也会被回收。
解决内存泄漏当线程结束时ThreadLocalMap 会被回收其中的弱引用 ThreadLocal 对象也会被回收从而避免了内存泄漏。
总体而言使用弱引用可以帮助 ThreadLocal 更及时地释放其引用的对象从而避免因长时间保持引用而导致的内存泄漏问题。
35.ThreadLocal怎么解决内存泄露的问题高频考点
ThreadLocal 可能导致内存泄漏的情况通常是由于没有及时清理 ThreadLocal 引用导致的。以下是一些帮助避免 ThreadLocal 导致的内存泄漏问题
1.显式调用 remove 方法在不再需要使用 ThreadLocal 存储的数据时建议显式调用 remove 方法将 ThreadLocal 与其对应的值从当前线程的 ThreadLocalMap 中移除。
2.使用 try-with-resourcesJava 7在 Java 7 及更高版本中可以使用 try-with-resources 语句来自动管理资源包括 ThreadLocal 的清理。
3.使用弱引用ThreadLocal 自身在实现上使用了弱引用但如果存储的值是强引用仍然可能导致内存泄漏。尽量存储使用弱引用引用的对象或者确保在不需要时及时清理引用。
4.使用静态内部类如果需要使用 ThreadLocal 在静态范围内存储值可以考虑使用静态内部类并将 ThreadLocal 定义为该内部类的静态成员。这样可以避免直接持有外部类的引用降低内存泄漏的风险。
5.使用框架某些框架和库提供了专门用于解决 ThreadLocal 内存泄漏问题的解决方案例如 ThreadLocalCleaner 等。
注意每次线程结束时ThreadLocalMap 应该会被自动清理但在某些情况下例如线程池线程可能不会立即终止因此需要额外的注意来防止内存泄漏。
36.为什么要使用线程池
更详细的见文章对Java线程池ThreadPoolExecutor的理解分析
Java的线程池是运用场景最多的并发框架几乎所有需要异步或者并发执行任务的程序都可以使用线程池。 合理使用线程池能带来的好处
降低资源消耗。 通过重复利用已经创建的线程降低线程创建的和销毁造成的消耗。例如工作线程Woker会无线循环获取阻塞队列中的任务来执行。提高响应速度。 当任务到达时任务可以不需要等到线程创建就能立即执行。提高线程的可管理性。 线程是稀缺资源Java的线程池可以对线程资源进行统一分配、调优和监控。
37.线程池的线程数量确定状态分析关闭方式
更详细的见文章对Java线程池ThreadPoolExecutor的理解分析
线程池的线程数量怎么确定
一般来说如果是CPU密集型应用则线程池大小设置为N1。一般来说如果是IO密集型应用则线程池大小设置为2N1。在IO优化中线程等待时间所占比例越高需要越多线程线程CPU时间所占比例越高需要越少线程。这样的估算公式可能更适合最佳线程数目 线程等待时间线程CPU时间/线程CPU时间 * CPU数目
线程池的五种运行状态 RUNNING 该状态的线程池既能接受新提交的任务又能处理阻塞队列中任务。 SHUTDOWN该状态的线程池**不能接收新提交的任务****但是能处理阻塞队列中的任务**。处于 RUNNING 状态时调用 shutdown()方法会使线程池进入到该状态。 注意 finalize() 方法在执行过程中也会隐式调用shutdown()方法。 STOP 该状态的线程池不接受新提交的任务也不处理在阻塞队列中的任务还会中断正在执行的任务。在线程池处于 RUNNING 或 SHUTDOWN 状态时调用 shutdownNow() 方法会使线程池进入到该状态 TIDYING 如果所有的任务都已终止workerCount (有效线程数)0 。线程池进入该状态后会调用 terminated() 钩子方法进入TERMINATED 状态。 TERMINATED 在terminated()钩子方法执行完后进入该状态默认terminated()钩子方法中什么也没有做。
线程池的关闭shutdown或者shutdownNow方法
可以通过调用线程池的shutdown或者shutdownNow方法来关闭线程池遍历线程池中工作线程逐个调用interrupt方法来中断线程。 shutdown方法与shutdownNow的特点
shutdown方法将线程池的状态设置为SHUTDOWN状态只会中断空闲的工作线程。shutdownNow方法将线程池的状态设置为STOP状态会中断所有工作线程不管工作线程是否空闲。调用两者中任何一种方法都会使isShutdown方法的返回值为true线程池中所有的任务都关闭后isTerminated方法的返回值为true。通常使用shutdown方法关闭线程池如果不要求任务一定要执行完则可以调用shutdownNow方法。
38.如何控制线程池线程的优先级
在Java中线程的优先级可以通过设置线程的优先级属性来控制。线程池中的线程也可以通过设置优先级来调整其执行顺序。以下是设置线程池线程优先级的一般步骤
创建线程池对象首先使用Executors类或ThreadPoolExecutor类创建一个线程池对象。
ExecutorService executor Executors.newFixedThreadPool(10);
自定义线程工厂通过实现ThreadFactory接口自定义一个线程工厂类用于创建线程对象并设置线程的优先级。
class CustomThreadFactory implements ThreadFactory {Overridepublic Thread newThread(Runnable r) {Thread t new Thread(r);t.setPriority(Thread.MAX_PRIORITY); // 设置线程优先级return t;}
}
创建线程池并设置线程工厂使用自定义的线程工厂类创建线程池对象并将其设置为线程池的线程工厂。
ExecutorService executor Executors
.newFixedThreadPool(10, new CustomThreadFactory());
通过以上步骤线程池中的线程将使用自定义的线程工厂来创建从而可以设置线程的优先级。在上述示例中将线程优先级设置为Thread.MAX_PRIORITY也可以根据需求设置其他优先级如Thread.MIN_PRIORITY或Thread.NORM_PRIORITY。
需要注意的是线程的优先级并不是绝对的它只是给调度器一个提示告诉它线程的相对重要性。实际的线程调度行为还受到操作系统和底层硬件的影响。因此不能过度依赖线程的优先级来控制程序的执行顺序和性能。
此外需要注意的是在使用线程池时线程的优先级可能被线程池管理器调整以便更好地管理线程的执行顺序和资源利用。因此在设置线程池中线程的优先级时需要结合具体的场景和需求来评估其影响。
39.核心线程池ThreadPoolExecutor的参数/常见线程池的创建参数是什么样的高频考点
更详细的见文章对Java线程池ThreadPoolExecutor的理解分析
可以通过ThreadPoolExecutor来创建一个线程池先上代码吧
new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue, RejectedExecutionHandler handler)
常用的5个核心池、最大池、空闲时间、时间的单位、阻塞队列另外两个拒绝策略、线程工厂类
corePoolSize指定了线程池中的线程数量maximumPoolSize指定了线程池中的最大线程数量keepAliveTime线程池维护线程所允许的空闲时间unit: keepAliveTime 的单位。workQueue任务队列被提交但尚未被执行的任务。threadFactory线程工厂用于创建线程一般用默认的即可。handler拒绝策略。当任务太多来不及处理如何拒绝任务。
具体详细说明
corePoolSize线程池的基本大小
提交一个任务到线程池时线程池会创建一个新的线程来执行任务。注意 即使有空闲的基本线程能执行该任务也会创建新的线程。如果线程池中的线程数已经大于或等于corePoolSize则不会创建新的线程。如果调用了线程池的prestartAllCoreThreads()方法线程池会提前创建并启动所有基本线程。
maximumPoolSize线程池的最大数量 线程池允许创建的最大线程数。
阻塞队列已满线程数小于maximumPoolSize便可以创建新的线程执行任务。如果使用无界的阻塞队列该参数没有什么效果。
workQueue工作队列 用于保存等待执行的任务的阻塞队列。
ArrayBlockingQueue 基于数组结构的有界阻塞队列按FIFO先进先出原则对任务进行排序。使用该队列线程池中能创建的最大线程数为maximumPoolSize。LinkedBlockingQueue 基于链表结构的有界阻塞队列按FIFO先进先出原则对任务进行排序吞吐量高于ArrayBlockingQueue。使用该队列线程池中能创建的最大线程数为corePoolSize。静态工厂方法 Executor.newFixedThreadPool()使用了这个队列。SynchronousQueue 一个不存储元素的阻塞队列。添加任务的操作必须等到另一个线程的移除操作否则添加操作一直处于阻塞状态。静态工厂方法 Executor.newCachedThreadPool()使用了这个队列。PriorityBlokingQueue 一个支持优先级的无界阻塞队列。使用该队列线程池中能创建的最大线程数为corePoolSize。
keepAliveTime线程活动保持时间 线程池的工作线程空闲后保持存活的时间。如果任务多而且任务的执行时间比较短可以调大keepAliveTime提高线程的利用率。 unit线程活动保持时间的单位 可选单位有DAYS、HOURS、MINUTES、毫秒、微秒、纳秒。 handler饱和策略或者又称拒绝策略 当队列和线程池都满了即线程池饱和了必须采取一种策略处理提交的新任务。
AbortPolicy 无法处理新任务时直接抛出异常这是默认策略。CallerRunsPolicy用调用者所在的线程来执行任务。DiscardOldestPolicy丢弃阻塞队列中最靠前的一个任务并执行当前任务。DiscardPolicy 直接丢弃任务。
threadFactory 构建线程的工厂类
40.new ThreadPoolExecutor(10,100,10,TimeUnit.MILLISECONDS,new LinkedBlockingQueue(10));一个这样创建的线程池当已经有10个任务在运行时第11个任务提交到此线程池执行的时候会发生什么为什么
在这样创建的线程池中当已经有10个任务在运行时第11个任务提交到此线程池执行时会发生以下情况 因为线程池的核心线程数为10最大线程数为100所以前10个任务会立即启动并被线程池中的10个核心线程执行。 第11个任务会被放入线程池的任务队列中即LinkedBlockingQueue中。 由于任务队列的大小为10而此时已经有10个任务在队列中等待执行所以第11个任务会被成功添加到队列中。 如果任务队列被填满并且当前线程池中的线程数量还未达到最大线程数100则会创建新的线程执行任务。 如果任务队列已满并且当前线程池中的线程数量已经达到最大线程数100则新的任务将无法被提交到线程池中并且会根据线程池的拒绝策略进行处理。默认情况下线程池的默认拒绝策略是抛出 RejectedExecutionException 异常。
总之当已经有10个任务在运行时第11个任务提交到此线程池执行时如果任务队列未满则任务会被成功添加到队列中如果任务队列已满并且线程池中的线程数量已经达到最大线程数则根据线程池的拒绝策略进行处理。
41.实现一个自定义的ThreadFactory的作用通常是
实现一个自定义的 ThreadFactory 的作用通常包括以下几个方面 命名线程自定义的 ThreadFactory 可以为线程设置有意义的名称使得在日志和调试信息中能够清晰地识别线程的作用和来源。这样有助于跟踪线程的执行情况和定位问题。 设置线程属性通过自定义的 ThreadFactory可以为线程设置一些属性如线程的优先级、是否为守护线程等以满足特定的需求。 创建定制化的线程自定义的 ThreadFactory 可以根据应用的需求创建定制化的线程如自定义的异常处理器、线程组等以增强线程的管理和控制能力。 封装线程创建过程通过自定义的 ThreadFactory可以封装线程的创建过程使得应用代码与线程创建逻辑解耦提高代码的可维护性和可扩展性。 统一管理线程使用自定义的 ThreadFactory 可以统一管理应用中所有线程的创建集中处理线程的创建逻辑和管理策略方便进行统一的管理和调整。
总的来说自定义的 ThreadFactory 主要作用是为了提供更加灵活和定制化的线程创建和管理功能使得应用能够更好地满足特定的需求并提高线程的可观察性、可控性和可维护性。
42.常见的线程池创建和参数分析高频考点 更详细的见文章对Java线程池ThreadPoolExecutor的理解分析
常见的线程池创建主要依赖于 java.util.concurrent 包提供的 Executors 工厂类同时需要根据任务性质和工作负载来选择合适的线程池参数。以下是一些常见线程池的创建和参数分析1. FixedThreadPool固定大小的线程池
ExecutorService executor Executors.newFixedThreadPool(5);
固定大小的线程池核心线程数和最大线程数相等。适用于处理固定数量的任务控制并发线程数。
2. CachedThreadPool缓存线程池
ExecutorService executor Executors.newCachedThreadPool();
核心线程数为 0最大线程数不限制。适用于处理大量短时间的任务线程数量根据任务动态调整。
3. SingleThreadExecutor单一线程池
ExecutorService executor Executors.newSingleThreadExecutor();
只有一个核心线程确保所有任务按顺序执行。适用于需要顺序执行任务的场景。
4. ScheduledThreadPool定时任务线程池
ScheduledExecutorService scheduledExecutor Executors.newScheduledThreadPool(3);
适用于需要定时执行任务的场景可以延迟执行或定期执行。
5. ThreadPoolExecutor自定义线程池
ThreadPoolExecutor executor new ThreadPoolExecutor(corePoolSize, // 核心线程数maximumPoolSize, // 最大线程数keepAliveTime, // 线程空闲时间TimeUnit.SECONDS, // 时间单位new LinkedBlockingQueue(), // 工作队列Executors.defaultThreadFactory(), // 线程工厂new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
corePoolSize核心线程数线程池维护的最小线程数。maximumPoolSize最大线程数线程池维护的最大线程数。keepAliveTime线程空闲时间非核心线程在空闲时的最大存活时间。TimeUnit时间单位。workQueue工作队列存储未执行任务的队列。threadFactory线程工厂用于创建线程。handler拒绝策略用于处理任务无法被执行的情况。
参数选择注意事项
核心线程数和最大线程数的选择要根据任务的性质和系统资源进行权衡。workQueue 的选择要根据任务提交速度和处理速度的差异选择合适的阻塞队列。线程空闲时间的设置要根据系统负载和任务性质进行调整。拒绝策略的选择要根据业务需求例如抛出异常、丢弃任务、调用者运行等。
不同的线程池适用于不同的场景根据具体需求进行选择。在实际应用中参数的调优通常需要结合系统资源状况和任务的特性进行综合考虑。
43.ThreadPoolExecutor的工作流程高频考点 更详细的见文章对Java线程池ThreadPoolExecutor的理解分析
基本背景思路
一个新的任务到线程池时线程池的处理流程如下 线程池判断核心线程池里的线程是否都在执行任务。 如果不是创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务则进入下个流程。线程池判断阻塞队列是否已满。 如果阻塞队列没有满则将新提交的任务存储在阻塞队列中。如果阻塞队列已满则进入下个流程。线程池判断线程池里的线程是否都处于工作状态。 如果没有则创建一个新的工作线程来执行任务。如果已满则交给饱和策略来处理这个任务。
ThreadPoolExecutor类具体的处理流程
线程池的核心实现类是ThreadPoolExecutor类用来执行提交的任务。因此任务提交到线程池时具体的处理流程是由ThreadPoolExecutor类的execute()方法去完成的。 如果当前运行的线程少于corePoolSize则创建新的工作线程来执行任务执行这一步骤需要获取全局锁。如果当前运行的线程大于或等于corePoolSize而且BlockingQueue未满则将任务加入到BlockingQueue中。如果BlockingQueue已满而且当前运行的线程小于maximumPoolSize则创建新的工作线程来执行任务执行这一步骤需要获取全局锁。如果当前运行的线程大于或等于maximumPoolSize任务将被拒绝并调用RejectExecutionHandler.rejectExecution()方法。即调用饱和策略对任务进行处理。
44.ScheduledThreadPoolExecutor中的使用的是什么队列内部如何实现任务排序的
ScheduledThreadPoolExecutor继承自ThreadPoolExecutor使用的工作队列是 DelayedWorkQueue 。DelayedWorkQueue 是 DelayedQueue 的一个实现它继承自 AbstractQueue而后者实现了基本的队列操作。 在 ScheduledThreadPoolExecutor 中DelayedWorkQueue 用于存储实现了 ScheduledFuture 接口的任务其中的任务具有延迟执行或定期执行的特性。这样的任务会按照它们的延迟时间或周期进行排序。
内部任务排序机制
按照延迟时间排序
DelayedWorkQueue 内部维护了一个有序的优先级队列PriorityQueue按照任务的延迟时间进行排序。在插入任务时根据任务的延迟时间将任务放入队列的适当位置。
按照周期性任务的下次执行时间排序
对于周期性任务即有固定的执行周期在任务执行完一次后会根据下次执行的时间重新计算延迟时间然后放回队列的适当位置。这样就保证了周期性任务按照下次执行时间的顺序进行排序。
通过这种排序机制ScheduledThreadPoolExecutor 能够按照任务的延迟时间或者下次执行时间来执行任务确保任务按照预期的时间顺序执行。
值得注意的是DelayedWorkQueue 作为有界队列可以配置最大容量以控制任务的排队数量。如果队列已满新的任务可能会导致拒绝策略的触发。
45.线程池的运行逻辑FixedThreadPool、CachedThreadPool的原理高频考点
线程池是一种用于管理和调度线程的机制它可以有效地管理线程的创建、执行和销毁。下面是线程池的运行逻辑以及FixedThreadPool和CachedThreadPool的原理。
线程池的运行逻辑
初始化线程池设置线程池的核心线程数、最大线程数、任务队列等参数。当有任务提交给线程池时线程池会按照以下规则执行a. 如果当前运行的线程数小于核心线程数创建新的线程来执行任务。b. 如果当前运行的线程数等于核心线程数将任务放入任务队列等待执行。c. 如果任务队列已满且当前运行的线程数小于最大线程数创建新的线程来执行任务。d. 如果任务队列已满且当前运行的线程数达到最大线程数根据线程池的拒绝策略来处理任务。当线程执行完一个任务后它会从任务队列中获取下一个任务进行执行直到线程池关闭或出现异常。如果线程池闲置一段时间根据具体实现而定超过预设的存活时间额外的线程会被终止以节省资源。线程池可以通过调整核心线程数、最大线程数和任务队列等参数进行灵活的配置和优化。
FixedThreadPool的原理
FixedThreadPool是一种固定大小的线程池它会在初始化时创建指定数量的线程并且线程数不会改变。它的原理是
初始化FixedThreadPool时创建指定数量的线程即核心线程数和最大线程数都是固定的。当有任务提交给FixedThreadPool时如果有空闲线程则立即分配线程来执行任务。如果所有线程都在执行任务且任务队列已满则新任务将被阻塞直到有线程空闲或任务队列有空闲位置。因为FixedThreadPool的线程数是固定的所以它适合于需要固定线程数的场景例如需要控制资源消耗或并发度的应用。
CachedThreadPool的原理
CachedThreadPool是一种根据需要自动调整线程数量的线程池它的原理是
初始化CachedThreadPool时不会创建任何线程。当有任务提交给CachedThreadPool时它会尝试重用之前空闲的线程如果有可用的空闲线程则立即分配线程来执行任务。如果所有线程都在执行任务且任务队列已满则会创建新的线程来处理新的任务。如果某个线程在一段时间内没有任务可执行它将被终止并从线程池中移除以节省资源。当新的任务提交给CachedThreadPool时如果之前终止的线程数量不超过最大线程数那么会重新使用之前终止的线程来执行任务而不是创建新的线程。CachedThreadPool会根据任务的数量和执行时间的情况自动调整线程数量增加线程以处理更多的任务减少线程以释放闲置的资源。CachedThreadPool适用于任务量不固定、需要快速响应并且执行时间较短的场景可以根据需求动态调整线程数量以提高线程的利用率和系统的响应能力。
总而言之FixedThreadPool和CachedThreadPool是两种常见的线程池实现。FixedThreadPool适用于需要固定线程数的场景而CachedThreadPool适用于任务量不确定的场景它会根据需求动态调整线程数量以提高系统的性能。
46.用Executors.newCachedThreadPool创建的线程池在运行的过程中有可能产生的风险是
使用Executors.newCachedThreadPool()创建的线程池是一个可缓存的线程池它会根据需要动态地创建新线程如果线程在60秒内未被使用就会被终止并从池中移除。虽然这种线程池具有灵活性和高效性但也存在一些潜在的风险和问题 线程数量不受限制newCachedThreadPool()创建的线程池没有固定的线程数限制理论上可以创建大量的线程如果并发请求过多可能会导致服务器资源不足出现内存溢出或者CPU过载等问题。 长时间运行的任务可能导致内存泄漏由于线程池会在一定时间内清理未使用的线程长时间运行的任务可能会导致线程被长时间占用无法及时回收从而导致内存泄漏。 任务执行时间不可控线程池中的线程数量是动态调整的任务的执行时间可能受到线程池中其他任务的影响如果某些任务执行时间较长可能会影响其他任务的执行效率。 可能导致任务排队过多当任务提交速度大于线程池处理速度时任务会被放入任务队列中等待执行如果任务队列过长可能会导致系统资源耗尽造成系统性能下降或者宕机。 线程生命周期不受控制由于线程池中的线程是可缓存的因此线程的生命周期由线程池管理无法手动控制线程的生命周期可能会导致资源管理不当或者任务执行异常处理不及时。
基于以上风险使用newCachedThreadPool()时需要注意合理调整任务提交速度和任务执行时间避免出现资源不足或者任务排队过多的情况同时需要注意及时处理长时间运行的任务和异常情况以保障系统的稳定性和性能。
47.阻塞队列ArrayBlockingQueue、LinkedBlockingQueue分析高频考点
ArrayBlockingQueue和LinkedBlockingQueue都是Java中常见的阻塞队列实现它们都提供了线程安全的队列操作并且支持在队列为空或已满时的阻塞操作。
ArrayBlockingQueue
ArrayBlockingQueue是一个基于数组的有界阻塞队列它的容量在创建时被固定。内部使用一个固定大小的数组来存储元素因此在添加或移除元素时具有较高的效率。当尝试将元素添加到已满的队列中时操作将被阻塞直到队列有空闲位置可用。当尝试从空队列中移除元素时操作也将被阻塞直到队列中有元素可供移除。ArrayBlockingQueue的阻塞操作是通过使用内置的锁和条件变量实现的。
LinkedBlockingQueue
LinkedBlockingQueue是一个基于链表的可选界限阻塞队列它可以选择在创建时设置容量上限如果未指定容量则默认为无界队列。内部使用链表来存储元素因此可以动态调整大小并且没有固定容量的限制。当尝试将元素添加到已满的有界队列中时操作将被阻塞直到队列有空闲位置可用。当尝试从空队列中移除元素时操作也将被阻塞直到队列中有元素可供移除。LinkedBlockingQueue的阻塞操作是通过使用内置的锁和条件变量实现的。
两者的选择
如果你需要一个有界队列并且在队列已满时阻塞添加操作或队列为空时阻塞移除操作可以选择ArrayBlockingQueue。如果你需要一个可以动态调整大小的队列或者希望使用默认的无界队列可以选择LinkedBlockingQueue。
需要注意的是无界队列可能会在持续添加元素时耗尽系统的内存资源因此在选择队列实现时要根据场景和需求进行权衡和选择。
48.请合理的使用Queue来实现一个高并发的生产/消费的场景给些核心的代码片段。
CustomProducerConsumer 类封装了生产者消费者模型的实现细节使用了 wait() 和 notifyAll() 方法来实现线程之间的等待和通知。生产者通过调用 produce() 方法往缓冲区中放入数据消费者通过调用 consume() 方法从缓冲区中取出数据。
import java.util.LinkedList;
import java.util.Queue;class CustomProducerConsumer {private final QueueInteger buffer;private final int capacity;public CustomProducerConsumer(int capacity) {this.capacity capacity;this.buffer new LinkedList();}public void produce(int value) throws InterruptedException {synchronized (this) {while (buffer.size() capacity) {wait();}buffer.offer(value);System.out.println(Produced: value);notifyAll();}}public int consume() throws InterruptedException {synchronized (this) {while (buffer.isEmpty()) {wait();}int value buffer.poll();System.out.println(Consumed: value);notifyAll();return value;}}
}class Producer implements Runnable {private final CustomProducerConsumer pc;public Producer(CustomProducerConsumer pc) {this.pc pc;}Overridepublic void run() {for (int i 0; i 10; i) {try {pc.produce(i);Thread.sleep(100); // 模拟生产时间} catch (InterruptedException e) {e.printStackTrace();}}}
}class Consumer implements Runnable {private final CustomProducerConsumer pc;public Consumer(CustomProducerConsumer pc) {this.pc pc;}Overridepublic void run() {for (int i 0; i 10; i) {try {int value pc.consume();Thread.sleep(200); // 模拟消费时间} catch (InterruptedException e) {e.printStackTrace();}}}
}public class Main {public static void main(String[] args) {CustomProducerConsumer pc new CustomProducerConsumer(5); // 缓冲区容量为5Thread producerThread new Thread(new Producer(pc));Thread consumerThread new Thread(new Consumer(pc));producerThread.start();consumerThread.start();}
}49.线程池关闭原理
线程池的关闭原理涉及到线程池的生命周期管理和任务处理的终止过程。下面是线程池关闭的一般原理
停止接收新任务首先线程池需要停止接收新的任务提交。可以通过调用线程池的 shutdown() 方法来实现。此时线程池将拒绝新的任务提交并且不会再接受新的任务。处理已提交的任务一旦停止接收新的任务线程池会继续处理已提交的任务。已经在等待队列中的任务将继续执行正在执行的任务也会继续执行直到所有的任务都执行完毕。清空等待队列在处理完已提交的任务后线程池会尝试清空等待队列中的任务。可以通过调用线程池的 shutdownNow() 方法来实现。该方法将会尝试终止所有的任务并返回等待队列中未执行的任务列表。终止线程池一旦等待队列中的任务被清空线程池将会彻底终止。此时所有的工作线程都将停止并且线程池的状态将被标记为已终止。
需要注意的是线程池的关闭并不会立即停止所有的任务。已经在执行的任务需要等待其执行完成而等待队列中的任务可以选择是否继续等待执行或者被丢弃。同时如果线程池中的任务存在依赖关系需要注意任务之间的处理顺序以免产生不可预期的结果。
在使用线程池时建议在合适的时机进行关闭操作以确保资源的正确释放和程序的正常终止。可以通过适当的方式监听线程池的关闭状态以便在需要时进行后续的处理。
50.JUC下的常见类的使用take、poll的区别put、offer的区别
在 Java 并发编程中JUCJava Util Concurrent包提供了一些常见的类例如 BlockingQueue 接口和其实现类 ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue 等。这些类通常用于多线程之间的数据交换和协调。
take() 和 poll() 方法的区别
take() 方法用于从队列中取出元素如果队列为空则会阻塞线程直到队列中有元素可以取出。poll() 方法也用于从队列中取出元素如果队列为空则会立即返回 null不会阻塞线程。
put() 和 offer() 方法的区别
put() 方法用于向队列中添加元素如果队列已满则会阻塞线程直到队列有空闲位置可以添加元素。offer() 方法也用于向队列中添加元素如果队列已满则会立即返回 false不会阻塞线程。
简要总结一下
take() 和 put() 方法是阻塞的会等待队列状态满足条件再执行。poll() 和 offer() 方法是非阻塞的立即返回结果不会等待队列状态改变。
51.Future原理其局限性是什么并说说CompletableFuture核心原理
更详细的见文章CompletableFuture回调机制的设计与实现
Future的实现原理就是通过Future和FutureTask接口将任务封装成一个异步操作并在主线程中等待任务完成后获取执行结果。FutureTask是Future的一个具体实现通过阻塞方法和回调函数来实现异步操作的结果获取。
虽然Future在Java中提供了一种简单的异步编程技术但它也存在一些局限性包括以下几个方面
阻塞问题Future的get()方法是一个阻塞方法如果任务没有完成会一直阻塞当前线程这会导致整个应用程序的响应性下降。无法取消任务Future的cancel()方法可以用于取消任务的执行但如果任务已经开始执行则无法取消。此时只能等待任务执行完毕这会导致一定的性能损失。缺少异常处理Future的get()方法会抛出异常但是如果任务执行过程中抛出异常Future无法处理异常只能将异常抛给调用者处理。缺少组合操作Future只能处理单个异步操作无法支持多个操作的组合例如需要等待多个任务全部完成后再执行下一步操作。
综上所述Future虽然提供了一种简单的异步编程技术但它的局限性也是比较明显的。在实际应用中我们需要根据具体的业务需求和性能要求选择合适的异步编程技术。例如可以使用CompletableFuture来解决Future的一些问题它可以避免阻塞、支持异常处理和组合操作等功能。
CompletableFuture原理总述与回调机制总结 CompletableFuture是Java 8中引入的一个强大的异步编程工具它允许我们以非阻塞的方式处理异步操作并通过回调函数来处理异步操作完成后的结果。
CompletableFuture的核心原理是基于Java的Future接口和内部的状态机实现的。它可以通过三个步骤来实现异步操作 创建CompletableFuture对象通过CompletableFuture的静态工厂方法我们可以创建一个新的CompletableFuture对象并指定该对象的异步操作。通常情况下我们可以通过supplyAsync()或者runAsync()方法来创建CompletableFuture对象。 异步操作的执行在CompletableFuture对象创建之后异步操作就开始执行了。这个异步操作可以是一个计算任务或者一个IO操作。CompletableFuture会在另一个线程中执行这个异步操作这样主线程就不会被阻塞。 对异步操作的处理异步操作执行完成后CompletableFuture会根据执行结果修改其内部的状态并触发相应的回调函数。如果异步操作成功完成则会触发CompletableFuture的完成回调函数如果异步操作抛出异常则会触发CompletableFuture的异常回调函数。
CompletableFuture的优势在于它支持链式调用和组合操作。通过CompletableFuture的then系列方法我们可以创建多个CompletableFuture对象并将它们串联起来形成一个链式的操作流。在这个操作流中每个CompletableFuture对象都可以依赖于之前的CompletableFuture对象以实现更加复杂的异步操作。
总的来说CompletableFuture的原理是基于Java的Future接口和内部的状态机实现的它可以以非阻塞的方式执行异步操作并通过回调函数来处理异步操作完成后的结果。通过链式调用和组合操作CompletableFuture可以方便地实现复杂的异步编程任务。
52.你是否了解fork/join基本思想在工作中是如何使用的说说他们的优势是什么高频考点
基本内容的了解
Fork/Join 框架是 Java 并发编程中的一个重要工具用于实现分治任务的并行执行。在工作中可以使用 Fork/Join 框架来解决一些需要并行计算的问题比如大规模数据的排序、搜索和归约等任务。
Fork/Join 框架的核心是基于工作窃取Work Stealing算法实现的。这个算法的基本思想是让空闲的线程从其他线程的任务队列中窃取任务来执行以达到任务的动态负载均衡。
Fork/Join 框架主要包括以下几个核心组件 工作线程Worker Thread 每个工作线程都有自己的任务队列双端队列用于存放待执行的任务。当一个线程执行完自己任务队列中的任务后它会尝试从其他线程的任务队列中窃取任务来执行。 任务Task 任务是 Fork/Join 框架中的基本执行单元。通常使用 RecursiveTask 或 RecursiveAction 类来表示任务分别用于有返回值和无返回值的任务。任务可以递归地分解成更小的子任务直到达到某个阈值后停止分解。 工作窃取Work Stealing 当一个线程的任务队列为空时它会尝试从其他线程的任务队列中窃取任务来执行。这种机制能够充分利用多核 CPU 的计算资源提高任务的并行度和执行效率。 线程池ForkJoinPool Fork/Join 框架通过 ForkJoinPool 类来管理和调度线程。线程池中的每个线程都是一个工作线程它们会从任务队列中获取任务来执行并且可以相互之间进行工作窃取。
总的来说Fork/Join 框架利用工作窃取算法实现了任务的动态负载均衡通过递归地划分任务并利用多线程并行执行任务从而提高了并发程序的执行效率。
具体使用示例
当使用 Fork/Join 框架时一般需要以下几个步骤 定义任务类RecursiveTask 或 RecursiveAction 首先需要定义一个继承自 RecursiveTask 或 RecursiveAction 的任务类具体取决于任务是否有返回值。在任务类中需要实现 compute() 方法来执行实际的任务逻辑。 划分任务拆分 在 compute() 方法中需要根据具体的业务逻辑来划分任务将大任务拆分成多个小任务。这通常涉及到递归地划分任务直到任务达到某个阈值时停止拆分。 执行任务 使用 Fork/Join 框架的 ForkJoinPool 类来执行任务。创建一个 ForkJoinPool 实例并调用其 invoke() 方法来执行根任务。 合并结果如果有返回值的话 如果任务有返回值则需要在根任务执行完成后合并各个子任务的结果。通常是在 compute() 方法中进行结果的合并操作。
以下是一个简单的示例演示了如何使用 Fork/Join 框架来计算斐波那契数列的值
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;class FibonacciTask extends RecursiveTaskInteger {private final int n;public FibonacciTask(int n) {this.n n;}Overrideprotected Integer compute() {if (n 1) {return n;} else {FibonacciTask task1 new FibonacciTask(n - 1);FibonacciTask task2 new FibonacciTask(n - 2);task1.fork(); // 异步执行子任务1return task2.compute() task1.join(); // 同步执行子任务2同时等待子任务1完成}}public static void main(String[] args) {ForkJoinPool pool new ForkJoinPool();FibonacciTask task new FibonacciTask(10);int result pool.invoke(task);System.out.println(Result: result);}
}说说优势 高性能并行计算 Fork/Join 框架采用了工作窃取算法能够有效地利用多核 CPU 的计算资源提高任务的并行度和执行效率。通过将大任务分解成多个小任务并在多个线程之间动态地分配和执行任务Fork/Join 框架能够充分利用系统的计算资源实现高性能的并行计算。 简化并发编程 Fork/Join 框架提供了高层次的抽象使得开发者可以更轻松地编写并发程序。通过将任务的分解、执行和合并等细节封装在框架中开发者可以专注于业务逻辑的实现而无需关注线程的管理和同步等底层细节从而简化了并发编程的复杂性。 任务的动态调度与管理 Fork/Join 框架提供了自动线程管理的功能能够根据系统的负载和任务的执行情况动态地调整线程池的大小从而避免了线程过多或过少导致的资源浪费或性能下降问题。此外Fork/Join 框架还提供了一些监控和调优的工具可以帮助开发者更好地管理和优化并发任务的执行。
53.Java线程池的调优经验有哪些线程池的合理配置高频考点
更详细的见文章对Java线程池ThreadPoolExecutor的理解分析
这里直接推荐使用动态线程池配置和监控更加符合业务要求具体见上述博客
从以下几个角度分析任务的特性
任务的性质 CPU 密集型任务、IO 密集型任务和混合型任务。任务的优先级 高、中、低。任务的执行时间 长、中、短。任务的依赖性 是否依赖其他系统资源如数据库连接。
任务性质不同的任务可以用不同规模的线程池分开处理。 可以通过 Runtime.getRuntime().availableProcessors() 方法获得当前设备的 CPU 个数。
CPU 密集型任务配置尽可能小的线程如配置 个线程的线程池。IO 密集型任务则由于线程并不是一直在执行任务则配置尽可能多的线程如 。混合型任务如果可以拆分则将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务。只要这两个任务执行的时间相差不是太大那么分解后执行的吞吐率要高于串行执行的吞吐率如果这两个任务执行时间相差太大则没必要进行分解。
优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理它可以让优先级高的任务先得到执行。但是如果一直有高优先级的任务加入到阻塞队列中那么低优先级的任务可能永远不能执行。 执行时间不同的任务可以交给不同规模的线程池来处理或者也可以使用优先级队列让执行时间短的任务先执行。 依赖数据库连接池的任务因为线程提交 SQL 后需要等待数据库返回结果线程数应该设置得较大这样才能更好的利用 CPU。 建议使用有界队列有界队列能增加系统的稳定性和预警能力。可以根据需要设大一点比如几千。使用无界队列线程池的队列就会越来越大有可能会撑满内存导致整个系统不可用。
怎么对线程池进行有效监控
以通过线程池提供的参数读线程池进行监控有以下属性可以使用
taskCount线程池需要执行的任务数量包括已经执行完的、未执行的和正在执行的。completedTaskCount线程池在运行过程中已完成的任务数量completedTaskCount taskCount。largestPoolSize线程池曾经创建过的最大线程数量通过这个数据可以知道线程池是否满过。如等于线程池的最大大小则表示线程池曾经满了。getPoolSize: 线程池的线程数量。如果线程池不销毁的话池里的线程不会自动销毁所以线程池的线程数量只增不减。getActiveCount获取活动的线程数。
通过继承线程池并重写线程池的 beforeExecuteafterExecute 和 terminated 方法我们可以在任务执行前执行后和线程池关闭前干一些事情。
54.一个请求中计算操作需要50msdb操作需要100ms对于一台8核的机器来说如果要求cpu利用率达到100%如何设置线程数高频考点
要求 CPU 利用率达到 100% 并不意味着要将所有的 CPU 核心都使用起来而是要确保 CPU 在处理任务时始终处于繁忙状态尽量避免空闲。对于这个情况计算操作需要 50msDB 操作需要 100ms我们可以考虑使用以下的计算方式来确定线程数
计算 CPU 密集型任务需要的线程数
CPU 密集型任务的线程数可以根据 CPU 核心数来决定通常可以选择与 CPU 核心数相等的线程数。在一台 8 核的机器上如果要求 CPU 利用率达到 100%则可以选择使用 8 个线程。
计算 IO 密集型任务需要的线程数
IO 密集型任务例如 DB 操作由于需要等待 IO 操作的完成因此会出现线程阻塞的情况。一般来说IO 密集型任务的线程数可以设置为 CPU 核心数的倍数以确保 CPU 在等待 IO 操作完成时能够处理其他任务。在一台 8 核的机器上可以尝试设置为 2 * 8 16 个线程以充分利用 CPU 的处理能力。
因此在这种情况下可以将线程池的线程数设置为 16以确保 CPU 在执行计算操作时能够充分利用而在执行 DB 操作时能够保持线程不阻塞。同时需要根据实际情况进行性能测试和调优以确定最佳的线程数。
55.如果系统中不同的请求对应的cpu时间和io时间都不同那怎么设置线程数量高频考点
如果系统中不同的请求对应的 CPU 时间和 IO 时间都不同那么可以根据系统的性能特点和负载情况动态地调整线程池的大小来适应不同请求的处理需求。下面是一些思路
基于负载情况动态调整
可以根据系统的负载情况动态地调整线程池的大小。当系统负载较低时可以适当减少线程池的大小以节省资源。当系统负载较高时可以增加线程池的大小以处理更多的请求。
使用弹性线程池
弹性线程池可以根据任务队列的长度和负载情况动态地调整线程池的大小。当任务队列中的任务数量超过一定阈值时可以增加线程池的大小。当任务队列中的任务数量降低时可以减少线程池的大小。
根据请求类型和预估时间设置线程池参数
可以根据不同请求类型和预估的 CPU 时间和 IO 时间设置不同的线程池。对于 CPU 密集型任务可以设置较大的核心线程数以确保 CPU 能够充分利用。对于 IO 密集型任务可以设置较大的最大线程数以允许更多的线程等待 IO 完成。
监控和性能测试
可以通过监控系统的运行情况和进行性能测试收集系统的负载数据和响应时间等指标。根据监控数据和性能测试结果调整线程池的大小和参数以满足系统的性能需求。
总的来说针对不同的请求类型和预估的处理时间可以采用动态调整线程池大小的策略来适应不同请求的处理需求从而最大化地利用系统资源并保持良好的性能。
56.线程池核心数20最大600阻塞队列200当QPS200注意是qps的时候请求是调第三方阻塞超时请问怎么提高它的吞吐量注意不能加机器高频考点
根据条件线程池的核心线程数为 20最大线程数为 600阻塞队列大小为 200当 QPS 达到 200 时出现请求阻塞和超时的情况。
在不能增加机器的情况下分析具体原因如下
线程池参数设置不合理当 QPS 达到 200 时线程池的核心线程数和最大线程数都可能不足以处理这么多请求。如果请求处理时间较长会导致线程池中的线程被占用无法处理后续请求从而导致请求阻塞和超时。阻塞队列满导致请求被拒绝当线程池的核心线程数和最大线程数都已满且阻塞队列也已满时新的请求将被拒绝导致请求阻塞和超时。但当前的状态下QPS 达到 200阻塞队列大小为 200核心线程数为 20队列应该是不满的但不能排除需要增加埋点再次分析一下最好第三方调用导致的阻塞如果请求涉及到调用第三方服务且第三方服务响应时间较长或者不稳定会导致请求阻塞。当线程池中的线程都被阻塞等待第三方服务响应时无法处理后续请求从而导致请求超时。
基于以上分析可以采取以下措施来提高系统的吞吐量
调整线程池参数可以尝试增大线程池的核心线程数和最大线程数以及扩大阻塞队列的大小以适应更高的负载。当然是在机器参数满足的条件下进行。优化第三方调用对于耗时较长的第三方调用可以采用异步调用或者连接池等方式来优化减少请求阻塞时间。减少线程阻塞时间对于可能导致线程阻塞的业务逻辑进行优化尽量减少线程的等待时间以提高线程的利用率。
通过以上优化措施可以提高系统的吞吐量减少请求的阻塞和超时现象。
57.当前线程池是200线程单次处理请求20ms那么理论上单节点的qps 是多少呢
理论 QPSQueries Per Second每秒查询次数是衡量系统性能的一个重要指标它表示在一秒钟内系统能够处理的查询或请求的数量。对于单节点系统你可以使用以下公式来计算理论 QPS QPS 1 / 平均请求响应时间 其中平均请求响应时间是指系统从接收请求到完成响应的平均时间。这个时间通常以毫秒ms为单位。
注意这个公式是一个理论上的近似值实际的 QPS 可能会受到多种因素的影响包括系统的硬件性能、软件优化、负载、并发性等等。
假设你有一个线程池其中包含了 N 个线程而平均每个请求的响应时间仍然是 T 毫秒。在这种情况下你可以使用以下公式来计算理论 QPS QPS N / T 这里N 是线程池中的线程数量T 是平均请求响应时间以毫秒为单位。
线程池能够提高并发处理能力因此你可以同时处理更多的请求这会影响到系统的理论 QPS。但仍然要注意线程池的性能也会受到线程数量、线程调度、任务分配等因素的影响。因此你在使用线程池的情况下仍然需要进行性能测试和分析以确定系统的实际性能情况。
根据提供的信息当前线程池大小为 200每个线程处理一个请求的时间为 20 毫秒。可以使用以下公式来计算理论上的单节点 QPS QPS 线程池大小 / 单线程处理时间 将你提供的值代入公式 QPS 200 / (20 ms) 200 / 0.02 s 10000 QPS 理论上单节点的 QPS 可以达到 10000。
然而这个计算是基于理论情况下的近似值。在实际应用中系统性能可能会受到多个因素的影响包括线程调度、并发情况、硬件性能等。因此在实际场景中要进行性能测试和实际负载情况下的测试以确定系统的实际性能和 QPS。
58.多线程对Long数据进行加和会存在什么问题如何解决
在多线程环境下对Long数据进行加和会存在并发安全性问题主要涉及以下两个方面
数据竞争多个线程同时对同一个Long类型的数据进行加和操作时可能会发生数据竞争导致结果不正确。因为Long类型是64位的而多线程的并发操作可能在同一时刻读取和写入数据从而造成数据不一致性和错误的计算结果。原子性问题Long类型的加和操作不是原子性的即不能在一个单独的操作中完成。它涉及读取当前值、加和操作、写入结果这些操作之间可能被其他线程打断从而导致不正确的计算结果。
解决这些问题的方法通常是使用原子操作或加锁来保证数据的正确性和一致性。Java提供了多种解决方案其中一种常见的做法是使用AtomicLong类来进行原子操作
import java.util.concurrent.atomic.AtomicLong;public class AtomicLongExample {private AtomicLong sum new AtomicLong(0L);public void addToSum(long value) {sum.addAndGet(value);}public long getSum() {return sum.get();}public static void main(String[] args) throws InterruptedException {final AtomicLongExample example new AtomicLongExample();final int threadCount 10;final int iterations 100000;Runnable task () - {for (int i 0; i iterations; i) {example.addToSum(1L);}};Thread[] threads new Thread[threadCount];for (int i 0; i threadCount; i) {threads[i] new Thread(task);threads[i].start();}for (Thread thread : threads) {thread.join();}System.out.println(Final sum: example.getSum());}
}
在上述示例中使用AtomicLong来保证对sum的加和操作是原子的从而避免了数据竞争和不正确的计算结果。
另外一种解决方案是使用锁如synchronized关键字或ReentrantLock来保护对Long数据的并发访问确保每次只有一个线程能够对数据进行操作从而保证数据的一致性和正确性。这样的做法虽然可以解决并发安全问题但在高并发情况下可能会引起性能问题因为锁会导致线程竞争和阻塞。因此在选择解决方案时需要根据具体场景和需求来权衡利弊。
59.Netty 的线程机制是什么样的
Netty是一个基于Java的异步事件驱动的网络应用框架它的线程机制主要涉及两个方面EventLoopGroup和EventLoop。
EventLoopGroup
EventLoopGroup是一个线程池它包含一组EventLoop。它通常用于处理网络事件如接受新连接和处理读写事件。在应用程序启动时可以创建一个或多个EventLoopGroup实例其中一个用于处理连接的接受另外一个用于处理连接的读写操作。EventLoopGroup通常有两种类型单线程的和多线程的。
EventLoop
EventLoop是Netty的核心组件它代表了一个不断循环的处理任务。每个EventLoop都与一个线程绑定并负责处理任务队列中的事件。它在事件循环中不断地从任务队列中获取任务并执行直到任务队列为空或者遇到需要阻塞的任务。EventLoop使用非阻塞方式执行任务避免了线程切换的开销提高了性能和吞吐量。
Netty的线程模型采用了Reactor模式其中EventLoop充当了事件处理器的角色EventLoopGroup负责管理多个EventLoop可以根据需要创建单线程或多线程的EventLoopGroup来适应不同的场景。这种设计使得Netty能够高效地处理并发连接和网络事件提供了高性能的网络编程解决方案。 四其他扩展高频知识考点 针对人员 1.全部人员都适用 2.正式员工外包员工看情况但基本建议至少考察一道 1.LRU算法是怎么实现的大致说明下高频考点
LRU算法的设计原则如果一个数据在最近一段时间没有被访问到那么在将来它被访问的可能性也很小。也就是说当限定的空间已存满数据时应当把最久没有被访问到的数据淘汰。
当存在热点数据时LRU的效率很好但偶发性的、周期性的批量操作会导致LRU命中率急剧下降缓存污染情况比较严重。
实现LRU思路
第一种方法利用数组来实现
用一个数组来存储数据给每一个数据项标记一个访问时间戳每次插入新数据项的时候先把数组中存在的数据项的时间戳自增并将新数据项的时间戳置为0并插入到数组中每次访问数组中的数据项的时候将被访问的数据项的时间戳置为0。当数组空间已满时将时间戳最大的数据项淘汰。
第二种方法利用链表来实现
每次新插入数据的时候将新数据插到链表的头部每次缓存命中即数据被访问则将数据移到链表头部那么当链表满的时候就将链表尾部的数据丢弃。
第三种方法利用链表和hashmap来实现
当需要插入新的数据项的时候如果新数据项在链表中存在一般称为命中则把该节点移到链表头部如果不存在则新建一个节点放到链表头部若缓存满了则把链表最后一个节点删除即可。在访问数据的时候如果数据项在链表中存在则把该节点移到链表头部否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。
对于第一种方法需要不停地维护数据项的访问时间戳另外在插入数据、删除数据以及访问数据时时间复杂度都是O(n)。对于第二种方法链表在定位数据的时候时间复杂度为O(n)。所以在一般使用第三种方式来是实现LRU算法。
具体实现方案使用LinkedHashMap实现
LinkedHashMap底层就是用的HashMap加双链表实现的而且本身已经实现了按照访问顺序的存储。
此外LinkedHashMap中本身就实现了一个方法removeEldestEntry用于判断是否需要移除最不常读取的数方法默认是直接返回false不会移除元素所以需要重写该方法。即当缓存满后就移除最不常用的数。
public class LRUK,V {private static final float hashLoadFactory 0.75f;private LinkedHashMapK,V map;private int cacheSize;public LRU(int cacheSize) {this.cacheSize cacheSize;int capacity (int)Math.ceil(cacheSize / hashLoadFactory) 1;map new LinkedHashMapK,V(capacity, hashLoadFactory, true){private static final long serialVersionUID 1;/*将LinkedHashMap中的removeEldestEntry进行重写改造*/Overrideprotected boolean removeEldestEntry(Map.Entry eldest) {return size() LRU.this.cacheSize;}};}public synchronized V get(K key) {return map.get(key);}public synchronized void put(K key, V value) {map.put(key, value);}public synchronized void clear() {map.clear();}public synchronized int usedSize() {return map.size();}public void print() {for (Map.EntryK, V entry : map.entrySet()) {System.out.print(entry.getValue() --);}System.out.println();}
}
自编代码基于 HashMap 和 双向链表实现 LRU
基本代码见LRU缓存机制LRU Cache
整体的设计思路是可以使用 HashMap 存储 key这样可以做到 put 和 get key的时间都是 O(1)而 HashMap 的 Value 指向双向链表实现的 LRU 的 Node 节点如图所示。 LRU 存储是基于双向链表实现的下面的图演示了它的原理。其中 h 代表双向链表的表头t 代表尾部。首先预先设置 LRU 的容量如果存储满了可以通过 O(1) 的时间淘汰掉双向链表的尾部每次新增和访问数据都可以通过 O(1)的效率把新的节点增加到对头或者把已经存在的节点移动到队头。 总结一下核心操作的步骤:
put(key, value)首先在 HashMap 找到 Key 对应的节点如果节点存在更新节点的值并把这个节点移动队头。如果不存在需要构造新的节点并且尝试把节点塞到队头如果LRU空间不足则通过 tail 淘汰掉队尾的节点同时在 HashMap 中移除 Key。get(key)通过 HashMap 找到 LRU 链表节点把节点插入到队头返回缓存的值。
定义基本结构
class DLinkedNode {String key;int value;DLinkedNode pre;DLinkedNode post;
}
具体手写代码如下
package org.zyf.javabasic.letcode.hash;import java.util.HashMap;
import java.util.Map;/*** author yanfengzhang* description 设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作获取数据 get 和写入数据 put 。* 获取数据 get(key) - 如果密钥 (key) 存在于缓存中则获取密钥的值总是正数否则返回 -1。* 写入数据 put(key, value) - 如果密钥不存在则写入其数据值。当缓存容量达到上限时* 它应该在写入新数据之前删除最近最少使用的数据值从而为新的数据值留出空间。* p* 进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作* date 2023/4/9 19:11*/
public class LRUCache {class DLinkedNode {int key;int value;DLinkedNode prev;DLinkedNode next;}private MapInteger, DLinkedNode cache new HashMap();private int size;private int capacity;private DLinkedNode head, tail;public LRUCache(int capacity) {this.size 0;this.capacity capacity;head new DLinkedNode();tail new DLinkedNode();head.next tail;tail.prev head;}public int get(int key) {DLinkedNode node cache.get(key);if (node null) {return -1;}/*将节点移动到双向链表头部*/moveToHead(node);return node.value;}public void put(int key, int value) {DLinkedNode node cache.get(key);if (node null) {/*如果节点不存在则创建一个新节点并加入到双向链表头部和哈希表中*/DLinkedNode newNode new DLinkedNode();newNode.key key;newNode.value value;cache.put(key, newNode);addToHead(newNode);size;if (size capacity) {/*如果超出容量则删除双向链表尾部节点并在哈希表中删除对应的键值对*/DLinkedNode tail removeTail();cache.remove(tail.key);size--;}} else {/*如果节点存在则更新节点的值并将节点移动到双向链表头部*/node.value value;moveToHead(node);}}private void addToHead(DLinkedNode node) {/*将节点加入到双向链表头部*/node.prev head;node.next head.next;head.next.prev node;head.next node;}private void removeNode(DLinkedNode node) {/*从双向链表中删除节点*/node.prev.next node.next;node.next.prev node.prev;}private void moveToHead(DLinkedNode node) {/*将节点移动到双向链表头部*/removeNode(node);addToHead(node);}private DLinkedNode removeTail() {/*删除双向链表尾部节点并返回被删除的节点*/DLinkedNode tail this.tail.prev;removeNode(tail);return tail;}/*** 可以看到LRU 缓存机制在存储容量达到最大值时* 能够正确地淘汰最近最少使用的节点* 并保证每个节点的访问顺序符合 LRU 缓存机制的要求。*/public static void main(String[] args) {LRUCache cache new LRUCache(2);cache.put(1, 1);cache.put(2, 2);/*output: 1*/System.out.println(cache.get(1));cache.put(3, 3);/*output: -1*/System.out.println(cache.get(2));cache.put(4, 4);/*output: -1*/System.out.println(cache.get(1));/*output: 3*/System.out.println(cache.get(3));/*output: 4*/System.out.println(cache.get(4));}}
其他相关内容补充LRU-K
LRU-K中的K代表最近使用的次数因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。 相比LRULRU-K需要多维护一个队列用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候才将数据放入缓存。当需要淘汰数据时LRU-K会淘汰第K次访问时间距当前时间最大的数据。
数据第一次被访问时加入到历史访问列表如果数据在访问历史列表中没有达到K次访问则按照一定的规则FIFO,LRU淘汰当访问历史队列中的数据访问次数达到K次后将数据索引从历史队列中删除将数据移到缓存队列中并缓存数据缓存队列重新按照时间排序缓存数据队列中被再次访问后重新排序需要淘汰数据时淘汰缓存队列中排在末尾的数据即“淘汰倒数K次访问离现在最久的数据”。
LRU-K具有LRU的优点同时还能避免LRU的缺点实际应用中LRU-2是综合最优的选择。由于LRU-K还需要记录那些被访问过、但还没有放入缓存的对象因此内存消耗会比LRU要多。
其他相关内容补充two queue
Two queues以下使用2Q代替算法类似于LRU-2不同点在于2Q将LRU-2算法中的访问历史队列注意这不是缓存数据的改为一个FIFO缓存队列即2Q算法有两个缓存队列一个是FIFO队列一个是LRU队列。
当数据第一次访问时2Q算法将数据缓存在FIFO队列里面当数据第二次被访问时则将数据从FIFO队列移到LRU队列里面两个队列各自按照自己的方法淘汰数据。新访问的数据插入到FIFO队列中如果数据在FIFO队列中一直没有被再次访问则最终按照FIFO规则淘汰如果数据在FIFO队列中再次被访问到则将数据移到LRU队列头部如果数据在LRU队列中再次被访问则将数据移动LRU队列头部LRU队列淘汰末尾的数据。
其他相关内容补充Multi Queue(MQ)
MQ算法根据访问频率将数据划分为多个队列不同的队列具有不同的访问优先级其核心思想是优先缓存访问次数多的数据。Q0Q1....Qk代表不同的优先级队列Q-history代表从缓存中淘汰数据但记录了数据的索引和引用次数的队列
新插入的数据放入Q0每个队列按照LRU进行管理当数据的访问次数达到一定次数需要提升优先级时将数据从当前队列中删除加入到高一级队列的头部为了防止高优先级数据永远不会被淘汰当数据在指定的时间里没有被访问时需要降低优先级将数据从当前队列删除加入到低一级的队列头部需要淘汰数据时从最低一级队列开始按照LRU淘汰每个队列淘汰数据时将数据从缓存中删除将数据索引加入Q-history头部。如果数据在Q-history中被重新访问则重新计算其优先级移到目标队列头部。Q-history按照LRU淘汰数据的索引。
MQ需要维护多个队列且需要维护每个数据的访问时间复杂度比LRU高。
2.后台服务出现明显“变慢”谈谈你的诊断思路高频考点
首先需要对这个问题进行更加清晰的定义:
服务是突然变慢还是长时间运行后观察到变慢类似问题是否重复出现“慢”的定义是什么我能够理解是系统对其他方面的请求的反应延时变长吗?
第二理清问题的症状这更便于定位具体的原因有以下一些思路
问题可能来自于 Java 服务自身也可能仅仅是受系统里其他服务的影响。初始判断可以先确认是否出现了意外的程序错误例如检查应用本身的错误日志。
对于分布式系统很多公司都会实现更加系统的日志、性能等监控系统。一些 Java 诊断工具也可以用于这个诊断例如通过 JFRJava Flight Recorder监控应用是否大量出现了某种类型的异常。
如果有那么异常可能就是个突破点。
如果没有可以先检查系统级别的资源等情况监控 CPU、内存等资源是否被其他进程大量占用并且这种占用是否不符合系统正常运行状况。
监控 Java 服务自身例如 GC 日志里面是否观察到 Full GC 等恶劣情况出现或者是否 Minor GC 在变长等利用 jstat 等工具获取内存使用的统计信息也是个常用手段利用 jstack 等工具检查是否出现死锁等。如果还不能确定具体问题对应用进行 Profiling 也是个办法但因为它会对系统产生侵入性如果不是非常必要大多数情况下并不建议在生产系统进行。定位了程序错误或者 JVM 配置的问题后就可以采取相应的补救措施然后验证是否解决否则还需要重复上面部分过程。
3.你了解Java应用开发中的注入攻击吗
注入式Inject攻击是一类非常常见的攻击方式其基本特征是程序允许攻击者将不可信的动态内容注入到程序中并将其执行这就可能完全改变最初预计的执行过程产生恶意效果。
下面是几种主要的注入式攻击途径原则上提供动态执行能力的语言特性都需要提防发生注入攻击的可能。
首先就是最常见的 SQL 注入攻击。一个典型的场景就是 Web 系统的用户登录功能根据用户输入的用户名和密码我们需要去后端数据库核实信息。
假设应用逻辑是后端程序利用界面输入动态生成类似下面的 SQL然后让 JDBC 执行。
select * from use_info where username “input_usr_name” and password “input_pwd”但是如果我输入的 input_pwd 是类似下面的文本
“ or “””那么拼接出的 SQL 字符串就变成了下面的条件OR 的存在导致输入什么名字都是复合条件的。
select * from use_info where username “input_usr_name” and password “” or “” “”这里只是举个简单的例子它是利用了期望输入和可能输入之间的偏差。上面例子中期望用户输入一个数值但实际输入的则是 SQL 语句片段。类似场景可以利用注入的不同 SQL 语句进行各种不同目的的攻击甚至还可以加上“;delete xxx”之类语句如果数据库权限控制不合理攻击效果就可能是灾难性的。
第二操作系统命令注入。Java 语言提供了类似 Runtime.exec(…) 的 API可以用来执行特定命令假设我们构建了一个应用以输入文本作为参数执行下面的命令
ls –la input_file_name但是如果用户输入是 “input_file_name;rm –rf /*”这就有可能出现问题了。当然这只是个举例Java 标准类库本身进行了非常多的改进所以类似这种编程错误未必可以真的完成攻击但其反映的一类场景是真实存在的。
第三XML 注入攻击。Java 核心类库提供了全面的 XML 处理、转换等各种 API而 XML 自身是可以包含动态内容的例如 XPATH如果使用不当可能导致访问恶意内容。
还有类似 LDAP 等允许动态内容的协议都是可能利用特定命令构造注入式攻击的包括 XSSCross-site Scripting攻击虽然并不和 Java 直接相关但也可能在 JSP 等动态页面中发生。
4.在Java程序运行阶段可以用什么命令行工具来查看当前Java程序的一些启动参数值例如Heap Size等。
在Java程序运行阶段可以使用以下命令行工具来查看当前Java程序的一些启动参数值例如Heap Size等 jpsJava进程状态工具jps 命令用于列出当前系统中所有Java进程的进程ID和主类名。可以通过执行 jps -l 命令查看Java程序的启动参数和进程ID。 jcmdJava控制台命令jcmd 命令用于向正在运行的Java进程发送诊断命令可以用来查看Java进程的启动参数、内存使用情况等信息。例如可以执行 jcmd pid VM.flags 命令来查看Java进程的VM flags。 jstatJava统计监视工具jstat 命令用于监视Java虚拟机的各种运行时信息包括垃圾回收情况、类加载情况、JIT编译情况等。可以使用 jstat -gc pid 命令来查看Java进程的垃圾回收情况。 jmapJava内存映像工具jmap 命令用于生成Java进程的内存映像文件可以查看Java进程的堆内存使用情况、内存分布情况等。可以执行 jmap -heap pid 命令来查看Java进程的堆内存情况。 jconsoleJava监视与管理控制台jconsole 是Java自带的图形化监视与管理控制台工具可以实时监视Java应用程序的内存使用情况、线程情况、类加载情况等非常方便实用。
这些命令行工具提供了丰富的功能可以帮助开发人员深入了解Java程序的运行状态和性能特征从而进行调优和排查问题。
5.用什么命令行工具可以查看运行的Java程序的GC状况请具体写出命令行格式。高频考点
可以使用 jstat 命令查看运行的Java程序的GC垃圾回收状况。具体的命令行格式如下
jstat -gc pid interval count
其中各个参数的含义如下
pidJava进程的进程ID即要监视的Java程序的进程ID。interval监视数据输出的时间间隔单位为毫秒。表示每隔多少毫秒输出一次监视数据。count输出监视数据的次数。表示输出多少次监视数据后停止监视。
例如要查看进程ID为12345的Java程序的GC状况每隔1秒输出一次监视数据输出5次可以执行以下命令
jstat -gc 12345 1000 5
这样会输出指定Java进程的GC相关的监视数据包括GC时间、各代的GC统计信息如Eden区、Survivor区、Old区等。
6.用什么工具可以在Java程序运行的情况下跟踪某个方法的执行时间请求参数信息等并请解释下工具实现的原理。
在Java程序运行的情况下可以使用以下工具来跟踪某个方法的执行时间、请求参数信息等 Java ProfilerJava性能分析器Java性能分析器是一种用于监视和诊断Java应用程序性能问题的工具常见的Java性能分析器包括VisualVM、YourKit Java Profiler、JProfiler等。这些工具提供了方法级别的性能分析功能可以跟踪方法的执行时间、调用堆栈、请求参数信息等。 AspectJ面向切面编程框架AspectJ是一种基于Java语言的面向切面编程框架它可以在方法执行前后插入代码逻辑实现对方法的增强和跟踪。通过在目标方法的前后插入代码逻辑可以实现对方法执行时间、请求参数信息等的跟踪和监视。 自定义拦截器/过滤器在Java Web应用中可以通过自定义拦截器或过滤器来实现对方法的执行时间、请求参数信息等的跟踪。拦截器或过滤器可以在方法执行前后记录方法的执行时间、请求参数信息等并将这些信息记录到日志或输出到控制台。
这些工具和技术的实现原理主要包括以下几个方面 字节码注入Java性能分析器通常通过在Java程序的字节码中插入监视代码来实现性能监视和跟踪。AspectJ框架通过在编译期或运行期修改字节码来实现对方法的增强和跟踪。 AOP面向切面编程AspectJ框架采用面向切面编程的思想通过定义切点和切面来实现对方法的跟踪和监视。切点定义了哪些方法需要被跟踪切面定义了在方法执行前后需要执行的增强逻辑。 拦截器/过滤器链自定义拦截器或过滤器通常通过拦截器链或过滤器链来实现对方法的跟踪和监视。在方法执行前后拦截器或过滤器会执行相应的逻辑记录方法的执行时间、请求参数信息等并将这些信息输出到日志或控制台。
这些工具和技术提供了丰富的功能和灵活的扩展性可以帮助开发人员实现对Java程序的性能监视和跟踪从而及时发现和解决性能问题。
7.当一个Java程序接收请求很长时间都没响应的话通常你会怎么去排查这种问题
当一个Java程序接收请求后很长时间都没有响应通常会采取以下步骤来排查这种问题 确认是否出现了死锁或长时间阻塞首先需要确认是否出现了死锁或者某些线程长时间阻塞的情况。可以使用线程监视工具如jstack、VisualVM等来查看Java进程的线程堆栈信息以确定是否有线程被阻塞或者等待锁资源。 检查日志文件查看程序的日志文件查找异常信息、错误日志或者警告信息以确定是否有异常情况发生。有时候程序的运行状态会被记录在日志文件中通过查看日志可以发现一些隐藏的问题。 检查性能指标使用性能监视工具如VisualVM、JProfiler、Grafana等来监视Java程序的性能指标包括CPU利用率、内存使用情况、线程数量、GC情况等。通过检查性能指标可以发现程序的瓶颈和性能问题。 查看系统资源情况检查服务器的系统资源情况包括CPU使用率、内存使用情况、磁盘IO等。有时候程序没有响应是因为服务器资源不足或者被其他程序占用了过多的资源。 分析代码逻辑检查程序的代码逻辑尤其是处理请求的关键路径查看是否有死循环、长时间阻塞、数据库连接池耗尽等问题。有时候程序没有响应是因为代码中存在性能问题或者业务逻辑错误。
通过以上步骤的排查和分析通常可以找到程序没有响应的原因并采取相应的措施进行解决。
8.NIONew I/O用到的组件有哪些
NIONew I/O是 Java 中的一组非阻塞 I/O 类库引入了更为灵活和高效的 I/O 操作方式。在 NIO 中有一些重要的组件和概念以下是一些常见的 NIO 组件 通道Channels 通道是连接到文件、套接字或其他可进行 I/O 操作的实体。它们类似于传统的流但提供了更多的功能。通道可以用于读取、写入和操作数据。 缓冲区Buffers 缓冲区是一个内存区域用于在通道和应用程序之间传输数据。NIO 缓冲区提供了不同类型的缓冲区如 ByteBuffer、CharBuffer、IntBuffer 等以适应不同类型的数据。 选择器Selector 选择器允许单个线程同时监视多个通道的 I/O 事件。使用选择器可以实现非阻塞的多路复用 I/O 操作以管理多个连接。 选择键SelectionKey 选择键是通道在选择器上注册的标记。它包含了通道的事件和状态信息允许选择器跟踪通道的状态。 非阻塞 I/O NIO 提供了非阻塞 I/O 操作允许在数据没有准备好的情况下继续执行其他任务而不是阻塞等待数据的到来。这在高并发环境中非常有用。 多路复用 NIO 的选择器允许一个线程同时处理多个通道的事件从而实现多路复用。这在服务器端应用程序中非常有用可以处理多个客户端连接。 通道间的数据传输 NIO 提供了直接通道间的数据传输方法可以在通道之间高效地传输数据避免了通过缓冲区中转的开销。
这些组件一起构成了 NIO 的核心架构使得 Java 程序能够更高效地进行非阻塞 I/O 操作适用于需要高并发处理的网络应用程序。
9.Netty对比Java NIO做了什么优化?(必考)
Netty 是一个基于 Java NIO 的高性能网络应用框架它在 Java NIO 的基础上做了一些优化和扩展以提供更强大、更易用的网络编程能力。
下面是 Netty 相对于 Java NIO 做出的一些优化
简化了编程模型Netty 提供了更简单、更易用的编程模型隐藏了 Java NIO 复杂的细节。它的事件驱动模型和回调机制使得编写高效、可扩展的网络应用更加简单。提供了更高级的抽象Netty 提供了一系列高级的抽象组件如 Channel、ChannelHandler 和 ChannelPipeline它们使得网络应用的开发更加模块化和可扩展。同时Netty 还提供了丰富的编解码器简化了网络数据的编解码过程。内存管理优化Netty 在内存管理上进行了优化引入了零拷贝技术和内存池减少了数据在内存之间的复制操作提高了数据的传输效率。提供了更强大的并发模型Netty 支持多种并发模型包括单线程模型、多线程模型和多线程池模型。它提供了基于事件驱动的方式进行网络处理避免了传统的阻塞式 I/O 的性能瓶颈。支持更多的协议和特性Netty 提供了丰富的协议实现包括 TCP、UDP、HTTP、WebSocket 等同时还支持 SSL/TLS 加密和压缩、流量整形和拆包粘包处理等特性。
总体而言Netty 在基于 Java NIO 的基础上进行了一系列的优化和扩展使得开发者能够更轻松地构建高性能、可扩展的网络应用。它提供了简化的编程模型、高级抽象、内存管理优化、强大的并发模型和支持多种协议和特性使得网络编程变得更加灵活、高效和可靠。