字节跳动后端开发面经

提前批

No.1

  1. Java多态的原理?

    答:参考:

    https://blog.csdn.net/SEU_Calvin/article/details/52191321

    https://zhuanlan.zhihu.com/p/27912079

  2. Java 中接口和抽象类的区别?

    答:

    • 抽象类中可以有普通的成员变量,而接口中没有。
    • 抽象类中可以包含静态方法,而接口不可以。
    • 如果抽象类实现接口,则可以把接口中方法映射到抽象类中作为抽象方法而不必实现,而在抽象类的子类中实现接口中方法
    • 抽象类只能单继承,而接口可以被多个类实现。
    • 抽象类中可以有构造方法,而接口中不可以。
    • 抽象类可以但不是必须有抽象属性和抽象方法,但是一旦有了抽象方法,就一定要把这个类声明为抽象类。
    • 在传统版本上,接口中的所有方法必须是非静态的,且是abstract的,且是public的。普通方法可以不写修饰符,也会默认为public和abstract。但在java版本1.8中,你可以为方法添加默认方法,这时候实现类不继承该方法也是可以编译通过的。
  3. Java中四种引用的关系?

    答:

    • 强引用:强引用是最普遍的引用,如果一个对象具有强引用,垃圾回收器不会回收该对象,当内存空间不足时,JVM 宁愿抛出 OutOfMemoryError异常;只有当这个对象没有被引用时,才有可能会被回收。

    • 软引用:如果一个对象只具有软引用,则

      • 当内存空间足够,垃圾回收器就不会回收它。
      • 当内存空间不足了,就会回收该对象;
      • JVM会优先回收长时间闲置不用的软引用的对象,对那些刚刚构建的或刚刚使用过的“新”软引用对象会尽可能保留;
      • 如果回收完还没有足够的内存,才会抛出内存溢出异常。只要垃圾回收器没有回收它,该对象就可以被程序使用。

      软引用可以和一个引用队列(ReferenceQueue)联合使用。如果软引用所引用对象被垃圾回收JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
      String str = new String("abc");
      SoftReference<String> softReference = new SoftReference<>(str, referenceQueue);

      str = null;
      // Notify GC
      System.gc();

      System.out.println(softReference.get()); // abc

      Reference<? extends String> reference = referenceQueue.poll();
      System.out.println(reference); //null

      注意:软引用对象是在jvm内存不够的时候才会被回收,我们调用System.gc()方法只是起通知作用,JVM什么时候扫描回收对象是JVM自己的状态决定的。就算扫描到软引用对象也不一定会回收它,只有内存不够的时候才会回收。

      应用场景:

      浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。

      1. 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;
      2. 如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。

      这时候就可以使用软引用,很好的解决了实际的问题:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // 获取浏览器对象进行浏览
      Browser browser = new Browser();
      // 从后台程序加载浏览页面
      BrowserPage page = browser.getPage();
      // 将浏览完毕的页面置为软引用
      SoftReference softReference = new SoftReference(page);

      // 回退或者再次浏览此页面时
      if(softReference.get() != null) {
      // 内存充足,还没有被回收器回收,直接获取缓存
      page = softReference.get();
      } else {
      // 内存不足,软引用的对象已经回收
      page = browser.getPage();
      // 重新构建软引用
      softReference = new SoftReference(page);
      }
    • 弱引用:弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期,它只能生存到下一次垃圾收集发生之前。当垃圾回收器扫描到只具有弱引用的对象时,无论当前内存空间是否足够,都会回收它。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

      注意:如果一个对象是偶尔(很少)的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用Weak Reference来记住此对象。

    • 虚引用:虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

      应用场景:

      虚引用主要用来跟踪对象被垃圾回收器回收的活动。 虚引用软引用弱引用的一个区别在于:

      虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

      1
      2
      3
      4
      String str = new String("abc");
      ReferenceQueue queue = new ReferenceQueue();
      // 创建虚引用,要求必须与一个引用队列关联
      PhantomReference pr = new PhantomReference(str, queue);

      程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

      下面通过一张表格来说明它们的回收时间、用途:

      z1

      参考:https://juejin.im/post/5b82c02df265da436152f5ad#heading-3

  4. Java多线程实现的几种方式?Runnable接口有哪些优势?

    答:

    • 通过继承Thread类来实现

    • 通过实现Runnable接口

    • 通过内部类实现(有些情况我们的线程就想执行一次,以后就用不到了)

    • 基于线程池的方式实现

    • 带有返回值的线程实现方式,步骤如下:

      • 创建一个类实现Callable接口,实现call方法。这个接口类似于Runnable接口,但比Runnable接口更加强大,增加了异常和返回值。
      • 创建一个FutureTask,指定Callable对象,做为线程任务。
      • 创建线程,指定线程任务
      • 启动线程

      Runnable接口的实现优势有:

      • 使用接口的方式可以让我们的程序降低耦合度,Runnable就是一个线程任务,线程任务和线程的控制分离,那么一个线程任务可以提交给多个线程来执行。比如车站的售票窗口,每个窗口可以看做是一个线程,他们每个窗口做的事情都是一样的,也就是售票。这样我们程序在模拟现实的时候就可以定义一个售票任务,让多个窗口同时执行这一个任务。那么如果要改动任务执行计划,只要修改线程任务类,所有的线程就都会按照修改后的来执行。相比较继承Thread类的方式来创建线程的方式,实现Runnable接口是更为常用的。
      • java不允许多继承,因此实现了Runnable接口的类可以再继承其他类。

      参考:https://blog.csdn.net/king_kgh/article/details/78213576

  5. Java中堆栈的区别?堆栈的增长方向有哪些不同?

    答:

    • 堆中主要存放new出来的实例变量和数组栈描述的是Java方法执行的内存模型,而栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
    • 对比堆和栈,只要把握一个重要的信息,new的对象是存储在堆中的,但为了存取方便,会在栈中声明一个引用变量指向堆中的对象,这样存取方便而且速度快。另一方面,栈中的对象超过作用域即被释放,Java会立即释放掉内存空间另作他用;而堆中即使超出变量的作用范围也不会变成垃圾,只有在没有引用变量指向它时会变成垃圾,但又不会立刻被回收,只有在一个不确定的时间才会被垃圾回收器回收掉,这也就是为什么Java会比较占用内存的原因了。再一点还要注意,就是被static修饰的变量,它是在程序启动之初就会被分配内存,它的生命周期会一直到程序结束,所以使用static一定要慎重。

    栈的增长方向是从高地址到地址,堆的增长方向是从低地址到高地址(简化的Linux/x86模型)

  6. 输入一个url,发生了什么?

    答:

    1. 浏览器查询DNS,获取域名对应的IP地址,即是服务器的IP地址。其中查询DNS的过程先对浏览器自身DNS缓存的查询,浏览器自身没有缓存对应的DNS,就去查询操作系统中的DNS缓存,操作系统中没有,就去读取本地的Host文件。如果上述都找不到对应的DNS缓存,就去本地域名服务器查询对应的DNS缓存。
    2. 浏览器获取到域名对应的IP地址后,就向服务器发起三次握手,建立连接。
    3. TCP/IP连接建立后,浏览器向服务器发送HTTP请求。
    4. 服务器收到请求后,根据路径参数映射到特定的请求处理器进行处理,并将处理结果及响应的视图返回给浏览器。
    5. 浏览器解析视图,如果遇上js、css等文件的引用,则继续向服务器发送资源请求。
    6. 浏览器根据资源、数据进行渲染,呈现一个完整的页面。
    7. 最终,浏览器向服务器发起四次挥手,关闭连接。

    这个简单的过程涉及到的协议有:DNS协议、TCP协议、IP协议、OSPF路由选择协议、ARP协议、HTTP协议。

  7. ping的原理是什么?

    答:

    z3

    PING过程的两种情况,一种是在同一网段内,一种是在不同网段内:

    • 在同一网段内(主机Aping主机B):

      • 如果主机A当中缓存有主机B的MAC地址(即主机A当中的MAC地址表有B的MAC地址),就会直接将这个MAC地址封装到ICMP报文中向主机B发送,当主机B收到了这个报文后,发现是主机A的ICPM回显请求,就按同样的格式,将ICMP的回显报文发送回去给主机A,这样就完成了同一网段内的ping过程。

      • 如果主机A的MAC地址表没有B的MAC地址,则会向外发送一个ARP广播报文。交换机会收到这个报文后,交换机有学习MAC地址的功能,所以他会检索自己有没有保存主机B的MAC地址,如果有,就返回给主机A,如果没有,就会向所有端口发送ARP广播,其它主机收到后,发现不是在找自己,就纷纷丢弃了该报文,不去理会。直到主机B收到了报文后,就立即响应,我的MAC地址是多少,同时学到主机A的MAC地址,并按同样的ARP报文格式返回给主机A。这时候主机A学习到了主机B的MAC地址,就把这个MAC地址封装到ICMP协议的二层报文中向主机B发送。当主机B收到了这个报文后,发现是主机A的ICPM回显请求,就按同样的格式,将ICMP的回显报文发送回去给主机A,这样就完成了同一网段内的ping过程。

      • 下面是ICMP报文和ICMP回显报文:

        z2

    • 在不同网段内(主机Aping主机C),如果主机A要ping主机C,那么主机A发现主机C的IP和自己不是同一网段,他就去找网关转发:

      • 如果主机A的MAC地址表中有网关的MAC地址,则直接将这个MAC地址封装成ICMP报文发送给网关路由器。z5当路由器收到主机A发过来的ICMP报文,发现目的地址是其本身的MAC地址,根据目的IP2.1.1.1,查路由表,发现2.1.1.1/24的路由表项,得到一个出口指针,这个出口指针意指主机C的MAC地址。然后去掉原来的MAC头部,加上自己的MAC地址向主机C转发(如果网关也没有主机C的MAC地址,还是要像前面一个步骤一样,ARP广播一下即可相互学习。路由器2端口能学到主机C的MAC地址,主机C也能学到路由器2端口的MAC)。最后,在主机C已学到路由器2端口MAC,路由器2端口转发给路由器1端口,路由1端口学到主机A的MAC的情况下,他们就不需要再做ARP解析,就将ICMP的回显报文发送回去。

      • 如果主机A的MAC地址表没有网关的MAC地址,会先发送一个ARP广播,学到网关的MAC,再发封装ICMP报文给网关路由器z5后面的过程跟上述一样。

      • 下面是ICMP报文和ICMP回显报文:

        z4

    参考:

    https://blog.csdn.net/f2006116/article/details/51159895

    https://baike.baidu.com/item/ICMP

    https://zh.wikipedia.org/wiki/%E5%9C%B0%E5%9D%80%E8%A7%A3%E6%9E%90%E5%8D%8F%E8%AE%AE

  8. http1.0和http1.1之间有什么区别?

    答:

    • HTTP1.1支持长连接和请求的流水线处理,并且默认使用长连接。而HTTP1.0默认使用短连接,需要在request中增加“Connection:keep-alive”,header才能支持长连接。
      • 流水线的方式处理请求:客户端每遇到一个对象引用就立即发出一个请求,而不必等到收到前一个响应之后才能发出下一个请求。
    • 分块传输数据:发送方将消息实体分割成为任意大小的组块,并单独地发送。在每个组块之前,加上该组块的长度,使接收方可以确保自己能够完整地接收到这个组块。并且,在组块最末尾的地方,发送方生成了长度为零的组块,接收方可据此判断整条消息是否已安全地传输完毕。
    • HTTP1.1新增了一个状态码 100 Continue,用于客户端在发送POST数据给服务器前,征询服务器的情况,看服务器是否处理POST的数据
  9. HTTP的请求头里都包含了些什么?HTTP如何发起请求?

    答:

    HTTP报文由3部分组成(请求行+请求头+请求体)

    z1

    • 请求行:包括请求方法、请求URI、HTTP请求的协议及版本。

      方法-URI-协议/版本:如GET /index.jsp HTTP/1.1

      • GET就是请求方法,根据HTTP标准,HTTP协议请求可以使用多种请求方法。HTTP 1.1支持七种请求方法:GET、POST、HEAD、OPTIONS、PUT、DELETE和TRACE等。常用的为请求方法是GET和POST。
      • /index.jsp表示URI,URI指定了要访问的网络资源
      • HTTP/1.1是协议和协议的版本。
    • 请求头:即HTTP请求的报文头。报文头包含若干个属性,格式为“属性名:属性值”(键值对),服务端据此获取客户端的信息。

      z8

      z9

    • 请求体:即HTTP请求的报文体。它将一个页面表单中的组件值通过param1=value1&param2=value2的键值对形式编码成一个格式化串,它承载多个请求参数的数据。不但报文体可以传递请求参数,请求URL也可以通过类似于“/chapter15/user.html? param1=value1&param2=value2”的方式传递请求参数。

      z2

      下面列举常见的HTTP请求报文头属性

      • Accept:请求报文可通过一个“Accept”报文头属性告诉服务端 客户端接受什么类型的响应。

        如下报文头相当于告诉服务端,俺客户端能够接受的响应类型仅为纯文本数据啊,你丫别发其它什么图片啊,视频啊过来,那样我会歇菜的~~~:

        1
        Accept:text/plain
      • Cookie:客户端的Cookie就是通过这个报文头属性传给服务端的哦!如下所示:

        1
        Cookie: $Version=1; Skin=new;jsessionid=5F4771183629C9834F8382E23BE13C4C

        服务端是怎么知道客户端的多个请求是隶属于一个Session呢?

        注意到后台的那个jsessionid=5F4771183629C9834F8382E23BE13C4C木有?原来就是通过HTTP请求报文头的Cookie属性的jsessionid的值关联起来的!(当然也可以通过重写URL的方式将会话ID附带在每个URL的后面哦)

      • Referer:表示这个请求是从哪个URL过来的。

        假如你通过google搜索出一个商家的广告页面,你对这个广告页面感兴趣,鼠标一点发送一个请求报文到商家的网站,这个请求报文的Referer报文头属性值就是http://www.google.com。

        唐僧到了西天.
        如来问:侬是不是从东土大唐来啊?
        唐僧:厉害!你咋知道的!
        如来:哈哈,我偷看了你的Refere。

      • Cache-Control:对缓存进行控制,如一个请求希望响应返回的内容在客户端要被缓存一年,或不希望被缓存就可以通过这个报文头达到目的。

        如以下设置,相当于让服务端将对应请求返回的响应内容不要在客户端缓存:

        1
        Cache-Control: no-cache
      • 其他请求报文头属性:

        参见:http://en.wikipedia.org/wiki/List_of_HTTP_header_fields

      如何访问请求报文头

      由于请求报文头是客户端发过来的,服务端当然只能读取了。

      以下是HttpServletRequest一些用于读取请求报文头的API:

      1
      2
      3
      4
      5
      1. //获取请求报文中的属性名称  
      2. java.util.Enumeration<java.lang.String> getHeaderNames();
      3.
      4. //获取指定名称的报文头属性的值
      5. java.lang.String getHeader(java.lang.String name)

      由于一些请求报文头属性“太著名”了,因此HttpServletRequest为它们提供了VIP的API:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10


      1. //获取报文头中的Cookie(读取Cookie的报文头属性)
      2. Cookie[] getCookies() ;
      3.
      4. //获取客户端本地化信息(读取 Accept-Language 的报文头属性)
      5. java.util.Locale getLocale()
      6.
      7. //获取请求报文体的长度(读取Content-Length的报文头属性)
      8. int getContentLength();

      HttpServletRequest可以通过

      1
      HttpSession getSession()

      获取请求所关联的HttpSession,其内部的机理是通过读取请求报文头中Cookie属性的JSESSIONID的值,在服务端的一个会话Map中,根据这个JSESSIONID获取对应的HttpSession的对象。

    HTTP是如何发起请求的:

    DNS域名解析、三次握手建立连接、发起HTTP请求。

    参考:

    https://blog.csdn.net/u010256388/article/details/68491509

    https://blog.csdn.net/yezitoo/article/details/78193794

  10. HashMap的实现原理?

    答:

    • JDK1.7:HashMap底层数据结构为数组+链表,使用一个Entry数组存储数据,用key的hash值取模来决定key会被存放数组中的哪个位置,而hash值由key的hashcode通过某些运算得来。如果key的hash值取模后的结果相同,那些这些key会被放到数组中的同一个位置,这时候就会发生冲突。因此,相同的元素在同一个数组位置会形成链表。在hashcode特别差的情况下,冲突会频繁发生,这时候形成的链表就会很长,那么put/get操作可能会遍历整个链表,时间复杂度会退化到O(n)。
    • JDK1.8:HashMap底层数据结构为数组+链表+红黑树,使用Node数组存储数据,但这个Node结点可能是链表结构也可能是红黑树结构;与JDK1.7不同的是,如果插入同一位置的元素超过8个,就会调用treeifyBin函数,将链表转换为红黑树。那么即使hashcode完全相同,由于红黑树的特点,查找某个特定原色,也只需要O(logn)的开销,也就是说put/get操作的时间复杂度最差只有O(logn)。
  11. 进程的五种状态以及如何进行切换的?

    答:

    z7

    • 就绪 -> 运行:对就绪状态的进程,当进程调度程序按一种选定的策略从中选中一个就绪进程,为之分配了CPU(也叫处理机)后,该进程便由就绪状态变为运行状态;
    • 运行 -> 阻塞:正在执行的进程因发生某等待事件而无法执行,则进程由执行状态变为阻塞状态。如下:
      • 进程提出输入/输出请求(IO请求)而变成等待外部设备传输信息的状态;
      • 进程申请资源(主存空间或外部设备)得不到满足时变成等待资源状态;
      • 进程运行中出现了故障(程序出错或主存储器读写错等)变成等待干预状态等等。
    • 阻塞 -> 就绪:处于阻塞状态的进程,在其等待的事件已经发生,如输入/输出操作完成,资源得到满足或错误处理完毕时,处于等待状态的进程并不马上转入运行状态,而是先转入就绪状态,然后再由系统进程调度程序在适当的时候将该进程转为运行状态;
    • 运行 -> 就绪:正在执行的进程,因时间片用完而被暂停执行,或在采用抢先式优先级调度算法的系统中,当有更高优先级的进程要运行而被迫让出处理机时,该进程便由执行状态转变为就绪状态。
  12. 进程之间的通信方式?

    答:

    • 管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。进程的血缘关系通常指父子进程关系。
    • 有名管道(named pipe):有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间通信。
    • 信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
    • 消息队列(message queue):消息队列是由消息组成的链表,存放在内核中,并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
    • 信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某一事件已经发生
    • 共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间的通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。
    • 套接字(socket):套接口也是一种进程间的通信机制,与其他通信机制不同的是它可以用于不同及其间的进程通信。
  13. 电梯调度算法?

    答:

    • 先来先服务(FIFO)
    • 最短寻道时间优先(SSTF)
    • 扫描算法:
      • SCAN算法:也就是很形象的电梯调度算法。比如磁盘扫描,先按照一个方向,从当前磁道向内磁道方向访问(磁道减少的方向),当访问到最内层的磁道号以后,就会反向,向着磁道增加的方向继续访问未访问过的磁道号。这就好比坐电梯,中间一直往下面接人,下面没人的时候反向,向上去接人。
      • CSCAN算法:循环扫描算法,同SCAN算法不同的是,这个算法扫描到最里面的磁道后,会立即跳到最外层的磁道,然后按照原来访问的方向去扫描。故也称单向扫描调度算法。
  14. String、StringBuffer、StringBuilder的区别?

    答:

    • 首先明确一点,String是不可变的对象,而StringBuffer和StringBuilder是可变的字符序列,他们两个都是类似于String的字符串缓冲区。两者不同的是StringBuffer是线程安全的,而StringBuilder是非线程安全的。
    • 每次对String类型的对象进行改变的时候其实都等于新生成了一个新的String对象,然后将栈里的指针(引用)指向了新的String对象。而如果是使用StringBuffer或者StringBuilder的话,每次改变都是对原有对象本身进行操作。
    • 初始化的方式不同,StringBuffer和StringBuilder只能用构造函数的形式进行初始化,而String对象除了可以用构造函数进行初始化以外,还可以直接赋值。
  15. new一个String对象会产生几个对象?

    答:

    这种情况首先应该明白堆栈存储的是字符串对象和字符串的对象引用,而字符串常量池是在方法区中。

    下面我们看一段代码:

    1
    2
    3
    4
    5
    String str1 = “abc”;
    String str2 = “abc”;
    String str3 = “abc”;
    String str4 = new String(“abc”);
    String str5 = new String(“abc”);

    z3

    分析:我们在new str4这个对象的时候,会先去常量池中查找是否有“abc”这个字面量。如果有,则不做任何事情,没有则创建对应的常量对象。然后会在堆中new一个String对象,通过new操作符创建的字符串对象不指向字符串池中的任何对象,但是可以通过使用字符串的intern()方法来指向其中的某一个。最后,将堆中对象的地址赋值给str4,创建一个引用。(上图中堆和常量池中的指向只是一种对应关系)

    所以,常量池中没有“abc”字面量则创建两个对象,否则创建一个对象,以及创建一个引用。

    拓展:

    根据字面量,往往会提出这样的变式题:

    String str1 = new String(“A”+”B”) ; 会创建多少个对象?
    String str2 = new String(“ABC”) + “ABC” ; 会创建多少个对象?

    str1:
    字符串常量池:”A”,”B”,”AB” : 3个
    堆:new String(“AB”) :1个
    引用: str1 :1个
    总共 : 4个

    str2 :
    字符串常量池:”ABC” : 1个
    堆:new String(“ABC”) :1个
    引用: str2 :1个
    总共 : 2个

    参考:https://segmentfault.com/a/1190000009888357

  16. JDK动态代理和CGLIB动态代理的区别?

    答:

    • JDK动态代理的前提是目标类有要实现的接口,而CGLIB动态代理的前提是目标类不能被final修饰,因为被final修饰的类不能被继承。由于CGLIB动态代理的实质是动态生成的子类继承了目标类,在运行期动态地在内存中创建一个子类。

    • JDK动态代理的实质是动态地生成一个类去实现目标类所实现的接口,而CGLIB动态代理的实质是动态生成的子类继承了目标类,在运行期动态地在内存中创建一个子类。

    • JDK动态代理是不需要第三方库支持的,只需要JDK环境就可以代理,创建JDK代理工厂的条件:

      • 代理工厂类实现InvocationHandler接口;

      • 使用java.lang.reflect包中的Proxy类提供的newProxyInstance方法来生成代理对象;

        1
        Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
      • 被代理类(目标类)一定要实现接口。

    • CGLIB的实现依赖于cglib类库,创建CGLIB代理工厂的条件:

      • 通过cglib类库提供的Enhancer类的create静态方法来创建代理类;

        1
        Enhancer.create(Class type, Callback callback)
        • type是原对象的Class对象
        • callback是回调方法接口
      • cglib类库中的callback方法通过实现它的MethodInterceptor接口的intercept方法来进行回调,因此代理工厂类需要实现MethodInterceptor接口

        1
        public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable;
        • obj是被代理的对象
        • method是执行的方法
        • args是执行方法的参数数组
        • proxy用来执行未被拦截的原方法
      • 被代理类(目标类)不能被final所修饰。

    参考:https://blog.csdn.net/yhl_jxy/article/details/80635012

  17. Spring AOP的实现原理是什么?

    答:

    其实真要说AOP的实现原理,很难说的上来,但是我知道动态代理类的生成是依据JVM反射等机制动态生成的,并且代理类和委托类的关系是在运行时才确定的。而如果是从动态代理的两种方式的来讲,无非是看JDK和CGLIB动态代理是怎么实现的。(Spring AOP使用的是动态代理)接下来就要去看两种方式的实现区别和实现原理,并且清除的阐述代理模式是怎么运作的。

  18. 为什么重写equals时必须重写hashCode方法?

    答:

    简单来说,就是假如我们要比较HashMap中的key是否相等,我们首先会调用key的hashcode()方法,而没有重写过的hashcode()方法是这个对象在内存中的地址。重写hashcode()之后,若两个key的hashcode值一样。此时并不能说明这两个key是相等的(相同含义),这时候要调用equals()方法比较两个key对象的内容是否一样。如果我们没有重写equals()方法,则比较的是两个对象的内存地址是否相等(即是否指向同一个地方),就达不到比较两个key对象的意义,因为不同key对象equals(没有重写)的话一定会返回false。

  19. 算法题:

    • 判断一颗二叉树是否对称

    • 松鼠捡豆(动态规划)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      //松鼠捡豆
      //max数组表示当前元素能捡到的豆的最大数量
      public static int getMostBean(int row, int col, int[][] bean) {

      int[][] max = new int[row][col];
      max[0][0] = bean[0][0];
      for(int i = 0 ; i < row ; i++) {
      for(int j = 0 ; j < col ; j++) {
      if(i == 0 && j > 0)
      max[i][j] = max[i][j-1] + bean[i][j];
      else if(i > 0 && j == 0)
      max[i][j] = max[i-1][j] + bean[i][j];
      else if(i > 0 && j > 0)
      max[i][j] = bean[i][j] + Math.max(max[i-1][j], max[i][j-1]);
      }
      }
      return max[row-1][col-1];
      }

No.2

  1. 讲一讲你对volatile关键字的理解

    答:

    • 保证被修饰的变量对所有线程的可见性,即当一条线程修改了这个变量时,其他线程也是可以立即得知的,而普通变量的修改是需要通过工作内存和主内存之间的通信来完成。
    • 禁止指令重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序

    volatile关键字保证的是一个线程对于它的会立即刷新到主内存中去,并置其他线程的副本为无效,但是并不保证对volatile变量的操作都具有原子性。

  2. 讲一讲HashMap与HashTable的区别?多个线程同时使用一个HashMap时你觉得会发生什么?

    答:

    区别:

    • HashMap是线程不安全的,而HashTable是线程安全的。
    • HashMap最多只允许一条记录的键值为Null,允许多条记录的值为Null;HashTable不允许记录的键或值为Null。
    • HashTable是基于Dictionary类,而HashMap是基于AbstractMap。

多个线程同时使用一个HashMap,会出现链表闭环的情况(多个线程同时put数据)。这时候如果进行get数据,就会进入死循环。

多个线程同时使用一个HashMap,会出现数据覆盖的问题。举个栗子,HashMap进行put操作时是先计算hashCode找到桶,然后遍历桶内的链表找到插入位置插入。如果2个线程t1、t2分别put一个hashCode相同的元素e1、e2,就可能导致找到相同的插入位置(a),t1里a.next=e1,t2里a.next=e2,就只有一个数据保留了下来,丢了一个。

  1. 讲讲你对ThreadLocal的理解?(在多线程环境下,如何防止自己的变量被其它线程篡改?)

    答:

    ThreadLocal顾名思义可以理解为线程本地变量,用来维护线程中的变量不受其他线程的干扰。也就是说如果定义了一个ThreadLocal,每个线程往这个ThreadLocal中的读写是隔离的,互相之间不会影响。

    • 它大致的实现思路是怎样的?

      Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
    map.set(this, value);
    else
    createMap(t, value);
    }

    public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
    @SuppressWarnings("unchecked")
    T result = (T)e.value;
    return result;
    }
    }
    return setInitialValue();
    }

    ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
    }

    说明:可以发现,每个线程中都有一个ThreadLocalMap数据结构,当执行set方法时,其值是保存在当前线程的threadLocals变量中,当执行set方法时,是从当前线程的threadLocals变量获取。

    所以在线程1中set的值,对线程2来说是摸不到的,而且在线程2中重新set的话,也不会影响到线程1中的值,保证了线程之间不会相互干扰。

    一个ThreadLocal只能保存一个键值对,但是一个线程可以创建多个ThreadLocal对象,并且各个线程之间的数据互不干扰。

    在ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,是不是很神奇,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    /**
    * 初始容量,必须为2的幂
    */
    private static final int INITIAL_CAPACITY = 16;

    /**
    * Entry表,大小必须为2的幂
    */
    private Entry[] table;

    /**
    * 表里entry的个数
    */
    private int size = 0;

    /**
    * 重新分配表大小的阈值,默认为0
    */
    private int threshold;

    /**
    * 设置resize阈值以维持最坏2/3的装载因子
    */
    private void setThreshold(int len) {
    threshold = len * 2 / 3;
    }

    /**
    * 环形意义的下一个索引
    */
    private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
    }

    /**
    * 环形意义的上一个索引
    */
    private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
    }

    z8

    这里需要注意的是,ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    static class Entry extends WeakReference<java.lang.ThreadLocal<?>> {
    // 往ThreadLocal里实际塞入的值
    Object value;

    Entry(java.lang.ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
    }
    }
    • 为什么要弱引用?

      因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利

    • 内存泄漏

      1
      2
      3
      4
      5
      6
      7
      8
      9
      static class Entry extends WeakReference<ThreadLocal<?>> {
      /** The value associated with this ThreadLocal. */
      Object value;

      Entry(ThreadLocal<?> k, Object v) {
      super(k);
      value = v;
      }
      }

      通过之前的分析已经知道,当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中

      这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

      解释:如果一个ThreadLocal对象被回收了,我们往里面放的value对于【当前线程->当前线程的threadLocals(ThreadLocal.ThreadLocalMap对象)->Entry数组->某个entry.value】这样一条强引用链是可达的,因此value不会被回收。

    • 如何解决内存泄漏?

      当我们仔细读过ThreadLocalMap的源码,我们可以推断,如果在使用的ThreadLocal的过程中,显式地进行remove是个很好的编码习惯,这样是不会引起内存泄漏。
      那么如果没有显式地进行remove呢?只能说如果对应线程之后调用ThreadLocal的get和set方法都有很高的概率会顺便清理掉无效对象,断开value强引用,从而大对象被收集器回收。

      在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。

    • 使用场景

      直接定位到 ThreadLocal 的源码,可以看到源码注释中有很清楚的解释:它是线程的局部变量,这些变量只能在这个线程内被读写,在其他线程内是无法访问的。

      ThreadLocal 定义的通常是与线程关联的私有静态字段(例如,用户ID或事务ID)。

      变量有局部的还有全局的,局部变量没什么好说的,一涉及到全局,那自然就会出现多线程的安全问题,要保证多线程安全访问,不出现脏读脏写,那就要涉及到线程同步了。而 ThreadLocal 相当于提供了介于局部变量与全局变量中间的这样一种线程内部的全局变量。

      总结了半天,发现使用场景说到底就概括成一个:就是当我们只想在本身的线程内使用的变量,可以用 ThreadLocal 来实现,并且这些变量是和线程的生命周期密切相关的,线程结束,变量也就销毁了。

      所以说 ThreadLocal 不是为了解决线程间的共享变量问题的,如果是多线程都需要访问的数据,那需要用全局变量加同步机制。

      举几个例子说明一下:

      1、比如线程中处理一个非常复杂的业务,可能方法有很多,那么,使用 ThreadLocal 可以代替一些参数的显式传递;

      2、比如用来存储用户 Session。Session 的特性很适合 ThreadLocal ,因为 Session 之前当前会话周期内有效,会话结束便销毁。我们先笼统但不正确的分析一次 web 请求的过程:

      • 用户在浏览器中访问 web 页面;
      • 浏览器向服务器发起请求;
      • 服务器上的服务处理程序(例如tomcat)接收请求,并开启一个线程处理请求,期间会使用到 Session ;
      • 最后服务器将请求结果返回给客户端浏览器。

      从这个简单的访问过程我们看到正好这个 Session 是在处理一个用户会话过程中产生并使用的,如果单纯的理解一个用户的一次会话对应服务端一个独立的处理线程,那用 ThreadLocal 在存储 Session ,简直是再合适不过了。但是例如 tomcat 这类的服务器软件都是采用了线程池技术的,并不是严格意义上的一个会话对应一个线程。并不是说这种情况就不适合 ThreadLocal 了,而是要在每次请求进来时先清理掉之前的 Session ,一般可以用拦截器、过滤器来实现。

      3、在一些多线程的情况下,如果用线程同步的方式,当并发比较高的时候会影响性能,可以改为 ThreadLocal 的方式,例如高性能序列化框架 Kyro 就要用 ThreadLocal 来保证高性能和线程安全;

      4、还有像线程内上线文管理器、数据库连接等可以用到 ThreadLocal;

秉持初心,继续向前。
显示 Gitment 评论