fail-safe 和fail-fast机制分别有什么作用?
Fail-fast:快速失败
Fail-fast : 表示快速失败,在集合遍历过程中,一旦发现容器中的数据被修改了,会立刻抛出ConcurrentModificationException 异常,从而导致遍历失败
package com.tianju.test;import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;public class DemoTest {public static void main(String[] args) {ArrayList<Integer> list = new ArrayList<>();list.add(1);list.add(2);Iterator<Integer> iterator = list.iterator();while (iterator.hasNext()){Integer next = iterator.next();list.add(55);System.out.println(next);}}
}
Fail-safe:失败安全
fail-safe:表示失败安全,也就是在这种机制下, 出现集合元素的修改,不会抛出
ConcurrentModificationException。
原因是采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先
复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到。
package com.tianju.test;import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;public class DemoTest2 {public static void main(String[] args) {CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();list.add(1);list.add(2);Iterator<Integer> iterator = list.iterator();while (iterator.hasNext()){Integer next = iterator.next();list.add(55);System.out.println(next);}}
}
java.util.concurrent 包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。
常见的的使用fail-safe 方式遍历的容器有ConcerrentHashMap 和 CopyOnWriteArrayList 等。
HashMap
hash冲突的问题
散列表Hash table & 散列函数 & 哈希冲突
Hash 算法,就是把任意长度的输入,通过散列算法输出结果是散列值。
在hashMap中,每个关键字被映射到从0到TableSize-1这个范围中的某个数,并且被放到适当的单元中。这个映射就叫作散列函数(hash function),理想情况下它应该计算起来简单,并且应该保证任何两个不同的关键字映射到不同的单元。不过,这是不可能的,因为单元的数目是有限的,而关键字实际上是用不完的。因此,我们寻找一个散列函数,该函数要在单元之间均匀地分配关键字。
public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}
如何解决hash冲突:
-
(1)开放定址法:
也称为线性探测法,就是从发生冲突的那个位置开始,按照一定的次序从hash 表中找到一个空闲的位置,然后把发生冲突的元素存入到这个空闲位置中。ThreadLocal 就用到了线性探测法来解决hash 冲突的。 -
(2)链式寻址法:
这是一种非常常见的方法,简单理解就是把存在hash 冲突的key ,以单向链表的方式来存储,比如HashMap 就是采用链式寻址法来实现的。 -
(3)再hash 法:
就是当通过某个hash 函数计算的key 存在冲突时,再用另外一个hash 函数对这个key 做hash,一直运算直到不再产生冲突。这种方式会增加计算时间,性能影响较大。 -
(4)建立公共溢出区:
就是把hash 表分为基本表和溢出表两个部分,在冲突的元素,一律放入到溢出表中。
HashMap怎么解决哈希冲突的?
链式寻址法+红黑树解决hash 冲突
HashMap 在JDK1.8 版本中,通过链式寻址法+红黑树的方式来解决hash 冲突问题,其中红黑树是为了优化Hash 表链表过长导致时间复杂度增加的问题。当链表长度大于8 并且hash 表的容量大于64 的时候,再向链表中添加元素就会触发转化。
当链表长度大于8 并且hash 表的容量大于64 的时候,再向链表中添加元素就会触发转化。
HashMap 中的hash 方法为什么要右移16 位异或
之所以要对hashCode 无符号右移16 位并且异或,核心目的是为了让hash 值的散列度更高,尽可能减少hash 表的hash 冲突,从而提升数据查找的性能。
首先使用key 的hashCode 无符号右移16 位,意味着把hashCode 的高位移动到了低位。
然后再用hashCode 与右移之后的值进行异或运算,就相当于把高位和低位的特征进行和组合。
从而降低了hash 冲突的概率。
受检异常和非受检异常
Java基础(8)——java的异常机制初步 & 异常的捕获和处理 & 自定义异常
受检异常和非受检异常,都是继承自Throwable 这个类中,分别是Error 和Exception,
- Error 是程序报错,系统收到无法处理的错误消息,它和程序本身无关。
- Excetpion 是指程序运行时抛出需要处理的异常信息如果不主动捕获,则会被jvm 处理。
- 受检异常的定义是程序在编译阶段必须要主动捕获的异常,遇到该异常有两种处理方法
(1)通过try/catch 捕获该异常;
(2)通过throw 把异常抛出去; - 非受检异常的定义是程序不需要主动捕获该异常,一般发生在程序运行期间,比如
NullPointException
受检异常的定义是程序在编译阶段必须要主动捕获的异常,遇到该异常有两种处理方法
为什么阿里巴巴的Java 开发手册不建议使用Java 自带的线程池
了解Java中线程池
Java进阶(5)——创建多线程的方法extends Thread和implements Runnable的对比 & 线程池及常用的线程池
4.【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这
样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 里面默认提供的几个线程池是有一些弊端的,如果是不懂多线程、或者是新手直接盲目使用,就可能会造成比较严重的生产事故。
为什么不能用
- 1.FixedThreadPool 和SingleThreadPool 中,阻塞队列长度是Integer.Max_Value,一旦请求量增加,就会堆积大量请求阻塞在队列中,可能会造成内存溢出的问题;
- 2.CachedThreadPool 和ScheduledThreadPool 中最大线程数量是Integer.Max_value,一旦请求量增加,导致创建大量的线程,使得处理性能下降。
JDK 动态代理为什么只能代理有接口的类
在Java 里面,动态代理是通过Proxy.newProxyInstance()方法来实现的,它需要传入被动态代理的接口类。
加入如下代码进行运行:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
或者加入下面这句
System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
JDK 动态代理会在程序运行期间动态生成一个代理类$Proxy0,这个动态生成的代理类会继承java.lang.reflect.Proxy 类,同时还会实现被代理类的接口。
在Java 中,是不支持多重继承的,而每个动态代理类都会继承Proxy 类(这也是JDK动态代理的实现规范) ,所以就导致JDK 里面的动态代理只能代理接口,而不能代理实现类。
spring中的代理
Spring进阶(AOP的理解)——静态/动态代理 & 面向切面编程AOP(Aspect Oriented Programming) & 日志记录 & 增强方法
如果一定要针对普通类来做动态代理,可以选择cglib 这个组件,它会动态生成一个被代理类的子类,子类重写了父类中所有非final 修饰的方法,在子类中拦截父类的所有方法调用从而实现动态代理。
Java对象相关的面试题
对象的创建过程
(1)类加载检查
JVM 首先会去检查目标对象是否已经被加载并初始化了。
如果没有,JVM 需要立刻去加载目标类,然后调用目标类的构造器完成初始化。目标类的加载是通过类加载器来实现的,主要就是把一个类加载到内存里面。
然后初始化的过程,主要是对目标类里面的静态变量、成员变量、静态代码块进行初始化。
(2)分配内存空间
当目标类被初始化以后,就可以从常量池里面找到对应的类元信息,并且目标对象的大小在类加载之后就已经确定了,所以这个时候就需要为新创建的对象,根据目标对象的大小在堆内存里面分配内存空间。
内存分配的方式一般有两种,一种指针碰撞,另一种是空闲列表,JVM 会根据Java 堆内存是否规整来决定内存分配方式。
(3)初始化 ”零值”
JVM 会把目标对象里面的普通成员变量初始化为零值,比如int 类型初始化为0 ,对象类型初始化为null, (类变量在类加载的准备阶段就已经初始化过了) 。
这一步操作主要是保证对象里面的实例字段,不用初始化就可以直接使用,也就是程序能够获得这些字段对应数据类型的零值。
(3)设置对象头”
然后,JVM 还需要对目标对象的对象头做一些设置,比如对象所属的类元信息、对象的GC 分代年龄、hashcode、锁标记等等。
(4)执行init方法
完成这些步骤以后,对于JVM 来说,新对象的创建工作已经完成。但是对于Java 语言来说,对象创建才算是开始。
接下来要做的,就是执行目标对象内部生成的init 方法,初始化成员变量的值、执行构造块、最后执行目标对象的构造方法,完成对象的创建。
其中,init 方法是Java 文件编译之后在字节码文件中生成的,它是一个实例构造器,这个构造器会把语句块、变量初始化、调用父类构造器等操作组织在一起。
所以调用init方法能够完成一系列的初始化动作。
什么是深拷贝和浅拷贝?
Java进阶(4)——结合类加载JVM的过程理解创建对象的几种方式:new,反射Class,克隆clone(拷贝),序列化反序列化
浅拷贝
深拷贝
深拷贝和浅拷贝是用来描述对象或者对象数组这种引用数据类型的复制场景的。
浅拷贝, 就是只复制某个对象的指针,而不复制对象本身。这种复制方式意味着两个引用指针指向被复制对象的同一块内存地址。
在Java 里面,无论是深拷贝还是浅拷贝,都需要通过实现Cloneable 接口,并实现clone()方法。然后我们可以在clone()方法里面实现浅拷贝或者深拷贝的逻辑。
实现深拷贝的方法有很多,比如
-
通过序列化的方式实现,也就是把一个对象先序列化一遍,然后再反序列化回来,就会得到一个完整的新对象。
-
在clone()方法里面重写克隆逻辑,也就是对克隆对象内部的引用变量再进行一次克隆。
Book.java实体类
implements Cloneable{ // 可以克隆的
package com.tianju.auth.reflect;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book implements Cloneable{ // 可以克隆的private String title;private Author author;public double price;static {System.out.println("book的静态代码块");}// protected:代表本包或者继承// 继承的时候,可以将子类的访问控制符扩大,但不能缩小;// 子类不能比父类抛出更多的异常@Overridepublic Object clone() throws CloneNotSupportedException {return super.clone();}public Book deepClone(){Book book = new Book();Author au = new Author();au.setName(author.getName());book.setAuthor(au);book.setTitle(this.title);book.setPrice(this.price);return book;}
}
进行测试
package com.tianju.auth.reflect;public class TestDemo{public static void main(String[] args) throws CloneNotSupportedException {Author author = new Author();author.setName("吴承恩");Book book = new Book("三国演义", author,12.56);Book book1 = book;System.out.println(book1==book);// == 两个引用是否指向同一个对象// clone创建了一个新的对象,只是值一样Book bookClone = (Book) book.clone();// 深拷贝,创建了新的对象,上面的浅拷贝,只是拷贝了引用Book deepClone = book.deepClone();System.out.println(bookClone==book);System.out.println("克隆前:"+book);System.out.println("克隆后:"+bookClone);author.setName("小柯基");System.out.println("修改后的原对象:"+book);System.out.println("修改后的clone对象:"+bookClone);// 深拷贝System.out.println("***********");System.out.println("深拷贝的方法:"+deepClone);}
}
String相关的面试题
new String(“abc”)到底创建了几个对象
- 如果abc 这个字符串常量不存在,则创建两个对象,分别是abc 这个字符串常量,以及new String 这个实例对象。
- 如果abc 这字符串常量存在,则只会创建一个对象
Java基础(1)——数据类型&包装类,引用类型String&StringBuilder,正则表达式,定点数,日期类
(1)String str = new String(“hello”);
执行上述代码,底层进行了如下工作,在栈内存里存放变量str,在堆内存新创建一个String的对象,在常量池空间中创建常量hello,如果存在则不创建;创建了一个或两个对象。
(2)String str = “hello”;
栈内存中的str指向常量池中的hello
String、StringBuffer、StringBuilder 区别
(1)可变性
String 内部的value 值是final 修饰的,所以它是不可变类。所以每次修改String 的值,都会产生一个新的对象。
StringBuffer 和StringBuilder 是可变类,字符串的变更不会产生新的对象。
StringBuffer 和StringBuilder 是可变类
(2)线程安全性
String 是不可变类,所以它是线程安全的。
StringBuffer 是线程安全的,因为它每个操作方法都加了synchronized 同步关键字。
StringBuilder 不是线程安全的,所以在多线程环境下对字符串进行操作,应该使用StringBuffer ,否则使用StringBuilder
(3)性能方面
String 的性能是最的低的,因为不可变意味着在做字符串拼接和修改的时候,需要重新
创建新的对象以及分配内存。
其次是StringBuffer 要比String 性能高,因为它的可变性使得字符串可以直接被修改,最后是StringBuilder ,它比StringBuffer 的性能高,因为StringBuffer 加了同步锁。
(4)存储方面
String 存储在字符串常量池里面;
StringBuffer 和StringBuilder 存储在堆内存空间。
最后再补充一下, StringBuilder 和StringBuffer 都是派生自AbstractStringBuilder这个抽象类。
Integer 使用不当导致生产的事故
为什么两个Integer 的对象不能用==号来判断?
Integer 是一个封装类型。它是对应一个int 类型的包装。
在Java 里面之所以要提供Integer 这种基本类型的封装类,是因为Java 是一个面向对象的语言,而基本类型不具备对象的特征,所以在基本类型上做了一层对象的包装并且提供了相关的属性和访问方法来完善基本类型的操作。
在Integer 这个封装类里面,除了基本的int 类型的操作之外,还引入了享元模式的设计,对-128 到127 之间的数据做了一层缓存,也就是说,如果Integer 类型的目标值在-128 到127 之间,就直接从缓存里面获取Integer 这个对象实例并返回,否则创建一个新的Integer 对象。
这么设计的好处是减少频繁创建Integer 对象带来的内存消耗从而提升性能。
因此在这样一个前提下,如果定义两个Integer 对象,并且这两个Integer 的取值范围正好在-128 到127 之间。
如果直接用==号来判断,返回的结果必然是true ,因为这两个Integer 指向的内存地址是同一个。否则,返回的结果是false。
ArrayList 的自动扩容机制
Java进阶(3)——手动实现ArrayList & 源码的初步理解分析 & 数组插入数据和删除数据的问题
ArrayList 是一个数组结构的存储容器,默认情况下,数组的长度是10.
当然我们也可以在构建ArrayList 对象的时候自己指定初始长度。
随着在程序里面不断的往ArrayList 中添加数据,当添加的数据达到10 个的时候,ArrayList 就没有多余容量可以存储后续的数据。
这个时候ArrayList 会自动触发扩容。
扩容的具体流程很简单,
- 首先,创建一个新的数组,这个新数组的长度是原来数组长度的1.5 倍。
- 然后使用Arrays.copyOf 方法把老数组里面的数据拷贝到新的数组里面。扩容完成后再把当前要添加的元素加入到新的数组里面,从而完成动态扩容的过程。