性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗 资源;比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发,性能是 最重要的因素
没有面向对象易维护、易复用、易扩展
易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特 性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
性能比面向过程低
是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。通俗来说,就是将一大堆实现逻辑,放在一个盒子里面。我们使用的时候,只需要调用封装好的盒子即可。 良好的封装能减少耦合
类内部的结构可以自由修改
可以对成员变量进行更精确的控制
隐藏信息,实现细节
就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。 子类拥有父类非 private 的属性和方法
子类可以拥有自己属性和方法,即子类可以对父类进行扩展
子类可以用自己的方式实现父类的方法
同一个行为具有多个不同表现形式或形态的能力。 多态就是同一个接口,使用不同的实例而执行不同操作 继承
重写
父类引用指向子类对象
消除类型之间的耦合关系
可替换性
可扩充性
接口性
灵活性
简化性
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机
解释:Java Development Kit,它是功能齐全的 Java SDK
它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序
解释:Java 运行时环境
它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序
如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。 如果你需要进行一些 Java 编程方面的工作,那么你就需要安装 JDK 了。但 是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然 需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲, 您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用 程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet
Oracle JDK 版本将每三年发布一次,而 OpenJDK 版本每三个月发布一次
OpenJDK 是一个参考模型并且是完全开源的,而 Oracle JDK 是 OpenJDK 的一个实现,并不是完全开源的;
Oracle JDK 比 OpenJDK 更稳定。OpenJDK 和 Oracle JDK 的代码几乎 相同,但 Oracle JDK 有更多的类和一些错误修复。因此,如果您想开发 企业/商业软件,我建议您选择 Oracle JDK,因为它经过了彻底的测试和 稳定。某些情况下,有些人提到在使用 OpenJDK 可能会遇到了许多应 用程序崩溃的问题,但是,只需切换到 Oracle JDK 就可以解决问题;
顶级公司正在使用 Oracle JDK,例如 Android Studio,Minecraft 和 IntelliJ IDEA 开发工具,其中 Open JDK 不太受欢迎;
在响应性和 JVM 性能方面,Oracle JDK 与 OpenJDK 相比提供了更好的 性能;
Oracle JDK 不会为即将发布的版本提供长期支持,用户每次都必须通过 更新到最新版本获得支持来获取最新版本;
Oracle JDK 根据二进制代码许可协议获得许可,而 OpenJDK 根据 GPL v2 许可获得许可。
发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序 不同,方法返回值和访问修饰符可以不同,发生在编译时。
发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父 类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类 方法访问修饰符为 private 则子类就不能重写该方法。
Java 中是否可以重写一个 private 或者 static 方法?
构造方法有哪些特性?
在 Java 中定义一个不做事且没有参数的构造方法有什么作用?
Java 中创建对象的几种方式?
接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始 接口方法可以有默认实现),抽象类可以有非抽象的方法
接口中的实例变量默认是 final 类型的,而抽象类中则不一定
一个类可以实现多个接口,但最多只能实现一个抽象类
一个类实现接口的话要实现接口的所有方法,而抽象类不一定
接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口 的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是 行为的抽象,是一种行为的规范。
静态变量和实例变量的区别?
short s1 = 1;s1 = s1 + 1;有什么错?那么 short s1 = 1; s1 += 1;呢?有没有错误?
Integer 和 int 的区别?
将基本类型用它们对应的引用类型包装起来
将包装类型转换为基本数据类型
switch 语句能否作用在 byte 上,能否作用在 long 上,能否作用在 String 上?
final、finally、finalize 的区别 java中的关键字,修饰符
java的一种异常处理机制
Java中的一个方法名
== 和 equals 的区别?
两个对象的 hashCode() 相同,则 equals() 也一定为 true 吗?
为什么重写 equals() 就一定要重写 hashCode() 方法?
& 和 && 的区别?
Java 中的 Math.round(-1.5) 等于多少?
如何实现对象的克隆?
深克隆和浅克隆的区别?
什么是 Java 的序列化,如何实现 Java 的序列化? 序列化:把Java对象转换为字节序列的过程。
反序列化:把字节序列恢复为Java对象的过程。
把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
在网络上传送对象的字节序列
将需要被序列化的类实现Serializable接口
对于不想进行序列化的变量,使用 transient 关键字修饰
永久性保存对象
通过序列化以字节流的形式使对象在网络中进行传递和接收
通过序列化在进程间传递对象
Java 的泛型是如何工作的 ? 什么是类型擦除 ?
什么是泛型中的限定通配符和非限定通配符 ?
List 和 List 之间有什么区别 ?
Java 中的反射是什么意思?有哪些应用场景?
反射的优缺点?
Java 中的动态代理是什么?有哪些应用?
怎么实现动态代理?
static 关键字的作用?
super 关键字的作用?
字节和字符的区别?
String 为什么要设计为不可变类?
String、StringBuilder、StringBuffer 的区别? StringBuilder与StringBuffer 都继承自 AbstractStringBuilder 类,AbstractStringBuilder 中也是使用字符数组保存字符串 char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的
String 中的对象是不可变的,也就可以理解为常量,线程安全。
StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以 是线程安全的
StringBuilder 并没有对方法进行加同步锁,所以是非线程安全 的
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将 指针指向新的 String 对象
StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用
相同情况下使用StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险
操作少量的数据 = String
单线程操作字符串缓冲区下操作大量数据 = StringBuilder
多线程操作字符串缓冲区下操作大量数据 = StringBuffer
String 字符串修改实现的原理?
String str = "i" 与 String str = new String("i") 一样吗?
String 类的常用方法都有那些?
final 修饰 StringBuffer 后还可以 append 吗? 可以,final 修饰的是一个引用变量,那么这个引用始终只能指向这个对象,但是这个对象内部的属性是可以变化的
Java 中的 IO 流的分类?说出几个你熟悉的实现类?
字节流和字符流有什么区别?
BIO、NIO、AIO 有什么区别?
形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的 若干个字符
含义上: 字符常量相当于一个整形值( ASCII 值),可以参加表达式运算 字 符串常量代表一个地址值(该字符串在内存中存放位置)
占内存大小 字符常量只占 2 个字节 字符串常量占若干个字节(至少一个 字符结束标志) (注意: char 在 Java 中占两个字节)
finally 块中的代码什么时候被执行?
在 finally 语句块中发生了异常
在前面的代码中用了 System.exit()退出程序
程序所在的线程死亡
关闭 CPU
try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗? 会,当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行
try-catch-finally 中那个部分可以省略? try 块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
catch 块:用于处理 try 捕获到的异常。
finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。 当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回 之前被执行。
例如JVM内存溢出(OOM)
Exception(异常):是程序本身可以处理的异常 Exception异常类层次结构图
运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生
受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发。Java编译器要求方法必须声明抛出可能发生的受检异常,但是并不要求必须声明抛出未被捕获的运行时异常
Effective Java中对异常的使用给出了以下指导原则 不要将异常处理用于正常的控制流(设计良好的API不应该强迫它的调用者为了正常的控制流而使用异常)
对可以恢复的情况使用受检异常,对编程错误使用运行时异常
避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生)
优先使用标准的异常
每个方法抛出的异常都要有文档
保持异常的原子性
不要在catch中忽略掉捕获到的异常
throws:用来声明一个方法可能产生的所有异常,不做任何处理而是将异常往上传,谁调用我我就抛给谁 用在方法声明后面,跟的是异常类名
可以跟多个异常类名,用逗号隔开
表示抛出异常,由该方法的调用者来处理
throws表示出现异常的一种可能性,并不一定会发生这些异常
用在方法体内,跟的是异常对象名
只能抛出一个异常对象名
表示抛出异常,由方法体内的语句处理
throw则是抛出了异常,执行throw则一定抛出了某种异常
throws可以单独使用,throw不可以,必须搭配try catch,或者throws,若程序执行到throw exception 语句,则后面的语句不会再执行
常见的异常类有哪些?
主线程可以捕获到子线程的异常吗?
Vector 数组结构,线程安全
ArrayList 数组结构,非线程安全
LinkedList 链表结构,非线程安全
LinkedHashSet 哈希表和链表结构
TreeSet 红黑树结构
Properties
LinkedHashMap 哈希表和链表结构
TreeMap 红黑树结构
容量自增长;
提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量;
可以方便地扩展或改写集合,提高代码复用性和可操作性。
通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。
ArrayList 和 LinkedList 的区别? ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现
ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为ArrayList 增删操作要影响数组内的其他数据的下标
LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素
ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全
在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。
LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
ArrayList 实现 RandomAccess 接口有何作用?为何 LinkedList 却没实现这个接口? RandomAccess接口是一个标志接口(Marker) ArrayList接口源码实现
LinkedList接口源码实现
只要List集合实现这个接口,就能支持快速随机访问 通过查看Collections类中的binarySearch()方法
ArrayList用for循环遍历比iterator迭代器遍历快,LinkedList用iterator迭代器遍历比for循环遍历快
RandomAccess接口这个空架子的存在,是为了能够更好地判断集合是ArrayList或者LinkedList,从而能够更好选择更优的遍历方式,提高性能!
ArrayList 和 Vector 的区别是什么? 这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
性能:ArrayList 在性能方面要优于 Vector。
扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。
Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。
Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。
插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性? ArrayList和Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。
Vector 中的方法由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较ArrayList差。
LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 LinkedList 插入速度较快。
当以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组,当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为10,当容量不足时(容量为size,添加第size+1个元素时),先判断按照1.5倍(位运算)的比例扩容能否满足最低容量要求,若能,则以1.5倍扩容,否则以最低容量要求进行扩容。
为什么 ArrayList 的 elementData 加上 transient 修饰? 可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现
每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。
Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList有。
对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢
HashMap 的实现原理/底层数据结构?JDK1.7 和 JDK1.8 HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变
HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
存储时,如果出现hash值相同的key,此时有两种情况。 如果key相同,则覆盖原始值;
如果key不同(出现冲突),则将当前的key-value放入链表中
获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
在Java中,保存数据有两种比较简单的数据结构:数组和链表
数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;
所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突
Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)
JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可
相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
1. resize 扩容优化
2. 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红(Red)或黑(Black)。
红黑树的每个结点是黑色或者红色。当是不管怎么样他的根结点是黑色。每个叶子结点(叶子结点代表终结、结尾的节点)也是黑色 [注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!]
如果一个结点是红色的,则它的子结点必须是黑色的。
每个结点到叶子结点NIL所经过的黑色结点的个数一样的。[确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!]
红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。
判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容
根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③
判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals
判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5
遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可
插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容
HashMap 的 get 方法的执行过程?
HashMap 的 resize 方法的执行过程?
HashMap是采用链表解决Hash冲突,因为是链表结构,那么就很容易形成闭合的链路,这样在循环的时候只要有线程对这个HashMap进行get操作就会产生死循环
在单线程情况下,只有一个线程对HashMap的数据结构进行操作,是不可能产生闭合的回路的
那就只有在多线程并发的情况下才会出现这种情况,那就是在put操作的时候,如果size>initialCapacity*loadFactor,那么这时候HashMap就会进行rehash操作,随之HashMap的结构就会发生翻天覆地的变化。很有可能就是在两个线程在这个时候同时触发了rehash操作,产生了闭合的回路
多线程下HashMap的问题(主要死循环相关问题) 多线程put操作后,get操作导致死循环
多线程put非NULL元素后,get操作得到NULL值
多线程put操作,导致元素丢失
HashMap 的 get 方法能否判断某个元素是否在 map 中?
HashMap 与 HashTable 的区别是什么? HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap )
因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用 ConcurrentHashMap
HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException
创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍
创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方
JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈 值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制
在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代
HashMap 的 size 为什么必须是 2 的整数次方? 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。 我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方
这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的
TreeMap 是一个有序的key-value集合
TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法
TreeMap是线程非同步的
对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择
假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择
基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历
HashMap 与 ConcurrentHashMap 的区别是什么? ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
HashMap的键值对允许有null,但是ConCurrentHashMap都不允许
HashTable 和 ConcurrentHashMap 的区别? JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
Hashtable 和 JDK1.8之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本
ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。)
Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低
JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点)
ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题使用了synchronized 关键字,所以 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的
ConcurrentHashMap 的实现原理是什么? 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现
结构:一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁
该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色
Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁
在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍
comparable 和 comparator的区别? comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序
comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort()
HashSet 的实现原理?
HashSet 怎么保证元素不重复的?
LinkedHashMap 的实现原理?
Iterator 怎么使用?有什么特点?
Iterator 和 ListIterator 有什么区别?
Iterator 和 Enumeration 接口的区别?
fail-fast 与 fail-safe 有什么区别?
Collection 和 Collections 有什么区别? java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set
Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作
并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。 并发 = 俩个人用一台电脑。
并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。 并行 = 俩个人分配了俩台电脑。
串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。 串行 = 俩个人排队使用一台电脑。
可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
多线程需要协调和管理,所以需要 CPU 时间跟踪线程;
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
进程:一个在内存中运行的应用程序。 每个正在系统上运行的程序都是一个进程
线程:进程中的一个执行任务(控制单元), 它负责在程序里独立执行
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程与进程之间的地址空间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃有可能导致整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用 户线程
运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线 程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作 守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点
使用场景:守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。 经典案例:JVM 中的垃圾回收线程就是
当 JVM 中不存在任何一个正在运行的非守护线程时,则 JVM 进程即会退出。
Runnable 和 Callable 有什么区别? 都是接口
都可以编写多线程程序
都采用Thread.start()启动线程
Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息
Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值
Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果
新建(new):新创建了一个线程对象
就绪(可运行状态)(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。 注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。 (一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
(二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
(三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
死亡(dead)(结束):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
进程的状态转换
两者都可以暂停线程的执行
类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
是否释放锁:sleep() 不释放锁;wait() 释放锁。
用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(longtimeout)超时后线程会自动苏醒。
线程的 run() 和 start() 有什么区别? 每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start()只能调用一次。
start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就状态,当分配到 时间片 后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行run() 方法的内容,这是真正的多线程工作
而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
调用 start 方法可启动线程并使线程进入就绪状态,当时间片分配到才会执行。 直接调用run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
线程切换带来的原子性问题 解决办法:使用多线程之间同步synchronized或使用锁(lock)
缓存导致的可见性问题 解决办法:synchronized、volatile、LOCK,可以解决可见性问题
译优化带来的有序性问题 解决办法:Happens-Before 规则可以解决有序性问题
什么叫线程安全?servlet、struts2、springMvc是线程安全吗? Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的
Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁
SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程
Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题
Java 线程同步的几种方法?
Thread.interrupt() 方法的工作原理是什么?
interrupt、interrupted 和 isInterrupted 方法的区别? 用于中断线程。调用该方法的线程的状态为将被置为”中断”状态
是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回false 了。
是可以返回当前中断信号是true还是false,与interrupt最大的差别
interrupted 是作用于当前线程,isInterrupted 是作用于调用该方法的线程对象所对应的线程。(线程对象对应的线程不一定是当前运行的线程。例如我们可以在A线程中去调用B线程对象的isInterrupted方法。)
这两个方法最终都会调用同一个方法,只不过interrupted参数一个是true,isInterrupted参数是false;
Thread 类中的 yield 方法有什么作用? 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)
当前线程到了就绪状态,那么接下来哪个线程会从就绪状态变成执行状态呢?可能是当前线程,也可能是其他线程,看系统的分配了
为什么Thread 类的 sleep()和 yield ()方法是静态的? Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法
线程的 sleep()方法和 yield()方法有什么区别? sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会
线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态
sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常
sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行
分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU的时间片这个也比较好理解
Java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU
线程体中调用了 yield 方法让出了对 cpu 的占用权利
线程体中调用了 sleep 方法使线程进入睡眠状态
线程由于 IO 操作受到阻塞
另外一个更高优先级线程出现
在支持时间片的系统中,该线程的时间片用完
什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)? 线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现
时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间
线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)
使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁
使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常
唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关
唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它 们竞争,只有获得锁的线程才能进入就绪状态
线程生命周期开销非常高
资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开销。
在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常
谈谈对 ThreadLocal 的理解?
在哪些场景下会使用到 ThreadLocal?
说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗? JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
无锁状态
偏向锁状态
轻量级锁状态
重量级锁状态
他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率
说下对 ReentrantReadWriteLock 的理解?
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁
比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制 像数据库提供的类似于 write_condition 机制,其实 都是提供的乐观锁
在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观 锁的一种实现方式 CAS 实现的
乐观锁常见的两种实现方式是什么?
乐观锁的缺点有哪些?
CAS 和 synchronized 的使用场景? 在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量
在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销
synchronized关键字在项目中如何使用/最主要的三种使用方式 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
Synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成
每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码当它的monitor被占用时就会处于锁定状态并且尝试获取monitor的所有权 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1
如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权
重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁
在锁对象的对象头里面有一个 threadid 字段,第一次访问时,threadid为空,JVM让其持有偏向锁,并将threadid设置为其线程id,再次访问判断threadid和线程id是否一致,如果一致则直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数以后,如果还是没有正常获取要使用的对象,此时就会把轻量级锁升级为重量级锁,此过程就构成了synchronized锁的升级
顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,轻量级锁就会升级为重量级锁;
重量级锁是synchronized ,是 Java 虚拟机中最为基础的锁实现。在这种状态下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。
自旋锁
适应性自旋锁
锁消除
锁粗化
synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
CAS 是 compare and swap 的缩写,即我们所说的比较交换
cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加 version 来获取数据,性能较悲观锁有很大的提高
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。CAS是通过无限循环来获取数据的,如果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题 解决方式:从Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题
对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁
当一个线程进入一个对象的 synchronized 方法 A 之后,其它线程是否可进入 此对象的 synchronized 方法 B? 不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。
synchronized 和 Lock 有什么区别? 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
Lock 接口和synchronized 对比同步它有什么优势? Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
可以使锁更公平
可以使线程在等待锁的时候响应中断
可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
可以在不同的范围,以不同的顺序获取和释放锁
整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。
synchronized 和 ReentrantLock 区别是什么? 相同点:两者都是可重入锁
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量
synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在Java 6 中对 synchronized 进行了非常多的改进。
ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作
ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础 普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象
synchronized、volatile、CAS 比较 synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
volatile 提供多线程共享变量可见性和禁止指令重排序优化。
CAS 是基于冲突检测的乐观锁(非阻塞)
原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败
一个线程对共享变量的修改,另一个线程能够立刻看到(synchronized,volatile)
程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
程序执行的顺序按照代码的先后顺序执行
一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
则因为重排序,他还可能执行顺序为(这里标注的是语句的执行顺序) 2-1-3-4,1-3-2-4 但绝不可能 2-1-4-3,因为这打破了依赖关系
显然重排序对单线程运行是不会有任何问题,但是多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了
编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
指令级并行的重排序。现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
不管怎么排序,结果不能改变
不存在数据依赖的可以被编译器和处理器重排序
一个操作依赖两个操作,这两个操作如果不存在依赖可以重排序
单线程根据此规则不会有问题,但是重排序后多线程会有问题
as-if-serial规则和happens-before规则的区别 as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
synchronized 和 volatile 的区别是什么? synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程
volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序
volatile 是变量修饰符;synchronized 可以修饰类、方法、变量
volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些
对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。volatile 提供 happensbefore 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主内存中,当有其他线程需要读取时,它会去内存中读取新值
从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见java.util.concurrent.atomic 包下的类,比如 AtomicInteger。
volatile 常用于多线程环境下的单次操作(单次读或者单次写)。
能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。
volatile 修饰变量
synchronized 修饰修改变量的方法
wait/notify
while 轮询
简单说下对 Java 中的原子类的理解?
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
volatile 变量和 atomic 变量有什么不同? volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。 例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。
而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
volatile 能使得一个非原子操作变成原子操作吗? 关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同一个实例变量需要加锁进行同步。
虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性。
说下对同步器 AQS 的理解?
AQS 的原理是什么?
AQS 对资源的共享模式有哪些?
AQS 底层使用了模板方法模式,你能说出几个需要重写的方法吗?
说下对信号量 Semaphore 的理解?
CountDownLatch 和 CyclicBarrier 有什么区别?
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来许多好处。 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用
线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。
如果一个线程所需要执行的时间非常长的话,就没必要用线程池了(不是不能作长时间操作,而是不宜。本来降低线程创建和销毁,结果你那么久我还不好控制还不如直接创建线程),况且我们还不能控制线程池中线程的开始、挂起、和中止。
降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。
这是最正常的状态,接受新的任务,处理等待队列中的任务。
不接受新的任务提交,但是会继续处理等待队列中的任务。
不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
terminated()方法结束后,线程池的状态就会变成这个。
线程池中的的线程数一般怎么设置?创建线程池的参数有哪些?需要考虑哪些问题? ThreadPoolExecutor(线程池)这个类的构造参数
newCachedThreadPool创建一个可缓存线程池 如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 newCachedThreadPool创建一个可缓存线程池,如果当前线程池的长度超过了处理的需要时,它可以灵活的回收空闲的线程,当需要增加时, 它可以灵活的添加新的线程,而不会对池的长度作任何限制
他虽然可以无限的新建线程,但是容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为 Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问题的点,就可以去重写一个方法限制一下这个最大值
线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
newFixedThreadPool 创建一个定长线程池 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。
线程数量是固定的,但是阻塞队列是无界队列。如果有很多请求积压,阻塞队列越来越长,容易导致OOM(超出内存空间)
请求的挤压一定要和分配的线程池大小匹配,定线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()方法是查看电脑CPU核心数量
newScheduledThreadPool 创建一个定长线程池 创建一个固定长度的线程池,而且支持定时的以及周期性的任务执行,类似于 Timer(Timer是Java的一个定时器类)
由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)。
newSingleThreadExecutor 创建一个单线程化的线程池 它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,他必须保证前一项任务执行完毕才能执行后一项。保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
缺点的话,很明显,他是单线程的,高并发业务下有点无力
保证所有任务按照指定顺序执行的,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它
Java 中 Executor 和 Executors 的区别? Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
Executor 接口对象能执行我们的线程任务。
ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
使用 ThreadPoolExecutor 可以创建自定义线程池。
异步任务执行之前的钩子方法线程池工作线程在异步执行目标实例(如Runnable实例)前调用此钩子方法。此方法仍然由执行任务的工作线程调用。默认实现不执行任何操作,但可以在子类中对其进行自定义。
此方法由执行目标实例的工作线程调用,可用于重新初始化ThreadLocal线程本地变量实例、更新日志记录、开始计时统计、更新上下文变量等。
异步任务执行之后的钩子方法线程池工作线程在异步执行目标实例后调用此钩子方法。此方法仍然由执行任务的工作线程调用。此钩子方法的默认实现不执行任何操作,可以在调度器子类中对其进行自定义。
此方法由执行目标实例的工作线程调用,可用于清除ThreadLocal线程本地变量、更新日志记录、收集统计信息、更新上下文变量等。
terminated(在线程池中的所有任务执行完毕后回调) 线程池终止时的钩子方法terminated钩子方法在Executor终止时调用,默认实现不执行任何操作。
如果你提交任务时,线程池队列已满,这时会发生什么? 如果使用了无界队列,例如:LinkedBlockingQueue 没有影响,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务
如果使用了有界队列,例如:ArrayBlockingQueue 任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是 AbortPolicy
判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
要合理的分配线程池的大小要根据实际情况来定,简单的来说的话就是根据CPU密集和IO密集来分配 CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那样。
IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
CPU密集型时,任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务
IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数
任务的性质:CPU密集型任务、IO密集型任务、混合型任务。
任务的优先级:高、中、低。
任务的执行时间:长、中、短。
任务的依赖性:是否依赖其他系统资源,如数据库连接等。
线程等待时间比CPU执行时间比例越高,需要越多线程。
线程CPU执行时间比等待时间比例越高,需要越少线程。
执行 execute() 方法和 submit() 方法的区别是什么呢? 相同点就是都可以开启线程执行池中的任务
接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable Callable 类型的任务
返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有
异常处理:submit()方便Exception处理
ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。
线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销
为什么不推荐使用线程组?因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使用线程池。
说下对 Fork和Join 并行计算框架的理解?
一般软件开发中容器用的最多的就是HashMap、ArrayList,LinkedList ,等等,但是在多线程开发中就不能乱用容器,如果使用了未加锁(非同步)的的集合,你的数据就会非常的混乱。由此在多线程开发中需要使用的容器必须是加锁(同步)的容器。
可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable,以及Collections.synchronizedSet,synchronizedList 等方法返回的容器。可以通过查看 Vector,Hashtable 等这些同步容器的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字 synchronized。 Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,访问它比访问ArrayList慢很多 ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。当从 ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找 和遍历,不适合插入和删除。ArrayList的缺点是每个元素之间不能有间隔。
Vector方法带上了synchronized关键字,是线程同步的 ArrayList添加方法源码
Vector添加源码(加锁了synchronized关键字)
Hashtable
Collections.synchronized * 他完完全全的可以把List、Map、Set接口底下的集合变成线程安全的集合
使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。 ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。
在 JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度,具体内容还是查看源码吧。
HashTable就是实现了HashMap加上了synchronized,而ConcurrentHashMap底层采用分段的数组+链表实现,线程安全
ConcurrentHashMap通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。
ConcurrentHashMap并且读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
CopyOnWriteArrayList 是什么? CopyOnWriteArrayList 是一个并发容器。有很多人称它是线程安全的,我认为这句话不严谨,缺少一个前提条件,那就是非复合场景下操作它是线程安全的。
CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
CopyOnWriteArrayList 的使用场景? 合适读多写少的场景
CopyOnWriteArrayList 的缺点? 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc 或者 full gc。
不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。
由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。
CopyOnWriteArrayList 的设计思想? 读写分离,读和写分开
最终一致性
使用另外开辟空间的思路,来解决并发冲突
CopyOnWriteArraySet
ConcurrentSkipListMap
ConcurrentSkipListSet
并发队列是多个线程以有次序共享数据的重要组件
消息队列是分布式系统中重要的组件,是系统与系统直接的通信
队列遵循“先进先出”的规则,可以想象成排队检票,队列一般用来解决大数据量采集处理和显示的。
并发集合就是在多个线程中共享数据的
在并发队列上JDK提供了Queue接口,一个是以Queue接口下的BlockingQueue接口为代表的阻塞队列,另一个是高性能(无堵塞)队列。
当队列阻塞队列为空的时,从队列中获取元素的操作将会被阻塞。
当阻塞队列是满时,往队列里添加元素的操作会被阻塞。
试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素
试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来
是JDK容器中的一个双端队列实现,内部使用数组进行元素存储,不允许存储null值,可以高效的进行元素查找和尾部插入取出,是用作队列、双端队列、栈的绝佳选择,性能比LinkedList还要好。
一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。该队列不允许使用 null 元素也不允许插入不可比较的对象
ConcurrentLinkedQueue (基于链表的并发队列) 是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能。ConcurrentLinkedQueue的性能要好于BlockingQueue接口,它是一个基于链接节点的无界线程安全队列。该队列的元素遵循先进先出的原则。该队列不允许null元素。
DelayQueue(基于时间优先级的队列,延期阻塞队列) 是一个没有边界BlockingQueue实现,加入其中的元素必需实现Delayed接口。 当生产者线程调用put之类的方法加入元素时,会触发Delayed接口中的compareTo方法进行排序,也就是说队列中元素的顺序是按到期时间排序的,而非它们进入队列的顺序。排在队列头部的元素是最早到期的,越往后到期时间赿晚。
ArrayBlockingQueue (基于数组的并发阻塞队列) 是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。ArrayBlockingQueue是以先进先出的方式存储数据
LinkedBlockingQueue(基于链表的FIFO阻塞队列) 阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。
LinkedBlockingDeque(基于链表的FIFO双端阻塞队列) 是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。它是可选容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE。
PriorityBlockingQueue(带优先级的无界阻塞队列) 是一个无界队列,它没有限制,在内存允许的情况下可以无限添加元素;它又是具有优先级的队列,是通过构造函数传入的对象来判断,传入的对象必须实现comparable接口。
SynchronousQueue(并发同步阻塞队列) 是一个内部只能包含一个元素的队列。插入元素到队列的线程被阻塞,直到 另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。
在不超出队列长度的情况下插入元素,可以立即执行,成功返回true, 如果队列满了就抛出异常。
在不超出队列长度的情况下插入元素的时候则可以立即在队列的尾部插 入指定元素,成功时返回true,如果此队列已满,则返回false。
插入元素的时候,如果队列满了就进行等待,直到队列可用。
从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有 值,并且该方法取得了该值。
poll(long timeout,TimeUnit unit) 在给定的时间里,从队列中获取值,如果没有取到会抛出异常。
获取队列中剩余的空间。
从队列中移除指定的值。
判断队列中是否拥有该值。
将队列中值,全部移除,并发设置到给定的集合中。
类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有 一个任务A,它要等待其他3个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。
CyclicBarrie的作用就是会让所有线程都等待完成后才会继续下一步行动。
CyclicBarrier初始化时规定一个数目,然后计算调用了CyclicBarrier.await()进入等待的线程数。当线程数达到了这个数目时,所有进入等待状态的线程被唤醒并继续。
CyclicBarrier初始时还可带一个Runnable的参数, 此Runnable任务在CyclicBarrier的数目达到后,所有其它线程被唤醒前被执行。
Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量(允许 自定义多少线程同时访问)。就这一点而言,单纯的synchronized 关键字是实现不了的。
谈谈对 BlockingQueue 的理解?分别有哪些实现类?
谈谈对 ConcurrentSkipListMap 的理解?
通常指无名管道,是 UNIX 系统IPC最古老的形式。 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。
它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
FIFO可以在无关的进程之间交换数据,与无名管道不同。
FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
支持信号量组。
共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
因为多个进程可以同时操作,所以需要进行同步。
信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
当一个request发送出去以后,会得到一个response,这整个过程就是一个同步调用的过程。哪怕response为空,或者response的返回特别快,但是针对这一次请求而言就是一个同步的调用。
当一个request发送出去以后,没有得到想要的response,而是通过后面的callback、状态或者通知的方式获得结果。
阻塞调用是指调用方发出request的线程因为某种原因(如:等待系统资源)被服务方挂起,当服务方得到response后就唤醒挂起线程,并将response返回给调用方。
非阻塞调用是指调用方发出request的线程在没有等到结果时不会被挂起,直到得到response后才返回。阻塞和非阻塞最大的区别就是看调用方线程是否会被挂起。
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
互斥条件:在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等待,直至占有资源的进程用毕释放
占有且等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放
不可抢占条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A在 等B,B在等C,C在等A)
饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
死锁:指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。
高优先级线程吞噬所有的低优先级线程的 CPU 时间。
线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
死锁和饥饿的区别
首先,第一种方法就是预防死锁的产生,一次性合理地分配所有的资源,只要有一个资源得不到分配,也不给这个进程分配其他资源。这一方面主要就是预防死锁条件的产生。可以使用银行家算法,合理分配资源。
第二种就是发现系统中有进程死锁时,我们可以强制性地剥夺抢占某些进程的资源,然后分配给死锁进程,以解除死锁状态。用一部分死锁进程的资源来解决另外一部分进程的资源。
在第二种中,我们是直接销毁某些死锁的进程来解除另外一部分进程的死锁。但是现在我们可以不用销毁这部分的死锁进程。我们可以将其挂起到CPU外,将资源空出来让给死锁进程,这样也可以解决死锁进程。
避免一个线程同时获得多个锁
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
怎么解除死锁?
强迫一个线程睡眠N毫秒
判断一个线程是否存活
等待线程终止
程序中活跃的线程数
枚举程序中的线程
得到当前线程
一个线程是否为守护线程
设置一个线程为守护线程
为线程设置一个名称
强迫一个线程等待
通知一个线程继续运行
设置一个线程的优先级
说一下 Jvm 的主要组成部分?及其作用?
谈谈对运行时数据区的理解?
堆和栈的区别是什么?
堆中存什么?栈中存什么?
为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
Java 中的参数传递时传值呢?还是传引用?
Java 对象的大小是怎么计算的?
对象的访问定位的两种方式?
判断垃圾可以回收的方法有哪些?
垃圾回收是从哪里开始的呢?
被标记为垃圾的对象一定会被回收吗?
谈谈对 Java 中引用的了解?
谈谈对内存泄漏的理解?
内存泄露的根本原因是什么?
举几个可能发生内存泄漏的情况?
尽量避免内存泄漏的方法?
常用的垃圾收集算法有哪些?
为什么要采用分代收集算法?
分代收集下的年轻代和老年代应该采用什么样的垃圾回收算法?
什么是浮动垃圾?
什么是内存碎片?如何解决?
常用的垃圾收集器有哪些?
谈谈你对 CMS 垃圾收集器的理解?
谈谈你对 G1 收集器的理解?
说下你对垃圾回收策略的理解/垃圾回收时机?
谈谈你对内存分配的理解?大对象怎么分配?空间分配担保?
说下你用过的 JVM 监控工具?
如何利用监控工具调优?
JVM 的一些参数?
谈谈你对类文件结构的理解?有哪些部分组成?
谈谈你对类加载机制的了解?
类加载各阶段的作用分别是什么?
有哪些类加载器?分别有什么作用?
类与类加载器的关系?
谈谈你对双亲委派模型的理解?工作过程?为什么要使用
怎么实现一个自定义的类加载器?需要注意什么?
怎么打破双亲委派模型?
有哪些实际场景是需要打破双亲委派模型的?
谈谈你对编译期优化和运行期优化的理解?
为何 HotSpot 虚拟机要使用解释器与编译器并存的架构?
共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化
java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题
内存间的交互操作有哪些?需要满足什么规则?
springboot和springcloud的区别是什么?
使用 Spring 框架的好处是什么?
解释下什么是 AOP?
AOP 的代理有哪几种方式?
怎么实现 JDK 动态代理?
AOP 的基本概念:切面、连接点、切入点等?
通知类型(Advice)型(Advice)有哪些?
谈谈你对 IOC 的理解?
Bean 的生命周期?
Bean 的作用域?
Spring 中的单例 Bean 的线程安全问题了解吗?
谈谈你对 Spring 中的事物的理解?
Spring 中的事务隔离级别?
Spring 中的事物传播行为?
Spring 常用的注入方式有哪些?
Spring 框架中用到了哪些设计模式?
ApplicationContext 通常的实现有哪些?
谈谈你对 MVC 模式的理解?
SpringMVC 的工作原理/执行流程?
SpringMVC 的核心组件有哪些?
SpringMVC 常用的注解有哪些?
@RequestMapping 的作用是什么?
如何解决 POST 请求中文乱码问题,GET 的又如何处理呢?
SpringMVC 的控制器是不是单例模式,如果是会有什么问题,怎么解决?
SpringMVC 怎么样设定重定向和转发的?
SpringMVC 里面拦截器是怎么写的?
SpringMVC 和 Struts2 的区别有哪些?
谈谈你对 MyBatis 的理解?
MyBaits 的优缺点有哪些?
MyBatis 与 Hibernate 有哪些不同?
MyBatis 中 #{} 和 ${}的区别是什么?
MyBatis 是如何进行分页的?分页插件的原理是什么?
MyBatis 有几种分页方式?
MyBatis 逻辑分页和物理分页的区别是什么?
MyBatis 是否支持延迟加载?如果支持,它的实现原理是什么?
说一下 MyBatis 的一级缓存和二级缓存?
Mybatis 有哪些执行器(Executor)?
MyBatis 动态 SQL 是做什么的?都有哪些动态 SQL?能简述一下动态 SQL的执行原理不?
请说下你对 MySQL 架构的了解?
用 PreparedStatement, 一般来说比 Statement 性能高:一个 sql 发给服务器去执行,涉及步骤:语法检查、语义分析, 编译,缓存
有外键约束会影响插入和删除性能,如果程序能够保证数据的完整性, 那在设计数据库时就去掉外键
表中允许适当冗余,譬如,主题帖的回复数量和最后回复时间等
UNION ALL 要比 UNION 快很多,所以,如果可以确认合并的两个结 果集中不包含重复数据且不需要排序时的话,那么就使用 UNION ALL。 >>UNION 和 UNION ALL 关键字都是将两个结果集合并为一 个,但这两者从使用和效率上来说都有所不同。 >1. 对重复结果的处 理:UNION 在进行表链接后会筛选掉重复的记录,Union All 不会去除 重复记录。 >2. 对排序的处理:Union 将会按照字段的顺序进行排 序;UNION ALL 只是简单的将两个结果合并后就返回
一条 SQL 语句在数据库框架中的执行流程?
数据库的三范式是什么?
char 和 varchar 的区别?
varchar(10) 和 varchar(20) 的区别?
谈谈你对索引的理解?
索引的底层使用的是什么数据结构?
谈谈你对 B+ 树的理解?
为什么 InnoDB 存储引擎选用 B+ 树而不是 B 树呢?
谈谈你对聚簇索引的理解?
谈谈你对哈希索引的理解?
谈谈你对覆盖索引的认识?
即针对数据库表创建索引
与普通索引类似,不同的就是:MySQL 数据库索引列的值 必须唯一,但允许有空值
它是一种特殊的唯一索引,不允许有空值。一般是在建表的 时候同时创建主键索引
为了进一步榨取 MySQL 的效率,就要考虑建立组合索引。 即将数据库表中的多个字段联合起来作为一个组合索引
谈谈你对最左前缀原则的理解?
怎么知道创建的索引有没有被使用到?或者说怎么才可以知道这条语句运行很慢的原因?
什么情况下索引会失效?即查询不走索引?
查询性能的优化方法?
InnoDB 和 MyISAM 的比较?
谈谈你对水平切分和垂直切分的理解?
主从复制中涉及到哪三个线程?
主从同步的延迟原因及解决办法?
谈谈你对数据库读写分离的理解?
请你描述下事务的特性?
InnoDB 支持的四种事务隔离级别名称,以及逐级之间的区 别 Read Uncommitted(读取未提交内容) 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)
这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该 实例处理其间可能会有新的 commit,所以同一 select 可能返回不同结果
这是 MySQL 的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读(Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现 有新的“幻影” 行。InnoDB 和 Falcon 存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control 间隙锁)机制解决了该问题。注:其实多版本只是解决不可重复读问题,而加上间隙锁(也就是它这里所谓的并发控制)才解决了幻读问题。
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争
读取未提交的数据
在数据库访问中,一个事务范围内两个相同的查询却返回了不同数据
幻读
MySQL 默认的隔离级别是什么?
谈谈你对MVCC 的了解?
说一下 MySQL 的行锁和表锁?
InnoDB 行锁是通过给索引上的索引项加锁来实现的,这一点 MySQL 与 Oracle 不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB 这 种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB 才使用行级 锁,否则,InnoDB 将使用表锁!
MySQL 问题排查都有哪些手段?
若一张表中只有一个字段 VARCHAR(N)类型,utf8 编码,则 N 最大值 为多少(精确到数量级即可)? 由于 utf8 的每个字符最多占用 3 个字节。而 MySQL 定义行的长度不能超过 65535,因此 N 的最大值计算方法为:(65535-1-2)/3。减去 1 的原因是实 际存储从第二个字节开始,减去 2 的原因是因为要在列表长度存储实际的字符 长度,除以 3 是因为 utf8 限制:每个字符最多占用 3 个字节
[SELECT *] 和[SELECT 全部字段]的 2 种写法有何优缺点? 前者要解析数据字典,后者不需要
结果输出顺序,前者与建表列顺序相同,后者按指定字段顺序
表字段改名,前者不需要修改,后者需要改
后者可以建立索引进行优化,前者无法优化
后者的可读性比前者要高
语法上:where 用表中列名,having 用 select 结果别名
影响结果范围:where 从表读出数据的行数,having 返回客户端的行数
索引:where 可以使用索引,having 不能使用索引,只能在临时结果 集操作
where 后面不能使用聚集函数,having 是专门使用聚集函数的
MySQL 数据库 CPU 飙升到 500% 的话要怎么处理? 当 cpu 飙升到 500%时,先用操作系统命令 top 命令观察是不是 MySQLd 占用导致的,如果不是,找出占用高的进程,并进行相关处理
如果是 MySQLd 造成的, show processlist,看看里面跑的 session 情况,是不是有消耗资源的 sql 在运行。找出消耗高的 sql,看看执行计划是否准确, index 是否缺失,或者实在是数据量太大造成
一般来说,肯定要 kill 掉这些线程(同时观察 cpu 使用率是否下降),等进行相应的调整(比如说加索引、改sql、改内存参数)之后,再重新跑这些 SQL
也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等
Redis 的全称是:Remote Dictionary.Server,本质上是一个 Key-Value 类型的内存数据库,很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据 flush 到硬盘上进行保存
纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作,是已知性能最快的Key-Value DB
Redis 最大的魅力是支持保存多种数据结构
单个 value 的最大限制是 1GB,不像 memcached 只能保存 1MB 的数据
内存
会话缓存(Session Cache)
全页缓存(FPC)
队列
排行榜/计数器
发布/订阅
Redis 与 memcached 相比有哪些优势 memcached 所有的值均是简单的字符串,redis 作为其替代者,支持更为丰富的数据类型
redis 的速度比 memcached 快很多 redis 的速度比 memcached 快很多
redis 可以持久化其数据
数据缓存功能
分布式锁的功能
支持数据持久化
支持事务
支持消息队列
性能高,读的速度是100000次/s,写的速度是80000次/s
数据持久化,支持RDB、AOF
支持事务。通过MULTI和EXEC指令包起来
多种数据结构类型
主从复制
其他特性:发布/订阅、通知、key过期等
512M
散列
列表
集合
有序集合
因为是纯内存操作
Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以 redis 具有快速和数据持久化的特征,如果不将数据放在内存中,磁盘 I/O 速度为严重影响 redis的性能
如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值
返回错误当内存限制达到,并且客户端尝试执行会让更多内存被使用的命令
尝试回收最少使用的键(LRU),使得新添加的数据有空间存放
尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放
回收随机的键使得新添加的数据有空间存放
回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键
回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放
一些恶意的请求会故意查询不存在的 key,请求量很大,就会对后端系统造成很大的压力
方案一:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该 key 对应的数据 insert 了之后清理缓存
方案二:对一定不存在的 key 进行过滤。可以把所有的可能存在的 key 放到一个大的 Bitmap 中,查询时通过该 bitmap 过滤
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待
做二级缓存,A1 为原始缓存,A2 为拷贝缓存,A1 失效时,可以访问 A2,A1 缓存失效时间设置为短期,A2 设置为长期
不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀
怎么保证缓存和数据库数据的一致性?
对 redis 对外提供的读写服务,影响非常小,可以让 redis 保持高性能
相对于 AOF 持久化机制来说,直接基于 RDB 数据文件来重启和恢复 redis 进程,更加快速
因为是周期性的,那么当系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失
保存的数据额更加完整,能够更好的通过文件完成数据的重建
由于AOF一般每秒都在存储,可能对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快
先拿 setnx 来争抢锁,抢到之后,再用 expire 给锁加一个过期时间防止锁忘记了释放
如果在 setnx 之后执行 expire 之前进程意外 crash 或者要重启维护了,那会怎么样? set 指令有非常复杂的参数,这个应该是可以同时把 setnx 和 expire 合成一条指令来用的!
Redis是一个基于内存的数据库,如果内存不足,就会导致Redis崩溃。
解决方案是增加内存,或者通过开启Redis的虚拟内存功能来解决。虚拟内存可以将一部分数据写入磁盘,释放内存,但是这会带来一些性能损失。
当某些key被频繁访问,就会导致Redis的性能下降
解决方案是使用Redis集群,将热key分散到不同的节点上,或者使用缓存淘汰策略,如LRU、LFU等
Redis采用单线程模型,虽然能够减少并发冲突,但是在高并发场景下性能会受到影响
解决方案是通过使用Redis集群来实现分布式部署,或者使用多个Redis实例来提高性能
Redis支持两种持久化方式:RDB和AOF。RDB方式是将Redis的内存快照写入磁盘,AOF方式是将Redis的命令写入磁盘。如果持久化操作频繁,会导致Redis的性能下降
解决方案是合理设置持久化策略,选择合适的持久化方式,或者使用Redis的集群方式
当Redis的网络带宽不足时,会导致Redis性能下降。
解决方案是使用更高带宽的网络,或者通过使用Redis集群来实现负载均衡
可以通过使用 Redis 的持久化机制,将数据保存到磁盘上,减少内存占用;或者采用 Redis 的 LRU 策略,自动删除最近最少使用的键值对,释放内存空间。
如果业务场景需要大量使用过期键,可以考虑使用 Redis 的 volatile-lru 或 volatile-ttl 策略,自动删除最近最少使用或者 TTL 最近的键值对,避免占用过多内存。
如果 Redis 作为主从架构的主节点,频繁进行全量同步会影响性能,可以通过设置主从节点的复制方式为部分重同步(PSYNC),只传输增量数据,减少全量同步的次数和数据量。
如果客户端发起大量的短连接请求,会导致 Redis 服务器频繁地创建和销毁连接,可以考虑使用连接池技术,复用已经建立的连接,避免频繁地创建和销毁连接。
如果业务场景需要大量的数据写入操作,可以通过使用 Redis 的管道技术,将多个写入操作打包成一个请求发送给 Redis,减少网络通信的开销和延迟,提高写入性能。
Redis 官方为什么不提供 Windows 版本? 因为目前 Linux 版本已经相当稳定,而且用户量很大,无需开发 windows 版本,反而会带来兼容性等问题
为什么需要三次握手?两次不行?
为什么需要四次挥手?三次不行?
TCP与UDP有哪些区别?各自应用场景?
HTTP1.0,1.1,2.0 的版本区别
POST和GET有哪些区别?各自应用场景?
HTTP 哪些常用的状态码及使用场景?
HTTP状态码301和302的区别,都有哪些用途?
在交互过程中如果数据传送完了,还不想断开连接怎么办,怎么维持?
HTTP 如何实现长连接?在什么时候会超时?
TCP 如何保证有效传输及拥塞控制原理
IP地址有哪些分类?
GET请求中URL编码的意义
什么是SQL 注入?举个例子?
谈一谈 XSS 攻击,举个例子?
讲一下网络五层模型,每一层的职责?
简单说下 HTTPS 和 HTTP 的区别
对称加密与非对称加密的区别
简单说下每一层对应的网络协议有哪些?
ARP 协议的工作原理?
TCP 的主要特点是什么?
UDP 的主要特点是什么?
TCP 和 UDP 分别对应的常见应用层协议有哪些?
为什么 TIME-WAIT 状态必须等待 2MSL 的时间呢?
保活计时器的作用?
TCP 协议是如何保证可靠传输的?
谈谈你对停止等待协议的理解?
谈谈你对 ARQ 协议的理解?
谈谈你对滑动窗口的了解?
谈下你对流量控制的理解?
谈下你对 TCP 拥塞控制的理解?使用了哪些算法?
什么是粘包?
TCP 黏包是怎么产生的?
怎么解决拆包和粘包?
forward 和 redirect 的区别?
HTTP 方法有哪些?
在浏览器中输入 URL 地址到显示主页的过程?
DNS 的解析过程?
谈谈你对域名缓存的了解?
谈下你对 HTTP 长连接和短连接的理解?分别应用于哪些场景?
HTTPS 的工作过程?
HTTP 和 HTTPS 的区别?
HTTPS 的优缺点?
什么是数字签名?
什么是数字证书?
Cookie 和 Session 有什么区别?
什么是缓冲区溢出?有什么危害?
分页与分段的区别?
物理地址、逻辑地址、虚拟内存的概念
页面置换算法有哪些?
谈谈你对动态链接库和静态链接库的理解?
外中断和异常有什么区别?
一个程序从开始运行到结束的完整过程,你能说出来多少?
什么是用户态和内核态
用户态和内核态是如何切换的?
进程终止的方式
守护进程、僵尸进程和孤儿进程
如何避免僵尸进程?
介绍一下几种典型的锁?
常见内存分配内存错误
内存交换中,被换出的进程保存在哪里?
原子操作的是如何实现的
抖动你知道是什么吗?它也叫颠簸现象
消息队列的基本作用?
消息队列的优缺点有哪些?
如何保证消息队列的高可用?
如何保证消息不被重复消费?或者说,如何保证消息消费的幂等性?
如何保证消息的可靠性传输?或者说,如何处理消息丢失的问题?
如何保证消息的顺序性?
大量消息在 MQ 里长时间积压,该如何解决?
MQ 中的消息过期失效了怎么办?
RabbitMQ 有哪些重要的角色?
RabbitMQ 有哪些重要的组件?
RabbitMQ 有几种广播类型?
Kafka 可以脱离 zookeeper 单独使用吗?为什么?
Kafka 有几种数据保留的策略?
Kafka 的分区策略有哪些?
谈下你对 Zookeeper 的认识?
Zookeeper 都有哪些功能?
谈下你对 ZAB 协议的了解?
Zookeeper 怎么保证主从节点的状态同步?
Zookeeper 有几种部署模式?
说一下 Zookeeper 的通知机制?
集群中为什么要有主节点?
集群中有 3 台服务器,其中一个节点宕机,这个时候 Zookeeper 还可以使用吗?
说一下两阶段提交和三阶段提交的过程?分别有什么问题?
Zookeeper 宕机如何处理?
说下四种类型的数据节点 Znode?
Zookeeper 和 Dubbo 的关系?