java面试(六)
1、单例模式
- 懒汉、饿汉、双重校验锁、静态内部类
- 代码分别如下
- 懒汉模式
线程不安全,延迟初始化,严格意义上不是不是单例模式 -
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
饿汉模式
线程安全,比较常用,但容易产生垃圾,因为一开始就初始化public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } }
双重锁模式
线程安全,延迟初始化。这种方式采用双锁机制,安全且在多线程情况下能保持高性能。public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
双重检查模式,进行了两次的判断,第一次是为了避免不要的实例,第二次是为了进行同步,避免多线程问题。由于
singleton=new Singleton()
对象的创建在JVM中可能会进行重排序,在多线程访问下存在风险,使用volatile
修饰signleton
实例变量有效,解决该问题。静态内部类单例模式
public class Singleton { private Singleton(){ } public static Singleton getInstance(){ return Inner.instance; } private static class Inner { private static final Singleton instance = new Singleton(); } }
只有第一次调用getInstance方法时,虚拟机才加载 Inner 并初始化instance ,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。目前此方式是所有单例模式中最推荐的模式,但具体还是根据项目选择。
-
2、集合类的结构
- Iterator、Collection(List、Set、Queue)、Map(HashTable、HashMap、LinkedHashMap、TreeMap)
3、HashMap底层结构,为什么长度是2的倍数,Put 的过程,get的过程,JDK 1.7 和 1.8 中 HashMap 的区别,为什么线程不安全
数组 + 链表。HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。
长度是2的倍数(原因):找索引时 key 的 hash 值与数组的长度值减 1 进行与运算,长度为 2 的倍数时能减少碰撞
Put 的过程:当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标), 但是当插入的Entry越来越多的时候,再完美的Hash函数也难免会出现下表索引冲突的情况,如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,通过链表来解决下标冲突,主要是因为,HashMap数组中的每一个元素不仅仅是一个Entry对象,也是一个链表的头节点,每一个Entry对象通过上一个例子中的Next指针,指向它的下一个Entry节点。新加入的放在链头,最先加入的放在链尾(这种插入方法,也被称为“头插法”,至于为什么不是链表“尾插法”,主要是考虑到查询效率的问题,HashMap创建者认为,后插入的Entry被查询的可能性更大)。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。
get的过程:从HashMap中get元素时,首先计算key的hashCode,HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。找到数组中对应位置的某一元素后,然后通过key的equals方法在对应位置的链表中找到需要的元素。
JDK 1.7 和 1.8 中 HashMap 的区别:1.8 增加红黑树、头插变为尾插、扩容后元素位置要么在原位置,要么在原位置 + 扩容前旧容量
为什么线程不安全:扩容时链表可能形成闭环。
3、ConcurrentHashMap 和 HashMap 区别
*ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的syn关键字锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。
*HashMap的键值对允许有null,但是ConCurrentHashMap都不允许
4、线程池工作流程
(1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则执行第二步。
(2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里进行等待。如果工作队列满了,则执行第三步
(3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务
线程池饱和策略
这里提到了线程池的饱和策略,那我们就简单介绍下有哪些饱和策略:
AbortPolicy
为Java线程池默认的阻塞策略,不执行此任务,而且直接抛出一个运行时异常,切记ThreadPoolExecutor.execute需要try catch,否则程序会直接退出。
DiscardPolicy
直接抛弃,任务不执行,空方法
DiscardOldestPolicy
从队列里面抛弃head的一个任务,并再次execute 此task。
CallerRunsPolicy
在调用execute的线程里面执行此command,会阻塞入口
用户自定义拒绝策略(最常用)
实现RejectedExecutionHandler,并自己定义策略模式
5、线程池的类别和区别
(1)fixThreadPool 正规线程、定常的线程池
我的理解这是一个有指定的线程数的线程池,有核心的线程,里面有固定的线程数量,响应的速度快。正规的并发线程,多用于服务器。固定的线程数由系统资源设置。
(2)CaCheThreadPool 缓存线程池
只有非核心线程,最大线程数很大(Int.Max(values)),它会为每一个任务添加一个新的线程,这边有一个超时机制,当空闲的线程超过60s内没有用到的话,就会被回收。缺点就是没有考虑到系统的实际内存大小
(3)singleThreadPoll 单线程线程池
看这个名字就知道这个家伙是只有一个核心线程,就是一个孤家寡人,通过指定的顺序将任务一个个丢到线程,都乖乖的排队等待执行,不处理并发的操作,不会被回收。确定就是一个人干活效率慢。
(4)ScheduledThreadPoll 周期性执行任务的线程池
这个线程池就厉害了,是唯一一个有延迟执行和周期重复执行的线程池。它的核心线程池固定,非核心线程的数量没有限制,但是闲置时会立即会被回收
6、阻塞队列的类别和区别
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
·LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
·PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
·DelayQueue:一个使用优先级队列实现的无界阻塞队列。
·SynchronousQueue:一个不存储元素的阻塞队列。
·LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
·LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
求职面试题&解析