Java基础
数据结构
1. HashMap
Q:初始容量为多少?为什么?
16,只要是2次幂,其实用 8 和 32 都差不多。用 16 只是因为作者认为 16 这个初始容量是能符合常用而已。
为什么是 2 次幂,这样的桶个数在通过 hashCode 定位桶下标时,可采用 hash & (m - 1) 来实现
Q:何时扩容与如何扩容
A:插入一个新的元素时,判断size 与 扩容因子和桶个数乘积大小,超过的话执行扩容;扩容通过桶个数乘 2 实现,为什么是 2,一是为了能使用位运算中的逻辑左移来实现乘 2,二是为了保持桶个数为 2 次幂,这样的桶个数在通过 hashCode 定位桶下标时,可采用 hash & (m - 1) 来实现
并发问题的解决
- 使用Collections.synchronizedMap(Map)创建线程安全的map集合;
- Hashtable
- ConcurrentHashMap:不过出于线程并发度的原因,我都会舍弃前两者使用最后的ConcurrentHashMap,他的性能和效率明显高于前两者。
synchronizedMap
在SynchronizedMap内部维护了一个普通对象Map,还有排斥锁mutex。我们在调用这个方法的时候就需要传入一个Map,SynchronizedMap有两个构造器,如果你传入了mutex参数,则将对象排斥锁赋值为传入的对象;如果没有,则将对象排斥锁赋值为this,即调用synchronizedMap的对象,就是上面的Map。创建出synchronizedMap之后,再操作map的时候,就会对方法上对象锁synchronized。
HashTable
跟HashMap相比Hashtable是线程safe的,适合在多线程的情况下使用,但是效率可不太乐观。他在对数据操作的时候都会上锁,所以效率比较低下。
- HashMap与HashTable的区别
- ConcurrencyHashMap
《我们一起进大厂》系列-ConcurrentHashMap & Hashtable - RedBlackTree
- B+-Tree
- ArrayList与LinkedList
Java虚拟机
1. 运行时数据区域
程序计数器(最小的内存区域)
- 存储的是当前线程所执行的字节码的行号;线程私有
- 字节码指令流(分支、循环、跳转、异常处理、线程恢复等)都需要依赖程序计数器来完成
- 每个线程运行的代码不同,因此每个线程都需要记录自己的代码执行位置,因此程序计数器是线程私有的
虚拟机栈
- 存储的是Java方法的运行时信息,每个方法的信息存在一个栈帧里,信息包括:局部变量表、操作数栈、动态链接、方法出口;线程私有
- 其中的局部变量表存放数据有三类:基本数据类型、对象引用类型和返回地址类型。其中,基本数据类型就是(boolean byte short int float long double),对象引用是存放的地址,对于直接引用方式,存放是对象的首地址;对于句柄引用方式,存放的是指向对象的句柄的首地址。返回地址是字节码指令的地址
- 局部变量表所需的内存空间在编译期完成分配
方法区
- 存放已加载的类型信息、常量、静态变量;
- 线程共享
- 也是需要垃圾收集的区域,回收的内容包括:不用了的常量池和类型;
- 方法区里还包括了运行时常量池,存放编译期生成的各种字面量和符号引用以及运行期间新的常量。
堆
- 存放所有对象实例和数组
- 线程共享
- 虽然在 Java 虚拟机规范中,明确说了堆是线程共享的,但在具体实现中,还是会为不同的线程划分出TLAB(线程私有分配缓冲区),目的是提升对象的分配效率。如何提升的呢,若没有TLAB,那么多个线程在公共区域并发分配对象时,采用CAS+失败重试的方式同步,而在TLAB上不需要同步,所以更快。
- 堆是垃圾收集器管理的主要区域
设置堆的最小值:-Xms
设置堆的最大值:-Xmx
存储堆溢出时的快照:-XX:+HeapDumpOnOutOfMemoryError
处理 Java 堆内存问题的简略思路:判断出现内存泄漏还是内存溢出,通过对 Dump 出来的的堆转出快照进行分析
参考:Java虚拟机-内存区域
对象
对象的创建流程
- 虚拟机遇到字节码 new 指令时,根据指令参数在方法区的常量池中定位到一个类的符号引用,然后检查这个符号引用代表的类是否已被加载。如果没有,那先去执行类的加载。
- 对象所需内存的大小在类加载后便可以确定,虚拟机要从堆中划分出这个空间,具体如何划分与垃圾收集的具体方式有关。其中,并发分配内存,是通过 TLAB 或者采用 CAS+失败重试保证更新操作的原子性
- 内存分配完成后,虚拟机会将这个内存空间都置为零
- 设置对象的对象头信息
- 执行构造函数
对象的存储结构
- 第一部分 - 对象头:包括两类:第一类是存储对象的运行时信息 Mark Word;第二类是类型指针。其中,运行时信息由于是与数据本身无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word 被设计成一个动态定义的数据结构,根据对象的状态来复用同一块存储空间。
未锁定(无锁):Hash(25), Age(4), biased_lock(1), lock(2)
偏向锁:thread_id(23), epoch(2), Age(4), biased_lock(1), lock(2)
轻量级锁:锁指针
重量级锁:重量级锁指针
- 第二部分 - 数据:字段存储顺序遵循相同宽度字段分配到一起,在满足这个条件下,父类中定义的变量在子类定义的变量之前
- 第三部分 - 对齐填充:虚拟机要求对象起始地址必须是 8 字节的整数倍,这个部分用来补全
2. 垃圾收集
2.1 对象存活判断
两种方式:引用计数法和可达性分析。
2.1.1 引用计数法
每个对象有一个引用计数属性,新增一个引用时计数加1,引用失效时计数减1,计数为0时可以回收。此方法虽然简单,但是无法解决对象相互循环引用的问题。
2.1.2 可达性分析
从一些称为根节点对象开始,顺着引用链向下扫描,如果某个对象到根节点没有任何引用链相连,就认为这个对象是需要回收的。
Q: 那么根节点对象是哪些呢,
A: 主要为:虚拟机栈中的本地变量表中引用的对象,比如参数、局部变量等;在方法区中静态属性引用的对象;在方法区(常量池)中常量引用的对象
Q: 如何枚举这些根节点对象?
A: 根节点枚举的过程是借助 OopMap 完成的,OopMap 记录了引用的位置,这样收集器就可以直接得到这些信息了,而不需要一个不漏地从栈和方法区中查找,提升了扫描效率。这个过程在 CMS 和 G1 中被称为初始标记。
这些已标记的对象需要存储以进行引用扫描,使用 Work List 来存储那些被标记过、对象内部的引用仍未扫描的对象。在 Serial 等单线程收集器中,使用深度优先扫描,这里 Work list 使用就是栈结构。多线程收集器 CMS、G1 多线程收集器为了降低用户线程的暂停时间,使用的是广度优先的扫描,根节点后续的扫描是可以与用户线程并发执行的,这个过程被称为重新标记,并发标记的正确性是在一定的技术细节的前提下。
2.1.2.1 自我拯救 - finalize()
即使在可达性分析中被判定为不可达的对象也不是非死不可的,要真正宣布一个对象死亡,至少要经历两次标记过程:如果对象不可达,那它将被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行 finailize() 方法。若对象覆盖了 finalize() 方法,并且 finalize() 还未被执行过,那么此对象将执行 finalize() ;否则,此对象真的会被回收。
2.1.3 引用的类型
若一个对象只有“被引用”或“不被引用”两种状态,对于一些可有可无的对象是无法描述的,比如对于一类对象:当内存足够时,保留;当内存紧张时,释放。在JDK1.2版之后,Java对引用类型进行了扩充,将引用分为强引用、软引用、弱引用和虚引用,这四种强度依次降低。
- 强引用,是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值。任何情况下,只要强引用关系存在,垃圾收集器永远不会回收掉被引用的对象
- 软引用,用来描述有用但非必须的对象。被软引用关联的对象,在系统即将发生内存溢出时,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够内存,才会抛出内存溢出异常。
- 弱引用,也是用来描述非必须的对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止,无论内存是否足够,都会被垃圾收集器回收掉
- 虚引用,一个对象是否有虚引用,对其生存时间完全没有影响,也无法通过虚引用来取得一个对象实例。虚引用的唯一目的只是为了能在这个对象被回收时收到一个系统通知。
2.2 垃圾收集算法思想
2.2.1 分代收集思想
根据对象存活周期的不同将内存划分为多个区域,然后针对这些不同区域,采用与里面的对象的存亡特征相匹配的垃圾收集算法。一般是把 Java堆分为新生代和老年代,新生代中对象存活率低、回收率高;老年代中对象的存活率高、回收率低。
Q: 跨代引用问题
A: 假如要进行一次新生代区域的垃圾收集,但新生代中的对象可能被老年代引用,那么为了进行可达性分析,不仅要枚举新生代的根节点对象,也要枚举老年代的根节点对象。这肯定是造成很大的性能负担。
为了解决这个问题,由于跨带引用相对于同代引用来说只占极少数,因此在新生代上建立的一个记忆集,这个结构把老年代分成若干个小块,标识出哪块内存有跨带引用。这样一来,只需要对这些块进行根节点枚举就行了。
2.2.2 标记-清除算法
算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,然后对所有被标记了的对象进行统一回收。
缺点有两个:内存空间碎片化;执行效率不稳定。
2.2.3 标记-复制算法
将内存分为多块,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到其他的块上面。适用于存活率低的新生代。可以解决标记-清除算法中执行效率低和空间碎片化的问题。
缺点是:可用内存变小;对象存活率高的情况下开销更大。
2.2.4 标记-整理算法
让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。可以解决标记-复制算法中可用内存变小的问题。
2.3 垃圾收集器

2.3.1 Serial收集器
- 客户端模式下的默认新生代收集器
- 标记复制算法
- 优点是:单线程收集额外内存消耗最小,单核处理器下收集效率最高

2.3.2 ParNew收集器
标记复制
新生代;多线程并行;回收时暂停其他所有工作线程;线程数默认与逻辑处理器核心数量相同,服务器核多时高效;自 JDK 9开始,ParNew 合并入 CMS,成为它专门处理新生代的组成部分。

2.3.3 Parallel收集器
标记-复制; 新生代;多线程并行;关注吞吐率(=用户代码运行时间/(用户代码运行时间+垃圾收集时间);
2.3.4 CMS收集器

- 垃圾收集算法:标记-清除
- 分代收集区域:老年代
- 特点:目标是最短停顿时间
收集过程分为4步,前三步都是标记,最后一步是清除:
为什么标记要分为 3 步,是因为它们基于可达性分析(见 2.1.2),以及最短停顿时间
- 初始标记
- 并发标记
- 重新标记
- 并发清除
初始标记:标记根节点对象,速度很快;
并发标记:是从GC Root遍历整个对象图的过程,耗时长但是不需要停顿用户线程,可与垃圾收集线程并发;
重新标记:为了修正在并发标记阶段,用户线程继续运作而导致标记产生变动的那一部分对象的标记记录,时间比初始标记阶段长一点,但远不及并发标记阶段的时间;
并发清除:清理删除标记阶段判断死亡的对象,由于不需要移动存活对象,所以这个阶段可与工作线程并发。
为了判断哪些对象存活,哪些对象死亡,CMS 使用可达性分析的方法对堆上的对象进行标记,在标记过程中,将对象分为三类:
- 尚未被垃圾收集器访问过的对象。显然在分析刚开始时,所有对象都是未被访问过的,但是如果一个对象在分析结束时仍然未被访问的话,代表对象是不可达的、需要回收的
- 已经被垃圾收集器访问过的对象,且这个对象的所有引用都被扫描过了,垃圾收集器不再会对这类对象加以分析了
- 已经被垃圾收集器访问过、但是至少有一个引用还没有被访问过的对象,垃圾收集器会沿着这类对象的引用继续访问
由于并发标记过程中,工作线程与垃圾收集线程并发执行,也就是收集器在访问对象的同时,工作线程也可能会修改引用关系,这可能会引起两种后果:本应该回收的对象错误标记为存活,这虽然是个问题,但是问题不大,这些对象可以在下一次垃圾回收的时候被回收;另一种后果是,本该存活的对象错误标记为死亡,这个后果很严重。
问题可以通过增量更新来解决,在并发标记过程中,将新的引用关系记录下来,在下一步,重新标记的过程中重新标记。
CMS 缺点:
- 并发过程占用处理器资源,导致用户程序执行速度变慢
- 无法处理“浮动垃圾”,“浮动垃圾”是指并发标记和并发清除阶段,用户线程新产生的垃圾,对于这些垃圾只能下次垃圾收集时再整理,如果空间不足,会出现并发失败,会 stop the world
- 标记清除会产生内存碎片,碎片空间不足以分配一个大对象时,会使用stop the world 的标记整理
2.3.5 G1收集器
2.3.5.1 (基于)停顿事件模型
G1 是第一款支持【停顿时间模型】的垃圾收集器。其中【停顿时间模型是指能够保证在一段时间内消耗在垃圾收集上的时间最长不超过多少的目标】。
Q:为何 G1 出现之前的所有其他收集器没能实现停顿事件模型,而 G1 能做到这点?
A:因为 CMS 等收集器收集的目标范围太大了,要么一次性清理整个新生代,要么一次性清理整个老年代;而 G1 将堆内存划分为很多个大小相等的独立区域,并将这些独立的区域作为单次收集的最小单元。还对每个区域计算回收收益,维护一个区域的收益优先级列表,每次根据用户配置的停顿时间优先处理收益最大的那些区域。

- 初始标记:标记GC Root能直接关联到的对象,并且修改TAMS(Top at Mark Start)的值,目的是让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。
- 并发标记:从GC Root开始对对堆中的对象进行可达性分析,递归扫描整个堆里的对象图;当对象图扫描完成后,还要重新处理原始快照(Snapshot at the beginning,SATB)记录下的在并发时有引用变动的对象。
- 最终标记:处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
- 筛选回收:更新Rigion的统计数据,对各个Region的价值和成本进行排序,根据用户期望的停顿时间来制定回收计划,可以自由选择任意多个Region作为回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,在清理掉整个旧的Region的全部空间。
从整体上看是标记整理,从两个 Rigion 之间来看是标记复制
// 设置每个 Rigion 的大小,取值范围为 1MB-32MB
-XX:G1HeapRegionSize
2.2 与 CMS 收集器相比 G1 收集器有以下特点:
- 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。而空间碎片是不利于程序长时间运行的,因为它不仅会导致创建对象时寻找合适的空间的速度变慢,而且在极端情况下,如分配大对象时可能会因为无法找到连续空间而提前触发下一次GC。
- 停顿时间是可控制的,虽然降低停顿时间是G1和CMS的共同关注点,但 G1 除了追求低停顿外,还能设置不同的期望停顿时间,可使得 G1 在不同应用场景中去的关注吞吐量和关注延迟之间的最佳平衡
- 收益最大化:对每个区域进行价值评估,根据价值高低维护了一个优先级列表,每次回收都优先处理价值最高的那些区域,相同的停顿时间内收集了最该收集的那些对象
2.3 与 CMS 收集器相比的不足
- 垃圾收集的额外内存占用高
- 收集区域更细致导致存储跨带指针的卡表数量更多,记忆集可能占整个堆容量的 20%,而 CMS 的卡表很简单就一份,而且只需要处理老年代到新生代的引用,反过来是不需要的,维护开销更小
- 类的加载机制
Java多线程
- thread.start() 与 thread.run() 的区别
- start 调用后,虚拟机会创建一个新的子线程,并在这个子线程中执行这个子线程的 run 方法
- run 调用后,虚拟机会在当前线程中调用这个 run 方法,因为 run 方法只是 Thread 类的一个成员函数
start 和 run 的流程图:

- Thread 和 Runnable 的关系
- Thread 是实现了 Runnable的类,实现多线程
- 由于 Java 的单继承原则,为了提升系统的可扩展性,业务类实现 Runnable 接口,然后将业务逻辑封装在 run() 方法中,以便于给普通类附上多线程的特性
- 如何获取线程的返回值
- 主线程等待法:主线程循环等待,直到子线程执行完毕,这时子线程对应的 thread 对象中的成员可作为返回值
- thread.join():阻塞主线程,直到子线程执行完毕
- FutureTask:task.isDone() 配合 task.get() 获取
- 线程池 配合 Future:future = pool.submit(callable), future.isDone(), future.get()
- 线程的状态

-
新建(New):创建后尚未启动的状态
-
运行(Runnable):包含 OS 中的 Runnable 和 Ready,可能真的正在被 CPU 运行,也可能正在等待 CPU 分配时间
-
无限期等待(Waiting):不会被分配 CPU 执行时间,需要显式被唤醒
Object.wait();
Thread.join();
LockSupport.part(); -
有限期等待(Timed waiting):在一段时间后有系统自动唤醒
Thread.sleep(t);
Object.wait(t);
Thread.join(t);
LockSupport.parkNanos();
LockSupport.parkUntil(); -
阻塞(Blocking):等待获取排它锁
Blocking 与 Waiting 的区别:Blocking 等待的是获取排它锁时间,这个时间将在其他线程放弃锁的时候发生;Waiting 等待的事唤醒或者计时结束事件
在等需等待进入 Synchronized 区域时,线程进入 Blocking 状态 -
结束(Terminated):线程已被终止,而且已终止的线程不能再复生
run() 退出后的线程
- sleep和wait的区别
- 所属类:Thread.sleep(t); Object.wait();
- 使用限制:sleep(t) 可以在任何地方使用;wait() 只能在 synchronized 区域中使用
- 锁行为:sleep 只会让出 cpu,不会释放锁;wait 不仅让出 cpu,还释放已经占有的同步资源锁
- notify和notifyAll的区别
- 两个重要概念:锁池、等待池
锁池:所有等待该对象锁的线程
等待池:已释放对象锁的线程,池中的线程不会去竞争该对象的锁。wait() -> notify() 或 notifyAll() - notify 会从等待池中随机选取一个线程进入锁池去竞争获取锁的机会
- notifyAll 会让等待池中的所有线程进入锁池去竞争获取锁的机会
-
yield函数
-
如何中断线程
- stop 方法被弃用的原因:不安全,立刻终止某一个线程,线程会立即释放锁,可能引发数据不同步的问题
- 调用 interrupt() 通知线程应该被中断了,然后由该线程自行处理中断
若线程处于阻塞状态,那么线程将退出阻塞状态,并抛出 InterruptedException 异常;
若线程处于正常活动状态,那么会将该线程的中断标志设置为 true,但是线程仍会正常运行
对于被中断的线程而言,需要主动配合才可以完成中断。要经常检查线程的中断标志位,如果被设置为 true,就自行停止线程
synchronized
-
分类:对象锁,类锁
-
实现基础:ObjectMonitor
-
锁优化
锁优化技术有:自适应自旋锁、锁消除、锁粗化、轻量级锁、偏向锁
- 自旋锁:很多情况下,共享数据锁定时间短,对于等待线程挂起和恢复操作并不值得。因此,等待线程进行忙循环来等待锁
- 自适应自旋锁:不知道这个锁是否适合自旋,不适合的话会白白浪费 cpu 资源,就通过前一次在同一锁上的自选时间以及锁的持有者的状态来决定;
- 锁消除: JIT 编译时,通过对运行时上下文扫描,去除不可能存在竞争的锁
- 锁粗化:通过扩大锁的范围,避免频繁的加锁解锁行为
- synchronized 的四种状态
状态有:无锁、偏向锁、轻量级锁、重量级锁
- 偏向锁:大多数情况下所不存在竞争,都是同一个线程来回获取锁,因此为了见效统一鲜橙获取锁的代价,引入偏向锁。若一个线程获取了锁,对象进入偏向模式,当该线程再次请求,检查 MarkWord 中 ThreadID 是否相等,省去大量的申请操作
- 轻量级锁:偏向锁模式下,当一个线程拥有锁时,第二个线程加入锁池,偏向锁升级为轻量级锁,来适应线程交替进入同步块
- ReetrantLock
CAS
- CAS 三个步骤
- 比较 A 与 V 是否相等。(比较)
- 如果比较相等,将 B 写入 V。(交换)
- 返回操作是否成功。
- CAS 的缺点
- 乐观锁:竞争不激烈的时候,修改成功率高;否则,失败率高,多次的循环尝试会导致性能下降
- ABA 问题:仅仅通过比较无法判断数据是否被其他线程修改
- CAS 实现细节
- java 的 cas 利用的的是 unsafe 这个类提供的 cas 操作。
- unsafe 的cas 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg
- Atomic::cmpxchg 的实现使用了汇编的 cas 操作,并使用 cpu 硬件提供的 lock信号保证其原子性
线程池
- 线程池的好处
- 降低资源消耗:通过对已创建线程的复用,降低线程创建和销毁的资源消耗;
- 提高线程可管理性:线程是稀缺资源,大量创建不但会消耗资源,还会降低系统稳定性,而线程池可以统一分配、调优和监控。
- 线程池的种类
- newFixThreadPool(int Threads)
- newCachedThreadPool()
- newSingleThreadExrcutor()
- newScheduledThreadPool()
- newWorkStealingPool()
- 线程池的参数
- corePoolSize:常驻线程数量
- maxPoolSize:允许创建的最大线程数。当线程池中的线程数大于 corePoolSize 时,且没有新的任务提交时,常驻线程外的线程会在 keepAliveTime 之后被销毁
- workQueue:任务等待队列
- keepAliveTime:允许线程(常驻线程之外)的最长空闲时间
- threadFactory:线程工厂
- handler:线程池的饱和策略,如果任务队列已满,且无空闲线程,继续提交任务时,线程池采用饱和策略处理任务
其中包含 4 中策略:
AbortPolicy(默认):直接抛出异常
CallerRunPolicy:用调用者所在线程执行任务
discardOldestPolicy:丢弃队列中最老的任务
DiscardPolicy:直接丢弃
自定义策略:实现 RejectedExecutionHandler 接口
- 新任务提交后的判断
- 如果当前线程数小于 corePoolSize,则创建新线程来处理任务,即使有空闲线程
- 如果当前线程数处于 corePoolSize 和 maxPoolSize 之间,则只有当 workQueue 满了时,才会创建新线程
- 若 corePoolSize == maxPoolSize,放入 workQueue 等待空闲线程去处理
- 若果当前线程数 == maxPoolSize,workQueue 也满了,则通过 hanlder 指定的策略去处理
- J.U.C 三个 Executor 接口
- Executor:运行新任务的简单接口,将任务提交与任务执行细节相分离解耦。实现可能是创建新线程执行任务,也可能是使用已有工作线程去执行。execute();
- ExecutorService:拓展 Executor,添加了管理执行器和任务生命周期的方法,提交任务更完善。Future submit(Callable task);
- ScheduleExecutorService:拓展 ExecutorService,支持 Future 和定期执行任务
- Fork/Join 框架
- 把大任务分割成小任务来并行执行,最终对小任务的结果进行汇总得到大任务的结果
- 是 ExecutorService 的实现,更好地利用多核 cpu 的优势,可使用所有可用的运算能力来提升性能
- WorkStealing 算法:某个人物可从其他线程的任务队列中窃取任务来执行
10.4 阻塞队列的类别和区别
Java框架——Spring
- IOC的原理与使用
- AOP的原理与使用
- Spring、SpringBoot、SpringMVC 的区别
- SpringBoot 的启动流程
- Bean 的生命周期
- SpringMVC 的流程
计算机网络
分层模型
- OSI七层模型
- TCP/IP四层模型
TCP、UDP协议
- TCP段头
下面是报文段首部各个字段的含义。
- 源端口号以及目的端口号,各占2个字节,端口是传输层和应用层的服务接口,用于寻找发送端和接收端的进程,一般来讲,通过端口号和IP地址,可以唯一确定一个TCP连接,在网络编程中,通常被称为一个socket接口。
- 序号,占4字节,用来标识从TCP发送端向TCP接收端发送的数据字节流。
- 确认序号,占4字节,包含发送确认的一端所期望收到的下一个序号,因此,确认序号应该是上次已经成功收到数据字节序号加1.
- 数据偏移,占4位,用于指出TCP首部长度,若不存在选项,则这个值为20字节,数据偏移的最大值为60字节。
- 保留字段占6位,暂时可忽略,值全为0
- 标志位
URG(紧急) : 为1时表明紧急指针字段有效
ACK(确认):为1时表明确认号字段有效
PSH(推送):为1时接收方应尽快将这个报文段交给应用层
RST(复位):为1时表明TCP连接出现故障必须重建连接
SYN(同步):在连接建立时用来同步序号
FIN (终止): 为1时表明发送端数据发送完毕要求释放连接 - 接收窗口占2个字节,用于流量控制和拥塞控制,表示当前接收缓冲区的大小。在计算机网络中,通常是用接收方的接收能力的大小来控制发送方的数据发送量。TCP连接的一端根据缓冲区大小确定自己的接收窗口值,告诉对方,使对方可以确定发送数据的字节数。
- 校验和占2个字节,范围包括首部和数据两部分。
- 选项是可选的,默认情况是不选。
TCP 传输可靠性来源于确认重传机制
TCP 三次握手
三次握手的意义
客户端和服务器在建立一个 TCP 连接时,通过发送 3 个包来确认双方的接收、发送能力是否正常,同时指定自己的初始化序列号 ISN 来保证传输的可靠性。
三次握手的过程

我打算从两方面来说我对三次握手的认识:
-
从报文段的标志位,序号和确认号是多少,如何产生的,意义是什么
-
每次握手前后,客户端服务端的状态如何改变的
-
握手前
客户端处于 Closed 的状态,服务端处于 Listen 状态。 -
第一次握手:
客户端给服务端发一个连接请求 报文(同步位 SYN=1,初始序号 seq = x),并指明客户端的初始化序列号 ISN。此时客户端处于 SYN_SEND 状态。
Q : ISN 初始化序列号是固定的吗
A : ISM 是随时间变化的,每个连接都有不同的 ISN 。ISN 实质是一个 32 位计数器,每 4ms 加 1 。这样做的目的是防止在网络中延迟的分组在以后被传送,导致对方的误解。
- 第二次握手
服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN。同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。
在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
Q : 什么是半连接队列
A : 服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在半连接队列里。如果服务器发完SYN-ACK包后并未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。
当然,全连接队列就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
Q : 什么是 SYN 攻击
A : 由于服务器端会在二次握手时,就是成功连接之前就会为每个连接分配资源,半连接队列,若有人恶意地在短时间内伪造大量不存在的IP地址,并向服务器不断地发送第一次握手的 SYN 包,服务器则会回复确认包,并等待对端确认,由于源地址是伪造的,是不存在的,因此服务器会不断重发直至超时,这些伪造的 SYN 包将长时间占用未连接队列,导致正常的 SYN 请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。
- 第三次握手
客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
发送第一个SYN的一端将执行主动打开(active open),接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open)。
Q : 为什么需要三次握手,二次握手不行吗
A : 弄清这个问题,我们需要先弄明白三次握手的目的是什么,能不能只用两次握手来达到同样的目的。
第一次握手:客户端发送网络包,服务端收到了。
这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
第二次握手:服务端发包,客户端收到了。
这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
第三次握手:客户端发包,服务端收到了。
这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。
因此,需要三次握手才能确认双方的接收与发送能力是否正常。
Q : 三次握手可以携带数据吗
A : 第三次握手是可以携带数据的。但是,第一次、第二次握手是不可以携带数据的。
为什么前两次不能携带数据呢?假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,就在第一次握手中的 SYN 报文中放入大量的数据,然后就不再理服务器了,这时服务器还以为对方是要正常地请求连接,这样就会消耗服务器大量缓存来存储数据,从而使得好人没法来握手。到攻击而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态,发送数据合情合理。
TCP 四次挥手
四次挥手的意义
建立一个连接需要三次握手,而终止一个连接要经过四次挥手。这是因为 TCP 连接是全双工的,那么就会有半关闭现象。所谓半关闭,就是连接的一端在结束它的发送后还能接收来自另一端数据。
四次挥手的过程

我打算从两方面来说我对四次挥手的认识:
-
从报文段的标志位,序号和确认号是多少,如何产生的,意义是什么
-
每次挥手前后,客户端服务端的状态如何改变的
-
挥手前
刚开始双方都处于 ESTABLISHED 状态,理论上,客户端或服务器均可主动发起连接释放请求。现在假设是客户端先发起关闭请求,那么四次挥手的过程如下: -
第一次挥手
客户端向服务端发送一个连接释放报文段(FIN=1,seq=x),并且停止再发送数据,同时客户端的状态由 ESTABLISHED 转变为 FIN_WAIT1 状态。 -
第二次挥手
服务端收到连接释放报文段后即发出确认报文段(ACK=1,ack=x+1,seq=v),这时服务端的状态由 ESTABLISHED 转变为 CLOSE_WAIT。当客户端收到这个确认后,客户端的状态由 FIN_WAIT1 转变为 FIN_WAIT2,等待服务端发出连接释放报文段。 -
第三次挥手
如果服务端也想断开连接了,和客户端的第一次挥手一样,也向客户端发送一个连接释放报文段(FIN=1,ACK=1,seq=y,ack=x+1),这时服务端的状态由 FIN_WAIT1 转变为 LAST_ACK 。 -
第四次挥手
客户端收到服务端发来的连接释放请求后,向服务端回复确认报文(ACK=1,seq=x+1,ack=y+1),这时客户端的状态由 FIN_WAIT2 转变为 TIME_WAIT。这时开始计时 2MSL 的时间,如果时间到了,还没有收到服务器重发过来的请求报文的话,状态由 TIME_WAIT 转变为 CLOSED 状态。不直接转变为 CLOSED 状态,是为了确保服务端能收到自己的确认报文段。
Q : MSL (Maximum Segment Lifetime) 是什么
A : 任何一个报文段的最大寿命,在网络上存在的最长时间,超过这个时间,连接双方会丢弃报文
Q : 等待 2MSL 的意义
A : 两个理由:
一:为了保证客户端发送的最后一个 ACK 报文段能够到达服务器。如果 ACK 丢失了就会导致处在 LAST-ACK 状态的服务器收不到对 FIN-ACK 的确认报文。服务器会超时重传这个 FIN-ACK ,接着客户端再重传一次确认,然后重新开始等待的这个过程。这样一来,客户端和服务器都能正常关闭。
但是假如客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。
二:防止 “已失效的连接请求报文段” 出现在本连接中。 客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本次连接中所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。
拥塞控制
TCP的拥塞控制采用了四种算法,即 慢开始 、 拥塞避免 、快重传 和 快恢复。
- 慢开始: 慢开始算法的思路是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的符合情况。经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是由小到大逐渐增大拥塞窗口数值。cwnd初始值为1,每经过一个传播轮次,cwnd加倍。
- 拥塞避免: 拥塞避免算法的思路是让拥塞窗口cwnd缓慢增大,即每经过一个往返时间RTT就把发送放的cwnd加1.
- 快重传与快恢复:在 TCP/IP 中,快速重传和恢复(fast retransmit and recovery,FRR)是一种拥塞控制算法,它能快速恢复丢失的数据包。没有 FRR,如果数据包丢失了,TCP 将会使用定时器来要求传输暂停。在暂停的这段时间内,没有新的或复制的数据包被发送。有了 FRR,如果接收机接收到一个不按顺序的数据段,它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认,它会假定确认件指出的数据段丢失了,并立即重传这些丢失的数据段。有了 FRR,就不会因为重传时要求的暂停被耽误。 当有单独的数据包丢失时,快速重传和恢复(FRR)能最有效地工作。当有多个数据信息包在某一段很短的时间内丢失时,它则不能很有效地工作。
TCP 黏包 粘包问题
什么是黏包和粘包
一个数据包中包含了发送端发送的多个数据包的信息,这种现象即为粘包。这是由于:
- tcp 数据包是一串无结构的字节流,没有边界
- tcp 首部没有标识数据长度的字段
拆包和粘包的问题导致接收端在处理的时候会非常困难,因为无法区分一个完整的数据包。
黏包和粘包是如何产生的
- 发送方产生黏包:发送缓冲区中,多个较小数据包合并发送
- 接收方产生黏包:接收缓冲区中,后一个数据包来的过快,前一个数据包来不及取出
黏包和粘包是如何解决的
- 特殊字符控制
- 首部添加数据包长度
netty 使用专门的编码器和解码器解决黏包和粘包问题。
tips:UDP 没有粘包问题,但是有丢包和乱序。不完整的包是不会有的,收到的都是完全正确的包。传送的数据单位协议是 UDP 报文或用户数据报,发送的时候既不合并,也不拆分。
TCP 滑窗
TCP 滑窗的作用
tcp 滑动窗口的主要作用是流量控制
(逻辑从如下两点来描述)
- 为什么需要流量控制
- TCP 头部中的接收窗口
TCP 发送数据的方式是【将应用层的数据拆分成字节段,然后分开发送的】,考虑到传输效率,我们不能等到上一段数据被确认之后再发送下一段数据,而是要对数据进行批量发送,那么一次性发送多少数据合适呢,太少的话带宽利用不足,太多的话接收方可能处理不过来,流量控制解决的就是这个问题。
tcp 的滑动窗口有两个,一个是接收窗口,一个是发送窗口。tcp 头部中的窗口字段就是接收窗口
- 接收窗口(Advertised Window) : 还能接收多少字节
- 发送窗口(Effective Window) : 还能发送多少字节
tcp 头部中的【接收窗口字段】用于接收方通知发送方还有多少缓冲区可以接收数据,发送方根据接收方的处理能力来发送适当长度的数据,不会因为发送过多数据,而导致接收方处理不过来,从而做到流量控制。
发送窗口与接收窗口大小的计算
流量控制的实现与发送缓存和接收缓存的设计有关
- 计算方式如下:


对于 TCP 的发送方,任何时候其发送缓存内的数据都可以分为四类:
- 已发送,且已接收回执
- 已发送,但未接收回执
- 未发送,但对端允许发送
- 未发送,且对端不允许发送

对于 tcp 的接收方,接收缓存内有三种状态:
- 已接收,且已回执
- 未接收,但可以接收,就是接收窗口
- 未接收,且不能接收,超出窗口大小
滑窗的大小
滑窗的大小可依据一定的策略动态调整,应用可根据自身的处理能力,通过控制本端【接收窗口】的大小来实现对对端【发送窗口】的流量控制。
滑动窗口的移动细节
发送窗口:收到接收方对本段发送窗口内字节的 Ack 确认后才会移动窗口的左边界
接收窗口:前面所有端都确认时才会移动左边界,若前面有未接收的字节但已收到了后面字节的情况下是不会移动的,并且不会对后面的字节进行确认,以此来确保发送端重传
相关概念
RTT(Round Trip Time):发送一个数据包到收到对应的 Ack所花费的时间
RTO(Retransmission Time-out):重传时间间隔,TCP 在发送一个数据包之后,会启动重传定时器,RTO 就是这个定时器的重传时间,RTO 是基于 RTT 计算而得
TCP与UDP的区别

HTTP、HTTPS协议
HTTP 协议的主要特点
- 支持【客户/服务器模式】
Http 工作于【客户端服务端】架构之上,浏览器作为客户端,通过 url 向服务端(即 web 服务器)发送请求,然后 web 服务器根据发来的请求作出响应
- 简单快速
客户端向服务端请求服务时,只需要发送【请求方法】和【资源路径】即可。
- 灵活
http 允许传输任意类型的数据,具体类型由【content-type】加以标记
- 无连接
每次连接只处理一个请求,服务端处理完客户的请求并收到客户的应答之后,会立即断开与客户端的连接,采用这种方式可以节省时间。
从 HTTP1.1 起默认使用【长连接,即服务器需要等待一定时间后才断开连接】,以保证连接特性。虽然目前有一些技术比如 keep-alive 使用长连接来优化效率,但这些属于 http 的下层实现,对上层是透明的。因此,在每个独立的 http 请求中,仍然无法知道当前 http 是否处于长连接的状态,始终都认为请求处理完成后,连接就会关闭。
- 无状态
对事务处理没有记忆能力,不对之前一切的响应和请求的通信状态进行保存。
好处是在不需要前面的信息时,应答较快;
坏处是若后续处理需要前面的信息时,则必须重传,会导致每次传送的数据量增大
URL(Uniform Resource Location)
【统一资源定位符】是 Internet 上用来描述信息资源的字符串。
URL 格式
例如:
“test.com:8080/example/ind…
URL的格式由下列三部分组成:
- 【协议】(或称为服务方式),本例中为 HTTP 协议。
- 【存有该资源的主机IP地址或域名】,本例中为 test.com:8080。平时看到的都是域名,之后客户端会通过DNS(域名系统)查询域名对应的 IP,然后根据 IP 和端口号进行服务器的连接。
- 主机资源的具体地址,如目录和文件名等,本例中为 /example/index.html。
- HTTP消息结构
1.1 请求消息结构
请求行:请求方法 URL HTTP协议版本号
请求头部:头部字段名:值
空行
请求正文
1.2 响应消息结构
状态行:HTTP协议版本号 状态码 状态码描述
响应头部:头部字段名:值
空行
响应正文
HTTP 方法有哪些?
-
GET:获取资源(当前网络中绝大部分使用的都是 GET);
-
HEAD:获取报文首部,和 GET 方法类似,但是不返回报文实体主体部分;
-
POST:更新资源,传输实体主体
-
PUT:上传文件,由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般不使用该方法。
-
PATCH:对资源进行部分修改。PUT 也可以用于修改资源,但是只能完全替代原始资源,PATCH 允许部分修改。
-
OPTIONS:查询指定的 URL 支持的方法;
-
CONNECT:要求在与代理服务器通信时建立隧道。使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。
-
TRACE:追踪路径。服务器会将通信路径返回给客户端。发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1,当数值为 0 时就停止传输。通常不会使用 TRACE,并且它容易受到 XST 攻击(Cross-Site Tracing,跨站追踪)。
GET和POST的区别
get 和 post 都是 http 的请求
- 语义不同:【Get是获取资源】,【Post是更新资源】
- 参数位置不同:
Get的请求参数放在URL后面,用?隔开,使用键值对表示;
Post会把提交的数据放在报文体中,解析报文才能获取。post 的安全性更高。chrome、firefox 对 URL 的长度有限制 - Get具有幂等性(任意多次执行对资源本身所产生的影响均与一次执行的影响相同)和安全性(不改变资源),Post没有
- Get能被CDN缓存,Post不行
浏览器地址栏键入带有 http 前缀的 URL 后,发生了什么
- 【DNS解析,查询域名对应的 IP】
逐层查看DNS服务器缓存,缓存由近至远依次为,浏览器缓存、系统缓存、路由器缓存、ips服务器缓存、域名服务器缓存、顶级域名服务器缓存
- TCP连接
浏览器获得域名对应的 IP 地址以后,浏览器向服务器请求建立链接,发起三次握手;利用【ip地址】和对应【端口】与服务器连接,默认80端口
- 发送 HTTP 请求
TCP 连接建立起来后,浏览器向服务器发送 HTTP 请求
- 服务器处理请求并返回 HTTP 报文
服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器;
- 浏览器解析渲染页面
- 客户端浏览器加载了 html 文件,由上到下将 html 解析为DOM树(DOM Tree)
- 若遇到对 js 文件、css 文件及图片等【静态资源的引用】,则需要【重复上述步骤并向服务器请求这些资源】
- 由于 http1.1 默认使用长连接,因此 tcp 连接不会关闭,可复用
- 浏览器根据其请求到的资源、数据渲染页面,最终向用户呈现一个完整的页面。
- 断开连接
谈下你对 HTTP 长连接和短连接的理解?分别应用于哪些场景?
在 HTTP/1.0 中默认使用短连接。也就是说,客户端和服务器每进行一次 HTTP 操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个 HTML 或其他类型的 Web 页中包含有其他的 Web 资源(如:JavaScript 文件、图像文件、CSS 文件等),每遇到这样一个 Web 资源,浏览器就会重新建立一个 HTTP 会话。
而从 HTTP/1.1 起,默认使用长连接,用以保持连接特性。使用长连接的 HTTP 协议,会在响应头加入:
Connection:keep-alive
在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。
Keep-Alive 不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如:Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。
HTTP状态码
- 1xx:指示信息,表示请求已接收,继续处理
- 2xx:成功,请求接收并接受、理解、接受
- 3xx:重定向,要完成请求必须进行更进一步的操作
- 4xx:客户端错误,请求有语法错误或者无法实现
- 5xx:服务端错误,服务端未能实现合法请求
常见状态码:
200:正常返回信息
400 Bad request:客户端请求有语法错误
401 Unauthorized:请求未被授权
403 Forbidden:服务端收到请求,但拒绝提供服务
404 Not Found:请求资源不存在,可能为URL输入错误
500 Internal server error:服务器发生不可预期的错误
503 Server unavaliable:服务器不能提供服务,一段时间后恢复正常
cookie 和 session 的区别
cookie是客户端的解决方案,由服务端发送给客户端,并以文本的形式保存于客户端。客户端每次向服务端发送请求时都会带上这些特殊的信息。具体地说,当用户使用浏览器访问网站时,用户会提供用户名给服务器,服务器回复时也会发送这些信息,在响应头,当客户端接收后会将信息存储在一个位置,下次再发请求时,会将cookie发送。

session的实现:cookie;url回写。都与JSESSION ID有关。
区别:
- cookie存在客户端,session存在服务器
- cookie不安全
- session会在一定时间保存在服务器上,当访问增多,减小开销用cookie
- HTTP与HTTPS的区别
- 开销:HTTPS 协议需要到 CA 申请证书,一般免费证书很少,需要交费;
- 资源消耗:HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的 ssl 加密传输协议,需要消耗更多的 CPU 和内存资源;
- 端口不同:HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443;
- 安全性:HTTP 的连接很简单,是无状态的;HTTPS 协议是由 TSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。
- HTTPS的建立过程
- 客户端发送自己支持的加密规则给服务器,代表告诉服务器要进行连接了;
- 服务器从中选出一套加密算法和 hash 算法以及自己的身份信息(地址等)以证书的形式发送给浏览器,证书中包含服务器信息,加密公钥,证书的办法机构;
- 客户端收到网站的证书之后要做下面的事情:
验证证书的合法性
果验证通过证书,浏览器会生成一串随机数,并用证书中的公钥进行加密
用约定好的 hash 算法计算握手消息,然后用生成的密钥进行加密,然后一起发送给服务器。 - 服务器接收到客户端传送来的信息,要做下面的事情:
用私钥解析出密码,用密码解析握手消息,验证 hash 值是否和浏览器发来的一致
使用密钥加密消息 - 如果计算法 hash 值一致,握手成功。
数据库
SQL语法
索引
- B树与B+树的区别
- 稠密索引与稀疏索引
- 如何调优
- 联合索引的最左匹配原则
- 索引是越多越好吗
锁模块
- InnoDB与myISAM中锁的区别
- 当前读与快照读
- 可重复读如何避免幻读
- 如何实现乐观锁和悲观锁
事务
1. 事务的四大特性 ACID
- 原子性(Atomic,A):由于事务是数据库的逻辑工作单位,事务包含的所有操作要么全部执行,要么全部失败回滚
- 一致性(C):事务的执行应确保数据库从一个一致状态转变为另一个一致状态,其中,一致状态是指数据库中的数据应满足完整性约束
完整性约束包括三点:实体完整性约束、用户定义的完整性约束和参照完整性。其中,实体完整性是指不能有两条完全相同的数据;用户定义的完整性约束是指数据的值必须满足用户的要求;参照完整性约束是指相关联的两个表之间的约束,对于具有主从关系的两个表来说,从表中每条记录外键的值必须是主表中存在的
- 隔离性(I):在多个事务并发执行时,一个事务的执行不会影响其他事务的执行
- 持久性(D):指一个事务一旦提交,它对数据库中的数据的修改是永久保存在数据库中的,这意味着即使系统或者介质出现故障,已提交事务对数据的更新也不会丢失。InnoDB会将所有对页面的修改操作写入一个专门的文件(Redo-log file),并在数据库启动时,从此文件进行恢复操作。
2. 事务隔离级别
2.1 并发过程中可能出现的4个问题
- 更新丢失:多个事务同时对同一数据进行修改,一个事务的修改可能会覆盖另一事务的修改,数据库层面不会出现,应用程序中会出现
- 脏读:一个事务读取到另一个事务未提交的更新数据。
- 不可重复读:事务A对同一数据读取多次,在读取过程中,事务B对数据作更新并提交,导致事务A多次读取同一数据时结果不一致
- 幻读:事务A读取与搜索条件相匹配的若干行,事务B对事务A的结果集以插入行、删除行的形式进行修改,导致事务A查询到的数据行数与之前不一致
2.2 事务的4个隔离级别
- 未提交读(Read uncommitted)定义:一个事务读取到其他事务未提交的数据,是级别最低的隔离机制。
- 提交读(Read committed)定义:就是一个事务读取到其他事务提交后的数据。Oracle默认隔离级别。
- 可重复读(Repeatable read)定义:就是一个事务对同一份数据读取到的相同,不在乎其他事务对数据的修改。MySQL默认的隔离级别。
- 串行化(Serializable)定义:事务串行化执行,隔离级别最高,牺牲了系统的并发性。

查看事务的隔离级别:select @@tx_isolation;
设置隔离级别:set session transaction isolation level xxx(read committed);
开始事务:start transaction
提交事务:commit;
手动回滚:rollback;
2.3 当前读与快照读
Q : 为什么RR可避免不可重复读的问题
A : 在 RR 的级别下,事务开启后第一次执行快照读的时候,数据库会会对所查询的数据创建一个快照(决定了当前事务对数据的哪个版本可见)。之后这个事务再调用快照读的时候,调用的仍然是同一个快照;
而在 RC 级别下,每次快照读都会生成新的快照。
逗比一句:RR 有种掩耳盗铃的感觉
Q : 快照读是如何实现的? 基于三个东西:
undo 日志:记录的是老版本数据,比如当我们对数据进行更新时,更新前的数据就变成了这条数据的老版本数据
read view 快照:当我们执行快照读的时候,数据库会对所偶查询的数据创建一个快照,这个快照就决定了当前事务对数据的哪个版本可见
数据行中的额外数据:
DB_ROLL_PTR:指向数据的上一个版本在 undo 日志中的位置
(用于可见性算法) DB_TRX_ID:最近一次修改数据的事务的标识符
(选择性介绍) DB_ROW_ID:密集索引中没有主键和唯一键时,数据库会创建这个数据作为密集索引中的 key
Q : read view 的可见性算法
A : 将数据的 DB_TRX_ID 与其他活跃事务 id 相比,若大于等于,则取上一版本数据,一直循环,直到小于。
当前度和快照读的区别:
- 读到的数据版本不同:当前读的一定是最新版;快照读读到的有可能是历史版本,创建快照的时机决定了读取数据的版本。
- 使用当前读的方式有:select...in share mode、select...for update、update、insert;
使用快照读的方式有:select;由于串行化对所有操作加锁,使得串行化下的select也退化为当前读。
RR 如何避免幻读
原因:行锁 + gap 锁
- 行锁:锁记录
- gap 锁:锁住索引树中记录间的空隙
何时加 gap 锁:
- In 主键索引,唯一键索引:全部命中时,只加行锁不加 gap 锁;部分命中时,在未命中索引两侧加 gap 锁;全都不命中时,所有所有间隙加 gap 锁
- In 非唯一索引:左右开区间的两个索引之间的所有间隙上锁
- 不走索引:会对所有 gap 上锁
操作系统
Linux命令
- 进程
- 文件
touch // 创建文件
rm/mkdir/cp/mv // 管理文件
chmod/chown/passwd // 权利权限
// 以上创建的进程会很快被终止,而服务(操作系统自身所需要 crond atd 可以发现名称结尾为 d 用来标识 daemon、负责网络连接 apache named postfix)
进程
Linux 系统中:【触发任何一个事件时,系统都会将它定义为一个进程,并且给它一个进程 id(pid),同时根据触发这个进程的用户与相关属性关系,给这个 pid 一组有效的权限设置】
程序与进程的区别
- 存储的地方不同:程序以文件的形式存储在磁盘里;进程以存储单元的形式存储在内存中
- 内容不同:程序是二进制指令流;进程不仅包括程序代码,还包括 cpu 执行程序所需要的所有信息和执行者的权限和属性,操作系统也会给这个内存中的单元一个标识符。
父进程与子进程:fork & exec
- 操作系统先以 fork 的方式复制一个与父进程相同的临时进程,唯一差别就是 pid 和 ppid
- 临时进程再以 exec 的方式加载实际要执行的程序
感性认识:Linux 系统函数 fork() 可以在父进程中创建一个子进程,这样的话,在一个进程接到来自客户端新的请求时就可以复制出一个子进程让其来处理,父进程只需负责监控请求的到来,然后创建子进程让其去处理,这样就能做到并发处理。

进程与线程的区别
- 颗粒度大小不同:进程是资源分配的最小单位,线程是程序执行的最小单位(资源调度的最小单位)
- 内存空间是否独立:进程拥有独立的地址空间,系统每创建一个进程,就为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵;而同一进程下的所有线程是共享进程的数据的,使用相同的地址空间,因此 cpu 切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
- 通信遍历性:线程间通信很方便,可通过堆上数据的共享;而进程通信需要以通信的方式进行。
- 程序健壮性:多线程程序只要有一个线程死掉,整个进程也会死掉;多进程程序中一个进程死掉不会影响其他进程,因为进程有自己的独立地址空间
进程的状态
- 运行态(正在占用CPU)
- 就绪态(可运行,但因为其他进程正在运行而终止)
- 阻塞态(除非某种外部事件发生,否则不能进行)
- 进程通信方式 参考:www.ibm.com/developerwo… 在 linux 下进程间通信的几种主要手段简介:
-
管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
-
信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);
-
消息队列(Message):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
-
共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
-
信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
-
套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。
各种通信方式的比较和优缺点:
-
管道:速度慢,容量有限,只有父子进程能通讯
-
有名管道(named pipe):任何进程间都能通讯,但速度慢
-
消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
-
信号量:不能传递复杂消息,只能用来同步
-
共享内存:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存