文章目录
语法
switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上
在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。从 Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
Math.round(11.5) 等于多少?Math.round(-11.5)等于多少
Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5 然后进行下取整。
面向对象
六大原则
-
单一职责原则 英文名称是Single Responsibility Principle,简称SRP
软件中的对象(类、模块、函数等)应该对于扩展是开放的,但是,对于修改是封闭的。
-
开闭原则 英文全称是Open Close Principle,简称OCP
-
里氏替换原则 英文全称是Liskov Substitution Principle,简称LSP
父类能出现的地方子类就可以出现,
而且替换成子类也不会出现任何错误或者异常, -
依赖倒置原则 英文全称是Dependence Inversion Principle,简称DIP
-
接口隔离原则 英文全称是InterfaceSegregation Principles,简称ISP
-
迪米特原则 英文全称为Law of Demeter,简称LOD,也称为最少知识原则(Least Knowledge Principle)
序列化
什么是序列化
Java serialVersionUID 有什么作用?
serialVersionUID 就是控制版本是否兼容的,若我们认为修改的 Person 是向后兼容的,则不修改 serialVersionUID;反之,则提高 serialVersionUID的值。
比如在一个类中新加了一个类,如果和旧的类不兼容就提高uid,这样已经存储到磁盘的旧的类就会失效。
反射类
动态代理
三要素:
- 真实对象实现接口A
- 写一个
xxHandler
implementsInvocationHandler
,实现invoke
方法,在这里持有真实对象Object obj- 方法增强
- 执行
Object invoke = method.invoke(obj, args);
其中 obj是真实对象 - 返回method.invoke的执行方法返回结果或者返回代理类proxy。
- 调用代理方法时先使用
newProxyInstance()
调用
People proxy = (People)Proxy.newProxyInstance(handler.getClass().getClassLoader(), people.getClass().getInterfaces(), handler);
IO
https://tech.meituan.com/2016/11/04/nio.html
java NIO的底层
从阻塞IO到NIO的演进
- 单线程阻塞
- 每连接每线程(线程阻塞就一直傻等也不会返回)
- reactor模式
以下详细介绍
第一种忽略.
第二种每连接每线程(传统bio)
- 之所以使用多线程,主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的
- 在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。
- 这个模型最本质的问题在于,严重依赖于线程。但线程是很”贵”的资源,主要表现在:
- 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
- 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
- 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态。
- 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。
- 当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的。
{
ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(8088);
while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来
Socket socket = serverSocket.accept();
executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程
}
class ConnectIOnHandler extends Thread{
private Socket socket;
public ConnectIOnHandler(Socket socket){
this.socket = socket;
}
public void run(){
while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死循环处理读写事件
String someThing = socket.read()....//读取数据
if(someThing!=null){
......//处理数据
socket.write()....//写数据
}
}
}
}
常见I/O模型对比
所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。
需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在”干活”,而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。
传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。
对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。
最新的AIO(Async I/O)里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。
换句话说:
- BIO里用户最关心“我要读”,
- NIO里用户最关心”我可以读了”,
- AIO模型里用户更需要关注的是“读完了”。
BIO的局限
回忆BIO模型,之所以需要多线程,是因为在进行I/O操作的时候,一是没有办法知道到底能不能写、能不能读,只能”傻等”,即使通过各种估算,算出来操作系统没有能力进行读写,也没法在socket.read()和socket.write()函数中返回,这两个函数无法进行有效的中断。所以除了多开线程另起炉灶,没有好的办法利用CPU。
NIO
- 事件的引入
- NIO由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。
NIO的读写函数可以立刻返回,这就给了我们不开线程利用CPU的最好机会:如果一个连接不能读写(socket.read()返回0或者socket.write()返回0),我们可以把这件事记下来,记录的方式通常是在Selector上注册标记位,然后切换到其它就绪的连接(channel)继续进行读写。当这个连接能读写了,selector上马上获得通知.
通道在选择器上注册下一个通道可能发生的,也是选择器感兴趣的事件.
其次,用一个死循环选择就绪的事件,会执行系统调用(Linux 2.6之前是select、poll,2.6之后是epoll,Windows是IOCP),还会阻塞的等待新事件的到来。新事件到来的时候,会在selector上注册标记位,标示可读、可写或者有连接到来。
注意,select是阻塞的,无论是通过操作系统的通知(epoll)还是不停的轮询(select,poll),这个函数是阻塞的。所以你可以放心大胆地在一个while(true)里面调用这个函数而不用担心CPU空转。
interface ChannelHandler{
void channelReadable(Channel channel);
void channelWritable(Channel channel);
}
class Channel{
Socket socket;
Event event;//读,写或者连接
}
//IO线程主循环:
class IoThread extends Thread{
public void run(){
Channel channel;
while(channel=Selector.select()){//选择就绪的事件和对应的连接
if(channel.event==accept){
registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器
}
if(channel.event==write){
getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件
}
if(channel.event==read){
getChannelHandler(channel).channelReadable(channel);//如果可以读,则执行读事件
}
}
}
Map<Channel,ChannelHandler> handlerMap;//所有channel的对应事件处理器
}
这个程序很简短,也是最简单的Reactor模式
优化NIO线程模型
不足
- 单线程处理I/O的效率确实非常高,没有线程切换,只是拼命的读、写、选择事件。但现在的服务器,一般都是多核处理器,如果能够利用多核心进行I/O,无疑对效率会有更大的提高。
- Java的Selector对于Linux系统来说,有一个致命限制:同一个channel的select不能被并发的调用。因此,如果有多个I/O线程,必须保证:一个socket只能属于一个IoThread,而一个IoThread可以管理多个socket。
仔细分析一下我们需要的线程,其实主要包括以下几种: 1. 事件分发器,单线程选择就绪的事件。 2. I/O处理器,包括connect、read、write等,这种纯CPU操作,一般开启CPU核心个线程就可以。 3. 业务线程,在处理完I/O后,业务一般还会有自己的业务逻辑,有的还会有其他的阻塞I/O,如DB操作,RPC等。只要有阻塞,就需要单独的线程。
另外连接的处理和读写的处理通常可以选择分开,这样对于海量连接的注册和读写就可以分发。虽然read()和write()是比较高效无阻塞的函数,但毕竟会占用CPU,如果面对更高的并发则无能为力。
IO和NIO的区别:
-
可简单认为:IO是面向流的处理,NIO是面向块(缓冲区)的处理
-
- 面向流的I/O 系统一次一个字节地处理数据。
- 一个面向块(缓冲区)的I/O系统以块的形式处理数据。
java集合
讲一讲==,equals,hashcode
- ==直接返回两个对象是否地址相同
- hashcode没重写,返回这个对象的地址.
- equals如果不重写,也直接返回地址是否相同,所以equals为true的话hash一定相等.
- 但是在基本数据类型以及其封装类,String,或自定义的类中.有必要重新定义"相等"的概念,即使地址不同的两个对象,也会"相等",比如字符串的相等就可以定义为"每个字符都相等",所以需要重写equals
- 当equals重写了,hashcode也有必要重写.因为在hashmap等集合中,插入时是按照hash来判断是否已经有此元素的存在的.如果equals重写了,两个值已经相等,当时hashcode不等(因为重写前返回的是地址)就会造成集合中重复元素.所以需要重写hashcode,使得equals为true则hashcode相等.
hashmap
为什么jdk1.8中采用尾插入法
在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。
异常
异常分类
JDK 定义了 套完整的异常机制 所有异常都是 Throwable 的子类,分为error 致命异常)和 Exception (非致命异常)。 error是一种非常特殊的异类型,它的出现标识着系统发生了不可控的错误 例如 StackOverflowerror OutOfMemoryError 。针对此类错误,程序无法处理,只能人工介入。 Exception 又分checked异常(受检异常)和 unchecked 异常(非受检异常)。
全面了解了异常分类之后 当遇到需要处理异常的场景时,要明确该异常属于哪种类型,是需要调用方关注并处理的 checked 异常 还是由更高层次框架处理的 uncheck 异常。不论是哪一类异常。如果需要向上抛出 推荐的做法是根据当前场景自定义具有业务含义的异常 为了避免异常泛滥,可以优先使用或者团队已定义过的异常。例如 远程服务调用中发生服务超时会抛出自定义的DubboTimeoutException 而不是直接抛出 RuntimeException 更不是抛出 Exception或Throwable
try catch-finally 是处理程序异常的三部曲。当存在 try 时,可以只有 catch 代码块,
也可以只有 finally 代码块 就是不能单独只有 try 这个光杆司令。
( I ) try 代码块 监视代码执行过程,一旦发现异常则直接跳转至catch ,如果没有catch ,直接跳转至 finally
( 2 ) catch 代码块可选执行的代码块,如果没有任何异常发生则不会执行,如果发现异常则进行处理或向抛出。这一切都在catch代码块中执行。
( 3 ) finally代码块必选执行的代码块,不管是否有异常产生,即使发生OutOfMemory rror 也会执行通常用于处理善后清理工作。如果finally代码块没有执行,那么有三种可能·
- 没有进入 try 代码块。
- 进入 代码块 但是代码运行中出现了死循环或死锁状态。
- 进入 try 代码块 但是执行了 System.exit() 操作。
异常的抛与接
捕获的异常是被抛出异常的父类。
防止 NPE 定是调用方的责任, 需要调用方进行事先判断。------《码出高效》