BIO、NIO、AIO之间的区别
BIO表示同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
NIO表示同步非阻塞IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
AIO表示异步非阻塞IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成IO操作后再通知服务器应用来启动线程进行处理。
- 应用场景:
BIO适用于连接数目比较小且固定的架构,该方式对服务器资源要求比较高,JDK 1.4以前的唯一选择。
NIO适用于连接数目多且连接比较短(轻操作)的架构,如聊天服务器,编程复杂,JDK 1.4开始支持,如在Netty框架中使用。
AIO适用于连接数目多且连接比较长(重操作)的架构,如相册服务器,充分调用操作系统参与并发操作,编程复杂,JDK 1.7开始支持。
select/poll/epoll
select
- select系统调用是用来让我们的程序监视多个文件句柄的状态变化的。程序会停在select这里等待,直到被监视的文件句柄有一个或多个发生了状态改变;
- 当调用select()时,由内核根据IO状态修改fd_set的内容(fd_set实际上是一个long类型的数组,每一个数组元素都能与一打开的文件句柄建立联系,建立联系的工作由程序员完成);
- 拷贝fd_set到内核空间;
- 轮询的方式查找哪个文件可读
poll
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
重写equals()为什么要重写Hashcode()
以一个“类的用途”来区分hashcode()和equals()的关系,分两种情况。
(1). 不创建“类对应的散列表,也就是不会在Hashset、HashMap、Hashtable等这些本质是散链表的数据结构中用到该类”。在这种情况下,equals和Hashcode之间没有关系。因为hashcode()这个起定位作用的方法根本没有任何作用。
(2). 创建“类对于的散列表”,也就是说会在Hashset、HashMap、Hashtable等这些本质是散链表的数据结构中用到该类。这时候,hashcode()和equals()之间是有关系的:如果两个对象相等,那么它们的hashcode一定相同;如果两个对象的hashcode相等,它们不一定相等。这时候如果我们重写equals却不重写hashcode,那么两个内容相同的对象会因为hashcode不同而全部进入到散列表中。例如,HashSet会加入两个相同内容的元素。(hash操作添加元素会比较hashcode,然后计算插入位置)。
Fail-fast机制(禁止在foreach循环里进行add/remove操作)
使用foreach语法遍历集合或者数组的时候,可以起到和普通for循环同样的效果,并且代码更加简洁,所以foreach循环通常被称之为增强for循环。依赖iterator实现。如果在foreach中执行add/remove即会抛出ConcurrentModificationException。这个错误在modCount 不等于expectedModCount的时候就会抛出。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
(1). modCount是ArrayList中的一个成员变量,它表示该集合实际被修改的次数。
(2). expectedModCount是ArrayList中的一个内部类itr中的成员变量,表示迭代器期望被该集合修改的次数,只有通过迭代器对集合操作,该值才会改变。
(3). Itr是一个Iterator的实现,使用ArrayList.iterator方法可以获取到的迭代器的就是Itr的实例。
在foreach中之间调用ArrayList的add/remove方法,会改变modCount的值,但是并没有对expectedModCount做任何操作。所以,之所以会抛出错误,是因为我们的代码中使用了foreach,而在foreach中,集合遍历是通过iterator进行的,但是元素的add/remove却是直接使用集合类自己的方法,这就导致Iterator在遍历的时候会发现有一个元素在自己不知不觉的情况下被添加/删除了,就会抛出一个异常,用来提示用户可能发生了并发修改。
单例的创建方式:懒汉/饿汉式、双重检测(默写)、静态内部类实现单例模式。
- 静态内部类实现单例模式
public class Singleton {
//声明为 private 避免调用默认构造方法创建对象
private Singleton() {
}
// 声明为 private 表明静态内部该类只能在该 Singleton 类中被访问
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getUniqueInstance() {
return SingletonHolder.INSTANCE;
}
}
当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance() 方法从而触发 SingletonHolder.INSTANCE 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。
- 双重检测实现单例模式
public class Singleton{
//volatile 防止指令重排(一般来说是分配内存、初始化、返回对象引用)
//如果第一次创建实例的时候在没有完初始化,这时如果调用单例,可能会得到一个没有初始化的对象。
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
//防止当一个线程创建一个实例之后,singleton就不再为空了,但是后续的线程并没有做第二次非空检查
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
代理模式
JavaGuide代理模式
代理简单分为静态代理,动态代理(JDK代理和CGLIB代理)
- 静态代理
从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。
步骤:
(1)定义一个接口及其实现类;
(2)创建一个代理类同样实现这个接口
(3)将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。 - 动态代理
动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
JDK动态代理步骤:InvocationHandler 接口和 Proxy 类是核心。
(1)定义一个接口及其实现类;
(2)自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中我们
会调用原生方法(被代理类的方法)并自定义一些处理逻辑;
(3)通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;
JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。
CGLIB动态代理步骤:MethodInterceptor 接口和 Enhancer 类是核心。
(1)定义一个类;
(2)自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;
(3)通过 Enhancer 类的 create()创建代理类;
Java泛型类型擦除以及类型擦除带来的问题
泛型:类型参数化,在编译器层次实现,在生成的Java字节码中是不包含泛型中的类型信息的;使用泛型的时候加上类型参数,会在编译的时候去掉,只剩下原始类型。
(1)泛型类型不能是基本数据类型
不能用类型参数替换基本类型。就比如,没有ArrayList<double>
,只有ArrayList<Double>
。因为当类型擦除后,ArrayList
的原始类型变为Object
,但是Object
类型不能存储double
值,只能引用Double
的值。
(2)编译时集合的instanceof
ArrayList<String> arrayList = new ArrayList<String>();
因为类型擦除之后,ArrayList<String>
只剩下原始类型,泛型信息String
不存在了。那么,编译时进行类型查询的时候使用下面的方法是错误的
if( arrayList instanceof ArrayList<String>)
(3)泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数,因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。
(4)类型检查及编译
举个例子
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList();
list1.add("1"); //编译通过
list1.add(1); //编译错误
String str1 = list1.get(0); //返回类型就是String
ArrayList list2 = new ArrayList<String>();
list2.add("1"); //编译通过
list2.add(1); //编译通过
Object object = list2.get(0); //返回类型就是Object
new ArrayList<String>().add("11"); //编译通过
new ArrayList<String>().add(22); //编译错误
String str2 = new ArrayList<String>().get(0); //返回类型就是String
}
}
通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
(5)自动类型转换
因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?看下ArrayList.get()
方法:
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
可以看到,在return
之前,会根据泛型变量进行强转。假设泛型类型变量为Date
,虽然泛型信息会被擦除掉,但是会将(E) elementData[index]
,编译为(Date)elementData[index]
。所以我们不用自己进行强转。当存取一个泛型域时也会自动插入强制类型转换。
B+树和B树的区别
- B 树的所有节点既存放 键(key) 也存放 数据(data);而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。
- B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
- B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
Mysql索引的实现
InnoDB引擎 MyISAM引擎序列化与反序列化(参考)
序列化与反序列化: 序列化是将对象的状态信息转化为可以存储或者传输的形式的过程,可以是字节或者XML等格式;而字节码或者XML格式的可以还原成完全相等的对象,这个相反的过程称为反序列化。
java中序列化与反序列化:在Java中,我们创建出来的这些对象都存在于JVM中的堆(heap)内存中,只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止,这些对象也就随之消失;但是在真实的应用场景中,我们需要将这些对象持久化下来,并且在需要的时候将对象重新读取出来,Java的序列化可以帮助我们实现该功能。对象序列化机制(object serialization)是java语言内建的一种对象持久化方式,通过对象序列化,可以将对象的状态信息保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式转换成对象,对象的序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。在JAVA中,对象的序列化和反序列化被广泛的应用到RMI(远程方法调用)及网络传输中。
Executor框架创建线程池
线程池可以通过Executors创建,但是在阿里规范文档中不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
- Executors 返回线程池对象的弊端如下:
FixedThreadPool
和 SingleThreadExecutor
: 允许请求的队列长度为 Integer.MAX_VALUE
(无界队列),可能堆积大量的请求,从而导致 OOM。
CachedThreadPool
和 ScheduledThreadPool
: 允许创建的线程数量为 Integer.MAX_VALUE
,可能会创建大量线程,从而导致 OOM。
-
Executor框架
Executor框架
- 主线程首先要创建实现
Runnable
或者Callable
接口的任务对象。 - 把创建完成的实现
Runnable/Callable
接口的 对象直接交给ExecutorService
执行:ExecutorService.execute(Runnable command))
或者也可以把Runnable
对象或Callable
对象提交给ExecutorService
执行(ExecutorService.submit(Runnable task)
或ExecutorService.submit(Callable <T> task))
。 - 如果执行
ExecutorService.submit(…)
,ExecutorService
将返回一个实现Future
接口的对象(我们刚刚也提到过了执行execute()
方法和submit()
方法的区别,submit()
会返回一个FutureTask
对象)。由于FutureTask
实现了Runnable
,我们也可以创建FutureTask
,然后直接交给ExecutorService
执行。 - 最后,主线程可以执行
FutureTask.get()
方法来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)
来取消此任务的执行。
多态是如何实现的
多态:同一个接口,使用不同的实例而执行不同的操作(父类引用指向子类对象)
- 多态(动态绑定):在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。 原来父类对象中方法的地址指向自己的方法,当父类引用指向new子类对象的过程时指针会发生改变,父类对象中方法的指针从指向自己的方法变成指向new对象对应类中重写的那个方法。
- 父类引用指向子类对象:所能看到的只是父类那部分属性和方法
多态实现.png
note : 变量ployA指向对象ployB,但是只能看到对象ployA对应的属性和方法。当把对象ployB的地址赋给变量ployA时,原来对象ployA中ploy()方法的地址从原来指向PloyA类中的ploy( )方法变成指向PloyB类中的ploy( )方法。所以当ployA访问ploy( )方法的时候:首先访问的是ployA对象中ploy( )的地址,此地址指向的是重写以后的PloyB类中的方法。
voliate关键字为什么不能保证原子性(安全性)
voliate_1.jpg voliate_2.jpg voliate_3.jpg还有一个重要原因是由于它没有回退机制。
原子类和CAS
Atomic :指的是一个操作不可中断。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他的线程干扰。
四种原子类 :基本类型(AtomicInteger等
)、数组类型(AtomicIntegerArray
)、引用类型(AtomicReference
)、对象的属性修改类型(AtomicIntegerFieldUpdater
)。
原子类与CAS的关系:
//Mr.ZZH
//我们以AtomicInteger为例进行分析
//获取unsafe对象,unsafe是由底层语言实现,提供了硬件级别的原子操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
//valueOffset用 final 修饰,用于保存偏移量(1. 不同属性的偏移量不同 2. 该类不同实例的同一属性偏移量相同)
private static final long valueOffset;
static {
try {
//2. 获取value属性的偏移量
valueOffset = unsafe.objectFieldOffset
//1. 利用getDeclaredField反射原理来访问私有属性value
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//value属性存放变量的值,用volatile修饰就是为了强制刷回主存,保证可见性
private volatile int value;
//我以其中的一个方法为例,这个方法是获取并自增 1
public final int getAndIncrement() {
//Atomic原子类底层都是通过unsafe来实现的
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//unsafe.getAndAddInt的实现过程,其实这个过程也就是CAS自旋锁的过程
//var1是调用对象,var2是变量的偏移量,var4是所要改变的量
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//1. 利用native操作在主存中获取var5的值
var5 = this.getIntVolatile(var1, var2);
//2. 利用var5和当前线程的工作内存的值(用过var1和var2获取)进行比较,相等则更新为var5 + var4
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
//返回var5 ,注意返回的并不是修改后的值
return var5;
}
ReentrantLock和synchronized
两种锁的比较.pngHTTP
HTTP 是一个在计算机世界里专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。
常见字段
Host
:客户端发送请求时,用来指定服务器的域名。
Content-Length
:服务器在返回数据时,会有 Content-Length 字段,表明本次回应的数据长度。
Connection
:最常用于客户端要求服务器使用 TCP 持久连接,以便其他请求复用。
Content-Type
: 用于服务器回应时,告诉客户端,本次数据是什么格式。
Content-Encoding
:说明数据的压缩方法。表示服务器返回的数据使用了什么压缩格式
Get和Post
Get
方法的含义是请求从服务器获取资源,这个资源可以是静态的文本、页面、图片视频等;而POST
方法则是相反操作,它向 URI
指定的资源提交数据,数据就放在报文的 body
里。
HTTP特性
- 优点:简单、灵活和易于扩展、应用广泛和跨平台
- 缺点:双刃剑,分别是「无状态、明文传输」,同时还有一大缺点「不安全」。
HTTP/1.1
- 长连接:减少了 TCP 连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。
- 管道网络传输:即可在同一个 TCP 连接里面,客户端可以发起多个请求,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。
- 队头堵塞:因为当顺序发送的请求序列中的一个请求因为某种原因被阻塞时,在后面排队的所有请求也一同被阻塞了,会招致客户端一直请求不到数据,这也就是「队头阻塞」。好比上班的路上塞车
HTTP/2
(已经基于HPPTs)
头部压缩、二进制格式、数据流、多路复用和服务器推送
HTTP/3
HTTP/3 把 HTTP 下层的 TCP 协议改成了 UDP(QUIC)!
HTTP和HTTPS
- HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输
- HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输
- HTTP 的端口号是 80,HTTPS 的端口号是 443
- HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的
HTTPS解决了哪些问题
- 混合加密的方式实现信息的机密性,解决了窃听的风险。
- 摘要算法的方式来实现完整性,它能够为数据生成独一无二的「指纹」,指纹用于校验数据的完整性,解决了篡改的风险。
- 将服务器公钥放入到数字证书(包含服务器公钥和数字签名)中,解决了冒充的风险。
SSL/TLS 协议建立的详细流程:
- 客户端:发送SSL/TLS协议版本号、随机数1和支持的密码套件
- 服务器:确认协议版本号、确认密码套件、发送随机数2和数字证书
- 客户端:确认数字证书并取出公钥,发送用公钥加密后的随机数3,计算会话密钥,发送所有握手数据摘要供服务器检验
- 服务器:收到随机数3,密钥解密,计算会话密钥,同时把之前所有内容的发生的数据做个摘要,用来供客户端校验。
note:双方各自利用三个随机数和协商的密码套件来生成会话密钥
范式
1NF:数据表的每一列都要保持它的原子特性,也就是列不能再被分割
2NF:满足1NF的前提下,属性必须完全依赖于主键(消除部分依赖)。
3NF:满足2NF的前提下,所有的非主属性不依赖于其他的非主属性(消除传递依赖)。
依赖
在数据表中,属性(属性组)X确定的情况下,能完全退出来属性Y完全依赖于X。
完全依赖
完全依赖是针对于属性组来说,当一组属性X能推出来Y的时候就说Y完全依赖于X。
部分依赖
一组属性X中的其中一个或几个属性能推出Y就说Y部分依赖于X。
JWT和token
传统token
传统的token.png- 优点
1.可以隐藏真实数据; 2.适用于分布式/微服务; 3.安全系数高 - 缺点
1.存放在Redis,必须依赖服务器,占用服务器资源;2. 有时候还得根据Redis查到的信息再数据库进行二次查询(例:用户是否vip)
JWT
JWT.png- 优点
- 无需服务器端存放数据,减缓了服务器压力;2. jwt可以在负载中添加一部分信息避免二次查询
Spring中过滤器与拦截器
单点登录
AQS
AQS 的全称为(AbstractQueuedSynchronizer
),这个类在 java.util.concurrent.locks
包下面,是一个用来构建锁和同步器的框架,比如ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等
- AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
- AQS利用了模板模式(不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码)
ReentrantLock
公平锁和非公平锁的实现区别
- 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
- 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,
- 如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
- 如果(state != 0),判断加锁线程是否是当前线程,如果是则重入。
- 如果加锁失败则返回false。
CountDownLatch和join
目前有三个任务work1、work2和work3
- 作用相同情况:work3需要等待work1和work2整个任务执行后才能执行。
- 作用不同的情况:work3需要等待work1或work2整个任务中某一阶段完成即可执行。
CountDownLatch和CyclicBarrier
CountDownLatch: 一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;
CyclicBarrier : 多个线程互相等待,直到到达同一个同步点,再继续一起执行。
Synchronized原理
synchronized原理.jpgSQL执行流程
查询语句的执行流程如下:权限校验(如果命中缓存)—>查询缓存—>分析器—>优化器—>权限校验—>执行器—>引擎
更新语句执行流程如下:分析器—>权限校验—>执行器—>引擎—>redo log(prepare 状态)—>binlog—>redo log(commit状态)
网友评论