java-core
1.hashcode相等两个类一定相等吗?equals呢?相反呢?
equals在Object的方法里的实现是== 也就是比较两个对象的地址是否相等,而equals的概念为,两个对象确确实实相等,在基本数据类型中,大多重写了equals方法,让其比较其内容是否相等。
hashcode时对对象内容经过散列算法的出来的一个int类型的值,这就说明,在这个程序里,如果两个不同的对象,其hashcode相同,则非常大的概率他们相同(99.9x%)。其散列出来的hashcode算法可能使用了程序的内存空间的基址一样的东西,他不保证在两台机器或者是在两次运行程序时,内容相同的对象其hashcode相同。但是如果我们想要让两个对象内容相同则认为它相同,就需要重写的equal()里一般比较的比较全面比较复杂,这样效率就比较低,而利用hashCode()进行对比,则只要生成一个hash值进行比较就可以了,效率很高。
总结:
1.equal()相等的两个对象他们的hashCode()肯定相等,也就是用equal()对比是绝对可靠的。
2.hashCode()相等的两个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的。
对于需要大量并且快速的对比的话如果都用equal()去做显然效率太低,所以解决方式是,每当需要对比的时候,首先用hashCode()去对比,如果hashCode()不一样,则表示这两个对象肯定不相等(也就是不必再用equal()去再对比了),如果hashCode()相同,此时再对比他们的equal(),如果equal()也相同,则表示这两个对象是真的相同了,这样既能大大提高了效率也保证了对比的绝对正确性。
2.介绍一下集合框架?
分为三块:List列表、Set集合、Map映射
List在数据结构上可以被看作为线性表,常用的有ArryList与LinkList,其底层存储结构有所不同,一个是数组,一个是链表。除了这两个常见的线性表外,还有古董级别的Vector(类比ArrayList)。但是这两种是注重数据存储结构的区分,众所周知数据结构数据操作方法层面上的区分,也就是栈和队列,在java集合框架里自然也有 就是Stack和Queue,但是Stack是一个继承了Vector的类(使用数组,最起码这个类限定死了)里面扩展和限定了线性表操作方法,可以拿来直接使用,而Queue是一个继承于Collection的接口(因为队列可以分多种),LinkList实现了Queue接口,常用的有ArrayBlockingQueue(基于数组),LinkedBlockingQueue(基于链表),PriorityBlockingQueue(实现优先级排序)等。
Map一种映射,用于储存关系型数据,保存着两种值,一组用于保存key,另一组用来保存value。并且key不允许重复。
HashMap底层就是一个数组,数组中根据存入的Key的HashCode决定存放位置,其Entry单元中有四个属性,分别为HashCode,Key,Vaule,和下一个Entry,这样就形成了一个链表,当HashMap中的另一个拥有相同的HashCode值的不同的Key存入时,会将原来的Entry赋到新Entry的属性中,然后形成Entry链,查询的时候先比较HashCode,如果相同且Key值相同则直接取出,如果HashCode相同Key值不同则继续顺着链表寻找直到寻找到相同的Key值。
TreeMap与HashMap的不同:表象上时TreeMap可以对Key进行排序,原因时TreeMap使用的时“红黑树”的二叉树结构储存Entry,也就是排序二叉树,左边恒放比此值小的数右边恒放比此值大的树,按照当前节点值与传入查询值的比较进行判断决定其存放位置/查询其数值;
Set集合:Set与Map可以手动的互相转换 Set转换Map只需要新建一个对象,对象中又key和value两个属性,新建一个类继承Set存储新建的对象即可实现。Map转换为Set只需要将Map的Value固定,只使用Key存储数据即可实现;
Table:Map的线程安全型号;
3.hashmap hastable 底层实现什么区别?hashtable和concurrenthashtable呢?
HashTable
底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
初始size为11,扩容:newsize = olesize*2+1
计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
HashMap
底层数组+链表实现,可以存储null键和null值,线程不安全
初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
计算index方法:index = hash & (tab.length – 1)
HashMap的初始值还要考虑加载因子:
哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。
加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。
空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。
HashMap和Hashtable都是用hash算法来决定其元素的存储,因此HashMap和Hashtable的hash表包含如下属性:
容量(capacity):hash表中桶的数量
初始化容量(initial capacity):创建hash表时桶的数量,HashMap允许在构造器中指定初始化容量
尺寸(size):当前hash表中记录的数量
负载因子(load factor):负载因子等于“size/capacity”。负载因子为0,表示空的hash表,0.5表示半满的散列表,依此类推。轻负载的散列表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢)
除此之外,hash表里还有一个“负载极限”,“负载极限”是一个0~1的数值,“负载极限”决定了hash表的最大填满程度。当hash表中的负载因子达到指定的“负载极限”时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing。
HashMap和Hashtable的构造器允许指定一个负载极限,HashMap和Hashtable默认的“负载极限”为0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing。
“负载极限”的默认值(0.75)是时间和空间成本上的一种折中:
较高的“负载极限”可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作(HashMap的get()与put()方法都要用到查询)
较低的“负载极限”会提高查询数据的性能,但会增加hash表所占用的内存开销
程序猿可以根据实际情况来调整“负载极限”值。
ConcurrentHashMap
底层采用分段的数组+链表实现,线程安全
通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
Hashtable和HashMap都实现了Map接口,但是Hashtable的实现是基于Dictionary抽象类的。Java5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来存储值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞时,对象将会储存在链表的下一个节点中。HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD,8),链表就会被改造为树形结构。
在HashMap中,null可以作为键,这样的键只有一个,但可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示HashMap中没有该key,也可以表示该key所对应的value为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个key,应该用containsKey()方法来判断。而在Hashtable中,无论是key还是value都不能为null。
Hashtable是线程安全的,它的方法是同步的,可以直接用在多线程环境中。而HashMap则不是线程安全的,在多线程环境中,需要手动实现同步机制。
Hashtable与HashMap另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。
先看一下简单的类图:
从类图中可以看出来在存储结构中ConcurrentHashMap比HashMap多出了一个类Segment,而Segment是一个可重入锁。
ConcurrentHashMap是使用了锁分段技术来保证线程安全的。
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。
ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。
4.hashmap和treemap什么区别?低层数据结构是什么?
HashMap底层就是一个数组,数组中根据存入的Key的HashCode决定存放位置,其Entry单元中有四个属性,分别为HashCode,Key,Vaule,和下一个Entry,这样就形成了一个链表,当HashMap中的另一个拥有相同的HashCode值的不同的Key存入时,会将原来的Entry赋到新Entry的属性中,然后形成Entry链,查询的时候先比较HashCode,如果相同且Key值相同则直接取出,如果HashCode相同Key值不同则继续顺着链表寻找直到寻找到相同的Key值。
TreeMap与HashMap的不同:表象上时TreeMap可以对Key进行排序,原因时TreeMap使用的时“红黑树”的二叉树结构储存Entry,也就是排序二叉树,左边恒放比此值小的数右边恒放比此值大的树,按照当前节点值与传入查询值的比较进行判断决定其存放位置/查询其数值;
5.线程池用过吗都有什么参数?底层如何实现的?
线程池:
1、线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
2、可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。
参数:
corePoolSize:线程池核心线程数量
maximumPoolSize:线程池最大线程数量
keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
unit:存活时间的单位
workQueue:存放任务的队列
handler:超出线程范围和队列容量的任务的处理程序
线程池的拒绝策略,是指当任务添加到线程池中被拒绝,而采取的处理措施。
当任务添加到线程池中之所以被拒绝,可能是由于:第一,线程池异常关闭。第二,任务数量超过线程池的最大限制。
线程池共包括4种拒绝策略,它们分别是:AbortPolicy,CallerRunsPolicy,DiscardOldestPolicy和DiscardPolicy。
AbortPolicy-- 当任务添加到线程池中被拒绝时,它将抛出 RejectedExecutionException 异常。CallerRunsPolicy -- 当任务添加到线程池中被拒绝时,会在线程池当前正在运行的Thread线程中处理被拒绝的任务。
DiscardOldestPolicy -- 当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中。
DiscardPolicy -- 当任务添加到线程池中被拒绝时,线程池将丢弃被拒绝的任务。
线程池默认的处理策略是AbortPolicy!
ThreadFactory:线程工厂 默认DefaultThreadFactory
底层核心实现为封装一层线程类work,在运行的时候再执行完自己的线程后主动去队列中拿取下一条线程去执行。
6.sychnized和Lock什么区别?sychnize 什么情况情况是对象锁? 什么时候是全局锁为什么?
synchronized是java中的一个关键字,用于线程同步。
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。
synchronized修饰不加static的方法,锁是加在单个对象上,不同的对象没有竞争关系;修饰加了static的方法,锁是加载类上,这个类所有的对象竞争一把锁。
Lock是一个java接口 里面有一些实现类,也用于实现线程同步,但是相比较于synchronized,无论功能还是性能都有很大提升,但是要注意需要手动释放。
性能上由于synchronized在编译时会对代码进行修正,最终由cpu通过调度线程的方式处理线程同步问题,开销很大。而lock使用了Java的unsafe类中的CAS方法实现对线程同步的控制,减小了消耗。
功能上:synchronized由线程控制,只有等待执行结束和异常释放,本身是可重入的,非公平锁,书写时无需手动释放。lock实现多样,除了最基础的可重入锁ReentrantLock,还有使用读写锁(ReadWriteLock)来实现读写分离,进一步提高效率,ReentrantLock默认使用的是非公平锁(竞争锁,可以设置)。
7.ThreadLocal 是什么 ?底层如何实现?写一个例子呗?
是一个解决线程并发问题的一个类,底层实现主要是存有一个map,以线程作为key,范型作为value。可以理解为线程级别的缓存。
使用起来比较简单 假设我们要实现一个线程级别的缓存。
private static final ThreadLocal
public static Map getMap(){
Map s = (Map) testInt.get();
if(s==null){
s=new HashMap();
testInt.set(s);
}
return s;
}
这样我们就能实现在调用这个方法的时候 获得的map是线程级别的。每一个线程都会获得一个单独的map。
8.volitile的工作原理?
定义:
java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。
在多个线程之间能够被共享的变量被称为共享变量。共享变量包括所有的实例变量,静态变量和数组元素。他们都被存放在堆内存中,volatile只作用于共享变量。
工作原理:
在汇编时 有volatile修饰符的变量将会被lock,这个lock做两件事:
1.将当前处理器缓存行的数据会写回到系统内存。
2.这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
你也可以理解为:volatile修饰的变量在修改时直接修改内存(越过缓存)
9.cas知道吗如何实现的?
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS全称Compare and swap 意思为比较和互换,使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值(比如 锁模式)。需要注意的一点是:CAS必须保证自己操作的原子性(可以使用synchronized确保原子性)。
class MyLock {
private boolean locked = false;
public synchronized boolean lock() {
if(!locked) {
locked = true;
return true;
}
return false;
}
}
现在CPU内部已经执行原子的CAS操作。Java5以来,你可以使用java.util.concurrent.atomic包中的一些原子类来使用CPU中的这些功能
public static class MyLock {
private AtomicBoolean locked = new AtomicBoolean(false);
public boolean lock() {
return locked.compareAndSet(false, true);
}
}
10.请用至少四种写法写一个单例模式?
四种写法:懒汉 饿汉 synchronized方法饿汉 synchronized代码块饿汉 双层判断synchronized饿汉 枚举
前两种线程不安全 懒汉直接写在定义变量中 饿汉在第一次使用时初始化
后三种线程安全 synchronized饿汉不够效率 synchronized代码块饿汉存在同时通过null判断的极端情况 双层判断synchronized饿汉基本认为是比较完美的解决方案 枚举就是单例 不常用
1. 饿汉:
缺点:不是线程安全的。
2. 懒汉:
优点:在类装载时实例化,避免了多线程不同步的问题。
缺点:可能没有人调用getInstance()方法也会被实例化,浪费了资源。
3. 静态内部类(对饿汉模式的改进):
跟饿汉不同的是,在Singleton类被装载的时候,不会实例化instance.
4. 双重校验锁(对懒汉模式的改进):
这样达到了线程安全的目的。
注:Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值