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的,重点学习什么

  • 有关注哪些新的技术

  • 工作任务非常多非常杂时如何处理

  • 项目出现延迟如何处理

  • 和同事的设计思路不一样怎么处理

  • 如何保证开发质量

  • 职业规划是什么?短期,长期目标是什么

  • 团队的规划是什么

  • 能介绍下从工作到现在自己的成长在那里