概述
1.常用设计模式
单例模式:懒汉式、饿汉式、双重校验锁、静态加载、内部加载类、枚举类加载;保证一个类仅有一个实例,并提供一个访问它的全局访问点
代理模式:动态代理和静态代理,什么时候使用动态代理?
适配器模式:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
装饰者模式:动态给类添加功能
观察者模式:有时候被称作发布/订阅模式,观察者模式定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己
策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换
外观模式:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口是的这一子系统更加容易使用。
命令模式:将一个请求封装成一个对象,从而使用户可以用不同的请求对客户进行参数化。
创建者模式:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示
抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
2.Java的基础类型:char、byte、short、float、int、double、long、boolean
类型 | 所占字节 | 范围 |
char | 16bits | ['u0000','uffff]or[0,65535] |
byte | 8bits | [-128,127] |
short | 16bits | [-32768,32767] |
float | 32bits | 32bit IEEE 754 floating-point |
int | 32bits | [-2147483648,2147483647] |
double | 64bits | 64bit IEEE 754floating-point |
long | 64bits | [-2^63,2^63-1] |
boolean |
3.set、list、map的区别
Set:不包含重复元素的子元素
List:有顺序的Collection,并且可以包含重复元素。
Map:可以把键(key)映射到值(value)的对象,键不能重复。
4.什么时候用HashMap
java中的HashMap是以键值对(key-value)的形式存储元素的。HashMap需要一个hash函数,它使用hashCode()和equals()方法来向集合、从集合添加和检索元素。当调用put()方法的时候,HashMap会计算key的hash值,然后把键值对存储在集合中合适的索引上。如果key已经存在了,value会被更新成新值。HashMap的一些重要的特性是它的容量(capacity),负载因子(load factor)和扩容极限(threshold resizing)。
5.什么时候用LinkedHashMap、ConcurrentHashMap、WeakHashMap?
6.哪些集合类是线程安全的?
vector
HashTable
7.为什么Set、List、map不能实现Cloneable和Serilizable?
克隆(cloning)或者是序列化(serialization)的语义和含义是跟具体的实现相关的。因此,应该由集合类的具体实现来决定如何被克隆或者是序列化。
8.Concurrenthashmap的实现1.7和1.8的实现?
1.7的实现:
数据结构:segment+hashEntry的方式进行实现
ConcurrentHashMap初始化时,计算出Segment数组的大小ssize和每个Segment中HashEntry数组的大小cap,并初始化Segment数组的第一个元素;其中ssize大小为2的幂次方,默认为16,cap大小也是2的幂次方,最小值为2,最终结果根据初始化容量initialCapacity进行计算,计算过程如下:
if
(c * ssize < initialCapacity)
++c;
int
cap = MIN_SEGMENT_TABLE_CAPACITY;
while
(cap < c)
cap <<=
1
;
其中Segment在实现上继承了ReentrantLock,这样就自带了锁的功能。
put的实现
当执行put方法插入数据时,根据key的hash值,在Segment数组中找到相应的位置,如果相应位置的Segment还未初始化,则通过CAS进行赋值,接着执行Segment对象的put方法通过加锁机制插入数据,实现如下:
场景:线程A和线程B同时执行相同Segment对象的put方法
1.线程A执行tryLock()方法成功获取锁,则把HashEntry对象插入到相应的位置;
2.线程B获取锁失败,则执行scanAndLockForPut()方法,在scanAndLockForPut方法中,会通过重复执行tryLock()方法尝试获取锁,在多处理器环境下,重复次数为64,单处理器重复次数为1,当执行tryLock()方法的次数超过上限时,则执行lock()方法挂起线程B;
3.当线程A执行完插入操作时,会通过unlock()方法释放锁,接着唤醒线程B继续执行;
size实现
因为ConcurrentHashMap是可以并发插入数据的,所以在准确计算元素时存在一定的难度,一般的思路是统计每个Segment对象中的元素个数,然后进行累加,但是这种方式计算出来的结果并不一样的准确的,因为在计算后面几个Segment的元素个数时,已经计算过的Segment同时可能有数据的插入或删除,在1.7的实现中,采用了如下方式:
try
{
for
(;;) {
if
(retries++ == RETRIES_BEFORE_LOCK) {
for
(
int
j =
0
; j < segments.length; ++j)
ensureSegment(j).lock();
// force creation
}
sum = 0L;
size =
0
;
overflow =
false
;
for
(
int
j =
0
; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if
(seg !=
null
) {
sum += seg.modCount;
int
c = seg.count;
if
(c <
0
|| (size += c) <
0
)
overflow =
true
;
}
}
if
(sum == last)
break
;
last = sum;
}
}
finally
{
if
(retries > RETRIES_BEFORE_LOCK) {
for
(
int
j =
0
; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
先采用不加锁的方式,连续计算元素的个数,最多计算3次
1、如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;
2、如果前后两次计算结果相同,则说明每个Segment进行加锁,再计算一次元素的个数;
1.8实现
数据结构
1.8放弃了Segment,用Node+CAS+Synchronized来保证并发安全进行实现:
只有在执行第一次put方法时才会调用initTable()初始化Node数组,实现如下:
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
put实现
当执行put方法插入数据时,根据key的hash值,在Node数组中找到相应的位置,实现如下:
1.如果相应位置的node还未初始化,则通过CAS插入相应的数据;
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
2.如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加Synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点。
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
3.如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
4.如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifBin方法转换为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
5.如果插入的一个是新节点,则执行addCount()方法尝试更新元素个数baseCount;
Size实现
1.8中使用一个volatile类型的变量baseCount记录元素的个数,当插入新数据或删除数据时,会通过addCount()方法更新baseCount,实现如下:
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
1.初始化时countercounterCells为空,在并发量很高时,如果存在两个线程同时执行CAS修改baseCount值,则失败的线程会继续执行方法体中的逻辑,使用CounterCell记录元素个数的变化;
2.如果CounterCell数组counterCells为空,调用fullAddCount()方法进行初始化,并插入对应的记录数,通过CAS设置cellsBusy字段,只有设置成功的线程才能初始化CounterCell数组,实现如下:
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
3.如果通过CAS设置cellsBusy字段失败的话,则继续尝试通过CAS修改baseCount字段,如果修改baseCount字段成功的话,就退出循环,否则继续循环插入CounterCell对象;
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
所以在1.8中size的实现比1.7中简单,因为元素保存在baseCount中,部分元素的变化个数存在CounterCell数组中,实现如下:
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
通过累加baseCount和CounterCeller数组中的数量,即可得到元素的总个数。
9.Array.sort的实现
先来看看Arrays.sort();
,一点进这个方法会看到是这样子的
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
- 果然没这么简单,
DualPivotQuicksort
翻译过来就是双轴快速排序,关于双轴排序可以去这里http://www.cnblogs.com/nullzx/p/5880191.html 看看。那再次点进去,可以发现有这么一段代码
if (right - left < QUICKSORT_THRESHOLD) {
sort(a, left, right, true);
return;
}
- 可以发现如果数组的长度小于
QUICKSORT_THRESHOLD
的话就会使用这个双轴快速排序,而这个值是286。
那如果大于286呢,它就会坚持数组的连续升序和连续降序性好不好,如果好的话就用归并排序,不好的话就用快速排序,看下面这段注释就可以看出
* The array is not highly structured,
* use Quicksort instead of merge sort.
- 那现在再回到上面的决定用双轴快速排序的方法上,再点进去,发现又会多一条判断
// Use insertion sort on tiny arrays
if (length < INSERTION_SORT_THRESHOLD)
- 即如果数组长度小于
INSERTION_SORT_THRESHOLD
(值为47)的话,那么就会用插入排序了,不然再用双轴快速排序。
所以总结一下Arrays.sort()方法,如果数组长度大于等于286且连续性好的话,就用归并排序,如果大于等于286且连续性不好的话就用双轴快速排序。如果长度小于286且大于等于47的话就用双轴快速排序,如果长度小于47的话就用插入排序。
10.什么时候用CopyOnWriteArrayList?
优点:
1.解决的开发工作中的多线程的并发问题。
缺点:
1.内存占有问题:很明显,两个数组同时驻扎在内存中,如果实际应用中,数据比较多,而且比较大的情况下,占用内存会比较大,针对这个其实可以用ConcurrentHashMap来代替。
2.数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器
11.volatile的使用
- 运算结果不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
- 变量不需要与其他的变量共同参与不变约束
12.synchronized的使用
https://blog.csdn.net/luoweifu/article/details/46613015
13.ReentrantLock的实现和Synchronized的区别
https://blog.csdn.net/chenchaofuck1/article/details/51045134
14.CAS原理以及问题
cas指令需要三个操作数,分别是内存位置(在Java中可以理解为变量的内存地址,用V表示),旧的预期值(用A表示)和新值(用B表示)
cas执行时当且仅当V符合旧预期值A时,处理器用新值B更新V的值,否则它就不执行更新,但无论是否执行了更新V的值,都会返回V的旧值,上述的处理是一个原子操作。
ABA问题:如果一个变量V出事读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那我们就说它的值没有被其他线程改变过吗?如果这段期间曾被改成了B,后来又被改成了A,那么CAS操作就会误认为重来没有被改变过。
15.AQS的实现原理
http://ifeve.com/java-special-troops-aqs/
16.接口和抽象类的区别
接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
类可以实现多个接口,但只能继承一个抽象类。
类可以不实现抽象类和接口声明的所有方法,当然,在这种情况下,类也必须得声明成抽象的。
抽象类可以在不提供接口方法实现的情况下实现接口。
Java接口声明的变量默认都是final的。抽象类可以包含非final的变量。
Java接口中的成员函数默认是public的。抽象类的成员函数可以是private,protected或者是public。
接口是绝对抽象的,不可以被实例化。抽象类也不可以被实例化,但是如果它包含了main方法可以被调用。
17.类加载机制的步骤,每一步做了神马?static和final修饰的成员变量的加载机制?
http://blog.51cto.com/tech4j2ee/630203
18.双亲委派模型
19.反射机制:反射动态擦除泛型,反射动态调用方法等
20.动态绑定:父类引用指向子类对象
21.JVM内存管理机制:有哪些区域,每个区域做了神马?
22.JVM垃圾回收机制:垃圾回收算法、垃圾回收器、垃圾回收策略
23.JVM参数设置和JVM调优
24.什么情况下产生年轻代内存溢出、什么情况下产生老年代内存溢出
25.mysql的基本操作,主从数据库一致性维护
https://blog.csdn.net/hangxing_2015/article/details/52585855
26.mysql优化策略
https://www.cnblogs.com/xuchenliang/p/6844093.html
27.mysql的索引实现,B+树的实现原理
https://blog.csdn.net/qq_23217629/article/details/52512041
28.什么情况索引不会命中,造成全表扫描?
29.java io的整体架构和使用的设计模式
30线程池的参数问题
31.乐观锁和悲观锁的实现
32.synchronized的实现原理
33.生产者消费者的实现代码
34.二分查找法:判断能否从数组中 找出两个数字和位给定值,随机生成1-10000不重复并放入数组,求数组的子数组的最大和。
35.Spring mvc的实现原理
https://www.cnblogs.com/xiaoxi/p/6164383.html
最后
以上就是落后可乐为你收集整理的高级Java面试通关知识点整理的全部内容,希望文章能够帮你解决高级Java面试通关知识点整理所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复