2019年华为JAVA面试题系列

面试发现经常有些重复的面试问题,自己也应该学会记录下来,最好自己能做成笔记,在下一次面的时候说得有条不紊,深入具体,面试官想必也很开心。以下是我个人总结,请参考:

HashSet底层原理:(问了大几率跟HashMap一起面)

HashMap底层原理:(非常大几率问到)

Hashtable底层原理:(问的少,问了大几率问你跟HashMap的区别)

synchronized底层如何实现?锁优化,怎么优化?

ReentrantLock 底层实现;

ConcurrentHashMap 的工作原理,底层原理(谈到多线程高并发大几率会问它)

JVM调优(JVM层层渐进问时大几率问)

JVM内存管理,JVM的常见的垃圾收集器,GC调优,Minor GC ,Full GC 触发条件(像是必考题)

java内存模型

线程池的工作原理(谈到多线程高并发大几率会问它)

扫描二维码关注公众号,回复: 10445441 查看本文章

ThreadLocal的底层原理(有时问)

voliate底层原理

NIO底层原理

IOC底层实现原理(Spring IOC ,AOP会问的两个原理,面试官经常会问看过源码吗?所以你有所准备吧)

AOP底层实现原理

MyisAM和innodb的有关索引的疑问(容易混淆,可以问的会深入)

HashSet底层原理:(面试过)
http://zhangshixi.iteye.com/blog/673143

https://blog.csdn.net/HD243608836/article/details/80214413

HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用null元素。

2.    HashSet的实现:

   对于HashSet而言,它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成, (实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。)

HashSet的源代码

对于HashSet中保存的对象,请注意正确重写其equals和hashCode方法,以保证放入的对象的唯一性。

插入
当有新值加入时,底层的HashMap会判断Key值是否存在(HashMap细节请移步深入理解HashMap),如果不存在,则插入新值,同时这个插入的细节会依照HashMap插入细节;如果存在就不插入

HashMap底层原理:

1.    HashMap概述:

   HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

2.    HashMap的数据结构:

HashMap实际上是一个“数组+链表+红黑树”的数据结构

3.    HashMap的存取实现:

(1.8之前的)

当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

1.8:

put():

根据key计算得到key.hash = (h = k.hashCode()) ^ (h >>> 16);

根据key.hash计算得到桶数组的索引index = key.hash & (table.length - 1),这样就找到该key的存放位置了:

①如果该位置没有数据,用该数据新生成一个节点保存新数据,返回null;

②如果该位置有数据是一个红黑树,那么执行相应的插入 / 更新操作

③如果该位置有数据是一个链表,分两种情况一是该链表没有这个节点,另一个是该链表上有这个节点,注意这里判断的依据是key.hash是否一样:如果该链表没有这个节点,那么采用尾插法新增节点保存新数据,返回null;如果该链表已经有这个节点了,那么找到該节点并更新新数据,返回老数据。注意: HashMap的put会返回key的上一次保存的数据。

get():

计算需获取数据的hash值(计算过程跟put一样),计算存放在数组table中的位置(计算过程跟put一样),然后依次在数组,红黑树,链表中查找(通过equals()判断),最后再判断获取的数据是否为空,若为空返回null否则返回该数据

树化与还原

哈希表的最小树形化容量

当哈希表中的容量大于这个值时(64),表中的桶才能进行树形化

否则桶内元素太多时会扩容,而不是树形化

为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD

一个桶的树化阈值

当桶中元素个数超过这个值时(8),需要使用红黑树节点替换链表节点

这个值必须为 8,要不然频繁转换效率也不高

一个树的链表还原阈值

当扩容时,桶中元素个数小于这个值(6),就会把树形的桶元素还原(切分)为链表结构

这个值应该比上面那个小,至少为 6,避免频繁转换

条件1. 如果当前桶数组为null或者桶数组的长度 < MIN_TREEIFY_CAPACITY(64),则进行扩容处理(见代码片段2:resize());

条件2. 当不满足条件1的时候则将桶中链表内的元素转换成红黑树!!!稍后再详细讨论红黑树。

扩容机制的实现

扩容(resize)就是重新计算容量。当向HashMap对象里不停的添加元素,而HashMap对象内部的桶数组无法装载更多的元素时,HashMap对象就需要扩大桶数组的长度,以便能装入更多的元素。

capacity 就是数组的长度/大小,loadFactor 是这个数组填满程度的最大比比例。 

size表示当前HashMap中已经储存的Node<key,value>的数量,包括桶数组和链表 / 红黑树中的的Node<key,value>。

threshold表示扩容的临界值,如果size大于这个值,则必需调用resize()方法进行扩容。 

在jdk1.7及以前,threshold = capacity * loadFactor,其中 capacity 为桶数组的长度。这里需要说明一点,默认负载因子0.75是是对空间和时间(纵向横向)效率的一个平衡选择,建议大家不要修改。 jdk1.8对threshold值进行了改进,通过一系列位移操作算法最后得到一个power of two size的值

什么时候扩容

当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值---即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。

扩容必须满足两个条件:

1、存放新值的时候   当前已有元素的个数  (size) 必须大于等于阈值

2、存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)

//如果计算的哈希位置有值(及hash冲突),且key值一样,则覆盖原值value,并返回原值value

      if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

        V oldValue = e.value;

        e.value = value;

        e.recordAccess(this);

        return oldValue;

      }

resize()方法:该函数有2种使用情况1.初始化哈希表 2.当前数组容量过小,需扩容

过程:

插入键值对时发现容量不足,调用resize()方法方法,

1.首先进行异常情况的判断,如是否需要初始化,二是若当前容量》最大值则不扩容,

2.然后根据新容量(是就容量的2倍)新建数组,将旧数组上的数据(键值对)转移到新的数组中,这里包括:(遍历旧数组的每个元素,重新计算每个数据在数组中的存放位置(原位置或者原位置+旧容量),将旧数组上的每个数据逐个转移到新数组中,这里采用的是尾插法。)

3.新数组table引用到HashMap的table属性上

4.最后重新设置扩容阙值,此时哈希表table=扩容后(2倍)&转移了旧数据的新table

synchronized底层如何实现?锁优化,怎么优化?

synchronized 是 Java 内建的同步机制,所以也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。

原理:

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

底层实现:

同步代码块是使用monitorenter和monitorexit指令实现的,,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁; 

同步方法(在这看不出来需要看JVM底层实现)依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。  synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示 Klass 做为锁对象。

Java对象头和monitor是实现synchronized的基础!

synchronized存放的位置:

synchronized用的锁是存在Java对象头里的。

其中, Java对象头包括: 

Mark Word(标记字段):用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。它是实现轻量级锁和偏向锁的关键

Klass Pointer(类型指针):是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

monitor:  可以把它理解为一个同步工具,它通常被描述为一个对象。 是线程私有的数据结构

锁优化,怎么优化?

jdk1.6对锁的实现引入了大量的优化。锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。重量级锁降级发生于STW阶段,降级对象为仅仅能被VMThread访问而没有其他JavaThread访问的对象。( HotSpot JVM/JRockit JVM是支持锁降级的)

偏斜锁:

当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。

自旋锁:

自旋锁 for(;;)结合cas确保线程获取取锁

就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。

轻量级锁:

引入偏向锁主要目的是:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁

重量级锁:

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

ReentrantLock 底层实现

https://blog.csdn.net/u011202334/article/details/73188404

AQS原理:

AQS和Condition各自维护了不同的队列,在使用lock和condition的时候,其实就是两个队列的互相移动。如果我们想自定义一个同步器,可以实现AQS。它提供了获取共享锁和互斥锁的方式,都是基于对state操作而言的。

概念+实现:

ReentrantLock实现了Lock接口,是AQS( 一个用来构建锁和同步工具的框架, AQS没有锁之类的概念)的一种。加锁和解锁都需要显式写出,注意一定要在适当时候unlock。ReentranLock这个是可重入的。其实要弄明白它为啥可重入的呢,咋实现的呢。其实它内部自定义了同步器Sync,这个又实现了AQS,同时又实现了AOS,而后者就提供了一种互斥锁持有的方式。其实就是每次获取锁的时候,看下当前维护的那个线程和当前请求的线程是否一样,一样就可重入了。

和synhronized相比:

synchronized相比,ReentrantLock用起来会复杂一些。在基本的加锁和解锁上,两者是一样的,所以无特殊情况下,推荐使用synchronized。ReentrantLock的优势在于它更灵活、更强大,增加了轮训、超时、中断等高级功能。

可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。

可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则z,dz提供了中断功能。

公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。 

lock()和unlock()是怎么实现的呢?

由lock()和unlock的源码可以看到,它们只是分别调用了sync对象的lock()和release(1)方法。而  Sync是ReentrantLock的内部类, 其扩展了AbstractQueuedSynchronizer。

lock():

final void lock() {

if (compareAndSetState(0, 1))

setExclusiveOwnerThread(Thread.currentThread());

else

acquire(1);

}

首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能乖乖的去排队啦。( “非公平”即体现在这里)。

设置state失败,走到了else里面。我们往下看acquire。

第一步。尝试去获取锁。如果尝试获取锁成功,方法直接返回。

2. 第二步,入队。(自旋+CAS组合来实现非阻塞的原子操作)

3. 第三步,挂起。让已经入队的线程尝试获取锁,若失败则会被挂起

public final void acquire(int arg) {

if (!tryAcquire(arg) &&

    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

    selfInterrupt();

}

unlock():
流程大致为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,

如果释放失败那么返回false表示解锁失败。这里我们也发现了,每次都只唤起头结点的下一个节点关联的线程。

public void unlock() {

sync.release(1);

}

public final boolean release(int arg) {

if (tryRelease(arg)) {

Node h = head;

if (h != null && h.waitStatus != 0)

unparkSuccessor(h);

return true;

}

return false;

}

ConcurrentHashMap 的工作原理

概念:

ConcurrentHashMap的目标是实现支持高并发、高吞吐量的线程安全的HashMap。

1.8之前:

数据结构:

ConcurrentHashMap是由Segment数组结构和多个HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap相似,是一种数组和链表结构,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中: ConcurrentHashMap中默认是把segments初始化为长度为16的数组

https://www.cnblogs.com/wuzhitong/p/8492228.html

1.8后:

变化:

ConcurrentHashMap的JDK8与JDK7版本的并发实现相比,最大的区别在于JDK8的锁粒度更细,理想情况下talbe数组元素的大小就是其支持并发的最大个数

实现:

改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

数据结构:

改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。

概念:

JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。

树化和还原:

与HashMap一样。

一些成员:

Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据。,就是一个链表,但是只允许对数据进行查找,不允许进行修改

通过TreeNode作为存储结构代替Node来转换成黑红树。

TreeBin
TreeBin就是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制

 // 读写锁状态

    static final int WRITER = 1; // 获取写锁的状态

    static final int WAITER = 2; // 等待写锁的状态

    static final int READER = 4; // 增加数据时读锁的状态

 

构造器

public ConcurrentHashMap() {

} 初始化其实是一个空实现,初始化操作并不是在构造函数实现的,而是在put操作中实现。还提供了其他的构造函数,有指定容量大小或者指定负载因子,跟HashMap一样。

存取实现:

put():对当前的table进行无条件自循环直到put成功

如果没有初始化就先调用initTable()方法来进行初始化过程

如果没有hash冲突就直接CAS插入

如果还在进行扩容操作就先进行扩容

如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,

最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环

如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容。

get()

计算hash值,定位到该table索引位置,如果是首节点符合就返回

如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回

以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null

概括版:

(1)对于get读操作,如果当前节点有数据,还没迁移完成,此时不影响读,能够正常进行。 

如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时get线程会帮助扩容。 

(2)对于put/remove写操作,如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时写线程会帮助扩容,如果扩容没有完成,当前链表的头节点会被锁住,所以写线程会被阻塞,直到扩容完成。 

扩容机制:https://www.e-learn.cn/content/java/1154828

引入了一个ForwardingNode类,在一个线程发起扩容的时候,就会改变sizeCtl这个值,

sizeCtl :默认为0,用来控制table的初始化和扩容操作,具体应用在后续会体现出来。  

-1 代表table正在初始化  

-N 表示有N-1个线程正在进行扩容操作  。

扩容时候会判断这个值,

如果超过阈值就要扩容,首先根据运算得到需要遍历的次数i,然后利用tabAt方法获得i位置的元素f,初始化一个forwardNode实例fwd,如果f == null,则在table中的i位置放入fwd,

否则采用头插法的方式把当前旧table数组的指定任务范围的数据给迁移到新的数组中,

然后 

给旧table原位置赋值fwd。直到遍历过所有的节点以后就完成了复制工作,把table指向nextTable,并更新sizeCtl为新数组大小的0.75倍,扩容完成。在此期间如果其他线程的有读写操作都会判断head节点是否为forwardNode节点,如果是就帮助扩容。 

Hashtable底层原理:

概念:

HashTable类继承自Dictionary类,实现了Map接口。 大部分的操作都是通过synchronized锁保护的,是线程安全的, key、value都不可以为null, 每次put方法不允许null值,如果发现是null,则直接抛出异常。

官方文档也说了:如果在非线程安全的情况下使用,建议使用HashMap替换,如果在线程安全的情况下使用,建议使用ConcurrentHashMap替换。

数据结构:

数组+链表。

存取实现:

put():

限制了value不能为null。

由于直接使用key.hashcode(),而没有向hashmap一样先判断key是否为null,所以key为null时,调用key.hashcode()会出错,所以hashtable中key也不能为null。

Hashtable是在链表的头部添加元素的。

 int index = (hash & 0x7FFFFFFF) %tab.length;获取index的方式与HashMap不同

扩容机制:

Hashtable默认capacity是11,默认负载因子是0.75.。当前表中的Entry数量,如果超过了阈值,就会扩容,即调用rehash方法,重新计算每个键值对的hashCode;

 判断新的容量是否超过了上限,没超过就新建一个新数组,大小为原数组的2倍+1,将旧数的键值对重新hash添加到新数组中。

JVM调优

查看堆空间大小分配(年轻代、年老代、持久代分配)

垃圾回收监控(长时间监控回收情况)

线程信息监控:系统线程数量

线程状态监控:各个线程都处在什么样的状态下

线程详细信息:查看线程内部运行情况,死锁检查

CPU热点:检查系统哪些方法占用了大量CPU时间

内存热点:检查哪些对象在系统中数量最大

jvm问题排查和调优:

jps主要用来输出JVM中运行的进程状态信息。

jstat命令可以用于持续观察虚拟机内存中各个分区的使用率以及GC的统计数据

jmap可以用来查看堆内存的使用详情。

jstack可以用来查看Java进程内的线程堆栈信息。 jstack是个非常好用的工具,结合应用日志可以迅速定位到问题线程。

Java性能分析工具
jdk会自带JMC(JavaMissionControl)工具。可以分析本地应用以及连接远程ip使用。提供了实时分析线程、内存,CPU、GC等信息的可视化界面。

JVM内存调优

对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量。

使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。

导致Full GC一般由于以下几种情况:

旧生代空间不足

调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象

新生代设置过小

一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入旧生代,占据了旧生代剩余空间,诱发Full GC 

2). 新生代设置过大

一是新生代设置过大会导致旧生代过小(堆总量一定),从而诱发Full GC;二是新生代GC耗时大幅度增加

3). Survivor设置过小

导致对象从eden直接到达旧生代

4). Survivor设置过大

导致eden过小,增加了GC频率

一般说来新生代占整个堆1/3比较合适

GC策略的设置方式

1). 吞吐量优先可由-XX:GCTimeRatio=n来设置

2). 暂停时间优先可由-XX:MaxGCPauseRatio=n来设置

JVM内存管理:

1.先讲内存5大模块以及他们各种的作用。

2.将垃圾收集器,垃圾收集算法

3.适当讲讲GC优化,JVM优化

JVM的常见的垃圾收集器:

(注:此回答源于杨晓峰的Java核心技术36讲之一)

GC调优:

GC日志分析

调优命令

调优工具

调优命令

Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo

jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。

jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。

jmap,JVM Memory Map命令用于生成heap dump文件

jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看

jstack,用于生成java虚拟机当前时刻的线程快照。

jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。

调优工具

常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。

jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控

GC触发的条件有两种。(1)程序调用System.gc时可以触发;(2)系统自身来决定GC触发的时机。

要完全回收一个对象,至少需要经过两次标记的过程。

第一次标记:对于一个没有其他引用的对象,筛选该对象是否有必要执行finalize()方法,如果没有执行必要,则意味可直接回收。(筛选依据:是否复写或执行过finalize()方法;因为finalize方法只能被执行一次)。

第二次标记:如果被筛选判定位有必要执行,则会放入FQueue队列,并自动创建一个低优先级的finalize线程来执行释放操作。如果在一个对象释放前被其他对象引用,则该对象会被移除FQueue队列。

Minor GC ,Full GC 触发条件

Minor GC触发条件:当Eden区满时,触发Minor GC。

Full GC触发条件:

(1)调用System.gc时,系统建议执行Full GC,但是不必然执行

(2)老年代空间不足

(3)方法区空间不足

(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

java内存模型

与JVM 内存模型不同。

Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。

Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步。

Java线程之间的通信采用的是过共享内存模型,这里提到的共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。

线程池的工作原理

1.先讲下作用

减少资源的开销    可以减少每次创建销毁线程的开销

 提高响应速度    由于线程已经创建成功

提高线程的可管理性   

2.讲实现

线程池主要有两部分组成,多个工作线程和一个阻塞队列。

其中工作线程是一组已经处在运行中的线程,它们不断地向阻塞队列中领取任务执行。而阻塞队列用于存储工作线程来不及处理的任务。

3.细分讲下线程的组成

创建一个线程池需要要的一些核心参数。

corePoolSize:基本线程数量它表示你希望线程池达到的一个值。线程池会尽量把实际线程数量保持在这个值上下。 

maximumPoolSize:最大线程数量这是线程数量的上界。如果实际线程数量达到这个值:阻塞队列未满:任务存入阻塞队列等待执行阻塞队列已满:调用饱和策略。

keepAliveTime:空闲线程的存活时间当实际线程数量超过corePoolSize时,若线程空闲的时间超过该值,就会被停止。 PS:当任务很多,且任务执行时间很短的情况下,可以将该值调大,提高线程利用率。 

timeUnit:keepAliveTime的单位

runnableTaskQueue:任务队列 

这是一个存放任务的阻塞队列,可以有如下几种选择:

ArrayBlockingQueue 它是一个由数组实现的阻塞队列,FIFO。 

LinkedBlockingQueue 它是一个由链表实现的阻塞队列,FIFO。吞吐量通常要高于ArrayBlockingQueue。fixedThreadPool使用的阻塞队列就是它。它是一个无界队列。 

SynchronousQueue 它是一个没有存储空间的阻塞队列,任务提交给它之后必须要交给一条工作线程处理;如果当前没有空闲的工作线程,则立即创建一条新的工作线程。 cachedThreadPool用的阻塞队列就是它。它是一个无界队列。 PriorityBlockingQueue 它是一个优先权阻塞队列。

handler:饱和策略当实际线程数达到maximumPoolSize,并且阻塞队列已满时,就会调用饱和策略。

AbortPolicy 默认。直接抛异常。 CallerRunsPolicy 只用调用者所在的线程执行任务。 DiscardOldestPolicy 丢弃任务队列中最久的任务。 DiscardPolicy 丢弃当前任务。

4.运行机制

当有请求到来时: 

1.若当前实际线程数量少于 corePoolSize,即使有空闲线程,也会创建一个新的工作线程;

2 若当前实际线程数量处于corePoolSize和maximumPoolSize之间,并且阻塞队列没满,则任务将被放入阻塞队列中等待执行; 

3.若当前实际线程数量小于 maximumPoolSize,但阻塞队列已满,则直接创建新线程处理任务; 

4.若当前实际线程数量已经达到maximumPoolSize,并且阻塞队列已满,则使用饱和策略。

ThreadLocal的底层原理

概括:

该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量

使用:

set(obj):向当前线程中存储数据 get():获取当前线程中的数据 remove():删除当前线程中的数据

实现原理:

ThreadLocal并不维护ThreadLocalMap(ThreadLocalMap是Thread的)并不是一个存储数据的容器,它只是相当于一个工具包,提供了操作该容器的方法,如get、set、remove等。而ThreadLocal内部类ThreadLocalMap才是存储数据的容器,并且该容器由Thread维护。每一个Thread对象均含有一个ThreadLocalMap类型的成员变量threadLocals,它存储本线程中所有ThreadLocal对象及其对应的值( ThreadLocalMap 是个弱引用类,内部一个Entry由ThreadLocal对象和Object构成,

为什么要用弱引用呢?

如果是直接new一个对象的话,使用完之后设置为null后才能被垃圾收集器清理,如果为弱引用,使用完后垃圾收集器自动清理key,程序员不用再关注指针。

操作细节

进行set,get等操作都是首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key ,再做相应的处理。

内存泄露问题

在ThreadLocalMap中,只有key是弱引用,value仍然是一个强引用。

每次操作set、get、remove操作时,ThreadLocal都会将key为null的Entry删除,从而避免内存泄漏。

当然,当如果一个线程运行周期较长,而且将一个大对象放入LocalThreadMap后便不再调用set、get、remove方法,此时该仍然可能会导致内存泄漏。这个问题确实存在,没办法通过ThreadLocal解决,而是需要程序员在完成ThreadLocal的使用后要养成手动调用remove的习惯,从而避免内存泄漏。

使用场景;

Web系统Session的存储 

当请求到来时,可以将当前Session信息存储在ThreadLocal中,在请求处理过程中可以随时使用Session信息,每个请求之间的Session信息互不影响。当请求处理完成后通过remove方法将当前Session信息清除即可。 

voliate 的实现原理

为什么volatile能保证共享变量的内存可见性?

volatile变量写 

当被volatile修饰的变量进行写操作时,这个变量将会被直接写入共享内存,而非线程的专属存储空间。 

volatile变量读 

当读取一个被volatile修饰的变量时,会直接从共享内存中读,而非线程专属的存储空间中读。

禁止指令重排序

volatile读 

若volatile读操作的前一行为volatile读/写,则这两行不会发生重排序 volatile读操作和它后一行代码都不会发生重排序 

volatile写 

volatile写操作和它前一行代码都不会发生重排序; 若volatile写操作的后一行代码为volatile读/写,则这两行不会发生重排序。

当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。

NIO底层原理

1概念:

NIO 指新IO,核心是同步非阻塞,解决传统IO的阻塞问题。操作对象是Buffer。其实NIO的核心是IO线程池,(一定要记住这个关键点)。 NIO中的IO多路复用调用系统级别的select和poll模型,由系统进行监控IO状态,避免用户线程通过反复尝试的方式查询状态。

Java NIO :同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

2.工作原理:

由一个专门的线程来处理所有的 IO 事件,并负责分发。

事件驱动机制:事件到的时候触发,而不是同步的去监视事件。

线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。

3.通信模型是怎么实现的呢?

java NIO采用了双向通道(channel)进行数据传输,而不是单向的流(stream),在通道上可以注册我们感兴趣的事件。

四种事件

服务端接收客户端连接事件SelectionKey.OP_ACCEPT(16)

客户端连接服务端事件SelectionKey.OP_CONNECT(8)

读事件SelectionKey.OP_READ(1)

写事件SelectionKey.OP_WRITE(4)

服务端和客户端各自维护一个管理通道的对象,我们称之为selector,该对象能检测一个或多个通道 (channel) 上的事件。我们以服务端为例,如果服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,阻塞I/O这时会调用read()方法阻塞地读取数据,而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。

IOC底层实现原理

概念:

IOC 是面向对象编程中的一种设计原则,IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。。所谓IoC,对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系。是说创建对象的控制权进行转移,以前创建对象的主动权和创建时机是由自己把控的,而现在这种权力转移到第三方。

实现原理:

它是通过反射机制+工厂模式实现的,在实例化一个类时,它通过反射调用类中set方法将事先保存在HashMap中的类属性注入到类中。

控制反转就是:获得依赖对象的方式反转了。

1、依赖注入发生的时间

(1).用户第一次通过getBean方法向IoC容索要Bean时,IoC容器触发依赖注入。

(2).当用户在Bean定义资源中为元素配置了lazy-init属性,即让容器在解析注册Bean定义时进行预实例化,触发依赖注入。

2.依赖注入实现在以下两个方法中:

(1).createBeanInstance:生成Bean所包含的java对象实例。

(2).populateBean :对Bean属性的依赖注入进行处理。

AOP底层实现原理

概念

AOP(Aspect-OrientedProgramming,面向方面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。  而AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。 

AOP的核心思想就是“将应用程序中的商业逻辑同对其提供支持的通用服务进行分离。

AOP的实现

实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。

如何使用Spring AOP 

可以通过配置文件或者编程的方式来使用Spring AOP。   配置可以通过xml文件来进行,大概有四种方式: 

       配置ProxyFactoryBean,显式地设置advisors, advice, target等 

2.        配置AutoProxyCreator,这种方式下,还是如以前一样使用定义的bean,但是从容器中获得的其实已经是代理对象 

3.        通过来配置 

4.        通过来配置,使用AspectJ的注解来标识通知及切入点

Spring AOP的实现

如何生成代理类:

Spring提供了两种方式来生成代理对象: JDKProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。默认的策略是如果目标类是接口,则使用JDK动态代理技术,否则使用Cglib来生成代理 

切面是如何织入的?

InvocationHandler是JDK动态代理的核心,生成的代理对象的方法调用都会委托到InvocationHandler.invoke()方法。

MyisAM和innodb的有关索引的疑问

两者都是什么索引?聚集还是非聚集https://www.cnblogs.com/olinux/p/5217186.html

MyISAM(非聚集)

使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。

MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。

InnoDB(聚集索引)

第一个重大区别是InnoDB的数据文件本身就是索引文件,这棵树的叶节点data域保存了完整的数据记录。

但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。

因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。

简单说:

如果我们定义了主键(PRIMARY KEY),那么InnoDB会选择其作为聚集索引;如果没有显式定义主键,则InnoDB会选择第一个不包含有NULL值的唯一索引作为主键索引

关于面试准备

  先推荐一个写的不错的博客,专门关于面试的,比较详尽仔细:关于面试。我在这里简单总结几点:

  1、简历要用心准备好,个人信息,特别是联系方式一定要清晰明确,自身掌握的技能要完成清晰,项目经历最好按照时间顺序,说明本人在项目中的职责,完成的工作,有什么样的提升或收获;

  2、一般面试流程是电面=HR现场面=》技术面=》结果,并不是每一个面试结果就能立马有结果,所以当面试官说回去等消息的时候,并不代表没有机会,有时候需要讨论筛选才能最终确定人选。

  3、关于自我介绍,最好简明扼要,能体现自身的特点,表达流畅、自信,提前最好准备;

  4、准备好扎实的基础知识,以及对经历过的项目要有足够的认识,每一个项目都是一次学习、提升的机会,一般JAVA集合类是考察的重点;

  5、一般好一点的面试官会顺着知识点逐渐深入或者逐渐扩展,所以对于知识点的掌握最好全面深入,不要走马观花式的学习;

  6、当遇到一些设计类的问题时,一般面试官考察的是你的思路,对问题的应变能力,对于事物观察的点;

JAVA基础(答案仅供参考,如有不对之处请批评指正)

  1HashMap源码,实现原理,JDK8以后对HashMap做了怎样的优化。

  答:HashMap是基于哈希表的Map接口的非同步实现,提供所有可选的映射操作,并允许使用null值和null键,不保证映射的顺序;HashMap是一个链表散列的数据结构,即数组和链表的结合体;它的底层就是一个数组结构,数组中的每一项又是一个链表,每当新建一个HashMap时,就会初始化一个数组;

可参考博客:彻底搞懂JAVA集合HashMap,HashTable,ConcurrentHashMap之关联

  而在JDK8中引入了红黑树的部分,当存入到数组中的链表长度大于(默认)8时,即转为红黑树;利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。本文不再对红黑树展开讨论,想了解更多红黑树数据结构的工作原理可以参考http://blog.csdn.net/v_july_v/article/details/6105630

https://images2018.cnblogs.com/blog/1073968/201805/1073968-20180509120713650-389970359.png

可参考博客:JAVA8系列之重新认识HashMap

   2HashMap的扩容是怎样扩容的,为什么都是2N次幂的大小。

  答:可以参考上文  JAVA8系列之重新认识HashMap  有详细的讲解

  3HashMapHashTableConcurrentHashMap的区别

  答:   

  aHashMap是非线程安全的,HashTable是线程安全的。

  bHashMap的键和值都允许有null值存在,而HashTable则不行。

  c、因为线程安全的问题,HashMap效率比HashTable的要高。

  HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为nullHashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 CollectionssynchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap

   HashtableHashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。

  4、极高并发下HashTableConcurrentHashMap哪个性能更好,为什么,如何实现的。

  答:当然是ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁,而HashTable则使用的是方法级别的锁;因此在新版本中一般不建议使用HashTable,不需要线程安全的场合可以使用HashMap,而需要线程安全的场合可以使用ConcurrentHashMap

  5HashMap在高并发下如果没有处理线程安全会有怎样的隐患,具体表现是什么。

  答:可能造成死循环,具体表现链表的循环指向;

  6JAVA中四种修饰符的限制范围。

  private:修饰的成员只能在同类中别访问,而在同包、子类和其他包中都不能被访问

  public:修饰的成员在同类、同包、子类(继承自本类)、其他包都可以访问

  protected:修饰的成员在同类、同包、子类中可以访问,其他包中不能被访问

  default:修饰的成员在同类、同包中可以访问,但其他包中不管是不是子类都不能被访问

  7Object中的方法

  构造函数

  hashCode():用户获取对象的hash值,用于检索

  queals():用于确认两个对象是否相等;补充,哈希值相同的对象不一定equals(),但equals()的两个对象,hash值一定相等

  toString():返回一个String对象,用来标识自己

  getClass():返回一个class对象,打印的格式一般为  class package.name.xxx,经常用于java的反射机制

  clone():用来另存一个当前存在的对象

  finalize():垃圾回收的时候回用到,匿名对象回收之前会调用到

  wait():用于让当前线程失去操作权限,当前线程进入等待序列

  wait(long)wait(long,int):用户设定下一次获取锁的距离当前释放锁的间隔时间

  notify():用于随机通知一个持有对象锁的线程获取操作的权限

  notifyAll():用于通知所有持有对象锁的线程获取操作权限

  8、接口和抽象类的区别 

  答:一个类可以实现多个接口,但只能继承一个抽象类;抽象类可以包含具体的方法,接口所有的方法都是抽象的(JDK8开始新增功能接口中有default方法);抽象类可以声明和使用字段,接口则不能,但可以创建静态的final常量;抽象类的方法可以是protectedpublicprivate或者默认的package,接口的方法都是public;抽象类可以定义构造函数,接口不能;接口被声明为public,省略后,包外的类不能访问接口;

  9、动态代理的两种方式,以及区别

  答:jdk动态代理和cglib动态代理;

  JDK动态代理只能对实现了接口的类生成代理,而不能针对类;cglib是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,因为是继承,所以该类或方法最好不要声明称finalfinal可以阻止继承和多态;

  10java序列化的方式

  答:实现Serializable接口、实现Externalizable接口(一般只希望序列化一部分数据,其他数据都使用transient修饰的话有点麻烦,这时候可以使用externalizable接口,指定序列化的属性)

  11、传值和传引用的区别,java是怎么样的,有没有传值传引用

  答:首先,java中是没有指针的,只存在值传递;而我们经常看到对于对象的传递似乎有点像引用传递,可以改变对象中的某个属性的值,请不要被这个假象蒙蔽了双眼,实际上这个传入函数的值是对象引用的拷贝,即传递的是引用的地址值,所以还是按值传递;

  传值调用时,改变的是形参的值,并没有改变实参的值,实参的值可以传递给形参,但是这个传递是单向的,形参不能传递会实参;

  传引用调用时,如果参数是对象,无论是对象做了何种操作,都不会改变实参对象的引用,但是如果改变了对象的内容,就会改变实参对象的内容;

  12@transactional注解在什么情况下会失效,为什么。

  答:一个目标对象的方法调用改目标对象的另外一个方法时,即使被调用的方法已使用了@Transactional注解标记,事务也不会有效执行;Spring的官方说明在代理下(默认或者配置为proxy-targer-class="true"),只有当前代理类的外部方法调用注解方法时代理才会被拦截。

数据结构和算法

  1B+

  参考:B+树介绍

  2、八大排序算法

  参考:八大排序算法JAVA实现

  3、一致性Hash算法,一致性Hash算法的应用

  答:一致性hash算法是一个负载均衡算法,可以用在分布式缓存、数据库的分库分表等场景,还可以应用在负载均衡器中作为负载均衡算法。在多台服务器时,对于某个请求资源通过hash算法,映射到某一台服务器,当增加或者减少一台服务器时,可能会改变这些资源对应的hash值,这样可能导致一部分缓存或者数据的丢失。一致性hash就是尽可能在将同一个资源请求到同一台服务器中;

JVM

  1JVM的内存结构

  答:主要分为三大块堆内存、方法区、栈;栈又分为JVM栈、本地方法栈

    堆(heap space),堆内存是JVM中最大的一块,有年轻代和老年代组成,而年轻代又分为三分部分,Eden区,From SurvivorTo Survivor,默认情况下按照8:1:1来分配

    方法区(Method area),存储类信息、常量、静态变量等数据,是线程共享的区域

    程序计数器(Program counter Register),是一块较小的内存空间,是当前线程所执行的字节码的行号指示器

    JVM栈(JVM stacks),也是线程私有的,生命周期与线程相同,每个方法被执行时都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息

    本地方法栈(Native Mthod Stacks,为虚拟机使用的native方法服务

  2、关于垃圾回收和常见的GC算法,请参考:GC专家系列-理解java垃圾回收

多线程

  1JAVA实现多线程的几种方式

  a、继承Thread类实现

public class MyThread extends Thread { 

  public void run() { 

   System.out.println("MyThread.run()"); 

  } 

MyThread myThread1 = new MyThread(); 

MyThread myThread2 = new MyThread(); 

myThread1.start(); 

myThread2.start();

  b、实现Runnable接口

  如果自己的类已经extends另一个类,就无法直接extends Thread,此时,必须实现一个Runnable接口,如下:

public class MyThread extends OtherClass implements Runnable { 

  public void run() { 

   System.out.println("MyThread.run()"); 

  } 

MyThread myThread = new MyThread(); 

Thread thread = new Thread(myThread); 

thread.start();

  c、使用ExecutorService、Callable、Future实现有返回结果的多线程

import java.util.concurrent.*; 

import java.util.Date; 

import java.util.List; 

import java.util.ArrayList; 

/**

* 有返回值的线程

*/ 

@SuppressWarnings("unchecked") 

public class Test { 

public static void main(String[] args) throws ExecutionException, 

    InterruptedException { 

   System.out.println("----程序开始运行----"); 

   Date date1 = new Date(); 

   int taskSize = 5; 

   // 创建一个线程池 

   ExecutorService pool = Executors.newFixedThreadPool(taskSize); 

   // 创建多个有返回值的任务 

   List<Future> list = new ArrayList<Future>(); 

   for (int i = 0; i < taskSize; i++) { 

    Callable c = new MyCallable(i + " "); 

    // 执行任务并获取Future对象 

    Future f = pool.submit(c); 

    // System.out.println(">>>" + f.get().toString()); 

    list.add(f); 

   } 

   // 关闭线程池 

   pool.shutdown(); 

   // 获取所有并发任务的运行结果 

   for (Future f : list) { 

    // 从Future对象上获取任务的返回值,并输出到控制台 

    System.out.println(">>>" + f.get().toString()); 

   } 

   Date date2 = new Date(); 

   System.out.println("----程序结束运行----,程序运行时间【" 

     + (date2.getTime() - date1.getTime()) + "毫秒】"); 

class MyCallable implements Callable<Object> { 

private String taskNum; 

MyCallable(String taskNum) { 

   this.taskNum = taskNum; 

public Object call() throws Exception { 

   System.out.println(">>>" + taskNum + "任务启动"); 

   Date dateTmp1 = new Date(); 

   Thread.sleep(1000); 

   Date dateTmp2 = new Date(); 

   long time = dateTmp2.getTime() - dateTmp1.getTime(); 

   System.out.println(">>>" + taskNum + "任务终止"); 

   return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】"; 

}

   2CallableFuture

  答:Callable接口类似于Runnable,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable更强大,被线程执行以后,可以返回值,这个返回值就是通过Future拿到,也就是说,Future可以拿到异步执行任务的返回值,可以看以下例子: 

import java.util.Random;

import java.util.concurrent.Callable;

import java.util.concurrent.FutureTask;

public class Test {

    public static void main(String[] args) {

        Callable<Integer> callable = new Callable<Integer>() {

            @Override

            public Integer call() throws Exception {

                return new Random().nextInt(100);

            }       

        };

        FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);

        new Thread(futureTask).start();

        try {

            Thread.sleep(1000);

            System.err.println(futureTask.get());

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

}

ExecutorService继承自Executor,目的是为我们管理Thread对象,从而简化并发变成,Executor使我们无需显示的去管理线程的声明周期,是JDK5之后启动任务的首选方式。

执行多个带返回值的任务,并取得多个返回值,代码如下:

import java.util.concurrent.Callable;

import java.util.concurrent.CompletionService;

import java.util.concurrent.ExecutorCompletionService;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

public class CallableAndFuture {

    public static void main(String[] args) {

        ExecutorService threadPool = Executors.newCachedThreadPool();

        CompletionService<Integer> cs = new ExecutorCompletionService<Integer>(threadPool);

        for( int i = 0; i < 5; i++ ){

            final int taskId = i;

            cs.submit(new Callable<Integer>() {

                @Override

                public Integer call() throws Exception {

                    return taskId;

                }

            });

        }

        for( int i = 0; i < 5; i++ ){

            try {

                System.err.println(cs.take().get());

            } catch (Exception e) {

                e.printStackTrace();

            }

        }

    }

}

  3、线程池的参数有哪些,在线程池创建一个线程的过程

  corePoolSize:核心线程数,能够同时执行的任务数量

  maximumPoolSize:除去缓冲队列中等待的任务,最大能容纳的任务数(其实就是包括了核心线程池的数量)

  keepAliveTime:超出workQueue的等待任务的存活时间,就是指maximumPoolSize里面的等待任务的存活等待时间

  unit:时间单位

  workQueue:阻塞等待线程的队列,一般使用new LinkedBlockingQueue()这个,如果不指定容量,会一直往里添加,没有限制,workQueue永远不会满,一般选择没有容量上限的队列

  threadFactory:创建线程的工厂,使用系统默认的类

  handler:当任务数超过maximumPoolSize时,对任务的处理策略,默认策略是拒绝添加

  执行流程:当线程数小于corePoolSize时,每添加一个任务,则立即开启线程执行;当corePoolSize满的时候,后面添加的任务将放入缓冲队列workQueue等待;当workQueue满的时候,看是否超过maximumPoolSize线程数,如果超过,则拒绝执行,如果没有超过,则创建线程理解执行;  

 import java.util.concurrent.Executors;

 import java.util.concurrent.LinkedBlockingQueue;

 import java.util.concurrent.ThreadPoolExecutor;

 import java.util.concurrent.TimeUnit

 /**

  * 对线程池进行管理和封装

  * @author guoqing

  *

  */

 public class ThreadPoolManager {

     private static ThreadPoolManager mInstance = new ThreadPoolManager();

     private ThreadPoolExecutor executor;

     private int corePoolSize;    //核心线程池数量,表示能够同时执行的任务数量

     private int maximumPoolSize;    //最大线程池数量,其实是包含了核心线程池数量在内的

     private long keepAliveTime = 1;        //存活时间,表示最大线程池中等待任务的存活时间

     private TimeUnit unit = TimeUnit.HOURS;        //存活时间的时间单位

     public static ThreadPoolManager getInstance() {

         return mInstance;

     }

     private ThreadPoolManager() {

         //核心线程数量的计算规则:当前设备的可用处理器核心数*2+1,能够让cpu得到最大效率的发挥

         corePoolSize = Runtime.getRuntime().availableProcessors()*2+1;

         maximumPoolSize = corePoolSize;    //虽然用不到,但是不能为0,否则会报错

         //线程池机制:领工资的机制

         executor = new ThreadPoolExecutor(corePoolSize,

                 maximumPoolSize,

                 keepAliveTime,

                 unit,

                 new LinkedBlockingQueue<Runnable>(),    //缓冲队列,超出核心线程池的任务会被放入缓冲队列中等待

                 Executors.defaultThreadFactory(),        //创建线程的工厂类

                 new ThreadPoolExecutor.AbortPolicy()    //当最大线程池也超出的时候,则拒绝执行

                 );   

     }

     /**

      * 往线程池中添加任务

      * @param r

      */

     public void executor(Runnable r) {

         if(r!=null) {

             executor.execute(r);

         }

     }

     /**

      * 从线程池中移除任务

      * @param r

      */

     public void remove(Runnable r) {

         if(r!=null) {

             executor.remove(r);

         }

     }

 } 

  4volatile关键字的作用,原理

  答:保证内存可见性和禁止指令重排。实现原理可参考:JAVA并发变成--valatile关键字剖

  5synchronized关键字的用法,优缺点

  答:java关键字,当它用来修饰一个方法或者代码块的时候,能够保证在同一时刻最多只有一个线程执行该代码段的代码;

    synchronized修饰的方法或者对象,只能以同步的方式执行,会引起性能问题;无法中断一个正在等候获得锁的线程,也无法通过投票获得锁;一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险;

  6Lock接口有哪些实现类,使用场景是什么

  答:Lock接口有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLockWriteLock

  使用场景:一般应用于多度少写,因为读的线程之间没有竞争,所以比起synchronzied,性能要好很多;

  7、悲观锁、乐观锁的优缺点,CAS有什么缺陷,该如何解决

  悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候都会上锁,这样别人拿数据的时候就会阻塞知道它拿到锁;比如关系型数据库的行锁、表锁、读锁、写锁;比如java里面的同步原语synchronized关键字的实现也是悲观锁;

  乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下再次期间别人有没有更新这个数据。乐观锁适用于多读的应用类型,可以提高吞吐量。javajava.util.conncurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的;

  CASCAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程不会被挂起,而是被告知这次竞争失败,并可以再次尝试;

  CAS的缺陷:ABA问题、循环时间长开销大,只能保证一个共享变量的原子操作;

  8ABC三个线程如何保证顺序执行

  答:用Thread.join() 方法,或者线程池newSingleThreadExecutor(原理是会将所有线程放入一个队列,而队列则保证了FIFO,也可以通过ReentrantLockstate整数用阿里判断轮到谁来执

  9、线程的状态都有哪些(五大状态)

  新建状态(new):当用new操作符创建一个线程时,如new Thread(),线程还没有开始运行,此时处于仙剑状态;

  就绪状态(runnable):一个新创建的线程并不自动开始运行,要执行线程,必须要调用线程的start()方法,当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态;

  运行状态(running):当线程获得cpu时间后,他才进入运行状态,真正开始实行run()方法

  阻塞状态(blocked):当线程运行过程中,可能由于各种原因进入阻塞状态;

    a.线程通过调用sleep方法进入睡眠状态

    b.线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者

    c.线程试图得到一个锁,而该锁正被其他线程持有

    d.线程正等待某个触发条件

  死亡状态(dead):run方法自然退出而自然死亡,或者一个未捕获的异常终止了run方法而使线程猝死

  https://images2018.cnblogs.com/blog/1073968/201805/1073968-20180510120402733-98466055.png

  10sleepwait的区别

  答:首先,sleep()方法属于Thread类的,而wait()方法是属于Object类的;sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持,当指定的时间到了又自动回恢复运行状态,调用了sleep()方法的过程中,线程不会释放对象锁;而当调用了wait()方法的时候,线程回放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备。

  11notify()notifyAll()的区别

  答:notify()方法表示,当前线程已经放弃对资源的占有,通知等待的线程来获取对资源的占有权,但是只有一个线程能够从wait状态中恢复;notifyAll()方法表示,当前的线程已经放弃对资源的占有,通知所有的等待线程从wait()方法后的语句开始执行,但最终只有一个线程能竞争获得锁并执行;notify()是对notifyAll()的一个优化,

   12ThreadLocal的了解,实现原理。

   答:ThreadLocal,线程本地变量。定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写都是线程隔离的,互相之间不会影响,他提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制;实现的思路,Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程都有一个自己的ThreadLocalMapThreadLocalMap有自己的独立实现,可以简单的将它的key视作ThreadLocalvalue为代码中放入的值(实际上key并不是ThreadLocal本省,而是它的一个弱引用)。每个线程在往ThreadLocalset值的时候,都会往自己的ThreadLocalMap里存,读也是已某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程的隔离。如果想详细了解,可以参考:ThreadLocal源码解读

数据库相关

  1、常见的数据库优化手段

  答:库表优化,表设计合理化,符合三大范式;添加适当的索引(普通索引、主键索引、唯一索引、全文索引);分库分表;读写分离等;sql语句优化,定位执行效率低,慢sql的语句,通过explain分析低效率的原因;

  2、索引的优缺点,什么字段上建立索引

  答:优点方面:第一,通过创建唯一索引可以保证数据的唯一性;第二,可以大大加快数据的检索速度,是主要目的;第三;在使用分组和排序子句进行数据检索时,可以显著减少查询中分组和排序的时间;第四,可以在查询中使用优化隐藏器,提高系统的性能;

  缺点方面:第一,创建索引和维护索引要耗费时间,并且随着数据量的增加而增加;第二,每一个索引需要占用额外的物理空间,需要的磁盘开销更大;第三,当对表中的数据进行增加、删除、修改操作时,索引也要动态维护,降低了数据的维护速度;

  一般来说,在经常需要搜索的列上,强制该列的唯一性和组织表中数据的排列结构的列,在经常用在链接的列上,在经常需要排序的列上,在经常使用在where字句的列上可以添加索引,以提升查询速度;同样,对于一些甚少使用或者参考的列,只有很少数值的列(如性别),定义为text,image,bit的列,修改性能远远大于检索性能的列不适合添加索引;

  3、数据库连接池

  答:数据库连接池(Connection pooling)是程序启动时建立足够的数据库连接,并将这些连接组成一个连接池,由程序动态的对池中的连接进行申请、使用、释放;

  (1)程序初始化时创建连接池

  (2)使用时向连接池申请可用连接

  (3)使用完毕,将连接返还给连接池

  (4)程序退出时,断开所有的连接,并释放资源

计算机网络

  1TCPUDP的区别

  答:TCP(传输控制协议),UDP(用户数据报协议)

  (1TCP面向连接(如打电话先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接;

  (2TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序达到;UDP尽最大努力交付,即不保证可靠交付;

  (3TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)

  (4)每一条TCP连接只能是点到点的,UDP支持一对一,一对多,多对一和多对多的交互通信;

  (5TCP首部开销20字节,UDP首部开销8字节;

  (6TCP的逻辑通信信道是全双工的可靠信道,DUP则是不可靠信道;

  2、三次握手,四次挥手,为什么要四次挥手。

  答:三次握手的目的是建立可靠的通信信道,简单来说就是数据的发送与接收,主要目的是双方确认自己与对方的发送和接收机能正常;    

    第一次握手:Client什么都不能确认,Server确认了对方发送正常;

    第二次握手:Clent确认了,自己发送、接收正常,对方发送、接收正常;Server确认了自己接收正常,对方发送正常;

    第三次握手:Clent确认了,自己发送、接收正常,对方发送、接收正常;Server确认了自己发送、接收正常,对方发送、接收正常;

  所以,经过三次握手之后,就能确认双方收发功能都正常;

https://images2018.cnblogs.com/blog/1073968/201805/1073968-20180510145603128-1725312889.png

  四次挥手:

    A:“喂,我不说了 (FIN)”A->FIN_WAIT1

    B:“我知道了(ACK)。等下,上一句还没说完Balabala…..传输数据”B->CLOSE_WAIT | A->FIN_WAIT2

    B:”好了,说完了,我也不说了(FIN)。”B->LAST_ACK

    A:”我知道了(ACK)。”A->TIME_WAIT | B->CLOSED

    A等待2MSL,保证B收到了消息,否则重说一次我知道了”,A->CLOSED

  https://images2018.cnblogs.com/blog/1073968/201805/1073968-20180510145849143-1912183142.png

  3、长连接和短连接。

  短连接:连接=》传输数据=》关闭连接

  HTTP是无状态的,浏览器和服务器之间每进行一次http操作,就建立一次连接,但任务结束就中断连接;也可以理解为短连接是指socket连接后,发送接收完数据马上断开连接;

  长连接:连接=》传输数据=》保持连接=》传输数据=》。。。=》关闭连接

  长连接指建立socket连接后不管是否使用都保持连接,但安全性较差;

 设计模式

  此处推荐阅读:java23种设计模式深入理解

  1、单例模式的几种写法

  懒汉模式

public class Singleton {

    private static Singleton instance = null;

private Singleton(){}

    public static synchronized Singleton getInstance(){

        //如果还没有被实例化过,就实例化一个,然后返回

        if(instance == null){

            instance = new Singleton();

        }

        return instance;

    }

}

  饿汉模式

public class Singleton {

    //类加载的时候instance就已经指向了一个实例

    private static Singleton instance = new Singleton();

    private Singleton(){}

 

    public static Singleton getInstance(){

        return instance;

    }

}

  双重检验锁

public class Singleton {

   

    private static Singleton instance = null;

    private Singleton(){}

 

    public static Singleton getInstance(){

        if(instance == null){

            synchronized (Singleton.class){

                if(instance == null){

                    instance = new Singleton();

                }

            }

        }

        return instance;

    }

}

  静态内部类:因为JAVA静态内部类的特性,加载的时候不会加载内部静态类,使用的时候才会加载,而使用的时候类加载又是线程安全的,这就完美达到了效果;

public class Singleton {

 

    private static class SingletonHolder{

        private static Singleton instance = new Singleton();

    }

 

    private Singleton(){}

 

    public static Singleton getInstance(){

        return SingletonHolder.instance;

    }

}

  枚举:

public enum Singleton {

    INSTANCE;

}

  

  2Spring使用了哪些设计模式

  (1)工厂模式,在各种BeanFactory以及ApplicationContext创建中都用到了;

  (2)模板模式,也是在各种BeanFactory以及ApplicationContext创建中都用到了;

  (3)代理模式,在AOP实现中用到了JDK的动态代理;

  (4)单例模式,比如创建bean的时候;

  (5)策略模式,第一个地方,加载资源文件的地方,使用了不同的方法,比如:classPathResource,FileSystemResource,ServletContextResource,UrlResource但他们都有共同的接口Resource;第二个地方就是AOP的实现中,采用了不同的方式,JDK动态代理和CGLIB代理;

分布式相关

  1、分布式事务的控制

  可以参考分布式系统事务一致性解决方案

  2、分布式锁

  答:一般使用zk瞬时有序节点实现的分布式锁,或者利用redissetnx()封装分布式锁;提供思路,具体的可以自行详细理解;

  3、分布式session如何设计

  答:一个比较成熟的方案是通过redis进行session共享。详细的原理可以参考一种分布式session实现方案

  4、关于dubbo

  可以参考博文:Dubbo学习总结(2——Dubbo架构详解

  5、可以了解zk相关知识

缓存相关

  1redismemcached的区别

  (1redismemcache都是将数据放入内存中,都是内存数据库。但是memcache可以缓存图片、视频等数据;

  (2redis不仅仅支持简单的k/v数据,还提供listsethash等数据结构的存储;

  (3)虚拟内存--redis当物理内存用完时,可以将一些很久没有用到的value交换到磁盘;

  (4)过期策略--memcacheset时就指定,例如set key1008,即永不过期,redis通过expire设定;

  (5)分布式--设定memcache集群,利用magent做一主多从;redis可以做一主多从或一主一从;

  (6)存储数据安全--memcache挂掉后,数据没了,redis可以定期保存到磁盘进行持久化;

  (7)灾难恢复--memcache挂掉后,数据不可恢复。redis数据丢失后可以通过aof恢复;

  (8redis支持数据备份,即master-slave主备模式;

  2redis是单线程的么(是的)

  3redis的持久化策略

  答:rdb:快照形式是直接把内存中的数据保存到一个dump文件中,定时保存

    aof:把所有的对redis的服务器进行修改的命令都存到一个文件里,命令的集合

框架相关

  1SpringMvc工作原理

  (1)用户发送请求至前端控制器DispatcherServlet

  (2DispatcherServlet收到请求调用HandlerMapping处理映射器

  (3)处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如有则生成)一并返回给DispatcherServlet

  (4DispatcherServlet调用HandlerAdapter处理器映射器

  (5HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)

  (6Controller执行完成返回ModelAndView

  (7HandlerAdapterController执行结果ModelAndView返回给DispatcherServlet

  (8DispatcherServletModelAndView传给ViewResolver视图解析器

  (9ViewResolver解析后返回具体的view

  (10DispatcherServlet根据view进行试图渲染(即将模型数据填充至视图中)

  (11DispatcherServlet响应用户  

  以下组件通常使用框架提供实现:

  DispatcherServlet:作为前端控制器,整个流程控制的中心,控制其它组件执行,统一调度,降低组件之间的耦合性,提高每个组件的扩展性。

  HandlerMapping:通过扩展处理器映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。 

  HandlAdapter:通过扩展处理器适配器,支持更多类型的处理器。

  ViewResolver:通过扩展视图解析器,支持更多类型的视图解析,例如:jspfreemarkerpdfexcel等。

  2Quartz概念及原理

  org.quartz.Job:它是一个抽象接口,表示一个工作,也是我们要执行的具体的内容,只定义了一个接口方法:void execute(JobExecutionContext context)

  org.quartz.JobDetailJobDetail表示一个具体的可执行的调度程序,Job是这个可执行调度程序所要执行的内容,它包含了这个调度任务的方案和策略

  org.quartz.TriggerTrigger是一个抽象接口,表示一个调度参数的配置,通过配置他,来告诉调度器什么时候去调用JobDetail

  org.quartz.Scheduler:一个调度容器,可以注册多个TriggerJobDetail。当TriggerJobDetail组合,就可以被Scheduler容器调度了

  3SpringIOC有什么优势

  答:要了解IOC首先要明白依赖倒置原则(Dependency Inversion Principle),就是把原本的高层建筑依赖底层建筑倒置过来,变成底层建筑依赖高层建筑。高层建筑决定需要什么,底层去实现这样的需求,但是高层并不用管底层的是怎么实现的;而控制反转(Inversion of Control)就是依赖倒置原则的一种代码的设计思路;

  https://images2018.cnblogs.com/blog/1073968/201805/1073968-20180510170422044-126446996.png

  IOC思想的核心,资源不由使用资源的双方管理,由不适用资源的第三方管理。

  优势:资源集中管理,实现资源的可配置和易管理;降低了使用资源双方的依赖程度,也就是降低了耦合度;

 

1. ZooKeeper是什么?

ZooKeeper是一个开放源码的分布式协调服务,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

分布式应用程序可以基于Zookeeper实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等功能。

Zookeeper保证了如下分布式一致性特性:

·顺序一致性

·原子性

·单一视图

·可靠性

·实时性(最终一致性)

客户端的读请求可以被集群中的任意一台机器处理,如果读请求在节点上注册了监听器,这个监听器也是由所连接的zookeeper机器来处理。对于写请求,这些请求会同时发给其他zookeeper机器并且达成一致后,请求才会返回成功。因此,随着zookeeper的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。

有序性是zookeeper中非常重要的一个特性,所有的更新都是全局有序的,每个更新都有一个唯一的时间戳,这个时间戳称为zxidZookeeper Transaction Id)。而读请求只会相对于更新有序,也就是读请求的返回结果中会带有这个zookeeper最新的zxid

2. ZooKeeper提供了什么?

1、文件系统
2、通知机制

3. Zookeeper文件系统

Zookeeper提供一个多层级的节点命名空间(节点称为znode)。与文件系统不同的是,这些节点都可以设置关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。
Zookeeper为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得Zookeeper不能用于存放大量的数据,每个节点的存放数据上限为1M

4. ZAB协议?

ZAB协议是为分布式协调服务Zookeeper专门设计的一种支持崩溃恢复的原子广播协议。

ZAB协议包括两种基本的模式:崩溃恢复和消息广播。

当整个zookeeper集群刚刚启动或者Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与Leader服务器保持正常通信时,所有进程(服务器)进入崩溃恢复模式,首先选举产生新的Leader服务器,然后集群中Follower服务器开始与新的Leader服务器进行数据同步,当集群中超过半数机器与该Leader服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。

5. 四种类型的数据节点 Znode

·PERSISTENT-持久节点
除非手动删除,否则节点一直存在于Zookeeper

·EPHEMERAL-临时节点
临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与zookeeper连接断开不一定会话失效),那么这个客户端创建的所有临时节点都会被移除。

·PERSISTENT_SEQUENTIAL-持久顺序节点
基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。

·EPHEMERAL_SEQUENTIAL-临时顺序节点
基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。

6. Zookeeper Watcher 机制 -- 数据变更通知

Zookeeper允许客户端向服务端的某个Znode注册一个Watcher监听,当服务端的一些指定事件触发了这个Watcher,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据Watcher通知状态和事件类型做出业务上的改变。

工作机制:

·客户端注册watcher

·服务端处理watcher

·客户端回调watcher

Watcher特性总结:

1.    一次性
无论是服务端还是客户端,一旦一个Watcher被触发,Zookeeper都会将其从相应的存储中移除。这样的设计有效的减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断的向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。

2.    客户端串行执行
客户端Watcher回调的过程是一个串行同步的过程。

3.    轻量

·Watcher通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。

·客户端向服务端注册Watcher的时候,并不会把客户端真实的Watcher对象实体传递到服务端,仅仅是在客户端请求中使用boolean类型属性进行了标记。

          watcher event异步发送watcher的通知事件从server发送到client是异步的,这就存在一个问题,不同的客户端和服务器之间通过socket进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,由于Zookeeper本身提供了ordering guarantee,即客户端监听事件后,才会感知它所监视znode发生了变化。所以我们使用Zookeeper不能期望能够监控到节点每次的变化。Zookeeper只能保证最终的一致性,而无法保证强一致性。

          注册watcher getDataexistsgetChildren

          触发watcher createdeletesetData

          当一个客户端连接到一个新的服务器上时,watch将会被以任意会话事件触发。当与一个服务器失去连接的时候,是无法接收到watch的。而当client重新连接时,如果需要的话,所有先前注册过的watch,都会被重新注册。通常这是完全透明的。只有在一个特殊情况下,watch可能会丢失:对于一个未创建的znodeexist watch,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这个watch事件可能会被丢失。

7. 客户端注册Watcher实现

1.    调用getData()/getChildren()/exist()三个API,传入Watcher对象

2.    标记请求request,封装WatcherWatchRegistration

3.    封装成Packet对象,发服务端发送request

4.    收到服务端响应后,将Watcher注册到ZKWatcherManager中进行管理

5.    请求返回,完成注册。

8. 服务端处理Watcher实现

1.            服务端接收Watcher并存储
接收到客户端请求,处理请求判断是否需要注册Watcher,需要的话将数据节点的节点路径和ServerCnxnServerCnxn代表一个客户端和服务端的连接,实现了Watcherprocess接口,此时可以看成一个Watcher对象)存储在WatcherManagerWatchTablewatch2Paths中去。

2.    Watcher触发
以服务端接收到 setData() 事务请求触发NodeDataChanged事件为例:

·封装WatchedEvent
将通知状态(SyncConnected)、事件类型(NodeDataChanged)以及节点路径封装成一个WatchedEvent对象

·查询Watcher
WatchTable中根据节点路径查找Watcher

·没找到;说明没有客户端在该数据节点上注册过Watcher

·找到;提取并从WatchTableWatch2Paths中删除对应Watcher从这里可以看出Watcher在服务端是一次性的,触发一次就失效了

          调用process方法来触发Watcher
这里process主要就是通过ServerCnxn对应的TCP连接发送Watcher事件通知。

9. 客户端回调Watcher

客户端SendThread线程接收事件通知,交由EventThread线程回调Watcher。客户端的Watcher机制同样是一次性的,一旦被触发后,该Watcher就失效了。

10. ACL权限控制机制

UGOUser/Group/Others

目前在Linux/Unix文件系统中使用,也是使用最广泛的权限控制方式。是一种粗粒度的文件系统权限控制模式。

ACLAccess Control List)访问控制列表

包括三个方面:

·权限模式(Scheme

o    IP:从IP地址粒度进行权限控制

o    Digest:最常用,用类似于 username:password 的权限标识来进行权限配置,便于区分不同应用来进行权限控制

o    World:最开放的权限控制方式,是一种特殊的digest模式,只有一个权限标识“world:anyone”

o    Super:超级用户

·授权对象
授权对象指的是权限赋予的用户或一个指定实体,例如IP地址或是机器灯。

·权限 Permission

o    CREATE:数据节点创建权限,允许授权对象在该Znode下创建子节点

o    DELETE:子节点删除权限,允许授权对象删除该数据节点的子节点

o    READ:数据节点的读取权限,允许授权对象访问该数据节点并读取其数据内容或子节点列表等

o    WRITE:数据节点更新权限,允许授权对象对该数据节点进行更新操作

o    ADMIN:数据节点管理权限,允许授权对象对该数据节点进行ACL相关设置操作

11. Chroot特性

3.2.0版本后,添加了 Chroot特性,该特性允许每个客户端为自己设置一个命名空间。如果一个客户端设置了Chroot,那么该客户端对服务器的任何操作,都将会被限制在其自己的命名空间下。

通过设置Chroot,能够将一个客户端应用于Zookeeper服务端的一颗子树相对应,在那些多个应用公用一个Zookeeper进群的场景下,对实现不同应用间的相互隔离非常有帮助。

12. 会话管理

分桶策略:将类似的会话放在同一区块中进行管理,以便于Zookeeper对会话进行不同区块的隔离处理以及同一区块的统一处理。

分配原则:每个会话的下次超时时间点ExpirationTime

计算公式:

ExpirationTime_ = currentTime + sessionTimeout
ExpirationTime = (ExpirationTime_ / ExpirationInrerval + 1) * ExpirationInterval , ExpirationInterval 是指 Zookeeper 会话超时检查时间间隔,默认 tickTime

13. 服务器角色

Leader

·事务请求的唯一调度和处理者,保证集群事务处理的顺序性

·集群内部各服务的调度者

Follower

·处理客户端的非事务请求,转发事务请求给Leader服务器

·参与事务请求Proposal的投票

·参与Leader选举投票

Observer

3.3.0版本以后引入的一个服务器角色,在不影响集群事务处理能力的基础上提升集群的非事务处理能力

·处理客户端的非事务请求,转发事务请求给Leader服务器

·不参与任何形式的投票

14. Zookeeper 下 Server工作状态

服务器具有四种状态,分别是LOOKINGFOLLOWINGLEADINGOBSERVING

·LOOKING:寻找Leader状态。当服务器处于该状态时,它会认为当前集群中没有Leader,因此需要进入Leader选举状态。

·FOLLOWING:跟随者状态。表明当前服务器角色是Follower

·LEADING:领导者状态。表明当前服务器角色是Leader

·OBSERVING:观察者状态。表明当前服务器角色是Observer

15. Leader 选举

Leader选举是保证分布式数据一致性的关键所在。当Zookeeper集群中的一台服务器出现以下两种情况之一时,需要进入Leader选举。

  (1) 服务器初始化启动。

  (2) 服务器运行期间无法和Leader保持连接。

  下面就两种情况进行分析讲解。

  1. 服务器启动时期的Leader选举

  若进行Leader选举,则至少需要两台机器,这里选取3台机器组成的服务器集群为例。在集群初始化阶段,当有一台服务器Server1启动时,其单独无法进行和完成Leader选举,当第二台服务器Server2启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,于是进入Leader选举过程。选举过程如下

  (1) 每个Server发出一个投票。由于是初始情况,Server1Server2都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myidZXID,使用(myid, ZXID)来表示,此时Server1的投票为(1, 0)Server2的投票为(2, 0),然后各自将这个投票发给集群中其他机器。

  (2) 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票、是否来自LOOKING状态的服务器。

  (3) 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行PKPK规则如下

    · 优先检查ZXIDZXID比较大的服务器优先作为Leader

    · 如果ZXID相同,那么就比较myidmyid较大的服务器作为Leader服务器。

  对于Server1而言,它的投票是(1, 0),接收Server2的投票为(2, 0),首先会比较两者的ZXID,均为0,再比较myid,此时Server2myid最大,于是更新自己的投票为(2, 0),然后重新投票,对于Server2而言,其无须更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。

  (4) 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于Server1Server2而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出了Leader

  (5) 改变服务器状态。一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader,就变更为LEADING

  2. 服务器运行时期的Leader选举

  在Zookeeper运行期间,Leader与非Leader服务器各司其职,即便当有非Leader服务器宕机或新加入,此时也不会影响Leader,但是一旦Leader服务器挂了,那么整个集群将暂停对外服务,进入新一轮Leader选举,其过程和启动时期的Leader选举过程基本一致。假设正在运行的有Server1Server2Server3三台服务器,当前LeaderServer2,若某一时刻Leader挂了,此时便开始Leader选举。选举过程如下

  (1) 变更状态。Leader挂后,余下的非Observer服务器都会讲自己的服务器状态变更为LOOKING,然后开始进入Leader选举过程。

  (2) 每个Server会发出一个投票。在运行期间,每个服务器上的ZXID可能不同,此时假定Server1ZXID123Server3ZXID122;在第一轮投票中,Server1Server3都会投自己,产生投票(1, 123)(3, 122),然后各自将投票发送给集群中所有机器。

  (3) 接收来自各个服务器的投票。与启动时过程相同。

  (4) 处理投票。与启动时过程相同,此时,Server1将会成为Leader

  (5) 统计投票。与启动时过程相同。

  (6) 改变服务器的状态。与启动时过程相同。

  2.2 Leader选举算法分析

  在3.4.0后的Zookeeper的版本只保留了TCP版本的FastLeaderElection选举算法。当一台机器进入Leader选举时,当前集群可能会处于以下两种状态

    · 集群中已经存在Leader

    · 集群中不存在Leader

  对于集群中已经存在Leader而言,此种情况一般都是某台机器启动得较晚,在其启动之前,集群已经在正常工作,对这种情况,该机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器而言,仅仅需要和Leader机器建立起连接,并进行状态同步即可。而在集群中不存在Leader情况下则会相对复杂,其步骤如下

  (1) 第一次投票。无论哪种导致进行Leader选举,集群的所有机器都处于试图选举出一个Leader的状态,即LOOKING状态,LOOKING机器会向所有其他机器发送消息,该消息称为投票。投票中包含了SID(服务器的唯一标识)和ZXID(事务ID),(SID, ZXID)形式来标识一次投票信息。假定Zookeeper5台机器组成,SID分别为12345ZXID分别为99988,并且此时SID2的机器是Leader机器,某一时刻,12所在机器出现故障,因此集群开始进行Leader选举。在第一次投票时,每台机器都会将自己作为投票对象,于是SID345的机器投票情况分别为(3, 9)(4, 8) (5, 8)

  (2) 变更投票。每台机器发出投票后,也会收到其他机器的投票,每台机器会根据一定规则来处理收到的其他机器的投票,并以此来决定是否需要变更自己的投票,这个规则也是整个Leader选举算法的核心所在,其中术语描述如下

    · vote_sid:接收到的投票中所推举Leader服务器的SID

    · vote_zxid:接收到的投票中所推举Leader服务器的ZXID

    · self_sid:当前服务器自己的SID

    · self_zxid:当前服务器自己的ZXID

  每次对收到的投票的处理,都是对(vote_sid, vote_zxid)(self_sid, self_zxid)对比的过程。

    规则一:如果vote_zxid大于self_zxid,就认可当前收到的投票,并再次将该投票发送出去。

    规则二:如果vote_zxid小于self_zxid,那么坚持自己的投票,不做任何变更。

    规则三:如果vote_zxid等于self_zxid,那么就对比两者的SID,如果vote_sid大于self_sid,那么就认可当前收到的投票,并再次将该投票发送出去。

    规则四:如果vote_zxid等于self_zxid,并且vote_sid小于self_sid,那么坚持自己的投票,不做任何变更。

  结合上面规则,给出下面的集群变更过程。

  (3) 确定Leader。经过第二轮投票后,集群中的每台机器都会再次接收到其他机器的投票,然后开始统计投票,如果一台机器收到了超过半数的相同投票,那么这个投票对应的SID机器即为Leader。此时Server3将成为Leader

  由上面规则可知,通常那台服务器上的数据越新(ZXID会越大),其成为Leader的可能性越大,也就越能够保证数据的恢复。如果ZXID相同,则SID越大机会越大。

  2.3 Leader选举实现细节

  1. 服务器状态

  服务器具有四种状态,分别是LOOKINGFOLLOWINGLEADINGOBSERVING

  LOOKING:寻找Leader状态。当服务器处于该状态时,它会认为当前集群中没有Leader,因此需要进入Leader选举状态。

  FOLLOWING:跟随者状态。表明当前服务器角色是Follower

  LEADING:领导者状态。表明当前服务器角色是Leader

  OBSERVING:观察者状态。表明当前服务器角色是Observer

  2. 投票数据结构

  每个投票中包含了两个最基本的信息,所推举服务器的SIDZXID,投票(Vote)在Zookeeper中包含字段如下

  id:被推举的LeaderSID

  zxid:被推举的Leader事务ID

  electionEpoch:逻辑时钟,用来判断多个投票是否在同一轮选举周期中,该值在服务端是一个自增序列,每次进入新一轮的投票后,都会对该值进行加1操作。

  peerEpoch:被推举的Leaderepoch

  state:当前服务器的状态。

  3. QuorumCnxManager:网络I/O

  每台服务器在启动的过程中,会启动一个QuorumPeerManager,负责各台服务器之间的底层Leader选举过程中的网络通信。

  (1) 消息队列。QuorumCnxManager内部维护了一系列的队列,用来保存接收到的、待发送的消息以及消息的发送器,除接收队列以外,其他队列都按照SID分组形成队列集合,如一个集群中除了自身还有3台机器,那么就会为这3台机器分别创建一个发送队列,互不干扰。

    · recvQueue:消息接收队列,用于存放那些从其他服务器接收到的消息。

    · queueSendMap:消息发送队列,用于保存那些待发送的消息,按照SID进行分组。

    · senderWorkerMap:发送器集合,每个SenderWorker消息发送器,都对应一台远程Zookeeper服务器,负责消息的发送,也按照SID进行分组。

    · lastMessageSent:最近发送过的消息,为每个SID保留最近发送过的一个消息。

  (2) 建立连接。为了能够相互投票,Zookeeper集群中的所有机器都需要两两建立起网络连接。QuorumCnxManager在启动时会创建一个ServerSocket来监听Leader选举的通信端口(默认为3888)。开启监听后,Zookeeper能够不断地接收到来自其他服务器的创建连接请求,在接收到其他服务器的TCP连接请求时,会进行处理。为了避免两台机器之间重复地创建TCP连接,Zookeeper只允许SID大的服务器主动和其他机器建立连接,否则断开连接。在接收到创建连接请求后,服务器通过对比自己和远程服务器的SID值来判断是否接收连接请求,如果当前服务器发现自己的SID更大,那么会断开当前连接,然后自己主动和远程服务器建立连接。一旦连接建立,就会根据远程服务器的SID来创建相应的消息发送器SendWorker和消息接收器RecvWorker,并启动。

  (3) 消息接收与发送。消息接收:由消息接收器RecvWorker负责,由于Zookeeper为每个远程服务器都分配一个单独的RecvWorker,因此,每个RecvWorker只需要不断地从这个TCP连接中读取消息,并将其保存到recvQueue队列中。消息发送:由于Zookeeper为每个远程服务器都分配一个单独的SendWorker,因此,每个SendWorker只需要不断地从对应的消息发送队列中获取出一个消息发送即可,同时将这个消息放入lastMessageSent中。在SendWorker中,一旦Zookeeper发现针对当前服务器的消息发送队列为空,那么此时需要从lastMessageSent中取出一个最近发送过的消息来进行再次发送,这是为了解决接收方在消息接收前或者接收到消息后服务器挂了,导致消息尚未被正确处理。同时,Zookeeper能够保证接收方在处理消息时,会对重复消息进行正确的处理。

  4. FastLeaderElection:选举算法核心

  · 外部投票:特指其他服务器发来的投票。

  · 内部投票:服务器自身当前的投票。

  · 选举轮次:Zookeeper服务器Leader选举的轮次,即logicalclock

  · PK:对内部投票和外部投票进行对比来确定是否需要变更内部投票。

  (1) 选票管理

  · sendqueue:选票发送队列,用于保存待发送的选票。

  · recvqueue:选票接收队列,用于保存接收到的外部投票。

  · WorkerReceiver:选票接收器。其会不断地从QuorumCnxManager中获取其他服务器发来的选举消息,并将其转换成一个选票,然后保存到recvqueue中,在选票接收过程中,如果发现该外部选票的选举轮次小于当前服务器的,那么忽略该外部投票,同时立即发送自己的内部投票。

  · WorkerSender:选票发送器,不断地从sendqueue中获取待发送的选票,并将其传递到底层QuorumCnxManager中。

  (2) 算法核心
https://images2018.cnblogs.com/blog/632316/201808/632316-20180803082744195-266416769.png

  上图展示了FastLeaderElection模块是如何与底层网络I/O进行交互的。Leader选举的基本流程如下

  1. 自增选举轮次。Zookeeper规定所有有效的投票都必须在同一轮次中,在开始新一轮投票时,会首先对logicalclock进行自增操作。

  2. 初始化选票。在开始进行新一轮投票之前,每个服务器都会初始化自身的选票,并且在初始化阶段,每台服务器都会将自己推举为Leader

  3. 发送初始化选票。完成选票的初始化后,服务器就会发起第一次投票。Zookeeper会将刚刚初始化好的选票放入sendqueue中,由发送器WorkerSender负责发送出去。

  4. 接收外部投票。每台服务器会不断地从recvqueue队列中获取外部选票。如果服务器发现无法获取到任何外部投票,那么就会立即确认自己是否和集群中其他服务器保持着有效的连接,如果没有连接,则马上建立连接,如果已经建立了连接,则再次发送自己当前的内部投票。

  5. 判断选举轮次。在发送完初始化选票之后,接着开始处理外部投票。在处理外部投票时,会根据选举轮次来进行不同的处理。

    · 外部投票的选举轮次大于内部投票。若服务器自身的选举轮次落后于该外部投票对应服务器的选举轮次,那么就会立即更新自己的选举轮次(logicalclock),并且清空所有已经收到的投票,然后使用初始化的投票来进行PK以确定是否变更内部投票。最终再将内部投票发送出去。

    · 外部投票的选举轮次小于内部投票。若服务器接收的外选票的选举轮次落后于自身的选举轮次,那么Zookeeper就会直接忽略该外部投票,不做任何处理,并返回步骤4

    · 外部投票的选举轮次等于内部投票。此时可以开始进行选票PK

  6. 选票PK。在进行选票PK时,符合任意一个条件就需要变更投票。

    · 若外部投票中推举的Leader服务器的选举轮次大于内部投票,那么需要变更投票。

    · 若选举轮次一致,那么就对比两者的ZXID,若外部投票的ZXID大,那么需要变更投票。

    · 若两者的ZXID一致,那么就对比两者的SID,若外部投票的SID大,那么就需要变更投票。

  7. 变更投票。经过PK后,若确定了外部投票优于内部投票,那么就变更投票,即使用外部投票的选票信息来覆盖内部投票,变更完成后,再次将这个变更后的内部投票发送出去。

  8. 选票归档。无论是否变更了投票,都会将刚刚收到的那份外部投票放入选票集合recvset中进行归档。recvset用于记录当前服务器在本轮次的Leader选举中收到的所有外部投票(按照服务队的SID区别,如{(1, vote1), (2, vote2)...})。

  9. 统计投票。完成选票归档后,就可以开始统计投票,统计投票是为了统计集群中是否已经有过半的服务器认可了当前的内部投票,如果确定已经有过半服务器认可了该投票,则终止投票。否则返回步骤4

  10. 更新服务器状态。若已经确定可以终止投票,那么就开始更新服务器状态,服务器首选判断当前被过半服务器认可的投票所对应的Leader服务器是否是自己,若是自己,则将自己的服务器状态更新为LEADING,若不是,则根据具体情况来确定自己是FOLLOWING或是OBSERVING

  以上10个步骤就是FastLeaderElection的核心,其中步骤4-9会经过几轮循环,直到有Leader选举产生。

16. 数据同步

整个集群完成Leader选举之后,LearnerFollowerObserver的统称)回向Leader服务器进行注册。当Learner服务器想Leader服务器完成注册后,进入数据同步环节。

数据同步流程:(均以消息传递的方式进行)

i. LearnerLearder注册

ii. 数据同步

iii. 同步确认

Zookeeper的数据同步通常分为四类

·直接差异化同步(DIFF同步)

·先回滚再差异化同步(TRUNC+DIFF同步)

·仅回滚同步(TRUNC同步)

·全量同步(SNAP同步)

在进行数据同步前,Leader服务器会完成数据同步初始化:

·peerLastZxid:从learner服务器注册时发送的ACKEPOCH消息中提取lastZxid(该Learner服务器最后处理的ZXID

·minCommittedLogLeader服务器Proposal缓存队列committedLog中最小ZXID

·maxCommittedLogLeader服务器Proposal缓存队列committedLog中最大ZXID

直接差异化同步(DIFF同步)

场景peerLastZxid介于minCommittedLogmaxCommittedLog之间

https://images2018.cnblogs.com/blog/632316/201808/632316-20180803082218708-1373657717.png

先回滚再差异化同步(TRUNC+DIFF同步)

场景:当新的Leader服务器发现某个Learner服务器包含了一条自己没有的事务记录,那么就需要让该Learner服务器进行事务回滚--回滚到Leader服务器上存在的,同时也是最接近于peerLastZxidZXID

仅回滚同步(TRUNC同步)

场景peerLastZxid 大于 maxCommittedLog

全量同步(SNAP同步)

场景一peerLastZxid 小于 minCommittedLog
场景二Leader服务器上没有Proposal缓存队列且peerLastZxid不等于lastProcessZxid

17. zookeeper是如何保证事务的顺序一致性的?

zookeeper采用了全局递增的事务Id来标识,所有的proposal(提议)都在被提出的时候加上了zxidzxid实际上是一个64位的数字,高32位是epoch(时期纪元新时代)用来标识leader周期,如果有新的leader产生出来,epoch会自增,低32位用来递增计数。当新产生proposal的时候,会依据数据库的两阶段过程,首先会向其他的server发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。

18. 分布式集群中为什么会有Master?

在分布式环境中,有些业务逻辑只需要集群中的某一台机器进行执行,其他的机器可以共享这个结果,这样可以大大减少重复计算,提高性能,于是就需要进行leader选举。

19. zk节点宕机如何处理?

Zookeeper本身也是集群,推荐配置不少于3个服务器。Zookeeper自身也要保证当一个节点宕机时,其他节点会继续提供服务。
如果是一个Follower宕机,还有2台服务器提供访问,因为Zookeeper上的数据是有多个副本的,数据并不会丢失;
如果是一个Leader宕机,Zookeeper会选举出新的Leader
ZK集群的机制是只要超过半数的节点正常,集群就能正常提供服务。只有在ZK节点挂得太多,只剩一半或不到一半节点能工作,集群才失效。
所以
3个节点的cluster可以挂掉1个节点(leader可以得到2>1.5)
2
个节点的cluster就不能挂掉任何1个节点了(leader可以得到1<=1)

20. zookeeper负载均衡和nginx负载均衡区别

zk的负载均衡是可以调控,nginx只是能调权重,其他需要可控的都需要自己写插件;但是nginx的吞吐量比zk大很多,应该说按业务选择用哪种方式。

21. Zookeeper有哪几种几种部署模式?

部署模式:单机模式、伪集群模式、集群模式。

22. 集群最少要几台机器,集群规则是怎样的?

集群规则为2N+1台,N>0,即3台。

23. 集群支持动态添加机器吗?

其实就是水平扩容了,Zookeeper在这方面不太好。两种方式:

·全部重启:关闭所有Zookeeper服务,修改配置之后启动。不影响之前客户端的会话。

·逐个重启:在过半存活即可用的原则下,一台机器重启不影响整个集群对外提供服务。这是比较常用的方式。

3.5版本开始支持动态扩容。

24. Zookeeper对节点的watch监听通知是永久的吗?为什么不是永久的?

不是。官方声明:一个Watch事件是一个一次性的触发器,当被设置了Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了Watch的客户端,以便通知它们。

为什么不是永久的,举个例子,如果服务端变动频繁,而监听的客户端很多情况下,每次变动都要通知到所有的客户端,给网络和服务器造成很大压力。
一般是客户端执行getData(“/节点A”,true),如果节点A发生了变更或删除,客户端会得到它的watch事件,但是在之后节点A又发生了变更,而客户端又没有设置watch事件,就不再给客户端发送。
在实际应用中,很多情况下,我们的客户端不需要知道服务端的每一次变动,我只要最新的数据即可。

25. Zookeeper的java客户端都有哪些?

java客户端:zk自带的zkclientApache开源的Curator

26. chubby是什么,和zookeeper比你怎么看?

chubbygoogle的,完全实现paxos算法,不开源。zookeeperchubby的开源实现,使用zab协议,paxos算法的变种。

27. 说几个zookeeper常用的命令。

常用命令:ls get set create delete等。

28. ZAB和Paxos算法的联系与区别?

·相同点:

o    两者都存在一个类似于Leader进程的角色,由其负责协调多个Follower进程的运行

o    Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将一个提案进行提交

o    ZAB协议中,每个Proposal中都包含一个 epoch 值来代表当前的Leader周期,Paxos中名字为Ballot

·不同点:
ZAB用来构建高可用的分布式数据主备系统(Zookeeper),Paxos是用来构建分布式一致性状态机系统。

29. Zookeeper的典型应用场景

Zookeeper是一个典型的发布/订阅模式的分布式数据管理与协调框架,开发人员可以使用它来进行分布式数据的发布和订阅。

通过对Zookeeper中丰富的数据节点进行交叉使用,配合Watcher事件通知机制,可以非常方便的构建一系列分布式应用中年都会涉及的核心功能,如:

·数据发布/订阅

·负载均衡

·命名服务

·分布式协调/通知

·集群管理

·Master选举

·分布式锁

·分布式队列

1. 数据发布/订阅

介绍

数据发布/订阅系统,即所谓的配置中心,顾名思义就是发布者发布数据供订阅者进行数据订阅。

目的

·动态获取数据(配置信息)

·实现数据(配置信息)的集中式管理和数据的动态更新

设计模式

·Push 模式

·Pull 模式

数据(配置信息)特性:

·数据量通常比较小

·数据内容在运行时会发生动态更新

·集群中各机器共享,配置一致

如:机器列表信息、运行时开关配置、数据库配置信息等

基于Zookeeper的实现方式

1.    数据存储:将数据(配置信息)存储到Zookeeper上的一个数据节点

2.    数据获取:应用在启动初始化节点从Zookeeper数据节点读取数据,并在该节点上注册一个数据变更Watcher

3.    数据变更:当变更数据时,更新Zookeeper对应节点数据,Zookeeper会将数据变更通知发到各客户端,客户端接到通知后重新读取变更后的数据即可。

2. 负载均衡

zk的命名服务
命名服务是指通过指定的名字来获取资源或者服务的地址,利用zk创建一个全局的路径,这个路径就可以作为一个名字,指向集群中的集群,提供的服务的地址,或者一个远程的对象等等。

分布式通知和协调
对于系统调度来说:操作人员发送通知实际是通过控制台改变某个节点的状态,然后zk将这些变化发送给注册了这个节点的watcher的所有客户端。
对于执行情况汇报:每个工作进程都在某个目录下创建一个临时节点。并携带工作的进度数据,这样汇总的进程可以监控目录子节点的变化获得工作进度的实时的全局情况。

7.zk的命名服务(文件系统)
命名服务是指通过指定的名字来获取资源或者服务的地址,利用zk创建一个全局的路径,即是唯一的路径,这个路径就可以作为一个名字,指向集群中的集群,提供的服务的地址,或者一个远程的对象等等。

8.zk的配置管理(文件系统、通知机制)
程序分布式的部署在不同的机器上,将程序的配置信息放在zkznode下,当有配置发生改变时,也就是znode发生变化时,可以通过改变zk中某个目录节点的内容,利用watcher通知给各个客户端,从而更改配置。

9.Zookeeper集群管理(文件系统、通知机制)
所谓集群管理无在乎两点:是否有机器退出和加入、选举master
对于第一点,所有机器约定在父目录下创建临时目录节点,然后监听父目录节点的子节点变化消息。一旦有机器挂掉,该机器与 zookeeper的连接断开,其所创建的临时目录节点被删除,所有其他机器都收到通知:某个兄弟目录被删除,于是,所有人都知道:它上船了。
新机器加入也是类似,所有机器收到通知:新兄弟目录加入,highcount又有了,对于第二点,我们稍微改变一下,所有机器创建临时顺序编号目录节点,每次选取编号最小的机器作为master就好。

10.Zookeeper分布式锁(文件系统、通知机制)
有了zookeeper的一致性文件系统,锁的问题变得容易。锁服务可以分为两类,一个是保持独占,另一个是控制时序。
对于第一类,我们将zookeeper上的一个znode看作是一把锁,通过createznode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。用完删除掉自己创建的distribute_lock 节点就释放出锁。
对于第二类, /distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选master一样,编号最小的获得锁,用完删除,依次方便。

11.获取分布式锁的流程
clipboard.png

在获取分布式锁的时候在locker节点下创建临时顺序节点,释放锁的时候删除该临时节点。客户端调用createNode方法在locker下创建临时顺序节点,
然后调用getChildren(“locker”)来获取locker下面的所有子节点,注意此时不用设置任何Watcher。客户端获取到所有的子节点path之后,如果发现自己创建的节点在所有创建的子节点序号最小,那么就认为该客户端获取到了锁。如果发现自己创建的节点并非locker所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,然后对其调用exist()方法,同时对其注册事件监听器。之后,让这个被关注的节点删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是locker子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。当前这个过程中还需要许多的逻辑判断。
clipboard.png

代码的实现主要是基于互斥锁,获取分布式锁的重点逻辑在于BaseDistributedLock,实现了基于Zookeeper实现分布式锁的细节。

12.Zookeeper队列管理(文件系统、通知机制)
两种类型的队列:
1、同步队列,当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达。
2、队列按照 FIFO 方式进行入队和出队操作。
第一类,在约定目录下创建临时目录节点,监听节点数目是否是我们要求的数目。
第二类,和分布式锁服务中的控制时序场景基本原理一致,入列有编号,出列按编号。在特定的目录下创建PERSISTENT_SEQUENTIAL节点,创建成功时Watcher通知等待的队列,队列删除序列号最小的节点用以消费。此场景下Zookeeperznode用于消息存储,znode存储的数据就是消息队列中的消息内容,SEQUENTIAL序列号就是消息的编号,按序取出即可。由于创建的节点是持久化的,所以不必担心队列消息的丢失问题。

发布了130 篇原创文章 · 获赞 66 · 访问量 30万+

猜你喜欢

转载自blog.csdn.net/zhuzj12345/article/details/103078905