HashMap是一个散列桶(数组和链表组成)。插入一个键值对的步骤:
1)对key的hashCode做hash,再计算下标;
2)如果没碰撞直接放到桶中(碰撞的意思是计算得到的hash值相同,需要放到同一个bucket中);
3)如果碰撞了,调用equals()比较key,相同则替换旧值,不同则以链表的方式链接到后面;
4)如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD = 8),就把链表转为红黑树.链表长度低于6(UNTREEIFY_THRESHOLD = 6),就把红黑树转为链表;
5)如果bucket满了(超过current capacity* LOAD_FACTOR),就要resize。
扰动函数可以减少碰撞,如果两个不相等的对象返回不同的hashCode,碰撞的几率就会小。(扰动即hash方法内部的算法实现,目的是让不同对象返回不同的hashCode)
使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法,会减少碰撞的发生。例如String、Integer。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } index = (n - 1) & hash; // n为capacity 复制代码
hashCode的高16位不变,低16位和高16位做了一个异或。因为table长度为2的幂,计算下标时采用&而不是%。
多线程环境下使用HashMap进行put操作会引起死循环。因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永不为null,就会产生死循环。
JDK1.8之前的实现中用链表解决冲突,产生碰撞的情况下,get的时间复杂度O(1) + O(n)。JDK1.8利用红黑树替换链表,get的时间复杂度为O(1) + O(logn)。
ConcurrentHashMap,Hashtable。
(1) HashMap允许null的key和value,而Hashtable不允许
(2) HashMap不是线程安全的,而Hashtable是线程安全的(方法是synchronized的)
(3) HashMap继承自AbstractMap,而Hashtable继承自Dictionary
(4) HashMap重新计算hash值,Hashtable直接使用对象的hashCode
(5) HashMap有containsKey和containsValue方法,Hashtable只有contains方法
(6) HashMap初始大小是16,且一定是2的指数。Hashtable初始大小是11,扩容方式是old*2+1。负载因子都是0.75f。
HashMap不是有序的。
有序的Map有LinkedHashMap(插入顺序),TreeMap(大小顺序)。
LinkedHashMap是通过插入排序(put的时候的顺序是什么,取出来的时候就是什么样子)和访问排序(改变排序把访问过的放到底部)让键值有序。
TreeMap是通过实现SortMap接口,能够把它保存的键值对根据key排序,基于红黑树,从而保证TreeMap中所有键值对处于有序状态。
二叉查找树在极端情况下会变成一条线性结构(跟使用链表结构一样了),遍历查找会很慢。红黑树在插入新数据时会左旋、右旋、变色来保持平衡,解决链表查询深度的问题,属于平衡二叉树。保持平衡需要付出代价,该代价损耗的资源比遍历线性链表要少,所以链表长度小于8时,引入红黑树反而会慢。
Hashtable的方法是同步的,所有访问Hashtable的线程都必须竞争同一把锁。当一个线程访问Hashtable的同步方法,其他线程也访问Hashtable的同步方法时,会进入阻塞或轮询状态。
例如: public synchronized V get(Object key) {...} public synchronized V put(K key, V value) {...} public synchronized int size() {...} 复制代码
锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据能被其他线程访问。
一个ConcurrentHashMap包含一个Segment数组。Segment是一种可重入锁(ReentrantLock)。一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素。每个Segment守护着一个HashEntry数组里的元素。当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。
get过程不加锁,除非读到的值是空会加锁重读。get()方法里将要使用的共享变量都定义成volatile。
LinkedHashMap继承自HashMap,使用双向链表存储Map中Entry的顺序关系。一种是插入有序,一种是访问有序。由构造函数的accessOrder参数决定。不是线程安全类。
一个类中应该是一组相关性很高的函数、数据的封装,应该仅有一个引起它变化的原因。
软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是,对于修改是关闭的。
所有引用基类的地方必须能透明地使用其子类的对象。只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本不需要知道是父类还是子类。
1.高层模块不应该依赖低层模块,两者都应该依赖其抽象
2.抽象不应该依赖细节
3.细节应该依赖抽象
模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
类间的依赖关系应该建立在最小的接口上。客户端不应该依赖它不需要的接口。
一个对象应该对其他对象有最少的了解。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
编译期:若是采用String s = ""的格式,最大长度为65534。在Java中,所有需要保存在常量池的数据,长度最大不超过2^16=65536,null值使用两个字节表示,因此字符串长度不超65536-2=65534。
运行期:长度不能超过Integer.MAX_VALUE。更多线程相关面试题
进程是一个独立的运行环境,有自己的内存空间,可以被看做一个程序或者一个应用。进程间的切换开销大。
线程是进程中执行的一个任务,可以被称为轻量级进程,创建一个线程比创建一个进程需要更少的资源。线程可以共享进程中的资源(线程安全的问题),每个进程都至少有一个线程。线程切换开销小。
进程是资源分配的最小单位 线程是CPU调度的最小单位new Thread(new Runnable() { @Override public void run() { } }).start(); 复制代码
class MyThread extends Thread { @Override public void run() { super.run(); } } new MyThread().start(); 复制代码
和普通方法一样,运行在当前线程中而不是新线程中。
在多线程程序中,多个线程并发执行可以提高程序的效率,CPU不会因为某个线程需要等待资源而进入空闲状态。多个线程共享堆内存,因此创建多个线程去执行一些任务会比创建多个进程更好。
详细 Java中的线程生命周期 - Java中的线程状态
死锁是指两个或两个以上的线程永远被阻塞的情况,这种情况的产生至少需要两个以上的线程和两个以上的资源。
private void deadLock() { Thread t1 = new Thread(new Runnable() { @Override public void run() { synchronized (A) { try { Thread.currentThread().sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (B) { Log.i(TAG, "B"); } } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { synchronized (B) { try { Thread.currentThread().sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (A) { Log.i(TAG, "A"); } } } }); t1.start(); t2.start(); } 复制代码
首先来看产生死锁的条件(缺一不可)
避免方法:
(1)避免一个线程同时获取多个锁;
(2)避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源;
(3)避免无限期等待,尝试使用定时锁。
tips(volatile实现原理)
1. 将当前处理器缓存行的数据写回到系统内存。
2. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
tips
方法同步和代码块同步实现细节不一样。代码块同步使用monitorenter和monitorexit指令实现
线程分配到的时间片决定了线程使用CPU资源的多少,线程优先级决定线程需要多或者少分配一些CPU资源的属性。Java中通过整形成员变量priority(0~10)来控制优先级,默认优先级是5。优先级高的线程分配的时间片多于优先级低的线程。有些操作系统会忽略对优先级的设定。
ThreadLocal是一个以ThraedLocal为键、任意对象为值得存储结构,是一个线程内部的数据存储类,通过它可以在指定线程中存储数据,也只有在指定线程中可以获取到存储的数据。通过set()设值,get()取值。
// 例如,在Android中,Looper类利用ThreadLocal特性,保证了每个线程只存在一个Looper对象 static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); } 复制代码
1.共享内存(volatile、synchronized)
线程之间共享程序的公共状态,通过读写内存中的公共状态进行隐式通信。
同步是显示进行的,程序员必须指定某个方法或某段代码需要在线程之间互斥进行。
2.消息传递(notify、notifyAll、wait)
线程之间没有公共状态,线程之间必须通过发送消息进行显示通信。
消息的发送必须在消息的接收之前,同步是隐式进行的。
Java并发采用的是共享内存模型。在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。局部变量、方法定义参数和异常处理参数不会在线程之间共享。
happens-before具体规则
DCL(双重检查锁定)是常见的延迟初始化技术,延迟初始化用来降低初始化类和创建对象的开销。
1 public static Singleton getInstance() { 2 if (instance == null) { // 第一次检查 3 synchronized (Singleton.class) { // 加锁 4 if (instance == null) { // 第二次检查 5 instance = new Singleton(); 6 } 7 } 8 } 9 return instance; 10 } 复制代码
在第5行如果发生重排序(1.线程A分配了对象的内存空间,2.并将instance指向内存空间,3.然后再初始化对象),另一个并发线程B可能在第2行判断instance不为null,线程B接下来将访问instance所引用的对象,但此时这个对象有可能还没被A线程初始化。
解决办法1:volatile(禁止指令重排序)
当声明对象的引用为volatile后,指令重排序在多线程环境中将被禁止。(1. 线程A分配对象的内存空间,2.初始化对象,3.设置instance指向刚分配的内存地址)
解决办法2:基于类初始化的解决方案(静态内部类单例模式)
在执行类的初始化期间,JVM会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化。
比较两种方法的差别:
方法1:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。
方法2:实现代码简洁。
如果需要对实例字段使用线程安全的延迟初始化,使用volatile。如果需要对静态字段使用线程安全的延迟初始化,使用方法2。
Java中的每个对象都有一个对象锁,wait、notify等方法用于等到对象的锁或通知其他线程对象的锁可用。Java线程中并没有可供任何对象使用的锁和同步器。这些方法定义在Object类里,那Java的每个类都有用于线程间通信的基本方法。
当一个线程需要调用对象的wait方法释放对象锁时,这个线程必须拥有该对象的锁。同样,当一个线程需要调用对象的notify方法时,它会释放这个对象的锁。这些方法都需要线程持有对象的锁,这样只能通过同步来实现,因此只能在同步方法或同步块中调用。
原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境下避免数据不一致必须的手段。
Java从JDK1.5开始提供java.util.concurrent.atomic包。Atomic包提供了13个原子操作类。
可重入锁:支持重进入的锁,表示该锁能支持一个线程资源的重复加锁。还支持获取锁时的公平和非公平性选择。
公平性与否是针对获取锁而言。如果一个锁是公平的,那么获取顺序应该符合请求的绝对时间顺序FIFO。
Lock是一个接口,定义了锁获取和锁释放的基本操作。特性有: