Java中Map和Collection接口是所有集合框架的顶接口;
Collection接口下有Set接口、List接口;
Set接口主要实现类:HashSet、TreeSet、LinkedHashSet等;
List接口主要实现类:ArrayList、LinkedList、Stack、Vector等
Map接口下是实现类:HashMap、TreeMap、HashTable、ConcurrentHashMap;
ConcurrentHashMap和HahsTable的区别?
我们要先知道HashMap和HashTable的区别;
ConcurrentHashMap结合了HahsMap和HashTable的优势:
hashMap没有同步,HashTable每次都要锁住整个结构,
ConcurrentHashMap的锁方式粒度更细;
ConcurrentHashMap将hash表分为16个桶,诸如get、put等常用操作只锁当前需要的桶。
ConcurrentHashMap具体实现?
Concurrent
该类包含两个静态内部类HashEntry和Segment;前者封装映射表的键值对,后者充当锁的角色;
segment是一种可重入锁ReentrantLock,每个segment守护一个hashEntry数组里的元素,当对hashentry数组的数据进行修改时,必须 首先获得对应的segment锁
HashEntry是目前我们提到的最小的逻辑处理单元了。一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。
HashMap的长度为什么是2的幂次方?
取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
- 通过将 Key 的 hash 值与 length-1 进行 & 运算,实现了当前 Key 的定位,2 的幂次方可以减少冲突(碰撞)的次数,提高 HashMap 查询效率;
- 如果 length 为 2 的次幂 则 length-1 转化为二进制必定是 11111……的形式,在于 h 的二进制与操作效率会非常的快,而且空间不浪费;
- 如果 length 不是 2 的次幂,比如 length 为 15,则 length-1 为 14,对应的二进制为 1110,在于 h 与操作,最后一位都为 0,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大。
更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。
hashmap
List、Set 和 Map 的初始容量和加载因子
list:arraylist初始容量是10,加载因子0.5,扩容增量0.5倍+1,一次扩容后容量为16;
Vector初始容量为10,加载因子1,扩容增量1倍,一次扩容后为20;
Set:HashSet初始容量16,加载因子0.75,扩容增量1倍,一次扩容后32;
Map:HashMap初始容量16,加载因子0.75,扩容增量1倍。
Comparable接口和Comparator接口有什么区别?
前者简单,在内部如果需要重新定义比较类型,需要修改源码
后者不需要修改源码,在外部,自定义一个比较器,实现自定义比较方法
Java 集合的快速失败机制 “fail-fast”
它是 java 集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
例如 :假设存在两个线程(线程 1、线程 2),线程 1 通过 Iterator 在遍历集合 A 中的元素,在某个时候线程 2 修改了集合 A 的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生 fail-fast 机制。
原因: 迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。
每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。
解决办法:
- 在遍历过程中,所有涉及到改变 modCount 值得地方全部加上 synchronized;
- 使用 CopyOnWriteArrayList 来替换 ArrayList。
安全失败(fail—safe)
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
多线程和单线程的区别和联系
- 在单核 CPU 中,将 CPU 分为很小的时间片,在每一时刻只能有一个线程在执行,是一种微观上轮流占用 CPU 的机制。
- 多线程会存在线程上下文切换,会导致程序执行速度变慢,即采用一个拥有两个线程的进程执行所需要的时间比一个线程的进程执行两次所需要的时间要多一些。
结论:即采用多线程不会提高程序的执行速度,反而会降低速度,但是对于用户来说,可以减少用户的响应时间。
如何指定多个线程的执行顺序?
如何让 10 个线程按照顺序打印 0123456789?(写代码实现)
答:
- 设定一个 orderNum,每个线程执行结束之后,更新 orderNum,指明下一个要执行的线程。并且唤醒所有的等待线程。
- 在每一个线程的开始,要 while 判断 orderNum 是否等于自己的要求值!!不是,则 wait,是则执行本线程。
线程和进程的区别
- 进程是一个 “执行中的程序”,是系统进行资源分配和调度的一个独立单位;
- 线程是进程的一个实体,一个进程中拥有多个线程,线程之间共享地址空间和其它资源(所以通信和同步等操作线程比进程更加容易);
- 线程上下文的切换比进程上下文切换要快很多。
• (1)进程切换时,涉及到当前进程的 CPU 环境的保存和新被调度运行进程的 CPU 环境的设置。
• (2)线程切换仅需要保存和设置少量的寄存器内容,不涉及存储管理方面的操作。
如何避免死锁?
指定获取锁的顺序;
比如某个线程只有获得A锁和B锁才能对某资源进行操作,在多线程条件下,规定获得锁的顺序是一定的,比如获得A锁的线程才有资格获得B锁,按顺序获取锁就可以避免死锁。
synchronized关键字
- 进入时,执行 monitorenter,将计数器 +1,释放锁 monitorexit 时,计数器-1;
- 当一个线程判断到计数器为 0 时,则当前锁空闲,可以占用;反之,当前线程进入等待状态。
含义:(monitor 机制)
Synchronized 是在加锁,加对象锁。对象锁是一种重量锁(monitor),synchronized 的锁机制会根据线程竞争情况在运行时会有偏向锁(单一线程)、轻量锁(多个线程访问 synchronized 区域)、对象锁(重量锁,多个线程存在竞争的情况)、自旋锁等。
该关键字是一个几种锁的封装。
volatile 关键字
该关键字可以保证可见性不保证原子性。
功能:
- 主内存和工作内存,直接与主内存产生交互,进行读写操作,保证可见性;
- 禁止 JVM 进行的指令重排序。
ThreadLocal(线程局部变量)工具类
当使用 ThreadLocal 维护变量时,其为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程对应的副本。
ThreadLocal 内部实现机制:
- 每个线程内部都会维护一个类似 HashMap 的对象,称为 ThreadLocalMap,里边会包含若干了 Entry(K-V 键值对),相应的线程被称为这些 Entry 的属主线程;
- Entry 的 Key 是一个 ThreadLocal 实例,Value 是一个线程特有对象。Entry 的作用即是:为其属主线程建立起一个 ThreadLocal 实例与一个线程特有对象之间的对应关系;
- Entry 对 Key 的引用是弱引用;Entry 对 Value 的引用是强引用。
Atomic
可以使基本数据类型以原子方式实现自增自减
线程池有了解嘛?
java.util.concurrent.ThreadPoolExecutor 类就是一个线程池。
客户端调用 ThreadPoolExecutor.submit(Runnable task) 提交任务,线程池内部维护的工作者线程的数量就是该线程池的线程池大小,有 3 种形态:
• 当前线程池大小 :表示线程池中实际工作者线程的数量;
• 最大线程池大小 (maxinumPoolSize):表示线程池中允许存在的工作者线程的数量上限;
• 核心线程大小 (corePoolSize ):表示一个不大于最大线程池大小的工作者线程数量上限。
- 如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队;
- 如果运行的线程等于或者多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不是添加新线程;
- 如果无法将请求加入队列,即队列已经满了,则创建新的线程,除非创建此线程超出 maxinumPoolSize, 在这种情况下,任务将被拒绝。