Java中高级问题整理(一)

本系列目录

文章目录

1. Java基础

java Queue中 remove/poll, add/offer, element/peek区别

  1. offer,add区别:

    一些队列有大小限制,因此如果想在一个满的队列中加入一个新项,多出的项就会被拒绝。
    这时新的 offer 方法就可以起作用了。它不是对调用 add() 方法抛出一个 unchecked 异常,而只是得到由 offer() 返回的 false。

  2. poll,remove区别:

    remove() 和 poll() 方法都是从队列中删除第一个元素。remove() 的行为与 Collection 接口的版本相似,但是新的 poll() 方法在用空集合调用时不是抛出异常,只是返回 null。因此新的方法更适合容易出现异常条件的情况。

  3. peek,element区别:

    element() 和 peek() 用于在队列的头部查询元素。与 remove() 方法类似,在队列为空时, element() 抛出一个异常,而 peek() 返回 null

Java是值传递还是引用传递

Java 到底是值传递还是引用传递?

重载和重写的区别?重载的方法能否根据返回类型区分?

方法的重载和重写都是实现多态的方式. 区别在于前者实现的是编译时的多态性、而后者实现的是运行时的多态性。重载发生在一个类中.同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载:重写发生在子类与父类之间、重写要求子类被重写方法与父类被重写方法有相同的参数列表、有兼容的返回类型、比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求,不能根据返回类型进行区分

谈谈HashMap 的工作原理?

HashMap 底层是 hash 数组和单向链表加红黑树实现,数组中的每个元素都是链表,由 Node 内部类(实现 Map.Entry<K,V>接口)实现,HashMap 通过 put & get 方法存储和获取。

  1. 存储对象时,将 K/V 键值传给 put() 方法:①、调用 hash(K) 方法计算 K 的 hash 值,然后结合数组长度,计算得数组下标;②、调整数组大小(当容器中的元素个数大于 capacity * loadfactor 时,容器会进行扩容resize 为 2n);③、如果 K 的 hash 值在 HashMap 中不存在,则执行插入,若存在,则发生碰撞;如果 K 的 hash 值在 HashMap 中存在,且它们两者 equals 返回 true,则更新键值对;如果 K 的 hash 值在 HashMap 中存在,且它们两者 equals 返回 false,则插入链表的尾部(尾插法)或者红黑树中(树的添加方式)。(JDK 1.7 之前使用头插法、JDK 1.8 使用尾插法)(注意:当碰撞导致链表大于 TREEIFY_THRESHOLD = 8 时,就把链表转换成红黑树)
  2. 获取对象时,将 K 传给 get() 方法:①、调用 hash(K) 方法(计算 K 的 hash 值)从而获取该键值所在链表的数组下标;②、顺序遍历链表,equals()方法查找相同 Node 链表中 K 值对应的 V 值。hashCode 是定位的,存储位置;equals是定性的,比较两者是否相等

HashMap 和 Hashtable 有什么区别?

  1. HashMap允许key和value为null、而HashTable不允许。
  2. HashTable是同步的,而HashMap不是。所以HashMap适合单线程环境.HashTable适合多线程环境。
  3. 在Javal.4中引入了LinkedHashMap、HashMap的一个子类,假如你想要遍历顺序、你很容易从HashMap转向LinkedHashMap,但是HashTable不是这样的、它的顺序是不可预知的。
  4. HashMap提供对key的Set进行遍历.因此它是fail-fast的、但.HashTable提供对key的Enumeration进行遍历.它不支持fail-fast。
  5. HashTable被认为是个遗留的类、如果你寻求在迭代的时候修改Map.你应该使用CocurrentHashMap

什么是fail-fast 机制

fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

StringBuilder类与string类的区别

  1. String 类不可变,内部维护的char[] 数组长度不可变,为final修饰,String类也是final修饰,不存在扩容。字符串拼接,截取,都会生成一个新的对象。频繁操作字符串效率低下,因为每次都会生成新的对象。
  2. StringBuilder 类内部维护可变长度char[] , 初始化数组容量为16,存在扩容, 其append拼接字符串方法内部调用System的native方法,进行数组的拷贝,不会重新生成新的StringBuilder对象。非线程安全的字符串操作类, 其每次调用 toString方法而重新生成的String对象,不会共享StringBuilder对象内部的char[],会进行一次char[]的copy操作。

Java类的实例化过程

  1. 初始化父类静态变量、静态代码块(静态变量和静态代码块的初始化顺序由编写顺序决定)
  2. 初始化子类静态变量、静态代码块(静态变量和静态代码块的初始化顺序由编写顺序决定)
  3. 初始化父类非静态变量、非静态代码块(非静态变量和非静态代码块的初始化顺序由编写顺序决定)
  4. 初始化父类构造函数
  5. 初始化子类非静态变量、非静态代码块(非静态变量和非静态代码块的初始化顺序由编写顺序决定)
  6. 初始化子类构造函数

接口设计(六大)原则

  1. 单一职责原则:应该有且仅有一个原因引起类的变更。如:一个图形类中包含了draw() 绘画功能和 area(), setWidth(), setHeight() 等图形自身的属性。这样的话 如果图形属性的计算方式发生改变,则这个类就要做出对应的修改。同样的,如果图形的绘画功能做出改变 那么这个类也要同步的做出修改。这样这个类其实已经开始违反SRP原则,随着Graphical类负责的职责越来越多,那么该类引起变化的原因也越来越多。就等于把这些职责耦合在一起了,这种耦合很容易引起脆弱的设计。
    用职责或变化原因来衡量接口或类,但职责或变化原因都是不可度量的,因项目而异。不能为了单一而单一,实现类就剧增了,或者使用聚合和组合,人为制造了系统的复杂性。

  2. 里氏替换原则(Liskov Substitution Principle, 简称LSP。):只要父类能出现的地方子类都可以出现,而且替换为子类也不会产生任何错误或异常。

    1. 子类必须完全实现父类的方法
    2. 子类可以有自己的个性(属性和方法)。
    3. 覆盖或实现父类的方法时输入参数可以被放大。
    4. 覆写或实现父类的方法时输出结果可以被缩小。

    注:在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。

  3. 依赖倒置原则(Dependence Inversion Principle, 简称DIP):精简的定义: 面向接口编程。

    1. 高层模块不应该依赖低层模块,两者都应该依赖其抽象。
    2. 抽象不应该依赖细节。
    3. 细节应该依赖抽象。
  4. 接口隔离原则:保证接口的纯结性

    1. 接口要尽量小。
    2. 接口要高内聚。
    3. 定制服务。
    4. 接口的设计是有限度的。
  5. 迪米特法则Law of Demeter, LOD。又称最少知识原则(Least Knowledge Principle, LKP)。通俗来讲:一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没有关系,那是你的事情,我就调用你提供的public方法,其他一概不关心。低耦合要求:

    1. 只和朋友交流朋友类:出现在成员变量、方法的输入输出参数中的类。方法体内部的类不属于朋友类。
    2. 朋友间也是有距离的迪米特法则要求类“羞涩”一点,尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、package-private、protected等访问权限。
    3. 是自己的就是自己的如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,就放置在本类中
    4. 谨慎使用Serializable
  6. 开闭原则: 一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

接口和抽象类的使用场景

  1. 概念上的区别:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。即接口是对动作的抽象,抽象类是对根源的抽象(即对本质的抽象与其他类的本质不同。抽象类表示的是,这个对象是什么。接口表示的是,这个对象能做什么。
  2. 抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语言来说(从实际设计上来说也是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计阶段会降低难度的。

ThreadLocal内存泄漏

在这里插入图片描述

  1. 实现原理:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需要存储的Object。也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
  2. 为什么会内存泄漏:ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
  3. 解决内存泄漏办法:ThreadLocal内部有个方法expungeStaleEntry,在调用get(),set(),remove()的时候都该方法会清除线程ThreadLocalMap里所有key为null的value

守护线程

所谓守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。在使用守护线程时需要注意一下几点:

  1. thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
  2. 在Daemon线程中产生的新线程也是Daemon的。
  3. 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
  4. 写java多线程程序时,一般比较喜欢用java自带的多线程框架,比如ExecutorService,但是java的线程池会将守护线程转换为用户线程,所以如果要使用后台线程就不能用java的线程池。

克隆,深克隆和浅克隆

某种场景下想使用已有对象的属性,由于new出来和反射出来的新对象是全新的对象,直接赋值又会影响到原有对象,克隆就是为了解决此类问题的。克隆又分为浅克隆和深克隆

  1. 浅克隆:被克隆类实现Cloneable接口,重写克隆方法。属性中的基本数据类型是直接赋值,引用类型克隆后的引用指向的还是同一个对象,改变原对象(P)和克隆对象(T)中任意的引用对象中的属性会彼此影响,这是为什么要使用深克隆。
  2. 深克隆:P和T之间任何属性不会彼此相互影响,彼此对立的个体。实现方法有两种:
    1. 类和类中的引用对象全部实现Cloneable接口并且重写clone方法,并且在克隆方法中逐级调用引用属性的克隆方法。
    2. 使用Java IO的对对象进行序列化和反序列化。
      在这里插入图片描述

Servlet生命周期

在这里插入图片描述

  1. 容器启动将Servlet Class加载到虚拟机
  2. 第一个请求到达时,实例化Servlet 调用init()初始化方法,调用service()方法
  3. 第二个以及第二个请求之后的请求到达时,调用service()方法
  4. Servlet容器正常关闭时,调用destroy()方法。

字节流与字符流有什么区别?如何选择?

计算机中的一切最终都是以二进制字节形式存在的,对于我们经常操作的字符串,在写入时其实都是先将字符转成对应的字节,然后将字节写入到输出流,在读取时其实都是先读到的是字节,然后将字节直接使用或者转换为字符给我们使用。由于对于字节和字符两种操作的需求比较广泛,所以 Java 专门提供了字符流与字节流相关IO类。

对于程序运行的底层设备来说永远都只接受字节数据,所以当我们往设备写数据时无论是字节还是字符最终都是写的字节流。字符流是字节流的包装类,所以当我们将字符流向字节流转换时要注意编码问题(因为字符串转成字节数组的实质是转成该字符串的某种字节编码)。

字符流和字节流的使用非常相似,但是实际上字节流的操作不会经过缓冲区(内存)而是直接操作文本本身的,而字符流的操作会先经过缓冲区(内存)然后通过缓冲区再操作文件。

如果是文本文件通常使用字符流,而像视频,图片,音频等文件都是二进制数据使用字节流。

BIO、NIO、AIO,分别是什么

  1. BIO:传统的网络通讯模型,就是BIO,同步阻塞IO,每次一个客户端接入,都需要在服务端创建一个线程来服务这个客户端,处理期间该客户端必须一致等待返回结果,当大量客户端来的时候,就会造成服务端的线程数量可能达到了几千甚至几万,并且发送消息的客户端可能也就几百几千个,这样就既可能造成资源浪费,也可能会造成服务端过载过高,最后崩溃死掉。

  2. NIO:NIO 模型中应用程序在一旦开始IO系统调用,会出现以下两种情况:

    1. 在内核缓冲区没有数据的情况下,系统调用会立即返回,返回一个调用失败的信息。
    2. 在内核缓冲区有数据的情况下,是阻塞的,直到数据从内核缓冲复制到用户进程缓冲。复制完成后,系统调用返回成功,应用进程开始处理用户空间的缓存数据。

    发起一个non-blocking socket的read读操作系统调用,流程是这个样子:

    1. 在内核数据没有准备好的阶段,用户线程发起IO请求时,立即返回。用户线程需要不断地发起IO系统调用。
    2. 内核数据到达后,用户线程发起系统调用,用户线程阻塞。内核开始复制数据。它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果。
    3. 用户线程才解除block的状态,重新运行起来。经过多次的尝试,用户线程终于真正读取到数据,继续执行。

    NIO的特点

    应用程序的线程需要不断的进行 I/O 系统调用,轮询数据是否已经准备好,如果没有准备好,继续轮询,直到完成系统调用为止。

    NIO的优点

    每次发起的 IO 系统调用,在内核的等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。

    NIO的缺点

    需要不断的重复发起IO系统调用,这种不断的轮询,将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低。

    总结

    总之,NIO模型在高并发场景下,也是不可用的。一般 Web 服务器不使用这种 IO 模型。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。java的实际开发中,也不会涉及这种IO模型。
    再次说明,Java NIO(New IO) 不是IO模型中的NIO模型,而是另外的一种模型,叫做IO多路复用模型( IO multiplexing )。

  3. IO多路复用模型(I/O multiplexing):IO多路复用模型的基本原理就是select/epoll系统调用,单个线程不断的轮询select/epoll系统调用所负责的成百上千的socket连接,当某个或者某些socket网络连接有数据到达了,就返回这些可以读写的连接。

    IO多路复用模型的读流程

    1. 进行select(windows)/epoll(linux)系统调用,查询可以读的连接。kernel会查询所有select的可查询socket列表,当任何一个socket中的数据准备好了,select就会返回。
    2. 当用户进程调用了select,那么整个线程会被block(阻塞掉)。
    3. 用户线程获得了目标连接后,发起read系统调用,用户线程阻塞。内核开始复制数据。它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果。
    4. 用户线程才解除block的状态,用户线程终于真正读取到数据,继续执行。

    多路复用IO的特点:

    1. IO多路复用模型,建立在操作系统kernel内核能够提供的多路分离系统调用select/epoll基础之上的。多路复用IO需要用到两个系统调用(system call), 一个select/epoll查询调用,一个是IO的读取调用。
    2. 和NIO模型相似,多路复用IO需要轮询。负责select/epoll查询调用的线程,需要不断的进行select/epoll轮询,查找出可以进行IO操作的连接。
    3. 另外,多路复用IO模型与前面的NIO模型,是有关系的。对于每一个可以查询的socket,一般都设置成为non-blocking模型。只是这一点,对于用户程序是透明的(不感知)。

    多路复用IO的优点
    用select/epoll的优势在于,它可以同时处理成千上万个连接(connection)。与一条线程维护一个连接相比,I/O多路复用技术的最大优势是:系统不必创建线程,也不必维护这些线程,从而大大减小了系统的开销。

    多路复用IO的缺点

    本质上,select/epoll系统调用,属于同步IO,也是阻塞IO。都需要在读写事件就绪后,自己负责进行读写,也就是说这个读写过程是阻塞的。

  4. AIO:如何进一步提升效率,解除最后一点阻塞呢?这就是异步IO模型,全称asynchronous I/O,简称为AIO。

    AIO的基本流程是
    用户线程通过系统调用,告知kernel内核启动某个IO操作,用户线程返回。kernel内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。
    kernel的数据准备是将数据从网络物理设备(网卡)读取到内核缓冲区;kernel的数据复制是将数据从内核缓冲区拷贝到用户程序空间的缓冲区。

    1. 当用户线程调用了read系统调用,立刻就可以开始去做其它的事,用户线程不阻塞。
    2. 内核(kernel)就开始了IO的第一个阶段:准备数据。当kernel一直等到数据准备好了,它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存)。
    3. kernel会给用户线程发送一个信号(signal),或者回调用户线程注册的回调接口,告诉用户线程read操作完成了。
    4. 用户线程读取用户缓冲区的数据,完成后续的业务操作。

    异步IO模型的特点

    在内核kernel的等待数据和复制数据的两个阶段,用户线程都不是block(阻塞)的。用户线程需要接受kernel的IO操作完成的事件,或者说注册IO操作完成的回调函数,到操作系统的内核。所以说,异步IO有的时候,也叫做信号驱动 IO 。

    异步IO模型缺点

    需要完成事件的注册与传递,这里边需要底层操作系统提供大量的支持,去做大量的工作。
    目前来说, Windows 系统下通过 IOCP 实现了真正的异步 I/O。但是,就目前的业界形式来说,Windows 系统,很少作为百万级以上或者说高并发应用的服务器操作系统来使用。
    而在 Linux 系统下,异步IO模型在2.6版本才引入,目前并不完善。所以,这也是在 Linux 下,实现高并发网络编程时都是以 IO 复用模型模式为主。

什么是零拷贝机制(Zero Copy)

wiki对零拷贝的定义:不需要cpu参与在内存之间复制数据的操作。
我们需要从磁盘读取一个文件通过网络输出到一个客户端。在操作系统内部经历了一个较为复杂的过程:

  1. 数据从磁盘复制到内核缓冲区
  2. 从内核缓冲区复制到用户空间缓冲区
  3. 从用户缓冲区复制到内核的socket缓冲区
  4. 从socket缓冲区复制到协议引擎(这里是网卡驱动)

这里 要把数据从磁盘复制到内核缓冲区是必须的,因为系统需要读取数据输出给网卡嘛。但是为啥还要从内核复制一份到用户空间呢?应用程序直接使用内核缓冲区的数据不就行了吗?这是因为对于操作系统来说,可能有多个应用程序会同时使用这些数据,并有可能进行修改,如果让大家都使用同一份内核空间的数据就会产生冲突。因此,操作系统设计为:每个应用程序想使用这些数据都必须复制一份到自己的用户空间,这样就不会互相影响了。所以这个机制在碰到数据不需要做修改的场景时就产生了浪费,数据本来可以呆在内核缓冲区不动,没必要再多此一举拷贝一次到用户空间。
随着linux内核版本迭代,数据复制到内核缓冲区以后,不再需要整个拷贝到socket缓冲区,而是只需要将数据的位置和长度信息(append dscr)传输到socket缓冲区,这样DMA1引擎会根据这些信息直接从内核缓存区复制数据给协议引擎。数据只需要从磁盘复制到内存,再从内存复制到协议引擎,跟最开始相比减少了从内核到用户空间,从用户空间到socket缓冲两次复制。但是明明还有两次数据的复制,为什么要叫“零拷贝”呢?这是因为从操作系统的角度来说,数据没有从内存复制到内存的过程,也就没有了CPU参与的过程, 所以对于操作系统来说就是零拷贝了。

2. 框架

谈谈对Spring IOC的理解

谈谈对Spring IOC的理解

Spring Bean生命周期

面试必备-Spring中Bean的加载到销毁(生命周期)

Spring从哪两个角度来实现自动装配?

组件扫描(component scanning):spring会自动发现应用上下文中所创建的bean;
自动装配(autowiring):spring自动满足bean之间的依赖,也就是我们说的IoC/DI;

Spring自动装配的方式

  1. No:即不启用自动装配。Autowire默认的值。不使用Autowire,引用关系显示声明,spring的reference也建议不用autoware,因为这会破坏模块关系的可读性。
  2. byName:通过属性的名字的方式查找JavaBean依赖的对象并为其注入。比如说类Computer有个属性printer,指定其autowire属性为byName后,Spring IoC容器会在配置文件中查找id/name属性为printer的bean,然后使用Seter方法为其注入。
  3. byType:通过属性的类型查找JavaBean依赖的对象并为其注入。比如类Computer有个属性printer,类型为Printer,那么,指定其autowire属性为byType后,Spring IoC容器会查找Class属性为Printer的bean,使用Seter方法为其注入。如果存在多个该类型bean,那么抛出异常,并指出不能使用byType方式进行自动装配;如果没有找到相匹配的bean,则什么事都不发生,也可以通过设置dependency-check="objects"让Spring抛出异常。
  4. constructor:通byType一样,也是通过类型查找依赖对象。与byType的区别在于它不是使用Seter方法注入,而是使用构造子注入。如果容器中没有找到与构造器参数类型一致的bean,那么抛出异常。
  5. autodetect:在byType和constructor之间自动的选择注入方式。通过bean类的自省机制(introspection)来决定是使用constructor还是byType方式进行自动装配。如果发现默认的构造器,那么将使用byType方式,否则采用
    constructor。
  6. default:由上级标签的default-autowire属性确定。

Spring AOP原理

AOP(Aspect Oriented Programming) 面向切面编程。在编程中,我们希望将日志记录,性能统计,事务处理,异常处理等代码逻辑相似又不影响正常业务流程的代码提取出来,然后通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。假设把应用程序想成一个立体结构的话,OOP的利刃是纵向切入系统,把系统划分为很多个模块(如:用户模块,文章模块等等),而AOP的利刃是横向切入系统,提取各个模块可能都要重复操作的部分(如:权限检查,日志记录等等)。由此可见,AOP是OOP的一个有效补充。AOP不是一种技术,实际上是编程思想。凡是符合AOP思想的技术,都可以看成是AOP的实现。
参考:SpringAOP原理分析

Spring事务使用方式

1. 编程式事务

  1. 注入事务管理器:org.springframework.jdbc.datasource.DataSourceTransactionManager
  2. 注入事务管理模板:org.springframework.transaction.support.TransactionTemplate
  3. 将事务模板注入到代码中使用,伪代码如下:
public void buy(){
    transactionTemplate.execute(status -> {
        //查询余额
        double banlance=queryFromDB();
        //余额不足抛异常
        if (banlance<10) {
            throw new RuntimeException("余额不足!请充值");
        }
        //更新
        update();
        return null;
    });
}

2. 声明式事务

声明式事务管理建立在AOP之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
首先要定义一个事务增强txAdvice,依赖于transactionManager。然后定义切面,切面内定义切点,然后指向具体的切面类。
在这里插入图片描述

Spring事务传播特性

事务行为说明
PROPAGATION_REQUIRED如果没有,就开启一个事务;如果有,就加入当前事务(方法B看到自己已经运行在 方法A的事务内部,就不再起新的事务,直接加入方法A)
RROPAGATION_REQUIRES_NEW如果没有,就开启一个事务;如果有,就将当前事务挂起。(方法A所在的事务就会挂起,方法B会起一个新的事务,等待方法B的事务完成以后,方法A才继续执行)
PROPAGATION_NESTED如果没有,就开启一个事务;如果有,就在当前事务中嵌套其他事务
PROPAGATION_SUPPORTS如果没有,就以非事务方式执行;如果有,就加入当前事务(方法B看到自己已经运行在 方法A的事务内部,就不再起新的事务,直接加入方法A)
PROPAGATION_NOT_SUPPORTED如果没有,就以非事务方式执行;如果有,就将当前事务挂起,(方法A所在的事务就会挂起,而方法B以非事务的状态运行完,再继续方法A的事务)
PROPAGATION_NEVER如果没有,就以非事务方式执行;如果有,就抛出异常。
PROPAGATION_MANDATORY如果没有,就抛出异常;如果有,就使用当前事务

其中前4种是开发中用到概率比较大的,建议熟记;后面3种不常用,了解就行。

3. 对比

不同点编程式事务声明式事务
粒度代码块级别方法级别
实现每个方法都需要单独实现,但业务量大功能复杂时,使用编程式事务无疑是痛苦的声明式事务属于无侵入式,不会影响业务逻辑的实现,只需要在配置文件中做相关的事务规则声明或者通过注解的方式,便可以将事务规则应用到业务逻辑中

SpringMVC的几个组件?

  1. DispatcherServlet:前端控制器,也叫中央控制器。相关组件都是它来调度。
  2. HandlerMapping:处理器映射器、根据URL路径映射到不同的Handler。
  3. HandlerAdapter :处理器适配器.按照Handler Adapter的规则去执行Handler。
  4. Handler :处理器,由我们自己根据业务开发。
  5. ViewResolver :视图解析器、把逻辑视图解析成具体的视图。
  6. View: 一个接口、它的实现支持不同的视图类型(freeMaker、JSP等)

SpringMVC运行流程

在这里插入图片描述

  1. 用户发送请求至前端控制器DispatcherServlet。
  2. DispatcherServlet收到请求调用HandlerMapping处理器映射器。
  3. 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
  4. DispatcherServlet调用HandlerAdapter处理器适配器。
  5. HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
  6. Controller执行完成返回ModelAndView。
  7. HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
  8. DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
  9. ViewReslover解析后返回具体View.
  10. DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
  11. DispatcherServlet响应用户。
    参考:SpringMVC的简介和工作流程

分析springboot运行机制

  1. 首先主类上@SpringBootApplication注解,点进去主要有三个重要注解:@Configuration、@ComponentScan、@EnableAutoConfiguration。
  2. @Configuration将该类标记为配置类;
  3. @ComponentScan没有指定basePackages的话就以当前类所在的包为basePackages,这就是为什么将Bean放于主类所在包范围之外无法扫描到的原因;
  4. @EnableAutoConfiguration有个注解@Import({AutoConfigurationImportSelector.class}),而AutoConfigurationImportSelector最终实现了ImportSelector接口,该接口selectImports方法返回一组bean全类名数组,将实现对导入类的收集。 那么导入的类从哪来呢?
    AutoConfigurationImportSelector调用SpringFactoriesLoader的loadSpringFactories 方法,该方法会加载class路径下META-INF/spring.factories配置文件里所有的配置类
    在这里插入图片描述

MyBatis的分页方式

  1. 逻辑分页:使用自带的RowBounds分页,一次查很多数据(不是全部数据),然后在这些数据里面检索。本质上是使用DB的limit进行分页,表里数据量小速度较快,数据量大就很慢。比如:limit 100 offset 20就会查询满足120条的数据,然后取出20条,可想而知随着limit后的数值增大越来越慢。
  2. 物理分页:使用分页插件PageHelper,直接去数据库里查询指定条数的数据。

谈谈MyBatis缓存

参考:简述MyBatis的一级缓存、二级缓存原理

MyBatis的Xml映射文件中,除了常见的select、insert、updae、delete标签之外,还有哪些标签?

resultMapparameterMapsqlincludeselectKey,加上动态sql的9个标签,trimwheresetforeachifchoosewhenotherwisebind等,其中sql为sql片段标签,通过include标签引入sql片段,selectKey为不支持自增的主键生成策略标签。

MyBatis通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?

Dao接口,就是人们常说的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方法名,就是映射文件中MappedStatement的id值,接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement,举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。在Mybatis中,每一个、、、标签,都会被解析为一个MappedStatement对象。
Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。
Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回

简述Mybatis的插件运行原理,以及如何编写一个插件

Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。
实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。

Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不?

Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能,Mybatis提供了9种动态sql标签trim|where|set|foreach|if|choose|when|otherwise|bind。
其执行原理为,使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。

Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?

Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。

Mybatis都有哪些Executor执行器?它们之间的区别是什么?

Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。

  1. SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
  2. ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。
  3. BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

3. JVM

Java内存区域(JVM内存结构)

  1. 程序计数器:线程私有的(为了保证线程切换后能恢复到正确位置),当前线程执行字节码的行号指示器。字节码解释器就是通过改变这个计数器的值来选取下条需要执行的字节码指令(分支、循环、跳转、异常处理、线程恢复等)。程序计数器是此区域是唯一一个没有规定OutOfMemoryError的区域。

  2. Java虚拟机栈:线程私有,描述的是Java方法执行的内存模型,每个方法执行的时都会创建一个栈帧用于储存局部变量表 2、操作数栈、动态链接、方法出口等信息。如果线程请求栈深度大于虚拟机允许,抛出StackOverflowError;如果无法申请到足够内存抛出OutOfMemoryError

  3. 本地方法栈:线程私有,与Java虚拟机栈相似,只不过是为虚拟机使用到的native方法服务的

  4. Java堆:线程共享,Java虚拟机管理的内存最大的一块区域,在虚拟机启动时创建。此区域唯一目的是存放对象实例,几乎所有对象实例都在这里分配内存3。无法申请所需内存时抛出OutOfMemoryError

  5. 方法区:线程共享,储存被虚拟机加载的类信息、常亮、静态变量、即时编译器编译的代码等数据。无法申请所需内存时抛出OutOfMemoryError。方法区还包含运行时常量池,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息-常量池,用于存放编译期生成的各种字面量和符号引用,这部分信息将存放于运行时常量池中。

    1. 操作数栈:是一个后入先出的栈。其最大深度在编译时写入到code属性的max_stacks数据项中。每个元素可以使任意的Java数据类型,包括long、double。32位占栈容量1,64位位2。方法执行时,操作数栈深度不会超过设定的max_stacks。
      参考:《深入理解Java虚拟机》

class文件、class content、Class文件、class 对象

  1. class文件:java文件反编译后存储在硬盘上的文件
  2. class content:class文件加载到虚拟机的内容
  3. Class文件:类加载器对class content解析后生成的文件
  4. class 对象:使用new关键词生成的对象

Java引用类型

JDK1.2之前Java中引用只有引用和没被引用两种状态,过于狭隘,对于“食之无味弃之可惜”的对象无能为力。我们希望某些对象在内存足够时保留,内存不足时抛弃。JDK1.2之后对引用进行了扩充,由以下4中以此减弱:

  1. 强引用(Strong Reference):类似“Object obj=new Object()”,只要强引用存在对象永远不会被回收
  2. 软引用(Soft Reference):有用,非必须对象。软引用关联的对象会在内存将要溢出时被系统列入回收范围,进行二次回收
  3. 弱引用(Weak Reference):被弱引用关联的对象无论内存是否足够,只能存活到下次垃圾回收发生之前
  4. 虚引用(Phantom Reference):被虚引用关联的对象,其生存完全不受引用影响,也无法通过该引用获取对象实例。唯一作用是在对象被回收时收到一个通知

参考:《深入理解Java虚拟机》

JVM垃圾回收机制(判断对象是否存活)

  1. 判断对象是否可用
    1. 引用计数算法:给对象添加一个引用计数器,每当一个地方引用它计数器就加1,每当一个引用失效时计数器减1,任意时刻计数器为0时,该对象不可用。缺点是:无法解决循环引用问题。
    2. 可达性分析算法:通过一系列成为“GC Roots”对象作为起点向下搜索,搜索走过的路径被称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,改对象不可用。Java中,可作为GC Roots对象包括:
      ① 虚拟机栈(栈帧中本地变量表)中引用的对象
      ②方法区中静态属性引用的对象
      ③方法区中常量引用的对象
      ④本地方法栈中JNI引用的对象
  2. 判断对象是否死亡
    如果根搜索算法中判断对象不可用,并不代表对象真正死亡。对象真正死亡要经历两次标记:
    1. 如果对象不可达那么将会被第一次标记,并且进行一次筛选,筛选条件是是否需要执行finalize()方法(对象没有覆盖finalize()方法,或者执行过了finalize()就没必要执行,任何对象的finalize()只会执行一次)
    2. 如果对象有必要执行finalize()方法,将会被放进F-Queue队列中。然后GC将对该对象进行第二次标记,对象如果在执行finalize()方法时成功自救(重新与引用链上任意对象建立关联),将被移除即将回收的集合,否则就离死不远了

参考:《深入理解Java虚拟机》

垃圾收集算法

  1. 标记-清除:首先标记处所有需要回收的对象,标记完成后统一进行回收。有两个不足:
    1. 标记和清除效率低下
    2. 标记清除后产生大量不连续的内存碎片
  2. 复制算法:将内存等分为两块,每次只使用其中一块,当一块内存用完了就将活着的对象复制到另一块,然后清除掉。优点是实现简单,效率高,内存连续。缺点是内存使用率低,代价高。

    现在的商业虚拟机使用这种方式回收新生代,新生代百分之98是朝生夕死对象,所以不需要1:1分配内存,而是将内存分为一块较大的Eden空间和两块较小的Survior(Survior from、Survior to)空间,回收时将Eden和Servior From存活对象复制到Servior to上,然后清理掉自己。HotSpot默认Eden和Servior为8:1,我们没法保证每次回收存活对象不多于10%,当内存不够时需要依赖老年代进行分配担保,也就是当Servior to没有足够内存存放上一次新生代的存活对象时,这些对象将通过分配担保机制进入老年代。

  3. 假设对象存活率在100%(老年代完全有可能),那么复制算法就不适合了,所以提出来标记-整理方法。标记过程如同标记-清除算法,然后将标记的对象向一侧移动,最后一次清理掉边界之外的内存
    在这里插入图片描述
  4. 分代收集算法:当代虚拟机都采用这种方法,将Java堆分为新生代、老年代。新生代每次收集都会有大量的对象死亡,采用复制算法。老年代对象存活率高、没有额外空间对它进行担保,采用标记-清理或者标记-整理算法
    参考:《深入理解Java虚拟机》

类加载时机

在这里插入图片描述
加载、验证、准备、初始化、卸载这5个阶段顺序是固定的。为了支持Java运行时绑定,解析阶段可以在初始化之后进行。以下5种情况必须初始化:

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK 1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且该方法句柄所对应的类没有初始化过,则先触发初始化。

Java类加载的过程

  1. 加载:在加载阶段,虚拟机需要完成3件事情:
    1. 通过一个类的全限定名来获取定义此类的二进制字节流;

      获取二进制流途径:

      1. 从ZIP包获取,是JAR、EAR、WAR格式基础
      2. 网络中获取,如:Applet
      3. 运行时计算机生成,这种场景使用最多的是动态代理技术,在java.lang.reflect.Proxy就是使用ProxyGenerator.generateProxyClass来为特定的接口生成二进制字节流。
      4. 其他文件生成,如JSP
      5. 从数据库中读取,如某些中间件服务器将代码安装到数据库中来实现代码在集群中分发
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
    3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
  2. 验证:验证阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。整体来看,验证阶段大致分为4个验证动作:
    1. 文件格式验证:第一阶段是验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。该阶段是基于二进制字节流验证的,只有通过了这个阶段的验证,字节流才会进入内存的方法去中存储,后面的3个验证都是基于方法区的存储结构进行的。
      这一阶段可能的验证点:

      a.是否以魔数0xCAFEBABE开头
      b.主、次版本号是否在当前虚拟机处理范围内
      c.常量池的常量数据类型是否被支持(检查常亮tag标志)
      d. 指向常量的各种索引值是否有指向不存在或者复合类型的常亮
      e. CONSTANT_utf8_info型常量中是否有不符合utf8编码的数据
      f. Class文件中各个部分以及文件本身是否有被删除或者附件的其他信息

    2. 元数据验证:元数据验证是对字节码描述信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。这个阶段可能的验证点:

      a. 是否有父类
      b. 是否继承了不被允许继承的类
      c. 如果该类不是抽象类,是否实现了其父类或接口要求实现的所有方法
      d. 类中的字段、方法是否与父类产生矛盾(类如覆盖父类final字段,或者错误方法重载)

    3. 字节码验证:字节码验证的主要目的是通过数据流和控制流分析,确定程序语义的合法性和逻辑性。该阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事情。这个阶段可能的验证点:

      a. 保证任何时候操作数栈的数据类型与指令代码序列的一致性,不会出现这种情况:在操作栈中放置了一个int类型数据,使用时却按照long类型加载入本地变量表中;
      b.跳转指令不会跳转到方法体以外的字节码指令上

    4. 符号引用验证:符号引用验证的主要目的是保证解析动作能正常执行,如果无法通过符号引用验证,则会抛出异常。这个阶段可能的验证点:

      a. 符号引用的类、字段、方法的访问性(public、private等)是否可被当前类访问
      b. 指定类是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
      c. 符号引用中的类、字段、方法的访问性(private、protected…)是否可被当前类访问

数组的加载

数组是直接由Java虚拟机创建的,但是数组组件是由类加载器创建的,一个数组创建遵循以下规则:

  1. 如果数组组件是引用类型,就是用上节讲到的加载过程去递归加载,数组将在加载该组件的类加载器的类名空间上被标识(一个类必须与类加载器一起确定唯一性)
  2. 如果数组组件不是引用类型(如int[]数组),Java虚拟机将会把数组标识为与引导类加载器关联
  3. 数组类的可见性和组件可见性一致,如果组件不是引用类型,那么数组可见性默认为public。

什么是类加载器

为了让应用程序自己去决定如何获取自己需要的类,将通过一个全类名来获取类的二进制字节流的这个动作放到Java虚拟机外部去实现。实现这个动作的代码块就是类加载器。

类加载器的分类

  1. 启动类加载器(Bootstrap ClassLoader):负责加载存放在<JAVA_HOME>\lib目录中或者被-Xbootclasspath指定路径中并且是Java虚拟机识别的类库加载到虚拟机内存中。开发者不可使用该类加载器
  2. 拓展类加载器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader实现,负责加载存放在<JAVA_HOME>\lib\ext目录中或者被系统变量java.ext.dirs所指定的路径的所有类库加载到虚拟机内存中。开发者可以直接使用该类加载器
  3. 应用程序类加载器(Application ClassLoader):由sun.misc.Launcher$AppClassLoader实现,是ClassLoader中getSystemClassLoader()方法的返回值,所以也成为系统类加载器。负责加载用户类路径classpath上的类。开发者可以直接使用,这是程序中使用的默认类加载器,用户可以自定义类加载器。

类加载器双亲委派模型

在这里插入图片描述

  1. 定义:如果一个类加载器收到加载类请求,它首先不会自己去加载这个类,而是将该类加载工作委派给父加载器,因此所有的类加载请求最终都会传到顶层Bootstrap ClassLoader进行加载。只要父加载器反馈自己无法完成这个加载请求(它在自己的搜索范围内没找到所需的类)时,子加载器才会尝试自己加载
  2. 意义:Java类随着类加载器一起具有优先级关系。例如java.lang.Object,存放在rt.jar总,无论哪个类加载器加载它最终都会委派给处于模型最顶层类加载器进行加载,因此Object在程序中各种类加载器加载的结构都是同一个类。反之,若没有使用双亲委派模型,如果用户自己编写一个java.lang.Object类,放于classpath路径下,再新建了个类加载器去加载它,系统中就会出现不同的Object对象。

参考:《深入理解Java虚拟机》

Java多线程实现原理

Java多线程是通过线程轮流切换并分配处理器执行时间来实现的,在任何时刻,一个处理器只能执行一条线程中的指令。多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使用率更高。


  1. DMA技术是Direct Memory Access的缩写。其意思是“存储器直接访问”。它是指一种高速的数据传输操作,允许在外部设备和存储器之间直接读写数据,既不通过CPU,也不需要CPU干预。 ↩︎

  2. 局部变量表存放编译器可知的各种基本变量类型、对象引用、返回类型(指向一条字节码指令的地址)。其中64位的long和double占两个局部变量空间(slot)。局部变量表所需内存空间编译期间完成分配,当进入某方法时这个方法需要在栈帧中分配的局部变量表空间是完全确定的,方法运行时不会改变。 ↩︎

  3. 随着JIT编译器的发展和逃逸分析技术的成熟,栈上分配、标量替换优化技术将会导致一些微妙变化,所有对象实例在堆上分配内存变得不“绝对”了。 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值