复习

#TCP UDP

TCP: 传输控制协议(英语:Transmission Control Protocol,缩写为 TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的 RFC 793 定义。

UDP:用户数据报协议(英语:User Datagram Protocol,缩写为 UDP),又称使用者资料包协定,是一个简单的面向数据报的传输层协议,正式规范为 RFC 768。

#TCP 与 UDP 基本区别

  1. 基于连接与无连接
  2. TCP 要求系统资源较多,UDP 较少;
  3. UDP 程序结构较简单
  4. 流模式(TCP)与数据报模式(UDP);
  5. TCP 保证数据正确性,UDP 可能丢包
  6. TCP 保证数据顺序,UDP 不保证
  7. UDP 是不处理堵塞,应用需要发,就会发送。TCP 还拥有堵塞控制,TCP 会根据网络环境调整发包的频率。
  8. UDP 支持多播和广播
  9. TCP 有流量控制(滑动窗口)和拥塞控制(慢开始、拥塞避免、快重传、快恢复)

#UDP 应用场景

  1. 面向数据报方式
  2. 网络数据大多为短消息
  3. 拥有大量 Client
  4. 对数据安全性无特殊要求
  5. 网络负担非常重,但对响应速度要求高
  6. QQ 的文件传输
  7. DNS、TFTP、RIP(路由选择协议)、SNMP、NFS

#TCP 三次握手

TCP三次握手

#TCP 四次握手

TCP四次握手

#为什么两次握手不行

为了防止已失效的连接请求报文段突然又传送到了 B,因而产生错误。A 发送的包丢了,又发送一个包建立连接,结果这个丢的包又被找回来了,B 又建立了一个 A 不认可的连接。

#为什么需要设置一个 TIME-WAIT 的时间

  1. 保证 TCP 协议的全双工连接能够可靠关闭。先说第一点,如果 Client 直接 CLOSED 了,那么由于 IP 协议的不可靠性或者是其它网络原因,导致 Server 没有收到 Client 最后回复的 ACK。那么 Server 就会在超时之后继续发送 FIN,此时由于 Client 已经 CLOSED 了,就找不到与重发的 FIN 对应的连接,最后 Server 就会收到 RST 而不是 ACK,Server 就会以为是连接错误把问题报告给高层。这样的情况虽然不会造成数据丢失,但是却导致 TCP 协议不符合可靠连接的要求。所以,Client 不是直接进入 CLOSED,而是要保持 TIME_WAIT,当再次收到 FIN 的时候,能够保证对方收到 ACK,最后正确的关闭连接。
  2. 保证这次连接的重复数据段从网络中消失。再说第二点,如果 Client 直接 CLOSED,然后又再向 Server 发起一个新连接,我们不能保证这个新连接与刚关闭的连接的端口号是不同的。也就是说有可能新连接和老连接的端口号是相同的。一般来说不会发生什么问题,但是还是有特殊情况出现:假设新连接和已经关闭的老连接端口号是一样的,如果前一次连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达 Server,由于新连接和老连接的端口号是一样的,又因为 TCP 协议判断不同连接的依据是 socket pair,于是,TCP 协议就认为那个延迟的数据是属于新连接的,这样就和真正的新连接的数据包发生混淆了。所以 TCP 连接还要在 TIME_WAIT 状态等待 2 倍 MSL,这样可以保证本次连接的所有数据都从网络中消失。

#TCP 如何保证可靠的传输?

  1. 确认和重传
  2. 数据校验
  3. 数据合理分片和排序
  4. 流量控制:当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。
  5. 拥塞控制:当网络拥塞时,减少数据的发送。

#相关名词

  • MSL(Maximum Segment Lifetime):报文最大生存时间
  • MTU(Maximum Transmission Unit):用来通知对方所能接受数据服务单元的最大尺寸,说明发送方能够接受的有效载荷大小。

#进程和线程

#定义

进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.

线程:进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

#关系

一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。

#区别

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

  1. 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
  2. 线程的划分尺度小于进程,使得多线程程序的并发性高。
  3. 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
  4. 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  5. 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位

在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过 CPU 调度,在每个时间片中只有一个线程执行)

内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了 CPU 外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。

包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

#协程

协程,是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;
不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

#红黑树

#性质

  1. 每个节点要么是黑色,要么是红色。
  2. 根节点是黑色。
  3. 每个叶子节点(NIL)是黑色。
  4. 每个红色结点的两个子结点一定都是黑色。
  5. 任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

#插入

插入

#删除

删除

#时间复杂度

Algorithm Average Worst case
Space O(n) O(n)
Search O(log n) O(log n)
Insert O(log n) O(log n)
Delete O(log n) O(log n)

#与 AVL 区别

AVL 是严格的平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多;
红黑树是用非严格的平衡来换取增删节点时候旋转次数的降低开销;
所以简单说,如果你的应用中,搜索的次数远远大于插入和删除,那么选择 AVL 树,
如果搜索,插入删除次数几乎差不多,应选择红黑树。即,有时仅为了排序(建立-遍历-删除),不查找或查找次数很少,R-B 树合算一些。
AVL 树在顺序插入和删除时有 20%左右的性能优势,但随机性能反而落后 15%左右,现实应用当然一般都是随机情况,所以红黑树得到了更广泛的应用。

#死锁

死锁是指多个进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象(互相挂起等待),若无外力作用,它们都将无法推进下去。

#常见死锁

  1. 线程将自己锁住

为了保证线程之间的同步和互斥,我们往往需要给其加锁,有时候,线程申请了锁资源,还没有等待释放,又一次申请这把锁,结果就是挂起等待这把锁的释放,但是这把锁是被自己拿着,所以就会永远挂起等待,就造成了死锁。

  1. 多线程竞争资源循环等待

有两个线程 P1 和 P2,P1 首先申请得到了锁 L1,P2 申请得到了锁 L2,这个时候 P1 有向去申请锁 L2,结果是被挂起等待 P2 释放锁 L2,而 P2 恰好也想申请锁 L1,结果是挂起等待 P1 释放锁 L1,此时就造成两个线程互相僵持,造成死锁。

  1. 进程推进顺序不当引起的死锁问题

有三个线程,P1,P2 和 P3,分别生产数据 M1,M2,M3,同时分别接收别的线程产生的数据 M3,M2,M1,如果线程推进的顺序正确,即三个线程都先生产数据,再接收,那么没有问题,但是一旦线程先接受数据,再生产数据,因为一开始没有数据产生,那么就会造成三个线程的死锁问题。

#产生原因

  1. 系统的资源不足。
  2. 进程(线程)推进的顺序不对。
  3. 资源的分配不当。

#必要条件

  1. 互斥条件:进程(线程)申请的资源在一段时间中只能被一个进程(线程)使用。
  2. 请求与等待条件:进程(线程)已经拥有了一个资源,但是又申请新的资源,拥有的资源保持不变 。
  3. 不可剥夺条件:在一个进程(线程)没有用完,主动释放资源的时候,不能被抢占。
  4. 循环等待条件:多个进程(线程)之间存在资源循环链。

#打破

  1. 打破互斥条件:改造独占性资源为虚拟大资源,但是大部分资源无法改造,因此不建议使用这个方法。
  2. 打破请求与保持条件:在进程(线程)运行之前,就把需要申请的资源一次性申请到位,满足则运行,不满足就等待,这样就不会造成在占有资源的情况下,还要申请新资源。
  3. 打破不可剥夺条件:在占有资源并且还想要申请新资源的时候,归还已经占有的资源。
  4. 打破循环等待条件:实现资源的有序分配,即对所有的设备进行分类编号,只能以升序的方式来申请资源。

#处理方法

  1. 预防死锁:破坏死锁产生的四个条件之一,注意,互斥条件不能破坏。
  2. 避免死锁:合理的分配资源。
  3. 检查死锁:利用专门的死锁机构检查死锁的发生,然后采取相应的方法。
  4. 解除死锁:发生死锁时候,采取合理的方法解决死锁。一般是强行剥夺资源。
  • 等待某个资源时,使用超时机制。
  • 采用消息通信的通信机制,而不是共享内存的通信机制。

#HTTP

HTTP 协议是 Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网服务器传输超文本到本地浏览器的传送协议。
HTTP 是一个基于 TCP/IP 通信协议来传递数据。
应用层协议。

  1. HTTP 是无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
  2. HTTP 是媒体独立的:这意味着,只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过 HTTP 发送。客户端以及服务器指定使用适合的 MIME-type 内容类型。
  3. HTTP 是无状态:HTTP 协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

#HTTP 2.0

HTTP/2(超文本传输协议第 2 版,最初命名为 HTTP2.0),是 HTTP 协议的第二个主要版本。HTTP/2 是 HTTP 协议自 1999 年 HTTP1.1 发布后的首个更新,主要基于 SPDY 协议。
HTTP2.0 的特点是:在不改动 HTTP 语义、方法、状态码、URI 及首部字段的情况下,大幅度提高了 web 性能。

SPDY 协议:Google 开发的基于 TCP 协议的应用层协议。目标是优化 HTTP 协议的性能,通过压缩、多路复用和优先级等技术,缩短网页的加载时间并提高安全性。SPDY 协议的核心思想是尽量减少 TCP 连接数。SPDY 并不是一种用于替代 HTTP 的协议,而是对 HTTP 协议的增强。

#二进制传输

HTTP2.0 中所有加强性能的核心是二进制传输,在 HTTP1.x 中,我们是通过文本的方式传输数据。基于文本的方式传输数据存在很多缺陷,文本的表现形式有多样性,因此要做到健壮性考虑的场景必然有很多,但是二进制则不同,只有 0 和 1 的组合,因此选择了二进制传输,实现方便且健壮。
在 HTTP2.0 中引入了新的编码机制,所有传输的数据都会被分割,并采用二进制格式编码。
为了保证 HTTP 不受影响,那就需要在应用层(HTTP2.0)和传输层(TCP or UDP)之间增加一个二进制分帧层。在二进制分帧层上,HTTP2.0 会将所有传输的信息分为更小的消息和帧,并采用二进制格式编码,其中 HTTP1.x 的首部信息会被封装到 Headers 帧,而 Request Body 则封装到 Data 帧。

#多路复用

在 HTTP1.0 中,我们经常会使用到雪碧图、使用多个域名等方式来进行优化,都是因为浏览器限制了同一个域名下的请求数量,当页面需要请求很多资源的时候,队头阻塞(Head of line blocking)会导致在达到最大请求时,资源需要等待其他资源请求完成后才能继续发送。
HTTP2.0 中,有两个概念非常重要:帧(frame)和流(stream)。
帧是最小的数据单位,每个帧会标识出该帧属于哪个流,流是多个帧组成的数据流。
所谓多路复用,即在一个 TCP 连接中存在多个流,即可以同时发送多个请求,对端可以通过帧中的表示知道该帧属于哪个请求。在客户端,这些帧乱序发送,到对端后再根据每个帧首部的流标识符重新组装。通过该技术,可以避免 HTTP 旧版本的队头阻塞问题,极大提高传输性能。

#Header 压缩

在 HTTP1.0 中,我们使用文本的形式传输 header,在 header 中携带 cookie 的话,每次都需要重复传输几百到几千的字节,这着实是一笔不小的开销。
在 HTTP2.0 中,我们使用了 HPACK(HTTP2 头部压缩算法)压缩格式对传输的 header 进行编码,减少了 header 的大小。并在两端维护了索引表,用于记录出现过的 header,后面在传输过程中就可以传输已经记录过的 header 的键名,对端收到数据后就可以通过键名找到对应的值。

#服务器 Push

在 HTTP2.0 中,服务端可以在客户端某个请求后,主动推送其他资源。
可以想象一下,某些资源客户端是一定会请求的,这时就可以采取服务端 push 的技术,提前给客户端推送必要的资源,就可以相对减少一点延迟时间。在浏览器兼容的情况下也可以使用 prefetch。

#更安全

HTTP2.0 使用了 tls 的拓展 ALPN 做为协议升级,除此之外,HTTP2.0 对 tls 的安全性做了近一步加强,通过黑名单机制禁用了几百种不再安全的加密算法。

#数据库

#三个问题

#脏读(读取未提交数据)

A 事务读取 B 事务尚未提交的数据,此时如果 B 事务发生错误并执行回滚操作,那么 A 事务读取到的数据就是脏数据。就好像原本的数据比较干净、纯粹,此时由于 B 事务更改了它,这个数据变得不再纯粹。这个时候 A 事务立即读取了这个脏数据,但事务 B 良心发现,又用回滚把数据恢复成原来干净、纯粹的样子,而事务 A 却什么都不知道,最终结果就是事务 A 读取了此次的脏数据,称为脏读。

时间顺序 转账事务 取款事务
1 开始事务
2 开始事务
3 查询账户余额为 2000 元
4 取款 1000 元,余额被更改为 1000 元
5 查询账户余额为 1000 元(产生脏读)
6 取款操作发生未知错误,事务回滚,余额变更为 2000 元
7 转入 2000 元,余额被更改为 3000 元(脏读的 1000+2000)
8 提交事务
备注 按照正确逻辑,此时账户余额应该为 4000 元

#不可重复读(前后多次读取,数据内容不一致)

事务 A 在执行读取操作,由整个事务 A 比较大,前后读取同一条数据需要经历很长的时间 。而在事务 A 第一次读取数据,比如此时读取了小明的年龄为 20 岁,事务 B 执行更改操作,将小明的年龄更改为 30 岁,此时事务 A 第二次读取到小明的年龄时,发现其年龄是 30 岁,和之前的数据不一样了,也就是数据不重复了,系统不可以读取到重复的数据,成为不可重复读。

时间顺序 事务 A 事务 B
1 开始事务
2 第一次查询,小明的年龄为 20 岁
3 开始事务
4 其他操作
5 更改小明的年龄为 30 岁
6 提交事务
7 第二次查询,小明的年龄为 30 岁
备注 按照正确逻辑,事务 A 前后两次读取到的数据应该一致

#幻读(前后多次读取,数据总量不一致)

事务 A 在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务 B 执行了新增数据的操作并提交后,这个时候事务 A 读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。

时间顺序 事务 A 事务 B
1 开始事务
2 第一次查询,数据总量为 100 条
3 开始事务
4 其他操作
5 新增 100 条数据
6 提交事务
7 第二次查询,数据总量为 200 条
备注 按照正确逻辑,事务 A 前后两次读取到的数据总量应该一致

#隔离级别

#读未提交(Read uncommitted)

在这种隔离级别下,所有事务能够读取其他事务未提交的数据。读取其他事务未提交的数据,会造成脏读。因此在该种隔离级别下,不能解决脏读、不可重复读和幻读。

#读已提交(Read committed)

在这种隔离级别下,所有事务只能读取其他事务已经提交的内容。能够彻底解决脏读的现象。但在这种隔离级别下,会出现一个事务的前后多次的查询中却返回了不同内容的数据的现象,也就是出现了不可重复读。

这是大多数数据库系统默认的隔离级别,例如 Oracle 和 SQL Server,但 mysql 不是。

#可重复读(Repeatable read)

在这种隔离级别下,所有事务前后多次的读取到的数据内容是不变的。也就是某个事务在执行的过程中,不允许其他事务进行 update 操作,但允许其他事务进行 add 操作,造成某个事务前后多次读取到的数据总量不一致的现象,从而产生幻读。

mysql 的默认事务隔离级别。

#可串行化(Serializable)

在这种隔离级别下,所有的事务顺序执行,所以他们之间不存在冲突,从而能有效地解决脏读、不可重复读和幻读的现象。但是安全和效率不能兼得,这样事务隔离级别,会导致大量的操作超时和锁竞争,从而大大降低数据库的性能,一般不使用这样事务隔离级别。

#总结

隔离级别 脏读 不可重复读 幻读
读未提交(Read uncommitted) ×(未解决) × ×
读已提交(Read committed) √(解决) × ×
可重复读(Repeatable read) ×
可串行化(Serializable)

#事务

数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作(对数据库的相关增删改查的操作),要么完全地执行,要么完全地不执行。

#特性(ACID)

  1. 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
  2. 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
  3. 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
  4. 持久性(Durability):一个事务一旦提交,他对数据库的修改应该永久保存在数据库中。

用一个常用的“A 账户向 B 账号汇钱”的例子来说明如何通过数据库事务保证数据的准确性和完整性。熟悉关系型数据库事务的都知道从帐号 A 到帐号 B 需要 6 个操作:

1、从 A 账号中把余额读出来(500)。
2、对 A 账号做减法操作(500-100)。
3、把结果写回 A 账号中(400)。
4、从 B 账号中把余额读出来(500)。
5、对 B 账号做加法操作(500+100)。
6、把结果写回 B 账号中(600)。

原子性:
保证 1-6 所有过程要么都执行,要么都不执行。一旦在执行某一步骤的过程中发生问题,就需要执行回滚操作。 假如执行到第五步的时候,B 账户突然不可用(比如被注销),那么之前的所有操作都应该回滚到执行事务之前的状态。

一致性
在转账之前,A 和 B 的账户中共有 500+500=1000 元钱。在转账之后,A 和 B 的账户中共有 400+600=1000 元。也就是说,数据的状态在执行该事务操作之后从一个状态改变到了另外一个状态。同时一致性还能保证账户余额不会变成负数等。

隔离性
在 A 向 B 转账的整个过程中,只要事务还没有提交(commit),查询 A 账户和 B 账户的时候,两个账户里面的钱的数量都不会有变化。
如果在 A 给 B 转账的同时,有另外一个事务执行了 C 给 B 转账的操作,那么当两个事务都结束的时候,B 账户里面的钱应该是 A 转给 B 的钱加上 C 转给 B 的钱再加上自己原有的钱。

持久性
一旦转账成功(事务提交),两个账户的里面的钱就会真的发生变化(会把数据写入数据库做持久化保存)!

原子性与隔离行
一致性与原子性是密切相关的,原子性的破坏可能导致数据库的不一致,数据的一致性问题并不都和原子性有关。
比如刚刚的例子,在第五步的时候,对 B 账户做加法时只加了 50 元。那么该过程可以符合原子性,但是数据的一致性就出现了问题。

因此,事务的原子性与一致性缺一不可。

#索引

优点:

1.大大加快数据的检索速度

2.创建唯一性索引,保证数据库表中每一行数据的唯一性

3.加速表和表之间的连接

4.在使用分组和排序子句进行数据检索时,可以显著减少查询中分组和排序的时间

缺点

1.索引需要占用数据表以外的物理存储空间

2.创建索引和维护索引要花费一定的时间

3.当对表进行更新操作时,索引需要被重建,这样降低了数据的维护速度

#分类

主键索引(PRIMAY KEY)

主键:

某一个属性组能唯一标识一条记录

如:学生表(学号,姓名,班级,性别等等),学号时唯一标识的,可以作为主键

特点:

最常见的索引类型

确保数据记录的唯一性

确定特定数据记录在数据库中的位置

实例:

1
2
3
4
5
6
7
CREATE TABLE `表名`(、

  `GradeID` INT(11) AUTO_INCREMENT PRIMARY KEY,

  #或 PRIMARY KEY(`GradeID`)


唯一索引(UNIQUE)

作用:

避免同一个表中某数据列中的值重复

与主键索引的区别

主键索引只能有一个

唯一索引可有多个

实例:

1
2
3
4
5
6
7
CREATE TABLE `Grade`(、

  `GradeID` INT(11) AUTO_INCREMENT PRIMARY KEY,

  `GradeName` VARCHAR(32) NOT NULL UNIQUE

  #或 UNIQUE KEY ` GradeID`(`GradeID`)

常规索引(INDEX)

作用:

快速定位特定数据

注意:

index 和 key 关键字都可以设置常规索引

应加在查询条件的字段

不易添加太多常规索引,影响数据的插入,删除和修改操作

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
##创建表时添加

CREATE TABLE `result`{

  //省略一些代码

  INDEX / KEY `ind` (`studentNo`,`subjectNo`)

}

##创建后追加

ALTER TABLE `result` ADD INDEX `ind` (`studentNo`,`subjectNo`);

全文索引(FULLTEXT)

作用:

快速定位特定数据

注意:

只能用于 MyISAM 类型的数据表

只能用于 CHAR ,VARCHAR,TEXT 数据列类型

使用大型数据集

实例:

1
2
3
4
5
6
7
8
9
CREATE TABLE `student`(

  #省略一些sql语句

    FULLTEXT(`StudentName`)

)ENDINE=MYISAM;

ALTER TABLE employee ADD FULLTEXT(`first_name`)

#覆盖索引与回表

#聚簇索引

  • 如果表设置了主键,则主键就是聚簇索引
  • 如果表没有主键,则会默认第一个 NOT NULL,且唯一(UNIQUE)的列作为聚簇索引
  • 以上都没有,则会默认创建一个隐藏的 row_id 作为聚簇索引

InnoDB 的聚簇索引的叶子节点存储的是行记录(其实是页结构,一个页包含多行数据),InnoDB 必须要有至少一个聚簇索引。
由此可见,使用聚簇索引查询会很快,因为可以直接定位到行记录。

#普通索引

普通索引也叫二级索引,除聚簇索引外的索引,即非聚簇索引。
InnoDB 的普通索引叶子节点存储的是主键(聚簇索引)的值,而 MyISAM 的普通索引存储的是记录指针。

#回表查询

先通过普通索引的值定位聚簇索引值,再通过聚簇索引的值定位行记录数据,需要扫描两次索引 B+树,它的性能较扫一遍索引树更低。

#索引覆盖

只需要在一棵索引树上就能获取 SQL 所需的所有列数据,无需回表,速度更快。
例如:select id,age from user where age = 10;

常见的方法是:将被查询的字段,建立到联合索引里去。

#B+树

B+ 树是一种树数据结构,是一个 n 叉树,每个节点通常有多个孩子,一颗 B+树包含根节点、内部节点和叶子节点。B+ 树通常用于数据库和操作系统的文件系统中。 B+ 树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。 B+ 树元素自底向上插入。

  1. 根结点至少有两个子女。
  2. 每个中间节点都至少包含 ceil(m / 2)个孩子,最多有 m 个孩子。
  3. 每一个叶子节点都包含 k-1 个元素,其中 m/2 <= k <= m。
  4. 所有的叶子结点都位于同一层。
  5. 每个节点中的元素从小到大排列,节点当中 k-1 个元素正好是 k 个孩子包含的元素的值域分划。
  6. 每个叶子节点都有一个指针,指向下一个数据,形成一个有序链表。
  7. 而只有叶子节点才会有 data,其他都是索引。

#与 B 树的区别

  1. 有 k 个子结点的结点必然有 k 个关键码。
  2. 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
  3. 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。

#

这里我们就需要了解页(page)的概念,在计算机里,无论是内存还是磁盘,操作系统都是按页的大小进行读取的(页大小通常为 4 kb),磁盘每次读取都会预读,会提前将连续的数据读入内存中,这样就避免了多次 IO,这就是计算机中有名的局部性原理,即我用到一块数据,很大可能这块数据附近的数据也会被用到,干脆一起加载,省得多次 IO 拖慢速度, 这个连续数据有多大呢,必须是操作系统页大小的整数倍,这个连续数据就是 MySQL 的页,默认值为 16 KB,也就是说对于 B+ 树的节点,最好设置成页的大小(16 KB),这样一个 B+ 树上的节点就只会有一次 IO 读。

#优点

B+树相比 B 树的存储效率更高。
B+树的高度更小,检索效率更高。
B+树支持范围检索,在实际应用中更为广泛。
插入和删除更为方便,其实流程和 B 树类似,但 B+树里面,关键码的个数和子节点的个数是对等的,所以从记忆角度来说,B+树更方便记忆使用,而 B 树则需要时刻注意节点数和关键码的对应关系。

#Mysql

#常用引擎

#区别

  1. InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
  2. InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;
  3. InnoDB 是聚集索引,MyISAM 是非聚集索引。聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
  4. InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而 MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
  5. InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

#如何选择

  1. 是否要支持事务,如果要请选择 InnoDB,如果不需要可以考虑 MyISAM;
  2. 如果表中绝大多数都只是读查询,可以考虑 MyISAM,如果既有读写也挺频繁,请使用 InnoDB。
  3. 系统奔溃后,MyISAM 恢复起来更困难,能否接受,不能接受就选 InnoDB;
  4. MySQL5.5 版本开始 Innodb 已经成为 Mysql 的默认引擎(之前是 MyISAM),说明其优势是有目共睹的。如果你不知道用什么存储引擎,那就用 InnoDB,至少不会差。

#binlog 和 redolog

  1. 功能不同,binlog 主要用于归档,而 redolog 主要用于崩溃恢复
  2. 内容不同,binlog 是逻辑日志,而 redolog 是物理日志
  3. 写入时机不同,binlog 是 server 层记录的,所有存储引擎可共享,而 redolog 是 InnoDB 引擎特有的
  4. 写入方式不同,binlog 容量无限,追加写入,而 redolog 容量有限,循环写入

#JAVA

#类加载过程

#加载

简单来说,加载指的是把 class 字节码文件从各个来源通过类加载器装载入内存中。

这里有两个重点:

字节码来源。一般的加载来源包括从本地路径下编译生成的.class 文件,从 jar 包中的.class 文件,从远程网络,以及动态代理实时编译

类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。

注:为什么会有自定义类加载器?

一方面是由于 java 代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。

另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。

#验证

主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。

包括对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?

对于元数据的验证,比如该类是否继承了被 final 修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?

对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。

对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public 等)是否可被当前类访问?

#准备

主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。

特别需要注意,初值,不是代码中具体写的初始化的值,而是 Java 虚拟机根据不同变量类型的默认初始值。

比如 8 种基本类型的初值,默认为 0;引用类型的初值则为 null;常量的初值即为代码中设置的值,final static tmp = 456, 那么该阶段 tmp 的初值就是 456

#解析

将常量池内的符号引用替换为直接引用的过程。

两个重点:

符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。

直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量

举个例子来说,现在调用方法 hello(),这个方法的地址是 1234567,那么 hello 就是符号引用,1234567 就是直接引用。

在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

#初始化

这个阶段主要是对类变量初始化,是执行类构造器的过程。

换句话说,只对 static 修饰的变量或语句进行初始化。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

#ClassLoader

它是用来加载 Class 的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 _.class,也可以是 jar 包里的 _.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。

JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。它可以从不同的文件目录加载,也可以从不同的 jar 文件中加载,也可以从网络上不同的静态文件服务器来下载字节码再加载。

#BootstrapClassLoader

启动类加载器:负责加载 JVM 运行时核心类,加载 System.getProperty(“sun.boot.class.path”)所指定的路径或 jar

#ExtensionClassLoader

拓展类加载器:负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVAHOME/lib/rt.jar 文件中.
加载 System.getProperty(“java.ext.dirs”)所指定的路径或 jar。在使用 Java 运行程序时,也可以指定其搜索路径,例如:java -Djava.ext.dirs=d:\projects\testproj\classes HelloWorld。

#AppClassLoader

用户类加载器:它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。
加载 System.getProperty(“java.class.path”)所指定的路径或 jar。在使用 Java 运行程序时,也可以加上-cp 来覆盖原有的 Classpath 设置,例如: java -cp ./lavasoft/classes HelloWorld

#URLClassLoader

那些位于网络上静态文件服务器提供的 jar 包和 class 文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。

#关于 jvm 类的加载

在 Java 中,类装载器把一个类装入 Java 虚拟机中,要经过三个步骤来完成:装载、链接和初始化,其中链接又可以分成校验、准备和解析三步,除了解析外,其它步骤是严格按照顺序完成的,各个步骤的主要工作如下:

装载:查找和导入类或接口的二进制数据; //byte[]

链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;//包含了虚方法表的初始化

校验:检查导入类或接口的二进制数据的正确性; //魔数 babycafe?

准备:给类的静态变量分配并初始化存储空间; //分配内存空间

解析:将符号引用转成直接引用;

初始化:激活类的静态变量的初始化 Java 代码和静态 Java 代码块。
//对应的为()方法,该方法在多线环境中如果有多个线程同时去初始化一个类,那么久只有一个线程去执行。
//这也是我们写单例时,为什么可以使用静态内部类了,保证了内部重排序对外部线程时不可见的,具体可以见对 DCL 的分析

#范型

Java 的泛型只存在于编译期,一旦编译成字节码,泛型将被擦除。泛型的作用在于在编译阶段保证我们使用了正确的类型,并且由编译器帮我们加入转型动作,使得转型是不需要关心且安全的。

#GC

#可以作为 GC Roots 对象

  1. 虚拟机栈中局部变量表中引用的对象
  2. 本地方法栈中 native 方法引用的对象
  3. 方法区中类静态属性引用的对象
  4. 方法区中的常量引用的对象

#四种引用类型

强引用:使用 new 一个新对象的方式来创建强引用。只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用:一些还有用但并非必须的对象。软引用关联着的对象,在系统要发生内存溢出之前,会把这些对象进行垃圾回收。
弱引用:也是描述一些非必须对象,强度比软引用更弱,只要发生垃圾回收,它就一定会被回收。
虚引用:又称幽灵引用、幻影引用,是最弱的一种引用。为对象设置虚引用的唯一目的是能在这个对象被回收时会收到一个系统通知。

#垃圾收集算法

  1. 标记 - 清除算法:适用老年代
  2. 标记 - 整理算法:适用老年代
  3. 复制算法:适用年轻代

#线程池

#好处

我们知道不用线程池的话,每个线程都要通过 new Thread(xxRunnable).start()的方式来创建并运行一个线程,线程少的话这不会是问题,而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的 CPU 和内存资源,也会造成 GC 频繁收集和停顿,因为每次创建和销毁一个线程都是要消耗系统资源的,如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。所以,线程池中的线程复用极大节省了系统资源,当线程一段时间不再有任务处理时它也会自动销毁,而不会长驻内存。

#工作流程

  1. 如果线程池中的线程小于 corePoolSize 时就会创建新线程直接执行任务。
  2. 如果线程池中的线程大于 corePoolSize 时就会暂时把任务存储到工作队列 workQueue 中等待执行。
  3. 如果工作队列 workQueue 也满时,当线程数小于最大线程池数 maximumPoolSize 时就会创建新线程来处理,而线程数大于等于最大线程池数 maximumPoolSize 时就会执行拒绝策略。

#分类

  1. newFixedThreadPool
    固定线程池,核心线程数和最大线程数固定相等,而空闲存活时间为 0 毫秒,说明此参数也无意义,工作队列为最大为 Integer.MAX_VALUE 大小的阻塞队列。当执行任务时,如果线程都很忙,就会丢到工作队列等有空闲线程时再执行,队列满就执行默认的拒绝策略。

  2. newCachedThreadPool
    带缓冲线程池,从构造看核心线程数为 0,最大线程数为 Integer 最大值大小,超过 0 个的空闲线程在 60 秒后销毁,SynchronousQueue 这是一个直接提交的队列,意味着每个新任务都会有线程来执行,如果线程池有可用线程则执行任务,没有的话就创建一个来执行,线程池中的线程数不确定,一般建议执行速度较快较小的线程,不然这个最大线程池边界过大容易造成内存溢出。

  3. newSingleThreadExecutor
    单线程线程池,核心线程数和最大线程数均为 1,空闲线程存活 0 毫秒同样无意思,意味着每次只执行一个线程,多余的先存储到工作队列,一个一个执行,保证了线程的顺序执行。

  4. newScheduledThreadPool
    调度线程池,即按一定的周期执行任务,即定时任务,对 ThreadPoolExecutor 进行了包装而已。

#拒绝策略

  1. AbortPolicy
    简单粗暴,直接抛出拒绝异常,这也是默认的拒绝策略。
  2. CallerRunsPolicy
    如果线程池未关闭,则会在调用者线程中直接执行新任务,这会导致主线程提交线程性能变慢。
  3. DiscardPolicy
    从方法看没做任务操作,即表示不处理新任务,即丢弃。
  4. DiscardOldestPolicy
    抛弃最老的任务,就是从队列取出最老的任务然后放入新的任务进行执行。

#多态

面向对象的三大特性:封装、继承、多态
多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)
实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。
多态的作用:消除类型之间的耦合关系。
现实中,关于多态的例子不胜枚举。比方说按下 F1 键这个动作,如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是 Word 帮助;在 Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不同的对象上会产生不同的结果。
下面是多态存在的三个必要条件,要求大家做梦时都能背出来!

多态存在的三个必要条件
一、要有继承;
二、要有重写;
三、父类引用指向子类对象。

#epoll

1、支持一个进程所能打开的最大连接数

select:单个进程所能打开的最大连接数有 FD_SETSIZE 宏定义,其大小是 32 个整数的大小(在 32 位的机器上,大小就是 3232,同理 64 位机器上 FD_SETSIZE 为 3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。

poll:poll 本质上和 select 没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。

epoll:虽然连接数有上限,但是很大,1G 内存的机器上可以打开 10 万左右的连接,2G 内存的机器可以打开 20 万左右的连接。

2、FD 剧增后带来的 IO 效率问题

select:因为每次调用时都会对连接进行线性遍历,所以随着 FD 的增加会造成遍历速度慢的“线性下降性能问题”。

poll:同上

epoll:因为 epoll 内核中实现是根据每个 fd 上的 callback 函数来实现的,只有活跃的 socket 才会主动调用 callback,所以在活跃 socket 较少的情况下,使用 epoll 没有前面两者的线性下降的性能问题,但是所有 socket 都很活跃的情况下,可能会有性能问题。

3、 消息传递方式

select:内核需要将消息传递到用户空间,都需要内核拷贝动作

poll:同上

epoll:epoll 通过内核和用户空间共享一块内存来实现的。

总结:

综上,在选择 select,poll,epoll 时要根据具体的使用场合以及这三种方式的自身特点。

1、表面上看 epoll 的性能最好,但是在连接数少并且连接都十分活跃的情况下,select 和 poll 的性能可能比 epoll 好,毕竟 epoll 的通知机制需要很多函数回调。

select,poll 实现需要自己不断轮询所有 fd 集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而 epoll 其实也需要调用 epoll_wait 不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中进入睡眠的进程。虽然都要睡眠和交替,但是 select 和 poll 在“醒着”的时候要遍历整个 fd 集合,而 epoll 在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的 CPU 时间。这就是回调机制带来的性能提升。

2、select 低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善

select,poll 每次调用都要把 fd 集合从用户态往内核态拷贝一次,并且要把 current 往设备等待队列中挂一次,而 epoll 只要一次拷贝,而且把 current 往等待队列上挂也只挂一次(在 epoll_wait 的开始,注意这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列)。这也能节省不少的开销。

#计算机网络

#数据链路层

#封装成帧

封装成帧(framing)就是在一段数据的前后分别添加首部和尾部,这样就构成了一个帧。接收端在收到物理层上交的比特流后,就能根据首部和尾部的标记,从比特流中识别帧的开始和结束。

首部和尾部的一个重要作用就是进行帧定界(即确定帧的界限)。此外,首部和尾部还包括许多必要的控制信息。

为了提高帧的传输效率,应当使帧的数据部分长度尽量大于首部和尾部的长度。
最大传输单元 MTU(Maximum Transfer Unit):帧数据部分长度上限。

控制字符SOH(start of header)放在一帧的最前面,表示帧的首部开始。另一个控制字符EOT(end of transmission)表示帧的结束。注意:SOHEOT都只是控制字符的名称,他们的十六进制编码分别是 01(二进制是 00000001)和 04(二进制是 00000100)。SOH、EOT 并不是 S O H E O T 这几个字符,只是名字而已。

#透明传输

为了解决透明传输问题,字节填充法或字符填充:在控制字符 SOH、EOT 的前面插入一个转义字符 ESC(其十六进制编码是 1B,二进制是 00011011)。而接收端的数据链路层在把数据送往网络层之前删除这个插入的转义字符。

#差错检测

目前在数据链路层广泛使用了循环冗余检验(CRC)的检错技术。

在数据链路层的 CRC 检验都是用硬件完成的,处理很迅速,因此不会延误数据的传输。

为什么数据链路层要以帧为单位来传送数据呢?因为如果不以帧为单位,就无法加入冗余码来进行差错检验。

现在的互联网采用了区别对待的方法:

对于通信质量良好的有线传输链路,数据链路层协议不使用确认和重传机制,即不要求数据链路层向上提供可靠传输的服务。如果在数据链路层传输数据时出现了差错并且需要进行改正,那么改正差错的任务就由上层协议(例如,运输层的 TCP 协议)来完成。

对于通信质量较差的无线传输链路,数据链路层协议使用确认和重传机制,数据链路层向上提供可靠传输的服务。

#Exception和Error的区别

继承关系

Error:表示由 JVM 所侦测到的无法预期的错误,由于这是属于 JVM 层次的严重错误,导致 JVM 无法继续执行,因此,这是不可捕捉到的,无法采取任何恢复的操作,顶多只能显示错误信息。

Exception:表示可恢复的例外/异常,这是可捕捉到的。

Java 提供了两类主要的异常:RuntimeException和checked exception。

checked异常也就是我们经常遇到的 IO 异常,以及SQL 异常等。对于这种异常,JAVA 编译器强制要求我们必需对出现的这些异常进行处理。

但是RuntimeException,也称运行时异常,我们可以不处理。当出现这样的异常时,总是由虚拟机接管。比如:我们从来没有人去处理过NullPointerException 异常,它就是运行时异常,并且这种异常还是最常见的异常之一。

出现运行时异常后,系统会把异常一直往上层抛,一直遇到处理代码。如果没有处理块,到最上层,如果是多线程就由 Thread.run() 抛出 ,如果是单线程就被 main() 抛出 。抛出之后,如果是线程,这个线程也就退出了。如果是主程序抛出的异常,那么这整个程序也就退出了。运行时异常是 Exception 的子类,也有一般异常的特点,也是可以被 Catch 块处理的。只不过往往我们不对他处理罢了。也就是说,你如果不对运行时异常进行处理,那么出现运行时异常之后,要么是线程中止,要么是主程序终止。
如果不想终止,则必须捕捉所有的运行时异常,决不让这个处理线程退出。队列里面出现异常数据了,正常的处理应该是把异常数据舍弃,然后记录日志。不应该由于异常数据而影响下面对正常数据的处理。在这个场景这样处理可能是一个比较好的应用,但并不代表在所有的场景都应该如此。如果在其它场景,遇到了一些错误,如果退出程序比较好,这时你就可以不理会运行时异常 。
异常处理的目标之一就是为了把程序从异常中恢复出来 。

对于Checked异常的处理方式有两种:

(1)当前方法明确知道如何处理该异常,程序应该使用try…catch块来捕获该异常,然后在对应的catch块中修补该异常。

(2)当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。

#assert

在Java中,assert关键字是从JAVA SE 1.4 引入的,为了避免和老版本的Java代码中使用了assert关键字导致错误,Java在执行的时候默认是不启动断言检查的(这个时候,所有的断言语句都将忽略!),如果要开启断言检查,则需要用开关-enableassertions或-ea来开启。

assert关键字语法很简单,有两种用法:

1、assert <boolean表达式>
如果<boolean表达式>为true,则程序继续执行。
如果为false,则程序抛出AssertionError,并终止执行。

2、assert <boolean表达式> : <错误信息表达式>
如果<boolean表达式>为true,则程序继续执行。
如果为false,则程序抛出java.lang.AssertionError,并输入<错误信息表达式>。

#陷阱

assert关键字用法简单,但是使用assert往往会让你陷入越来越深的陷阱中。应避免使用。笔者经过研究,总结了以下原因:

1、assert关键字需要在运行时候显式开启才能生效,否则你的断言就没有任何意义。而现在主流的Java IDE工具默认都没有开启-ea断言检查功能。这就意味着你如果使用IDE工具编码,调试运行时候会有一定的麻烦。并且,对于Java Web应用,程序代码都是部署在容器里面,你没法直接去控制程序的运行,如果一定要开启-ea的开关,则需要更改Web容器的运行配置参数。这对程序的移植和部署都带来很大的不便。

2、用assert代替if是陷阱之二。assert的判断和if语句差不多,但两者的作用有着本质的区别:assert关键字本意上是为测试调试程序时使用的,但如果不小心用assert来控制了程序的业务流程,那在测试调试结束后去掉assert关键字就意味着修改了程序的正常的逻辑。

3、assert断言失败将面临程序的退出。这在一个生产环境下的应用是绝不能容忍的。一般都是通过异常处理来解决程序中潜在的错误。但是使用断言就很危险,一旦失败系统就挂了。

#final

final关键字可以用来修饰引用、方法和类。

  1. 用来修饰一个引用
    如果引用为基本数据类型,则该引用为常量,该值无法修改;
    如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。
    如果引用时类的成员变量,则必须当场赋值,否则编译会报错。
  2. 用来修饰一个方法
    当使用final修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。
  3. 用来修饰类
    当用final修改类时,该类成为最终类,无法被继承。简称为“断子绝孙类”。

#volatile

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
由于java中的每个线程有自己的工作空间,这种工作空间相当于上面所说的高速缓存,因此多个线程在处理一个共享变量的时候,就会出现线程安全问题。

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。

#解决哈希冲突的四种方法

1.开放地址方法

(1)线性探测

按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上往后加一个单位,直至不发生哈希冲突。

(2)再平方探测

按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上先加1的平方个单位,若仍然存在则减1的平方个单位。随之是2的平方,3的平方等等。直至不发生哈希冲突。

(3)伪随机探测

按顺序决定值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来值的基础上加上随机数,直至不发生哈希冲突。

2.链式地址法(HashMap的哈希冲突解决方法)

对于相同的值,使用链表进行连接。使用数组存储每一个链表。

优点:

(1)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

(2)由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;

(3)开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

(4)在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
  缺点:

指针占用较大空间时,会造成空间浪费,若空间用于增大散列表规模进而提高开放地址法的效率。

3.建立公共溢出区

建立公共溢出区存储所有哈希冲突的数据。

4.再哈希法

对于冲突的哈希值再次进行哈希处理,直至没有哈希冲突。

#参考链接