1. Arrays.sort()实现原理和 Collections.sort() 的实现原理
JDK1.7 Arrays.sort() 使用双枢轴快速排序(DualPivotQuicksort)作为默认排序方法。数组大小小于286
时,使用DualPivotQuicksort排序。数组大小大于286
时,使用TimSort排序。
JDK1.7 Collections.sort()内部先将列表转成数组后再调用 Arrays.sort(),在 Arrays.sort()里面有一个分支判断是采用 legacyMergeSort(注释了在将来会被移除) 还是ComparableTimSort。
2. 线程池的种类、区别和使用场景
Java通过Executors提供四种线程池,分别为:
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
newSingleThreadScheduledExecutor 线程池中只有一个工作线程,它能按时间计划来执行任务。
3. 分析线程池的实现原理和线程的调度过程
线程池原理
:预先启动一些线程,线程无限循环从任务队列中获取一个任务进行执行,直到线程池被关闭。如果某个线程因为执行某个任务发生异常而终止,那么重新创建一个新的线程而已。如此反复。
一个线程池包括以下四个基本部分:
- 线程池管理器(ThreadPool):用于创建并管理线程池,包括创建线程池、销毁线程池、添加新任务。
- 工作线程(PoolWorker):我们把用来执行用户任务的线程称为工作线程,工作线程就是不断从队列中获取任务对象并执行对象上的业务方法。线程池中的线程,在没有任务时处于等待状态,可以循环的执行任务。
- 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等。
- 任务队列(TaskQueue):用于存放没有处理的任务。提供一种缓冲机制。
4. 线程池如何调优
调整线程池的大小,线程池的最佳大小取决于可用处理器的数目以及工作队列中的任务的性质。一般需要根据任务的类型来配置线程池大小:
如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为NCPU+1;
如果是IO密集型任务,参考值可以设为2NCPU。
5. 动态代理的几种方式
JDK 动态代理和 cglib 动态代理。JDK动态代理是由Java内部的反射机制实现的,cglib 动态代理底层则是借助 asm 来实现的。总的来说,反射机制在生成类的过程中比较高效,而 asm 在生成类之后的相关执行过程中比较高效。还有一点是 JDK动态代理的前提必须是目标类基于统一的接口。
6. HashMap的工作原理是什么?
HashMap内部是通过一个数组实现的,只是这个数组比较特殊,数组里存储的元素是一个Entry实体(jdk 8为Node),这个Entry实体主要包含key、value以及一个指向自身的next指针。HashMap是基于hashing实现的,当我们进行put操作时,根据传递的key值得到它的hashcode,然后再用这个hashcode与数组的长度进行模运算,得到一个int值,就是Entry要存储在数组的位置(下标);当通过get方法获取指定key的值时,会根据这个key算出它的hash值(数组下标),根据这个hash值获取数组下标对应的Entry,然后判断Entry里的key,hash值或者通过equals()比较是否与要查找的相同,如果相同,返回value,否则的话,遍历该链表(有可能就只有一个Entry,此时直接返回null),直到找到为止,否则返回null。
HashMap之所以在每个数组元素存储的是一个链表,是为了解决hash冲突问题,当两个对象的hash值相等时,那么一个位置肯定是放不下两个值的,于是hashmap采用链表来解决这种冲突,hash值相等的两个元素会形成一个链表。
7. HashMap与HashTable的区别是什么?
1. HashTable基于Dictionary类,而HashMap是基于AbstractMap。Dictionary是任何可将键映射到相应值的类的抽象父类,而AbstractMap是基于Map接口的实现,它以最大限度地减少实现此接口所需的工作。
2. HashMap的key和value都允许为null,而Hashtable的key和value都不允许为null。HashMap遇到key为null的时候,调用putForNullKey方法进行处理,而对value没有处理;Hashtable遇到null,直接返回NullPointerException。
3. Hashtable是同步的,而HashMap是非同步的,但是我们也可以通过Collections.synchronizedMap(hashMap),使其实现同步。
8. CorrentHashMap的工作原理?
JDK 1.6版
:ConcurrenHashMap可以说是HashMap的升级版,ConcurrentHashMap是线程安全的,但是与Hashtablea相比,实现线程安全的方式不同。Hashtable是通过对hash表结构进行锁定,是阻塞式的,当一个线程占有这个锁时,其他线程必须阻塞等待其释放锁。ConcurrentHashMap是采用分离锁的方式,它并没有对整个hash表进行锁定,而是局部锁定,也就是说当一个线程占有这个局部锁时,不影响其他线程对hash表其他地方的访问。
具体实现:ConcurrentHashMap内部有一个Segment
每个ConcurrentHashMap在默认并发级下会创建包含16个Segment对象的数组,每个数组有若干个桶,当我们进行put方法时,通过hash方法对key进行计算,得到hash值,找到对应的segment,然后对该segment进行加锁,然后调用segment的put方法进行存储操作,此时其他线程就不能访问当前的segment,但可以访问其他的segment对象,不会发生阻塞等待。
JDK 1.8版
:在jdk 8中,ConcurrentHashMap不再使用Segment分离锁,而是采用一种乐观锁CAS算法来实现同步问题,但其底层还是“数组+链表->红黑树”的实现。
9. fail-fast与fail-safe有什么区别?
Iterator的fail-fast属性与当前的集合共同起作用,因此它不会受到集合中任何改动的影响。Java.util包中的所有集合类都被设计为fail->fast的,而java.util.concurrent中的集合类都为fail-safe的。当检测到正在遍历的集合的结构被改变时,Fail-fast迭代器抛出ConcurrentModificationException,而fail-safe迭代器从不抛出ConcurrentModificationException。
10. HashSet的底层实现是什么?
通过看源码知道HashSet的实现是依赖于HashMap的,HashSet的值都是存储在HashMap中的。在HashSet的构造法中会初始化一个HashMap对象,HashSet不允许值重复,因此,HashSet的值是作为HashMap的key存储在HashMap中的,当存储的值已经存在时返回false。
11. LinkedHashMap的实现原理?
LinkedHashMap也是基于HashMap实现的,不同的是它定义了一个Entry header,这个header不是放在Table里,它是额外独立出来的。LinkedHashMap通过继承hashMap中的Entry,并添加两个属性Entry before,after,和header结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。LinkedHashMap定义了排序模式accessOrder,该属性为boolean型变量,对于访问顺序,为true;对于插入顺序,则为false。一般情况下,不必指定排序模式,其迭代顺序即为默认为插入顺序。
12. Java中有哪几种锁?
自旋锁
自旋锁在JDK1.6之后就默认开启了。基于之前的观察,共享数据的锁定状态只会持续很短的时间,为了这一小段时间而去挂起和恢复线程有点浪费,所以这里就做了一个处理,让后面请求锁的那个线程在稍等一会,但是不放弃处理器的执行时间,看看持有锁的线程能否快速释放。为了让线程等待,所以需要让线程执行一个忙循环也就是自旋操作。
在jdk6之后,引入了自适应的自旋锁,也就是等待的时间不再固定了,而是由上一次在同一个锁上的自旋时间及锁的拥有者状态来决定
偏向锁:
在JDK1.之后引入的一项锁优化,目的是消除数据在无竞争情况下的同步原语。进一步提升程序的运行性能。 偏向锁就是偏心的偏,意思是这个锁会偏向第一个获得他的线程,如果接下来的执行过程中,改锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。偏向锁可以提高带有同步但无竞争的程序性能,也就是说他并不一定总是对程序运行有利,如果程序中大多数的锁都是被多个不同的线程访问,那偏向模式就是多余的,在具体问题具体分析的前提下,可以考虑是否使用偏向锁。
轻量级锁:
为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
13. synchronized内置锁
java中以synchronize的形式,为防止资源冲突提供了内置支持。当任务要执行被synchronize关键字保护的代码段时,它将检查锁是否可用,然后获取锁——执行代码——释放锁。
所有对象都自动含有单一的锁。当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法,但可以访问非synchronized方法。因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。
对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。
14. ThreadLocal理解
ThreadLocal是一个创建线程局部变量的类。通常情况下我们创建的变量,可以被多个线程访问并修改,通过ThreadLocal创建的变量只能被当前线程访问。
ThreadLocal内部实现
ThreadLocal提供了set和get方法。
set方法会先获取当前线程,然后用当前线程作为句柄,获取ThreadLocaMap对象,并判断该对象是否为空,如果为空则创建一个,并设置值,不为空则直接设置值。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocal的值是放入了当前线程的一个ThreadLocalMap实例中,所以只能在本线程中访问,其他线程无法访问。
ThreadLocal并不会导致内存泄露,因为ThreadLocalMap中的key存储的是ThreadLocal实例的弱引用,因此如果应用使用了线程池,即便之前的线程实例处理完之后出于复用的目的依然存活,也不会产生内存泄露。
15. 为什么wait, notify 和 notifyAll这些方法不在thread类里面?
这是个设计相关的问题,它考察的是面试者对现有系统和一些普遍存在但看起来不合理的事物的看法。回答这些问题的时候,你要说明为什么把这些方法放在Object类里是有意义的,还有不把它放在Thread类里的原因。一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
16. Java 类的初始化顺序
1. 父类的静态变量/静态初始化块
2. 子类类的静态变量/静态初始化块
3. 父类的动态初始化块(与静态初始化块类似,只是没有static关键字,即放在一对大括号中的代码块)、非构造方法和set方法的成员变量初始化
4. 父类的构造方法
5. 子类的动态初始化块、非构造方法和set方法的成员变量初始化
6. 子类的构造方法
7. 父类本地变量
8. 子类的本地变量