语言技能
JAVA基础
操作符、控制执行流程
JAVA的重要特性:自动内存管理机制、异常处理。
ArrayList的优缺点
因为ArrayList底层使用数组实现,所以优缺点与数组类似。
优点:
1、根据下标遍历元素效率较高。
2、根据下标访问元素效率较高。
3、在数组的基础上封装了对元素操作的方法。
4、可以自动扩容。
缺点:
1、插入和删除的效率比较低。
2、根据内容查找元素的效率较低。
扩容规则:每次扩容现有容量的50%。
ArrayList vs LinkedList
- 存储结构不同
- 操作不同
ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
ArrayList:内部使用数组的形式实现了存储,实现了RandomAccess接口,利用数组的下面进行元素的访问,因此对元素的随机访问速度非常快。
因为是数组,所以ArrayList在初始化的时候,有初始大小10,插入新元素的时候,会判断是否需要扩容,扩容的步长是0.5倍原容量,扩容方式是利用数组的复制,因此有一定的开销;
ArrayList vs Vector
List接口下一共实现了三个类:ArrayList,Vector,LinkedList。LinkedList就不多说了,它一般主要用在保持数据的插入顺序的时候。ArrayList和Vector都是用数组实现的,主要有这么三个区别:
1、Vector是多线程安全的,而ArrayList不是,这个可以从源码中看出,Vector类中的方法很多有synchronized进行修饰,这样就导致了Vector在效率上无法与ArrayList相比;
2、两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同的,很多网友说Vector增加原来空间的一倍,ArrayList增加原来空间的50%,其实也差不多是这个意思,不过还有一点点问题可以从源码中看出,一会儿从源码中分析。
3、Vector可以设置增长因子,而ArrayList不可以,最开始看这个的时候,我没理解什么是增量因子,不过通过对比一下两个源码理解了这个,先看看两个类的构造方法:
如何决定使用 HashMap 还是 TreeMap?
TreeMap<K,V>的Key值是要求实现java.lang.Comparable,所以迭代的时候TreeMap默认是按照Key值升序排序的;TreeMap的实现是基于红黑树结构。适用于按自然顺序或自定义顺序遍历键(key)。
HashMap<K,V>的Key值实现散列hashCode(),分布是散列的、均匀的,不支持排序;数据结构主要是桶(数组),链表或红黑树。适用于在Map中插入、删除和定位元素。
结论:如果你需要得到一个有序的结果时就应该使用TreeMap(因为HashMap中元素的排列顺序是不固定的)。除此之外,由于HashMap有更好的性能,所以大多不需要排序的时候我们会使用HashMap。
ConcurrentHashMap
put
rehash
get
红黑树
红黑树是一种特定类型的二叉树,它是在计算机科学中用来组织数据比如数字的块的一种结构。
性质1. 结点是红色或黑色。
性质2. 根结点是黑色。
性质3. 所有叶子都是黑色。(叶子是NIL结点)
性质4. 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
性质5. 从任一节结点其每个叶子的所有路径都包含相同数目的黑色结点。
nio 和 io 的区别
1、IO基于字节流和字符流进行操作的
2、NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
3、NIO引入了选择器的概念,选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。
IO NIO
面向流 面向缓冲区
阻塞IO 非阻塞IO
IO模型
- 就IO而言:概念上有5中模型:blocking I/O,nonblocking I/O,I/O multiplexing (select and poll),signal driven I/O (SIGIO),asynchronous I/O (the POSIX aio_functions)。
- 然后呢 不同的操作系统对上述模型支持不同: unix支持io多路复用,不同系统叫法不同 :freebsd里面叫 kqueue;linux 是epoll。而windows: 2000的时候就诞生了IOCP支持最后一种异步I/O
3.java是一种跨平台语言,为了支持异步IO,诞生了nio,Java1.4引入的NIO 1.0是基于I/O复用的。在各个平台上会选择不同的复用方式。Linux用的epoll,BSD上用kqueue,Windows上应该是重叠I/O(肯定不是IOCP)。
4:基于jdk的nio ,不同公司出了一堆框架:apache mina ,jboss的netty,sun的grizzly。 这些都是直接封装传输层的tcp/udp。
nio直接使用比较难用,所以有了netty等针对网络io部分(tcp/udp-传输层)的封装(nio也有非网络io部分),为了使nio更易用。 netty等只是一个nio框架,不需要web容器的额外支持,也就是说不限定web容器。
java 中,直接缓冲区与非直接缓冲器有什么区别
非直接缓冲区:通过allocate()分配缓冲区,将缓冲区建立在JVM的内存中
直接缓冲区:通过allocateDirect()分配直接缓冲区,将缓冲区建立在物理内存中,可以提高效率。
深拷贝与浅拷贝的区别
深拷贝:除了对象本身被复制外,对象所包含的所有成员变量都会被复制,包括引用类型的成员对象
浅拷贝:只复制对象其中包含的值类型的成员变量,而引用类型的成员对象没有被复制
this 关键字
this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。编译器做了一些幕后工作,它暗自把“所使用对象的引用”作为第一个参数传递给被调用的方法。
Static关键字
通常来说,当创建类时,就是在描述那个类的对象的外观与行为。除非用new创建那个类的对象,否则,实际上并未获得任何对象。执行new来创建对象时,数据存储空间才被分配,其方法才供外界调用。
有两种情形用上述方法是无法解决的。一种情形是,只想为某特定域分配单一存储空间,而不去考虑究竟要创建多少对象,甚至根本就不创建任何对象。另一种情形是,希望某个方法不与包含它的类的任何对象关联在一起。也就是说,即使没有创建对象,也能够调用这个方法。
通过static关键字可以满足这两方面的需要。Static方法就是没有this的方法。
实例变量与静态变量有什么不同
区别一、定义不同
静态变量定义时候前面要加上static,实例变量不需要加。
区别二、初始化不同
静态变量随着类的加载而初始化,实例变量是new对象后才进行初始化。
区别三、内存位置不同
静态变量存储在静态变量区,实例变量存储在堆内存区
区别四、调用方式不同
静态变量通过类名调用,实例变量通过对象调用
区别五、生命周期不同
静态变量随着类的加载而加载,虚拟机停止运行时,静态变量周期结束。实例变量随着对象的产生而产生,随着对象的消失而失去引用,等待垃圾回收。
final关键字
final数据:一块数据是恒定不定的
final方法:把方法锁定,以防任何继承类修改它的含义。这是出于设计的考虑:想要确保在继承中使方法行为保持不变,并且不会被覆盖。
final类:当将某个类的整体定义为final时,就表明了你不打算继承该类,而且也不允许别人这样做。换句话说,出于某种考虑,你对该类的设计永不需要做任何变动,或者出于安全的考虑,你不希望它有子类。
finalize()方法
假定你的对象(并非使用new)获得了一块“特殊”的内存区域,由于垃圾回收器只知道释放那些经由new分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存。为了应对这种情况,Java允许在类中定义一个名为finalize()的方法。它的工作原理“假定”是这样的:一时垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以要是你打算用finalize(),就能在垃圾回收时刻做一些重要的清理工作。
不同于C++中的析构函数,在C++中对象一定会被销毁,而Java里的对象却并非总是被垃圾回收。或者换句话说:
- 对象可能不被垃圾回收
- 垃圾回收并不等于“析构”
类的初始化顺序
初始化顺序:
父类–静态变量/父类–静态初始化块
子类–静态变量/子类–静态初始化块
父类–变量/父类–初始化块
父类–构造器
子类–变量/子类–初始化块
子类–构造器
结论:
- 子类的静态变量和静态初始化块的初始化是在父类的变量、初始化块和构造器初始化之前就完成了;
- 静态变量、静态初始化块顺序取决于它们在类中出现的先后顺序
- 变量、初始化块初始化顺序取决于它们在类中出现的先后顺序
Java有哪些类型引用
- 强引用(Strong Reference):类似于Object object = new Object(),强引用存在,垃圾收集器就不会回收它所指向的对象
- 软引用(Soft Reference):内存足够的时候,软引用指向的对象存留,内存将要溢出的时候,回收软引用指向的对象
- 弱引用(Weak Reference):只要垃圾收集器开始工作,不管内存够不够,弱引用指向的对象都被回收
- 虚引用(Phantom Reference):虚引用对指向对象没有任何影响
ThreadLocal原理
每个Thread对象都有一个ThreadLocalMap,当创建一个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类型。
应用场景:
每个线程需要有自己单独的实例
实例需要在多个方法中共享,但不希望被多线程共享
存在泄露问题:
在上面提到过,每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。
线程池的工作原理
线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
线程池运行机制主要分为:
线程池如何维护自身状态。
线程池如何管理任务。
线程池如何管理线程。
线程池状态
任务执行机制
任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。
首先,所有任务的调度都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:
首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
接口和抽象类有什么区别
接口和抽象类的概念不一样。接口是对动作的抽象,抽象类是对根源的抽象。
抽象类表示的是,这个对象是什么。接口表示的是,这个对象能做什么。比如,男人,女人,这两个类(如果是类的话……),他们的抽象类是人。说明,他们都是人。
人可以吃东西,狗也可以吃东西,你可以把“吃东西”定义成一个接口,然后让这些类去实现它.
所以,在高级语言上,一个类只能继承一个类(抽象类)(正如人不可能同时是生物和非生物),但是可以实现多个接口(吃饭接口、走路接口)。
第一点. 接口是抽象类的变体,接口中所有的方法都是抽象的。而抽象类是声明方法的存在而不去实现它的类。
第二点. 接口可以多继承,抽象类不行
第三点. 接口定义方法,不能实现,而抽象类可以实现部分方法。
第四点. 接口中基本数据类型为static 而抽象类不是的。
当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。
抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语言来说(从实际设计上来说也是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的
所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计阶段会降低难度的。
servlet 生命周期详解
Servlet 生命周期可以归纳为:Servlet 加载--->实例化--->服务--->销毁
在正常情况下,Servlet只会初始化一次,而处理服务会调用多次,销毁也只会调用一次;但是如果一个Servlet长时间不使用的话,也会被容器自动销毁,而如果需要再次使用时会重新进行初始化的操作,即在特殊情况下初始化可能会进行多次,销毁也可能进行多次。
在servlet实例创建之后,在servlet能为客户请求提供服务之前,容器会在servlet实例上调用init()方法。如果你有初始化代码,就应该覆盖servlet类的init()方法,否则会调用GenericServlet的init()方法。而对应每个客户请求(无论是谁,无论是不是同一个人,只针对请求),容器都会创建一对新的请求和响应对象,创建一个新的线程/栈。任何servlet类都不会有多个实例,除非一种特殊情况(SingleThreadModel)。
servlet生命周期的4个周期总结如下:
a. 实例化以及加载servlet,new的过程
b. 初始化init(ServletConfig)。
c. 处理请求,调用servlet的service,doget,dopost方法将Request和Response,作为参数传递。
d. 退出服务,调用destory方法释放资源。
servlet3异步原理
](https://my.oschina.net/wangxindong/blog/1555194)
接收到request请求之后,由tomcat工作线程从HttpServletRequest中获得一个异步上下文AsyncContext对象,然后由tomcat工作线程把AsyncContext对象传递给业务处理线程,同时tomcat工作线程归还到工作线程池,这一步就是异步开始。在业务处理线程中完成业务逻辑的处理,生成response返回给客户端。在Servlet3.0中虽然处理请求可以实现异步,但是InputStream和OutputStream的IO操作还是阻塞的,当数据量大的request body 或者 response body的时候,就会导致不必要的等待。从Servlet3.1以后增加了非阻塞IO,需要tomcat8.x支持。
通讯模型中的NIO可以利用很少的线程处理大量的连接,提高了机器的吞吐量。Servlet的异步处理机制使得我们可以将请求异步到独立的业务线程去执行,使得我们能够将请求线程和业务线程分离。通讯模型的NIO跟Servlet3的异步没有直接关系。但是我们将两种技术同时使用就更增加了以tomcat为容器的系统的处理能力。自从Servlet3.1以后增加了非阻塞的IO,这里的非阻塞IO是面向inputstream和outputstream流,通过jdk的事件驱动模型来实现,更一步增强了Servlet异步的高性能,可以认为是一种增强版的异步机制。
OOP
OOP ,面向对象程序设计,一切皆对象。
面向对象的三个特性:封装、继承、多态
抽象/封装
隐藏具体实现:访问限制符,private、protected、public
复用具体实现:组合(composition)也叫聚合(aggregation), “has-a”(拥有)的关系。
继承
“是一个” 与“象是x一个” 关系,"is-a" and "is-like-a"关系。
继承如何确保合理性
可以用一个导出类对象完全来完全替代一个基类对象。
里氏替换原则:
多态
向上转型(upcasting):把导出类看做是它的基类的过程。
多态的作用是消除类型之间耦合的关系,多态方法调用允许一种类型表现出与其他相似类型之间的区别。
动态绑定(后期绑定),它的含义是在运行时根据对象的类型进行绑定。
对象的创建和生命周期
Java完全采用了动态内存分配方式。
对象生命周期
对象作用域,作用域之外依靠垃圾回收器
复用类的方式
- 组合
- 继承
- 代理
内部类
内部类是指在一个外部类的内部再定义一个类。内部类作为外部类的一个成员,并且依附于外部类而存在的。内部类可为静态,可用protected和private修饰(而外部类只能使用public和缺省的包访问权限)。内部类主要有以下几类:成员内部类、局部内部类、静态内部类、匿名内部类
异常处理
Throwable这个Java类被用来表示任何可以作为异常被抛出的类。Throwable对象可分为两种类型:Error用来表示编译时和系统错误;Exception是可以抛出的基本类型,在Java类库,用户方法以及运行时故障中都可能抛出Exception型异常。
- 受检异常和运行时异常(RuntimeException非受检异常)
- finally的使用场景:把除内存之外的资源恢复到它们的初始状态,比如已经打开的文件或网络连接。
并发编程
想把问题切分成多个可独立运行的部分(任务),从而提高程序的响应能力。在程序中,这些彼此独立运行的部分称之为线程,上述概念被称为“并发”。
如何合理设置线程池大小
要想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析:
任务的性质:CPU密集型任务、IO密集型任务、混合型任务。
任务的优先级:高、中、低。
任务的执行时间:长、中、短。
任务的依赖性:是否依赖其他系统资源,如数据库连接等。
性质不同的任务可以交给不同规模的线程池执行。
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程
多线程
synchronized 和 Lock 有什么区别?
1、 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
2、 synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
3、 synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
4、 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
reentrentlock
AQS
CountDownLatch
java对象的访问定位方式
1.句柄访问
- 直接指针访问
equals vs ==
1、 ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同
2、 ==是指对内存地址进行比较 equals()是对字符串的内容进行比较
3、==指引用是否相同 equals()指的是值是否相同
Java通过几种原子操作完成工作内存和主内存的交互
lock:作用于主内存,把变量标识为线程独占状态。
unlock:作用于主内存,解除独占状态。
read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。
volatile作用
volatile保持内存可见性和防止指令重排序
volatile的特殊规则就是:
read、load、use动作必须连续出现。
assign、store、write动作必须连续出现。
所以,使用volatile变量能够保证:
每次读取前必须先从主内存刷新最新的值。
每次写入后必须立即同步回主内存当中。
也就是说,volatile关键字修饰的变量看到的随时是自己的最新值。线程1中对变量v的最新修改,对线程2是可见的。
本地事务
InnoDB 是通过 日志和锁 来保证的事务的 ACID特性,具体如下:
(1)通过数据库锁的机制,保障事务的隔离性;
(2)通过 Redo Log(重做日志)来,保障事务的持久性;
(3)通过 Undo Log (撤销日志)来,保障事务的原子性;
(4)通过 Undo Log (撤销日志)来,保障事务的一致性;
Undo Log 如何保障事务的原子性呢?
具体的方式为:在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为 Undo Log),然后进行数据的修改。如果出现了错误或者用户执行了 Rollback 语句,系统可以利用 Undo Log 中的备份将数据恢复到事务开始之前的状态。
Redo Log如何保障事务的持久性呢?
具体的方式为:Redo Log 记录的是新数据的备份(和 Undo Log 相反)。在事务提交前,只要将 Redo Log 持久化即可,不需要将数据持久化。当系统崩溃时,虽然数据没有持久化,但是 Redo Log 已经持久化。系统可以根据 Redo Log 的内容,将所有数据恢复到崩溃之前的状态。