Java复习笔记之java常用类库
Java异常
异常处理机制主要回答了三个问题
What:异常类型回答了什么被抛出
Where:异常堆栈跟踪回答了在哪抛出
Why: 异常信息回答了为什么被抛出
Java异常体系
从概念角度解析java的异常处理机制
Error: 程序无法处理的系统错误,编译器不做检查
Exception: 程序可以处理的异常,捕获后可能恢复
总结:前者是程序无法处理的错误,后者是可以处理的异常
RuntimeException: 不可预知的,程序应当自行避免
非RuntimeException: 可预知的,从编译器校验的异常
在责任的角度看:
- Error属于JVM需要负担的责任
- RuntimeException是程序应该负担的责任
- Checked Exception可检查异常是Java编译器应该负担的责任
常见Error以及Exception
RuntimeException
- NullPointerException —— 空指针引用异常
- ClassCassException—— 类型强制转换异常
- IllegalArgumentException—— 传递非法参数异常
- IndexOutOfBoundsException——下标越界异常
- NumberFormatException—— 数字格式异常
非RuntimeException
- ClassNotFoundException——找不到指定class的异常
- IOException—— IO操作异常
Error
- NoClassDefFoundError—— 找不到class定义的异常
- StackOverflowError—— 深递归导致栈被耗尽而抛出的异常
- OutOfMemoryError—— 内存溢出异常
NoClassDefFoundError的成因
- 类依赖的class或者jar不存在
- 类文件存在,但是存在不同的域中
- 大小写问题,javac编译的时候是无视大小写的,很有可能编译出来的class文件就与想要的不一样
Java的异常处理机制
抛出异常:创建异常对象,交由运行时系统处理
捕获异常:寻找合适的异常处理器处理异常,否则终止运行
Java异常的处理原则
具体明确:抛出的异常应能通过异常类名和message准确说明异常的类型和产生异常的原因
提早抛出:应尽可能早的发现并抛出异常,便于精确定位问题
延迟捕获:异常的捕获和处理应尽可能延迟,让掌握更多信息的作用域来处理异常
高效主流的异常处理框架
在用户看来,应用系统发生的所有异常都是应用系统内部的异常
设计一个通用的继承自RuntimeException的异常来统一处理
其余异常都统一转译为上述异常AppException
在catch之后,抛出上述异常的子类,并提供足以定位的信息
由前端接收AppException做统一处理
try-catch的性能
Java异常处理消耗性能的地方
try-catch块影响JVM的优化
异常处理对象实例需要保存栈快照等信息,开销较大
Java集合框架
工作中消失而面试却长存的算法与数据结构
优秀的算法和数据结构被封装到了java的集合框架中
数组结构考点
数组和链表的区别
链表的操作,如反转,链表环路检测,双向链表,循环链表相关操作
队列,栈的应用
二叉树的遍历方式及其递归和非递归的实现
红黑树的旋转
算法考点
内部排序:如递归排序,交换排序(冒泡,快排),选择排序,插入排序
外部排序:应掌握如何利用有限的内存配合海量的外部存储来处理超大的数据集,写不出来也要有相关的思路
考点扩展
哪些排序是不稳定的,稳定意味着什么?
不同的数据集,各种排序最好或最差的情况
如何优化算法
集合之List和Set
集合之Map
HashMap(java8以前):数组+ 链表
当哈希冲突较大时,复杂度可能从O(1)变为O(n)
(java8以后):数组+ 链表 + 红黑树(最差情况从O(n)变为O(logn) 链表转换为红黑树的阈值为8
HashMap: put方法的逻辑
- 如果HashMap未被初始化,则初始化
- 对Key求Hash值,然后再计算下标
- 如果没有碰撞,直接放入桶中
- 如果碰撞了,以链表的方式链接到后面
- 如果链表长度超过阈值,就把链表转成红黑树
- 如果链表长度低于6,就把红黑树装换成链表
- 如果节点已经存在就替换旧值
- 如果桶满了(容量16*加载因子0.75),就需要resize(扩容2倍后重排)
HashMap:如何有效减少碰撞
扰动函数:促使元素位置分布均匀,减少碰撞几率
使用final对象,并采用合适的equals()和hashCode()方法
HashMap: 扩容的问题
多线程环境下,调整大小会存在条件竞争,容易造成死锁
rehashing是一个比较耗时的过程
Hashtable
早期java类库提供的哈希表实现
线程安全:涉及到修改Hashtable的方法,使用Synchronized修饰
串行化的方式运行,性能较差
ConcurrentHashMap
早期实现:通过分段锁Segment来实现 数组+链表
当前实现:CAS+ synchronized使锁更细化 数组+链表+红黑树
ConcurrentHashMap: put方法的逻辑
- 判断Node[]数组是否初始化,没有则进行初始化操作
- 通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败则进入下次循环。
- 检查到内部正在扩容,就帮助它一块扩容
- 如果f!=null,则使用synchronized锁住f元素(链表/红黑二叉树的头元素)
- 如果是Node(链表结构)则执行链表的添加操作
- 如果是TreeNode(树形结构)则执行树添加操作。
5.判断链表长度是否已经达到临界值8,8为默认值,可以自己调整,当节点数超过这值就需要把链表转换为树结构。
ConcurrentHashMap总结;比起Segment,锁拆得更细
首先使用无锁操作CAS插入头节点,失败则循环重试
若头节点已经存在,则尝试获取头节点的同步锁,再进行操作
ConcurrentHashMap: 别的需要注意的点
Size()方法和mappingCount()方法的异同,两者的计算是否准确
多线程环境下如何进行扩容?
HashMap , Hashtable, ConcurrentHashMap 三者的区别
HashMap 线程不安全,数组+链表+红黑树
Hashtable线程安全,锁住整个对象 数组+链表
ConcurrentHashMap: 线程安全,CAS+同步锁,数组+链表+红黑树
HashMap的key,value均可为null,而其他的两个类不支持
J.U.C知识梳理
Java.util.concurrent:提供了并发编程的解决方案
CAS是java.util.concurrent.atomic包的基础
AQS是java.util.concurrent.locks包以及一些常用类比如Semophore, ReentrantLock等类的基础
J.U.C包的分类
线程执行器executor
锁locks
原子变量类atomic
并发工具类tools
并发集合collections
并发工具类
- 闭锁CountDownLatch
让主线程等待一组事件发生后继续执行(子线程不阻塞)
事件指的是CountDownLatCh里的countDown()方法
- 栅栏CyclicBarrier
阻塞当前进程,等待其他线程
等待其他线程,且会阻塞自己当前线程,所有线程必须同时到达栅栏位置后,才能继续执行;
所有线程到达栅栏处,可以触发执行另外一个预先设置的线程
- 信号量Semaphore
控制某个资源可被同时访问的线程个数
- 交换器Exchanger
两个线程到达同步点后,相互交换数据
线程到达同步点会被阻塞,直到另一个线程也到达同步点
BlockingQueue:提供可阻塞的入队和出队操作
主要用于生产者-消费者模式,在多线程场景时生产者线程在队列尾部添加元素,而消费者线程则在队列头部消费元素,通过这种方式能够达到将任务的生产和消费进行隔离的目的。
阻塞队列提供了四种处理方法:
方法/处理方式 |
抛出异常 |
返回特殊值 |
一直阻塞 |
超时退出 |
插入方法 |
add(e) |
offer(e) |
put(e) |
offer(e,time,unit) |
移除方法 |
remove(e) |
poll() |
take() |
poll(time,unit) |
检查方法 |
element(e) |
peek() |
不可用 |
不可用 |
四组不同行为方式解释:
抛异常:如果试图的操作无法立即执行,抛出一个异常
特定值:如果试图的操作无法立即执行,返回一个特定的值(常常是true/false)
阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行
超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知高操作是否成功(典型是true/false)
BlockingQueue提供的七种实现
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列;
- LinkedBlockingQueue: 一个由链表结构组成的有界/无界阻塞队列;
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列;
- DelayQueue:一个使用优先级队列实现的无界阻塞队列;
- SynchronousQueue:一个不存储元素的阻塞队列(生产一个,阻塞,消费后再生产);
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列;
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列;
Java的IO机制
BIO, NIO, AIO的区别:
Block-IO:
InputStream和OutputStream, Reader和Writer
客户端和服务端都被阻塞住,一个客户请求需要开启一个线程
NonBlock-IO: 构建多路复用的,同步非阻塞的IO操作
NIO的核心
Channels
Buffers
Selectors
NIO-Channels的类型
FileChannel: 拥有两个方法
transferTo: 把FileChannel中的数据拷贝到另一个Channel
transferFrom: 把另外一个Channel中数据拷贝到FileChannel
避免了两次用户态和内核态之间上下文切换,即“零拷贝”,效率较高
DatagramChannel
SocketChannel
ServerSocketChannel
NIO-Buffers的类型
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
MappedByteBuffer
NIO-Selector
IO多路复用:调用系统级别的select\poll\epoll
Select , poll, epoll的区别
支持一个进程所能打开的最大连接数
Select |
单个进程所能打开的最大连接数由FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小是32*32,64位机器上是FD_SETSIZE为32*64),我们可以对其进行修改,然后重新编译内核,但是性能无法保证,需要做进一步测试 |
poll |
本质上与select没有区别,但是它没有最大连接数的限制,原因是它基于链表来存储 |
epoll |
虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接 |
FD剧增后带来的IO效率问题
Select |
因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度的“线性下降”的性能问题 |
poll |
同上 |
epoll |
由于epoll是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll不会有“线性下降”的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题 |
消息的传递方式上
Select |
内核需要将消息传递到用户空间,需要内核的拷贝动作 |
poll |
同上 |
epoll |
通过内核和用户空间共享一块内存来实现,性能较高 |
Asynchronous IO: 基于事件和回调机制
AIO如何进一步加工处理结果
基于回调: 实现CompletionHandler接口,调用时触发回调函数
返回Future: 通过isDone()查看是否准备好,通过get()等待返回数据
BIO, NIO, AIO对比
属性\模型 |
阻塞BIO |
非阻塞NIO |
异步AIO |
blocking |
阻塞并同步 |
非阻塞但同步 |
非阻塞并异步 |
线程数(server:client) |
1:1 |
1:N |
0:N |
复杂度 |
简单 |
较复杂 |
复杂 |
吞吐量 |
低 |
高 |
高 |
同步/异步, 阻塞/非阻塞之间的区别
同步/异步主要针对客户端:
同步:就是当客户端发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是说必须一件一件的事情去做,等一件做完了才能去做下一件。
异步:就是当客户端发出一个功能调用时,调用者不用等待接收方发出响应。实际处理这个调用的部件在完成后,会通过状态,通知和回调来通知调用者。客户端可以接着做后面的事情。
虽然主要是针对的客户端,但是服务器端也不是完全没有关系的,同步/异步必须配合服务器端才能实现。同步/异步是由客户端自己控制的,但是服务器端是否阻塞/非阻塞,客户端完全不需要担心/
阻塞与非阻塞主要是针对服务器:
阻塞:阻塞调用是指服务器端被调用者调用返回结果之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞;指在不能立即得到结果之前,该调用不会阻塞当前线程