JAVA架构师面试题01
答案
基础题目
Java多线程
java中有几种方法可以实现一个线程?用什么关键字修饰同步方法?
有两种实现方法,分别使用new Thread()和new Thread(runnable)形式,第一种直接调用thread的run方法,所以,我们往往使用Thread子类,即new SubThread()。第二种调用runnable的run方法。
stop()和suspend()方法为何不推荐使用?
用synchronized关键字修饰同步方法 反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。
sleep() 和 wait() 有什么区别? (网上的答案:sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。 wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。)
sleep就是正在执行的线程主动让出cpu,cpu去执行其他线程,在sleep指定的时间过后,cpu才会回到这个线程上继续往下执行,如果当前线程进入了同步锁,sleep方法并不会释放锁,即使当前线程使用sleep方法让出了cpu,但其他被同步锁挡住了的线程也无法得到执行。wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放。如果notify方法后面的代码还有很多,需要这些代码执行完后才会释放锁,可以在notfiy方法后增加一个等待和一些代码,看看效果),调用wait方法的线程就会解除wait状态和程序可以再次得到锁后继续向下运行。对于wait的讲解一定要配合例子代码来说明,才显得自己真明白。
package com.huawei.interview; public class MultiThread { /** @param args */ public static void main(String[] args) { // TODO Auto-generated method stub new Thread(new Thread1()).start(); try { Thread.sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } new Thread(new Thread2()).start(); }
private static class Thread1 implements Runnable { @Override public void run() { // TODO Auto-generated method stub //由于这里的Thread1和下面的Thread2内部run方法要用同一对象作为监视器,我们这里不能用this,因为在Thread2里面的this和这个Thread1的this不是同一个对象。我们用MultiThread.class这个字节码对象,当前虚拟机里引用这个变量时,指向的都是同一个对象。 synchronized (MultiThread.class) { System.out.println("enter thread1..."); System.out.println("thread1 is waiting"); try { //释放锁有两种方式,第一种方式是程序自然离开监视器的范围,也就是离开了synchronized关键字管辖的代码范围,另一种方式就是在synchronized关键字管辖的代码内部调用监视器对象的wait方法。这里,使用wait方法释放锁。 MultiThread.class.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("thread1 is going on..."); System.out.println("thread1 is being over!"); } } } private static class Thread2 implements Runnable { @Override public void run() { // TODO Auto-generated method stub synchronized (MultiThread.class) { System.out.println("enter thread2..."); System.out.println("thread2 notify other thread can release wait status.."); //由于notify方法并不释放锁, 即使thread2调用下面的sleep方法休息了10毫秒,但thread1仍然不会执行,因为thread2没有释放锁,所以Thread1无法得不到锁。 MultiThread.class.notify(); System.out.println("thread2 is sleeping ten millisecond..."); try { Thread.sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("thread2 is going on..."); System.out.println("thread2 is being over!"); } } } } ```
- 同步和异步有何异同,在什么情况下分别使用他们?举例说明。 如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。 当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
- 多线程同步有几种实现方法? 同步的实现方面有两种,分别是synchronized,wait与notify wait():使一个线程处于等待状态,并且释放所持有的对象的lock。 sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。 notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。 Allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
- 启动一个线程是用run()还是start()? 启动一个线程是调用start()方法,使线程就绪状态,以后可以被调度为运行状态,一个线程必须关联一些具体的执行代码,run()方法是该线程所关联的执行代码。
当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法? 分几种情况: 其他方法前是否加了synchronized关键字,如果没加,则能。 如果这个方法内部调用了wait,则可以进入其他synchronized方法。 如果其他个方法都加了synchronized关键字,并且内部没有调用wait,则不能。
线程的基本概念、线程的基本状态以及状态之间的关系 一个程序中可以有多条执行线索同时执行,一个线程就是程序中的一条执行线索,每个线程上都关联有要执行的代码,即可以有多段程序代码同时运行,每个程序至少都有一个线程,即main方法执行的那个线程。如果只是一个cpu,它怎么能够同时执行多段程序呢?这是从宏观上来看的,cpu一会执行a线索,一会执行b线索,切换时间很快,给人的感觉是a,b在同时执行,好比大家在同一个办公室上网,只有一条链接到外部网线,其实,这条网线一会为a传数据,一会为b传数据,由于切换时间很短暂,所以,大家感觉都在同时上网。
状态:就绪,运行,synchronize阻塞,wait和sleep挂起,结束。wait必须在synchronized内部调用。 调用线程的start方法后线程进入就绪状态,线程调度系统将就绪状态的线程转为运行状态,遇到synchronized语句时,由运行状态转为阻塞,当synchronized获得锁后,由阻塞转为运行,在这种情况可以调用wait方法转为挂起状态,当线程关联的代码执行完后,线程变为结束状态。
简述synchronized和java.util.concurrent.locks.Lock的异同? 主要相同点:Lock能完成synchronized所实现的所有功能 主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。Lock还有更强大的功能,例如,它的tryLock方法可以非阻塞方式去拿锁。 举例说明(对下面的题用lock进行了改写):
package com.huawei.interview; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ThreadTest { /** @param args */ private int j; private Lock lock = new ReentrantLock(); public static void main(String[] args) { ThreadTest tt = new ThreadTest(); for(int i=0;i<2;i++) { new Thread(tt.new adder()).start(); new Thread(tt.new subtractor()).start(); } } private class subtractor implements Runnable { @Override public void run() { while(true) { /*synchronized (ThreadTest.this) { System.out.println("j--=" + j--); //这里抛异常了,锁能释放吗? }*/ lock.lock(); try { System.out.println("j--=" + j--); }finally { lock.unlock(); } } } } private class adder implements Runnable { @Override public void run() { while(true) { /*synchronized (ThreadTest.this) { System.out.println("j++=" + j++); }*/ lock.lock(); try { System.out.println("j++=" + j++); }finally { lock.unlock(); } } } } }
- 设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。 以下程序使用内部类实现线程,对j增减的时候没有考虑顺序问题。
public class ThreadTest1 { private int j; public static void main(String args[]){ ThreadTest1 tt=new ThreadTest1(); Inc inc=tt.new Inc(); Dec dec=tt.new Dec(); for(int i=0;i<2;i++){ Thread t=new Thread(inc); t.start(); t=new Thread(dec); t.start(); } } private synchronized void inc(){ j++; System.out.println(Thread.currentThread().getName()+"-inc:"+j); } private synchronized void dec(){ j--; System.out.println(Thread.currentThread().getName()+"-dec:"+j); } class Inc implements Runnable{ public void run(){ for(int i=0;i<100;i++){ inc(); } } } class Dec implements Runnable{ public void run(){ for(int i=0;i<100;i++){ dec(); } } } }
进程和线程的区别,进程间如何通讯,线程间如何通讯
- 进程和线程的区别: 进程是操作系统分配资源(包括cpu)的基本单位 线程是cpu执行的基本单位,多个线程共享系统分配给进程的资源 一个进程可以有多个线程,他们是一对多的关系
- 进程间通信: rpc mq socket
- 线程间通信: 共享内存 wait/notify pipleline
HashMap的数据结构是什么?如何实现的。和HashTable,ConcurrentHashMap的区别
HashMap的数据结构: 数组+链表,数组中元素是个链表,存储Key的hashcode碰撞的元素 其中元素的节点为:
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
每个Node含有指向下一个Node的指针 数组(HashMap大小)的初始长度16
/** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 数组的增长因子,0.75 /** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
HashMap的实现重点需要注意的在两个方面,一个是链表结构,一个是table的resize()
HashMap处理hashcode碰撞的方式用链表,hashcode相同的元素头尾相连组成一个单链,并把最开始的那个节点存储在数组中,访问的时候,先通过hash(key)找到数组下标,再迭代单链找到equals()的value,然后返回
resize的时候,如果当前数组的占用率达到负载因子0.75,则会触发一次resize(),增长量为原来容量(table.length)的一倍,newCap = oldCap << 1
然后把老数组的数据迁移到新数组
HashMap和HashTable的区别: 他们的结构差不多,只不过HashTable是线程安全的,HashTable是所有暴露的操作都加锁,synchronized,这种情况下性能比较低,容易引起活跃性问题
HashTable跟java.util.Collections#synchronizedMap很接近
HashMap允许key和value为null
HashTable不允许key和value为null
ConcurrentHashMap也是线程安全的,是采用CAS的方式来处理并发操作,如果单链比较长就坍缩为一个红黑树,logn的时间复杂度
ConcurrentHashMap要分jdk1.8之前还是之后
1.8之前的ConcurrentHashMap是采用分段(Segment)的方式,加锁时直接在Segment上加锁,缩小了加锁范围,提高了性能
1.8之后的ConcurrentHashMap是重写的,加锁范围进一步缩小,采用CAS将加锁范围缩小到单个数组元素
性能上ConcurrentHashMap比前面的要高
Cookie和Session的区别 两者都是保存用户回话状态的方案
Cookie是将用户会话保存在浏览器端,安全性问题比较低,用户可见,容易被篡改和盗取,csrf攻击
Session是将用户会话状态保存在服务端,安全性较高,用户不可见
但是Session需要占用服务端资源,集群环境下需要注意Session同步的问题,比如tomcat的session同步方案,小集群还好,集群一大同步session就占用了很多内部带宽和cpu资源
比较常用的方案是将用户会话保存在中央缓存服务器上,在cookie里面记录一个缓存key,每次都从中央缓存服务器获取用户登录态
索引有什么用?如何建索引? 索引可以加快数据库访问的效率,相当于给原来的记录作一个key-value的结构
数据库里面索引是用树来做的,B+数
搜索中也用到了索引
索引分为:
普通索引
唯一索引
聚集索引
主键索引
联合索引
ALTER TABLE <表名> ADD INDEX (<字段>);
ArrayList是如何实现的,ArrayList和LinkedList的区别?ArrayList如何实现扩容。
ArrayList比较简单,主要是通过数组来实现的 需要注意的是其初始容量是10
/** * Default initial capacity. */ private static final int DEFAULT_CAPACITY = 10;
需要注意增长方法grow()
/** * Increases the capacity to ensure that it can hold at least the * number of elements specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */ private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
只要size > 数组的长度,就会触发grow,其中增长比例是原来的容量的一半
int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1);
然后把原来数组的内容拷贝到新的数组
ArrayList和LinkedList的区别 ArrayList是通过数组来实现的,读取性能很高,随机访问时间复杂度为O(1),适用于读大于写的场景
LinkedList是是通过双向队列来实现的,更新效率更高,写只需要修改前后两个节点的相关引用,但是读取效率比较低,需要最多遍历一半长度的队列,适用与写大于读的场景
equals方法实现 equals()方法需要根据业务而来,取对象属性中标识对象业务唯一标识来进行比较
实现了equals方法,同时需要实现hashcode方法,为了维护统一性
推荐 http://blog.sina.com.cn/s/blog_700aa8830101jtlf.html
说到equals不得不提到一个问题,equals和hashcode方法,实现了equals方法为什么要实现hashcode方法
是为了保证统一性,重写了equals方法,同时也需要重写hashcode方法
在HashMap场景下,put的时候,是先根据key的hashcode来定位到是在哪个bucket(数组下标),然后再通过遍历单链来查找equals的对象
如果重写了equals没有重写hashcode,那么容易导致混乱
equals相同,hashcode相同么?
:equals相同,hashcode一定相同
hashcode相同,equals相同么?
:hashcode相同,equals不一定相同,比如HashMap的bucket的单链全是hashcode相同的
面向对象
线程状态,BLOCKED和WAITING有什么区别 线程状态中Blocke和Waiting(Time_Waiting)的区别
入口区等待获取锁的线程状态为Blocked,获取锁失败,然后线程就排队等待
等待区等待被唤醒的线程状态为Waiting(Time_Waiting),线程在获取锁后调用自身的wait()方法,然后释放锁,进入等待区等待
JVM如何加载字节码文件
JVM GC,GC算法。 1,对象存活性检测
1,引用计数器算法
每增加引用+1,每引用失效-1,为0则未被使用
优点:简单,高效
缺点:不能解决对象循环依赖问题
Hotspot未采用引用计数器算法
2,可达性分析算法:GC Roots(Java中采用)
GC Roots对象作为起始点,向下搜索引用链,不可及的对象则视为不可用
GC Roots对象:
1,虚拟机栈(栈帧本地变量表)中引用的对象
2,方法区类静态属性引用的对象
3,方法区常量引用的对象
4,本地方法栈JNI引用的对象
3,引用分类
1,强引用:引用存在虚拟机永不回收
2,软引用:内存溢出之前列入回收队列,回收后还是内存不足则oom,SoftReference
3,弱引用:生存到下一次GC之前,WeakReference
4,虚引用:毫无影响,只是在被回收的时候收到系统通知,PhantomReference
强引用 > 软引用>弱引用>虚引用
4,回收两次标记过程:
1st:GC Roots引用链不可达&&对象finalize方法有必要执行,放入F-Queue队列,虚拟机开启一个Finalizer线程执行队列中对象的finalize方法
2nd:对F-Queue队列中的对象第二次标记,(如果在对象的finalize()方法中将自己关联到GC Roots的引用链上,则不会回收),未逃离回收集合则被回收
5,回收方法区(在永久代)
主要是常量和类
常量:没有引用则回收,包括类/接口/方法/字段的符号引用
类:同时满足3个条件
1,其所有实例都已经被回收
2,加载该类的ClassLoader已经被回收
3,该类的Class对象没有被引用,且无法通过反射访问该Class对象
2,GC 回收算法
1,标记-清除算法(最基础的收集算法)
分为标记(4,回收两次标记过程)和清除两个阶段
不足:
1,效率问题:标记和清除过程效率都不高
2,空间问题:标记清除产生大量不连续的内存碎片,无法分配较大的对象(触发另一次GC)
2,复制算法(解决效率问题)
1,内存分为大小相等的两块,使用其中一块儿分配内存,回收时将仍然存活的对象复制到另外一块儿内存,一次清理当前内存块(没有内存碎片),分配内存时顺序分配
优点:简单高效
缺点:内存缩小一半
2,jdk中,将内存分为一大(Eden空间)两小(Survivor空间),每次使用Eden+其中一块儿Survivor,回收时将Eden+和使用的那块儿Survivor中存活的对象复制到另外一个Survivor空间,清理掉原来的Eden+Survivor,默认Eden:Survivor=8:1
3,老年代的分配担保
3,标记-整理算法(老年代的回收)
与标记-清除的区别:标记完成后不清理,而是将存活的对象移动到内存的另一端,清理掉边界外的内存(对象)
4,分代收集算法
根据对象生命周期将内存划分为几块,一般划分为新生代和老年代,新生代回收用复制算法,老年代回收用标记-清理,标记-整理算法
堆区: 新生代 Eden,Survivor1,Survivor2 老年代 3, 1,枚举根节点 一致性,STW,OopMap 2,safepoint 让程序长时间执行的特征作为条件 3,safeRegion 代码片段中,引用关系不会发生变化 3,垃圾回收器 新生代:Serial,ParNew,Parallel Scavenage, G1 老年代:CMS,Serial Old(MSC),Parallel Old,G1 新老搭配关系: Serial---->CMS,Serial Old ParNew---->CMS,Serial Old(MSC) ParallelScanvenge----->Serial Old,Parallel Old //------------------------------------------------ G1---->G1 CMS---->Serial,ParNew,Serial Old(老) Serial Old--->CMS(老),Serial,ParNew,Parallel Scanvenge Parallel Old ---->Parallel Scanvenge G1-----> G1 1,Serial收集器 最基本的,STW,单线程 Client下新生代默认的收集器,因为新生代比较小,简单高效 复制算法 2,ParNew收集器 Serial的多线程版本 采用复制算法 Server模式下首选的新生代收集器,新生代只有ParNew和Serial能配合CMS(老年代)工作 单核CPU效果不明显 3,Parallel Scavenge收集器 复制算法,多线程 吞吐量优先,吞吐量=运行用户代码时间/(运行用户代码时间+GC垃圾收集时间),高吞吐量适合后台运算交互不多的任务 GC自适应调节策略 -XX:UseGCAdaptiveSizePolicy 4,Serial Old收集器 标记-整理算法,单线程 Client模式下使用,CMS收集器的后备预案 图见Serial收集器 5,Parallel Old收集器 标记-整理算法,多线程 注重吞吐量和CPU资源敏感的场景,Parallel Scanvenge + Parallel Old 6,CMS收集器 目标:最短回收停顿时间,注重相应性能 标记-清除算法 4个步骤: 初始标记:STW,标记GC Roots直接关联的对象,很快 并发标记:GC Roots Tracing,耗时长,可以用用户线程并发 重新标记:修正并发期间用户线程产生的标记变动,时间短于并发标记,长于初始标记 并发清除:可以用用户线程并发 优点:并发收集,低停顿 缺点: 对CPU资源比较敏感,CPU数量少的时候,占用较多CPU资源,改进算法i-CMS(然并卵) CMS无法处理浮动垃圾,并发清理时用户线程也在生成垃圾,在Concurrent Mode Failed时,后备预案,临时启用Serial Old来收集老年代,时间变长 大量空间碎片,标记-清除算法的通病,内存碎片的合并整理 7,G1收集器 目标是替换调CMS 整体来看是标记-整理算法 特点: 并行与并发:多CPU缩短STW时间,用户程序仍在并发 分代收集:单独的算法处理新创建对象和老对象 空间整合:Region局部采用复制算法,不会有空间碎片 可预测停顿:可预测停顿时间模型,降低停顿时间 内存布局的变化:没有新生代和老年代的区别,所有都划分为等大小的Region区,新老不再物理隔离 避免全区域垃圾回收,根据Region判断回收价值,回收价值最大的Region(G1),有限时间获取更高的回收效率 可达性判断问题更突出,采用Region的RemeberSet来处理 回收过程: 初始标记 并发标记 最终标记:并发标记将对象变化记录在线程的Remebered Set Log,在这个阶段将RemeberSetLog中的变化记录到RememberSet中 筛选回收 4,内存分配和回收策略 在堆(Heap)上分配(JIT标量类型间接在栈上分配),主要是在Eden区上分配,TLAB,少数直接在老年代分配 对象优先在Eden区分配(分配) Eden区空间不足,触发Minor GC 新生代(Eden区+一个Survivor区) 新生代GC:Minor GC 老年代GC:Full GC/Major GC,Full GC经常伴随至少一次Minor GC 大对象直接进入老年代(分配) -XX:PretenureSizeThreshold,大于这个参数值的对象直接分配在老年代(只对ParNew和Serial收集器有效) 长期存活的对象将进入老年代 对象年龄计数器,每熬过一次Minor GC,则年龄计数器+1,达到阈值(-XX:MaxTenuringThreshold,默认15)则对象晋升到老年代 动态对象年龄判断 Survivor中相同年龄X的对象大小总和 > 1/2 Survivor空间,则对象年龄>=X的对象直接进入老年代 空间分配担保
什么情况会出现Full GC,什么情况会出现yong GC。
JVM内存模型 Java内存模型 JMM:定义变量(字段实例,静态字段,不包括局部变量与方法参数,因为是线程私有的)的访问规则
1,主内存与工作内存
主内存
线程工作内存,变量副本
线程间变量传递,通过主内存传递
2,主内存和工作内存交互操作 交互协议:8种操作,每种操作原子化,不可以细分 lock unlock ##################### read---load store-write ##################### use assign 交互操作原则:8个规则 volatile的特性 可见性,不能保证原子性(所以并发下并非线程安全) 禁止指令重排序优化:内存屏障,指令重排序 volatile变量的特殊规则:read,load,use,assign,store,write的特殊规则 long和double的特殊规则: 64位的数据类型 非原子性协定 3, 原子性,基本类型数据操作为原子性的,更大范围的使用lock/unlock--->monitorenter/monitorexit--->synchronized关键字 可见性,变量修改后将新值同步会主内存,在变量读取前从主内存刷新变量值,共享主内存这种方式实现可见性 volatile变量的区别是新值能立即同步会主存,每次使用前立即从主存刷新,但是线程操作数栈中的变量不会立即刷新 有序性 线程内表现为串行语意 指令重排序 工作内存与主存同步延迟 volatile保证有序性:禁止指令重排序语意 synchronized保证有序性:一个变量在同一时刻只允许一个线程对其lock操作 4,先行发生原则(happens-before):8个 程序次序规则:同一线程,控制流顺序 管程锁定规则:同一个锁,unlock先于lock volatile变量规则:同一个变量,写先于读 线程启动规则 线程终止规则 线程中断规则 对象终结规则 传递性:
Java运行时数据区
事务的实现原理
技术深度
有没有看过JDK源码,看过的类实现原理是什么。
HTTP协议
TCP协议
一致性Hash算法
JVM如何加载字节码文件
类加载器如何卸载字节码
IO和NIO的区别,NIO优点
Java线程池的实现原理,keepAliveTime等参数的作用。 线程池ThreadPoolExecutor中其实有两个比较重要的概念,
一个是线程组,
一个任务队列,是一个LinkedBlockedQueue
通过外部把任务提交到任务队列当中,线程从任务队列中取出任务进行执行,任务执行完成之后线程本身不会释放,而是归还到线程组当中
下一个任务来的时候直接从线程组中取一个线程来处理
任务拒绝策略
LinkedBlockQueue需要是线程安全的,线程安全模型分析
线程池-ThreadPoolExecutor 线程池的原理:
由于咱们新版jdk中java线程和操作系统线程是一对一的,所以启动线程是通过jvm调用操作系统接口来创建线程的(Thread.start(start0()是个native方法))
然后start0方法启动的操作系统线程如何调用Thread的run方法呢?没错就是通过jvm来调用的java中Thread的run方法的
那么线程池的原理就是通过一个计数器和work队列来维护线程池的大小 每创建一个Thread(通过Work来包装的从queue队列中取出来的Runnable实现),则以CAS的方式增加计数器,并将新增的work放入work队列 在Work的执行过程中,异常退出,则计数器-1,并且重新生成一个work来继续处理
HTTP连接池实现原理
数据库连接池实现原理
数据库的实现原理
技术框架
看过哪些开源框架的源码
为什么要用Redis,Redis有哪些优缺点?Redis如何实现扩容?
Netty是如何使用线程池的,为什么这么使用
为什么要使用Spring,Spring的优缺点有哪些
Spring的IOC容器初始化流程
Spring的IOC容器实现原理,为什么可以通过byName和ByType找到Bean
Spring AOP实现原理
消息中间件是如何实现的,技术难点有哪些
系统架构
如何搭建一个高可用系统 高可用系统,就是说要保证系统在几乎任务时候都要有正常运行,功能正常
我们来看下哪些情况会造成系统不可用
单机系统下的可用性问题,从nginx->tomcat->db/soa来看,单点问题会影响系统高可用,比如要是这个这个链路上其中一个单点挂了,那么整个系统都不可用了
所以引申出来主备/集群模式,防止单点问题
高并发场景下,请求过多也会因为后端瓶颈点引起整个系统down掉,
所以一般情况下应对高并发场景我们会限流,比如今年的英雄联盟抢票,周杰伦抢票
通过采用mq等队列形式削峰,保证后端系统不会down掉
熔断机制
容灾机制,多机房部署
综上所述:
1,主备/集群模式,防止单点
2,限流,削峰,防止后端压力过大
3,熔断机制,类似与限流
4,容灾机制,多机房/异地部署
哪些设计模式可以增加系统的可扩展性 可扩展性:
工厂模式
抽象工厂模式
观察者模式:很方便增加观察者,方便系统扩展
模板方法模式:很方便的实现不稳定的扩展点,完成功能的重用
适配器模式:可以很方便地对适配其他接口
代理模式:可以很方便在原来功能的基础上增加功能或者逻辑
责任链模式:可以很方便得增加拦截器/过滤器实现对数据的处理,比如struts2的责任链
策略模式:通过新增策略从而改变原来的执行策略
介绍设计模式,如模板模式,命令模式,策略模式,适配器模式、桥接模式、装饰模式,观察者模式,状态模式,访问者模式。 模板模式:就是基类封装好了业务逻辑,抽象出了不稳定的部分,让子类来实现,比如
命令模式:
策略模式:将变化的部分抽象成策略,通过替换不同的策略来完成业务逻辑处理的变化,比如超时活动价格策略
适配器模式:将现有的功能转换成已经给定的接口实现,比如:jdbc的适配器模式,jdbc定义好操作模式,不同的db针对jdbc来做不同的适配
桥接模式:
装饰模式:
观察者模式:listener模式,将操作反向依赖到变化的事物上,例如:Spring的ApplicationEvent
状态模式:
访问者模式:
抽象能力,怎么提高研发效率。 我们需要解决的问题,我们需要通过程序来解决这些问题
如何将问题抽象成计算机可以识别逻辑,通过抽象能力,把现实生活中的问题域转化成计算机中可以识别的抽象问题,然后就可以通过计算机中的处理方式来解决现实生活中的问题
什么是高内聚低耦合,请举例子如何实现
什么情况用接口,什么情况用消息 接口的特点是同步调用,接口实时响应,阻塞等待
消息的特点是异步处理,非实时响应,消息发送后则返回,消息队列可以削峰
一般对实时性要求比较高的功能采用接口
对实时性要求不高的功能可以采用消息,削峰时可以采用消息
如果AB两个系统互相依赖,如何解除依赖 A--->B,同时B--->A
解除这种双向依赖的话,需要在AB之外增加一个C,用C封装A依赖的B的那部分功能,让A改为依赖C,C依赖B
然后就是这样
A--->C,C---->B,B--->A
不过这样依然存在环路依赖
如何写一篇设计文档,目录是什么
什么场景应该拆分系统,什么场景应该合并系统 拆分系统:
当系统通过集群的方式已经无法解决性能问题的时候,或者业务扩展到很大的时候,需要把拆分系统
按照业务的方式垂直拆分:将业务功能结合比较紧密的部分拆分成独立的系统,独立维护
按照性能瓶颈点拆分:将系统性能瓶颈点拆分出一个独立的系统,可以针对这个独立的系统集群部署,增加可伸缩性,提高系统整体的性能
合并系统:
或者系统间通过跨进程访问的性能损耗过高,可以将系统合并成一个系统,减少跨进程访问的消耗
系统和模块的区别,分别在什么场景下使用 系统和模块
系统是一个完整功能的系统,拥有独立的访问方式,和部署方式,拥有完整的生命周期,系统由模块组成
模块是系统的组成部分,不能单独工作,需要依附于系统才能发挥作用,通常是解决一定场景下的问题
系统用于系统性解决问题的方案
模块是针对单个问题方面的解决方案
分布式系统
分布式事务,两阶段提交。 分布式事务:
XA:两阶段提交,第一阶段锁资源,第二阶段commit
第一阶段通过begin预锁定其他库的资源
第二阶段再commit/rollback执行或者回滚之前预锁定的资源
这之间涉及到一个事务隔离级别
ACID中的事务隔离级别
读未提交
读已提交
可重复读
序列化
如何实现分布式锁 分布式锁实现方式:
两种:
一种是采用在全局缓存(redis/mc)中加一个锁
一种是采用zk来实现
如何实现分布式Session 把用户session存放在中央全局缓存
如何保证消息的一致性
负载均衡 负载均衡是平均后端集群系统的访问压力,从而提高集群的整理访问qps
有很多负载均衡策略
也有很多地方在用
nginx负载均衡tomcat集群
dns负载均衡nginx
数据库访问DAL的负载均衡db
soa服务框架负载均衡服务提供者,比如dubbo框架负载均衡
正向代理(客户端代理)和反向代理(服务器端代理)
CDN实现原理 CDN的原理
cdn其实就是一个带缓存的反向代理
通过把用户需要的资源推送到离用户最近的地方,加快用户访问资源的速度
这些缓存就是用户需要访问的资源
怎么提升系统的QPS和吞吐量 简单而言通过增加集群来提升qps和吞吐量
实际上要比这个要复杂
首先我们需要知道系统的瓶颈
我们所知道的系统拓扑架构
对于rest接口而言
系统设施依次是:
dns
nginx
tomcat
db/soa
首先我们可以通过增加集群来增加qps和吞吐量
其次考虑到负载均衡的问题,我们可以通过其他设施来保证集群节点的负载均衡,进一步提高系统qps
于是就有nginx集群+负载均衡
tomcat集群+负载均衡
到db/soa这一层的时候,同样也可以通过增加集群+负载均衡的方式来解决
我们还可以在每一层增加缓存来应对热点数据
然而另外一个方面,可以系统拆分,服务拆分,分别针对瓶颈的系统单独增加集群和负载均衡来解决
同样db也可以分库分表,
因为单表超过1000万条数据时就很慢了,所以这个时候就需要库拆分,于是就有垂直拆分,水平拆分。
异步化,可以不同调用的异步化,使用mq,比如发送短信,发送邮件等
综上所述: 集群+负载均衡 增加缓存 系统拆分 分库分表 垂直拆分+水平拆分 异步化+MQ
实战能力
有没有处理过线上问题?出现内存泄露,CPU利用率标高,应用无响应时如何处理的。
开发中有没有遇到什么技术问题?如何解决的
如果有几十亿的白名单,每天白天需要高并发查询,晚上需要更新一次,如何设计这个功能。
新浪微博是如何实现把微博推给订阅者
Google是如何在一秒内把搜索结果返回给用户的。
12306网站的订票系统如何实现,如何保证不会票不被超卖。
如何实现一个秒杀系统,保证只有几位用户能买到某件商品。 设计这个系统是一个考虑全面的问题,可以发散出很多问题,考察很多方面,不是仅仅回答通过redis的自减操作完成
比如简单的方案:
1,页面开启倒计时,要保证不能把下单接口暴露过早暴露出来,防止机器刷下单接口
2,前端限流,比如nginx对下单接口限流,命中限流则返回302到秒杀页
3,后端单独部署,独立域名和nginx,与线上正常运行的系统隔离开来,避免影响到线上环境
4,由于生成订单操作比较耗时,采用队列的方式来解耦下单成功和生成订单,针对进入后端的请求,采用redis自减,针对自减结果>0的请求则认为下单成功,触发一个生成订单的消息,然后立即返回给用户结果
5,用户方面,针对秒杀成功有两种处理方式
a,用户端收到秒杀成功的结果,则开启提示页面,并进入倒计时,倒计时时间为订单生成的预估时间
b,秒杀成功后,给当前用户在redis中生成一个订单生成状态的标识,用户端开启提示页面,loading,并轮询后端订单生成状态,生成成功之后让前端跳转到订单页面
6,订单服务订阅下单系统发送的消息,并开始生成订单,生成订单成功之后更新redis中用户秒杀订单的状态为已生成订单
系统应该有页面和接口
页面用于展示用户界面,接口用于获取数据
界面:秒杀页面,秒杀成功页面,秒杀失败页面,命中限流页面(查看订单页面不算秒杀系统的功能)
接口:秒杀下单接口,秒杀成功获取订单生成状态接口
软能力
如何学习一项新技术,比如如何学习Java的,重点学习什么
有关注哪些新的技术
工作任务非常多非常杂时如何处理
项目出现延迟如何处理
和同事的设计思路不一样怎么处理
如何保证开发质量
职业规划是什么?短期,长期目标是什么
团队的规划是什么
能介绍下从工作到现在自己的成长在那里