计算机基础

文章目录

一、数据结构

1. 排序算法

在这里插入图片描述
经典排序算法

快排

在这里插入图片描述
在这里插入图片描述

什么时候最坏?

  • 数组已经是正序(same order)排过序的。
  • 数组已经是倒序排过序的。
  • 所有的元素都相同(1、2的特殊情况)
private static int partition(int[] arr, int left, int right) {
        int temp = arr[left];
        while (right > left) {
            // 先判断基准数和后面的数依次比较
            while (temp <= arr[right] && left < right) {
                --right;
            }
            // 当基准数大于了 arr[right],则填坑
            if (left < right) {
                arr[left] = arr[right];
                ++left;
            }
            // 现在是 arr[right] 需要填坑了
            while (temp >= arr[left] && left < right) {
                ++left;
            }
            if (left < right) {
                arr[right] = arr[left];
                --right;
            }
        }
        arr[left] = temp;
        return left;
    }

    private static void quickSort(int[] arr, int left, int right) {
        if (arr == null || left >= right || arr.length <= 1)
            return;
        int mid = partition(arr, left, right);
        quickSort(arr, left, mid);
        quickSort(arr, mid + 1, right);
    }

归并排序

时间复杂度分析
在这里插入图片描述

//归并排序:分治法。将序列分为两半,分别进行归并排序,再合并
    void merge(int[] arr, int start, int end) {
        if (start == end) return;
        int mid = (start + end) / 2;
        merge(arr, start, mid);
        merge(arr, mid + 1, end);

        int[] temp = new int[end - start + 1];
        int i = start, j = mid + 1, k = 0;
        while(i <= mid && j <= end)
            temp[k++] = arr[i] < arr[j] ? arr[i++] : arr[j++];
        while(i <= mid)
            temp[k++] = arr[i++];
        while(j <= end)
            temp[k++] = arr[j++];
        System.arraycopy(temp, 0, arr, start, end);
    }

堆是一颗有大根堆和小根堆之分
在大根堆中每个节点的值都大于等于其子节点(如果有子节点的话)的值 。
堆的基本操作包括初始化堆,插入堆,获取堆元素,删除堆。其中获取和删除操作只能依次获取堆中优先级最高的元素。

  • 堆的插入:将元素插入最后一位,再进行向上调整,即如果父节点的值小于被插入的元素,父节点下移,被插入的元素上移。时间复杂度取决于树的高度h。而完全二叉树的树的高度为[logn+1]的上取整,所示时间复杂度为O(logn)
  • 堆的删除:删除堆的根节点,将最后一个节点补到根节点位置,得到一颗不符合规则的堆。再对根节点进行向下调整,即如果父节点小于某一孩子或所有孩子,将元素值最大 的孩子与父节点交换。孩子上移,父节点下移,下移后与孩子重复该操作,直到比孩子都大或没有孩子。操作时间还是取决于树的高度,时间复杂度为O(logn)
  • 初始化堆
    建堆是从堆从小到大的非叶子节点开始(堆中位置为n/2向下取整),对该节点以及之前的节点依次调整。根节点向下调整的操作所需时间为O(h),根节点的孩子节点所需时间为为O(h-1),第i层节点的向下冒泡操作时间为O(h-i)。将每层的节点数与要操作的次数相乘再求和,得到时间复杂度为O(n)
  • 堆排序
    初始化堆的时间复杂度为O(n)
    n-1次删除操作的时间复杂度为O(nlogn)
    所以总操作时间复杂度为O(nlogn)

2. 结构

堆和栈区别

  • 栈:栈是一种线性的数据结构,读取规则是先进后出。栈中的数据占用的内存空间的大小是确定的,便于代码执行时的入栈、出栈操作,并由系统自动分配和自动释放内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。

  • 堆:堆是一种树形数据结构,读取相对复杂。堆是动态分配内存,内存大小不一,也不会自动释放。栈中的数据长度不定,且占空间比较大。便于开辟内存空间,更加方便存储。

  • 堆栈内存分配:程序运行时,每个线程分配一个栈,每个进程分配一个堆。也就是说,栈是线程独占的,堆是线程共用的。此外,栈创建的时候,大小是确定的,数据超过这个大小,就发生stack overflow错误,而堆的大小是不确定的,需要的话可以不断增加。

  • 应用
    堆可以用于优先队列,堆排序等场景
    栈可以用于字符匹配

外部排序
内存中进行的排序称为内部排序,而在许多实际应用中,经常需要对大文件进行排序,因为文件中的记录很多,信息量庞大,无法将整个文件拷贝进内存进行排序。因此,需要将带排序的记录存储在外存上,排序时再把数据一部分一部分的调入内存进行排序,在排序中需要多次进行内外存的交互,对外存文件中的记录进行排序后的结果仍然被放到原有文件中。这种排序方法就称外部排序。

贪心算法和动态规划的区别
动态规划:重叠子问题+最优子结构(自下而上),每步所做选择依赖于子问题,本质是穷举法,可以保证结果最佳,复杂度高。
贪心算法:贪心选择+最优子结构(自上而下),仅在当前状态下做出最好选择。不能保证解最佳,复杂度低。
动态规划三要素:重叠子问题,最优子结构,状态转移方程

数组与链表

遍历一遍,哪个快?

  • 数组,数组开辟的空间是连续的,读入了缓存
  • 而链表的节点很可能是分散的,极端情况下,一次访存就有可能引起一次缓存失效

两者区别:

  • 在内存中,数组是一块连续的区域,需要预留空间,使用前先申请占内存的大小;而链表可以在内存中的任何地方,不要求连续,扩展方便。
  • 数组插入和删除效率低,插入数据时,这个位置后面的数据在内存中都要后移,删除时,都要前移。但是随机读取的效率高。
  • 链表增加和删除数据容易,查找效率低,因为不具有随机访问性。

数据库相关的

B树、B+树

在这里插入图片描述

跳表

在这里插入图片描述

跳表插入、删除、查找元素的时间复杂度跟红黑树都是一样量级的,时间复杂度都是 O(logn) 。而且可以按照范围区间查找元素

  • 跳表是可以实现二分查找的有序链表。
  • 时间复杂度:查找元素的过程是从最高级索引开始,一层一层遍历最后下沉到原始链表。所以,时间复杂度 = 索引的高度 * 每层索引遍历元素的个数。
  • 空间复杂度:跳表通过建立索引,来提高查找元素的效率,就是典型的“空间换时间”的思想。假如原始链表包含 n 个元素,则一级索引元素个数为 n/2、二级索引元素个数为 n/4、三级索引元素个数为 n/8。以此类推。所以,索引节点的总和是:n/2 + n/4 + n/8 + … + 8 + 4 + 2 = n-2,空间复杂度是 O(n)
  • 插入数据
  1. 假如一直往原始列表中添加数据,但是不更新索引,就可能出现两个索引节点之间数据非常多的情况,极端情况,跳表退化为单链表,从而使得查找效率从 O(logn) 退化为 O(n)。
  2. 需要在插入数据的时候,索引节点也需要相应的增加、或者重建索引,来避免查找效率的退化。
  3. 重建索引:随机选 n/2 个元素做为一级索引、随机选 n/4 个元素做为二级索引、随机选 n/8 个元素做为三级索引,依次类推,一直到最顶层索引。

跳表

LSM树
  • 传统关系型数据库 使用btree或一些变体作为存储结构,能高效进行查找。
    但保存在磁盘中时它也有一个明显的缺陷,那就是逻辑上相离很近但物理却可能相隔很远,这就可能造成大量的磁盘随机读写。
  • 随机读写比顺序读写慢很多,为了提升IO性能,我们需要一种能将随机操作变为顺序操作的机制,于是便有了LSM树。LSM树能让我们进行顺序写磁盘,从而大幅提升写操作,作为代价的是牺牲了一些读性能。
  • LSM Tree 结构的数据库有个特点,实时写入的数据先写入到内存,内存达到阈值往磁盘 flush 的时候,会生成类似于 StoreFile 的有序文件,而跳表恰好就是天然有序的,所以在 flush 的时候效率很高,而且跳表查找、插入、删除性能都很高。
  • LSM树对于具体如何组织有序地组织数据并没有明确的数据结构定义,例如Hbase使跳跃表来保证内存中key的有序
红黑树

红黑树
红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须满足下面性质:

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

红黑树总是通过旋转和变色达到自平衡

为什么用红黑树不用二叉查找树?

  • 二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。
  • 红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。
前缀树

Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

前缀树的3个基本性质:

  • 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
  • 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
  • 每个节点的所有子节点包含的字符都不相同。

二、Java基础

0 基础问题

1. 抽象类和接口的区别

(1)接口只有方法定义,不能有方法的实现,而抽象类可以有定义与实现,方法可在抽象类中实现。
(2)实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。(java只支持单继承)
在这里插入图片描述

接口:

  • 接口的使用,它不能创建对象,但是可以被实现( implements ,类似于被继承)。一个实现接口的类(可以看做 是接口的子类),需要实现接口中所有的抽象方法,创建该类对象,就可以调用方法了,否则它必须是一个抽象 类。

2. final关键字、内部类、static

final: 不可改变。可以用于修饰类、方法和变量。

  • 类:被修饰的类,不能被继承。
  • 方法:被修饰的方法,不能被重写。
  • 变量:被修饰的变量,不能被重新赋值。

内部类:

  • 内部类可以直接访问外部类的成员,包括私有成员。
  • 外部类要访问内部类的成员,必须要建立内部类的对象。

static

  • 可以用来修饰的成员变量和成员方法,被修饰的成员是属于类的,而不是单单是属 于某个对象的。也就是说,既然属于类,就可以不靠创建对象来调用了。
  • 静态方法只能访问静态成员。
  1. 静态方法可以直接访问类变量和静态方法。
  2. 静态方法不能直接访问普通成员变量或成员方法。反之,成员方法可以直接访问类变量或静态方法。
  3. 静态方法中,不能使用this关键字。

3. 多态

多态: 是指同一行为,具有多个不同表现形式。

  1. 继承或者实现【二选一】
  2. 方法的重写【意义体现:不重写,无意义】
  3. 父类引用指向子类对象【格式体现】

引用类型转换

  • 向上转型:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。当父类引用指向一个子类对象时,便是向上转型。
  • 向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。(解决了:不能调用子类拥有,而父类没有的方法)

方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。

  1. 方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
  2. 方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
  3. 方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

4. 成员变量和局部变量

在类中的位置不同 重点

  • 成员变量:类中,方法外
  • 局部变量:方法中或者

方法声明上(形式参数) 作用范围不一样 重点 -

  • 成员变量:类中
  • 局部变量:方法中

初始化值的不同 重点

  • 成员变量:有默认值
  • 局部变量:没有默认值。必须先定义,赋值,最后使用

在内存中的位置不同 了解

  • 成员变量:堆内存
  • 局部变量:栈内存

生命周期不同 了解

  • 成员变量:随着对象的创建而存在,随着对象的消失而消失
  • 局部变量:随着方法的调用而存在,随着方法的调用完毕而消失

5. String类、StringBuilder类

String类:

  • 字符串是常量,它们的值在创建之后不能更改
  • 字符串的底层是一个被final修饰的数组,不能改变,是一个常量。

StringBuilder类:

  • 字符串缓冲区,可以提高字符串的操作效率(看成一个可变长度的字符串)。底层也是一个数组,但没有被final修饰,可以改变长度
  • StringBufferStringBuilder都提供了一系列插入、追加、改变字符串里的字符序列的方法,它们的用法基本相同,
  • StringBuilder是线程不安全的,StringBuffer是线程安全的。如果只是在单线程中使用字符串缓冲区,则StringBuilder的效率会高些,但是当多线程访问时,最好使用StringBuffer

6. Java集合

Java集合
在这里插入图片描述

6.1 HashSet

元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果 equals 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。

7. 异常类

最上级接口类:Throwable
Throwable类有两个直接子类:

  • Error:代表了JVM本身的错误。错误不能被程序员通过代码处理。例如:JVM的内存溢出,系统崩溃,一般的,程序不会从错误中恢复。Error有两个典型的错误实现类StackOverflowError和OutOfMemoryError
  • Exception:代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心
    Exception又分为两类:
  1. CheckedException(编译时异常):需要用try catch显示的捕获,对于可恢复的异常使用CheckedException。最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。
  2. RuntimeException(运行时异常):不需要捕获,对于程序异常(不可恢复)的异常使用RuntimeException。运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略
    在这里插入图片描述

8、Java8的新特性

新特性

哈希表的构造

把Key通过一个哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。

哈希函数构造

  1. 直接定址法
    取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a·key + b,其中a和b为常数(这种散列函数叫做自身函数)。
  2. 除留余数法 f(key) = key mod p (p≤m),m为散列表长
  3. 折叠法:把关键词分割成位数相同的几个部分,然后叠加
  4. 平方取中法:假设关键字是1234、平方之后是1522756、再抽取中间3位227,用作散列地址。平方取中法比较适合于不知道关键字的分布,而位数又不是很大的情况。
  5. 随机数法
    f(key) = random(key),这里random是随机函数。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。

冲突处理

  1. 线性探查法
    如果表中下标为H(key)的位置已经被某个其他元素使用了,那么就检查下一个位置H(key)+1是否被占,如果还是被占,就继续检查下一个位置(hash值不断加1).如果检查过程中超过了表长,那么就回到表的首位继续循环,直到找到一个可以使用的位置。
  2. 平方探查法 H(key)+1^2
    H(key)-1^2、
    H(key)+2^2、
    H(key)-2^2、
    H(key)+3^2…
    如果H(key)+k^2
    超过了表长TSize,那么就把H(key)+k^2对表长TSize取模
  3. 链地址法(拉链法) 把所有H(key)相同的key连接成一条单链表。 设定一个数组Link,范围是Link[0]-Link[mod],Link[h]存放H[key]=h的一条单链表。
    在这里插入图片描述

1. HashMap(数组+链表+红黑树)

HashMap结构
在这里插入图片描述
HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。

  1. capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
  2. loadFactor:负载因子,默认为 0.75。
  3. threshold:扩容的阈值,等于 capacity * loadFactor
  • hash运算的过程其实就是对目标元素的Key进行hashcode,再对Map的容量进行取模

HashMap 怎么设定初始容量大小的?

  • 数组默认容量:16(为了提升取模的效率,使用位运算代替了取模运算,这就要求Map的容量一定得是2的幂)
  • 数组扩容:大于12时(12=默认容量16*装载因子0.75),扩容成原容量的2倍

java8相对于java7的优化

  1. 数组+链表改成了数组+链表或红黑树
    java7查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)
    java8的HashMap在java7的基础上增加了红黑树这种数据结构,使得在桶里面查找数据的复杂度从O(n)降到O(logn)

  2. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8 遍历链表,将元素放置到链表的最后;
    解释:因为 1.7 头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;
    A 线程在插入节点 B,B 线程也在插入,遇到容量不够开始扩容,重新 hash,放置元素,采用头插法,后遍历到的 B 节点放入了头部,这样形成了环,如下图所示:
    在这里插入图片描述

  3. 扩容的时候 1.7 需要对原数组中的元素进行重新 hash 定位在新数组的位置,1.8 采用更简单的判断逻辑,位置不变或索引+旧容量大小;
    解释:这是由于扩容是扩大为原数组大小的 2 倍,用于计算数组位置的掩码仅仅只是高位多了一个 1,怎么理解呢?
    扩容前长度为 16,用于计算(n-1) & hash 的二进制 n-1 为 0000 1111,扩容为 32 后的二进制就高位多了 1,为 0001 1111。
    因为是& 运算,1 和任何数 & 都是它本身,那就分二种情况,如下图:原数据 hashcode 高位第 4 位为 0 和高位为 1 的情况
    第四位高位为 0,重新 hash 数值不变,第四位为 1,重新 hash 数值比原来大 16(旧数组的容量)
    在这里插入图片描述

  4. 在插入时,1.7先判断是否需要扩容,再插入,1.8 先进行插入,插入完成再判断是否需要扩容;

  5. hash函数计算时: Java1.8 相比 1.7 做了调整,1.7 做了四次移位和四次异或,但明显 Java 8 觉得扰动做一次就够了,做 4 次的话,多了可能边际效用也不大,所谓为了效率考虑就改成一次了。

1. HashMap 的哈希函数怎么设计

  • 散列值计算:hash 函数是先拿到通过 key 的 hashcode,是 32 位的 int 值,然后让 hashcode 的高 16 位和低 16 位进行异或操作。
    在这里插入图片描述

2. 为什么这么设计

  1. 一定要尽可能降低 hash 碰撞,越分散越好;
  2. 算法一定要尽可能高效,因为这是高频操作, 因此采用位运算;

3. 为什么采用 hashcode 的高 16 位和低 16 位异或能降低 hash 碰撞?hash 函数能不能直接用 key 的 hashcode?

  • 因为 key.hashCode()函数调用的是 key 键值类型自带的哈希函数,返回 int 型散列值。int 值范围为-2147483648~2147483647,前后加起来大概 40 亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。
    但问题是一个 40 亿长度的数组,内存是放不下的。你想,如果 HashMap 数组的初始大
    小才 16,用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标
    数组下标计算:源码中模运算就是把散列值和数组长度-1 做一个"与"操作,位运算比%运算要快。h & (length-1)
  • 这也正好解释了为什么 HashMap 的数组长度要取 2 的整数幂。因为这样(数组长度-1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度
    16 为例,16-1=15。2 进制表示是 00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。

10100101 11000100 00100101
& 00000000 00000000 00001111
00000000 00000000 00000101 //高位全部归零,只保留末四位

但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。“扰动函数”的价值就体现出来了:

  • 右位移 16 位,正好是 32bit 的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

为了正确使用HashMap,选择恰当的Key是非常重要的。减少hash冲突,避免链表转为红黑树。

链表长度大于8 & 数组长度大于64时,链表转为红黑树

  • 链表长度大于8 :通常如果 hash 算法正常的话,那么链表的长度也不会很长,那么红黑树也不会带来明显的查询时间上的优势,反而会增加空间负担。所以通常情况下,并没有必要转为红黑树,所以就选择了概率非常小,小于千万分之一概率,也就是长度为 8 的概率,把长度 8 作为转化的默认阈值。
  • 数组长度大于64:当 table 数组容量比较小时,键值对节点 hash 的碰撞率可能会比较高,进而导致链表长度较长。这个时候应该优先扩容,而不是立马树化。

put() 方法
链表长度大于8 & 数组长度大于64时,会转为红黑树。(红黑树转链表的阈值是6

  1. 调用 hash(K) 方法计算 K 的 hash 值,然后结合数组长度,计算得数组下标;
  2. 调整数组大小(当容器中的元素个数大于 capacity * loadfactor 时,容器会进行扩容resize 为 2n);
  3. 如果 K 的 hash 值在HashMap 中不存在,则执行插入,
    若存在,则发生碰撞;
    ii.如果 K 的 hash 值在 HashMap 中存在,且它们两者equals相等 返回 true,则更新键值对;
    iii. 如果 K 的 hash 值在 HashMap 中存在,且它们两者 equals 不等返回 false,则插入链表的尾部(尾插法)或者红黑树中(树的添加方式)。 (JDK 1.7 之前使用头插法、JDK 1.8 使用尾插法)(注意:当碰撞导致链表大于 TREEIFY_THRESHOLD = 8 时,就把链表转换成红黑树)

get() 方法

  1. 调用 hash(K) 方法(计算 K 的 hash 值)从而获取该键值所在链表的数组下标;
  2. 顺序遍历链表,equals()方法查找相同 Node 链表中 K 值对应的 V 值。

hashCode 是定位的,存储位置;equals是定性的,比较两者是否相等。

1.1.1hashCode()和equals方法以及“==”

区别
hashCode() 决定了 key 放在这个桶里的编号,也就是在数组里的 index;
equals() 是用来比较两个 object 是否相同的。equals() 就是用 == 来实现的

"==“比较基本类型是比较值的大小,比较引用类型是比较地址
equals 比较引用类型是比较值

  • int与int,直接用==
  • Integer与int,==比较时,Integer会自动拆箱(调用intValue()方法)
  • 两个Integer比较
  • 当他们都在【-128,127】之间,使用“==”和equals比较都为true;原因是因为Integer中有一个内部类IntegerCache,当处于此区间的值都是相同的地址和值,因此此区间的 " =="和equlas都为true。
  • 在此范围之外,“==”比较为false, equals为true。
  • hashCode()返回该对象的哈希码值;equals()返回两个对象是否相等。

  • HashCode 用于在散列的存储结构中确定对象的存储地址。

  • 如果两个对象equals()相等,那么两个对象的hashCode()方法返回的结果也必然相等。

  • 如果两个对象的 hashCode()相同,则 equals()却不一定相等。

  • 如果重写equals()方法,必须重写hashCode()方法,以保证equals方法相等时两个对象hashcode返回相同的值。(API上有标注:请注意,通常需要在重写此方法时覆盖hashCode方法,以便维护hashCode方法的常规协定,该方法声明相等的对象必须具有相等的哈希代码。)

1.2 红黑树

红黑树
红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须满足下面性质:

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

红黑树总是通过旋转和变色达到自平衡

为什么用红黑树不用二叉查找树?

  • 二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。
  • 红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

1.3 线程安全问题

HashMap非线程安全

  • HashMap是非线程安全的(即即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致),只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap(加锁实现线程安全)。

为什么非线程安全

  • put时:假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失
  • 刪除键值对时: 当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去,其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改
  • add时: 当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。

怎么解决这个线程不安全的问题

  • Java 中有 HashTable以及 ConcurrentHashMap 可以实现线程安全的 Map
  • HashTable 是直接在操作方法上加 synchronized 关键字,锁住整个数组,粒度比较大
  • ConcurrentHashMap 使用分段锁,降低了锁粒度,让并发度大大提高。
1.3.1 hashMap与hashTable的区别
  1. HashMap 是线程不安全的,HashTable 是线程安全的;
  2. 由于线程安全,所以 HashTable 的效率比不上 HashMap;
  3. HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而 HashTable不允许;
  4. HashMap默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍+1;
  5. HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode
1.3.2 ConcurrentHashMap与hashTable

为什么 ConcurrentHashMap 比 HashTable 效率要高?

  • HashTable 使用一把锁(锁住整个链表结构)处理并发问题,多个线程竞争一把锁,容易阻塞; 任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。
  • ConcurrentHashMap
    JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。
    JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结 点)(实现 Map.Entry)。锁粒度降低了。
1.3.3 ConcurrentHashMap

在这里插入图片描述

ConcurrentHashMap
线程安全

  • ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。最多可以同时支持 16 个线程并发写
    JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。
    JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结 点)(实现 Map.Entry)。锁粒度降低了。

put() 方法

  • 如果没有初始化,就调用 initTable() 方法来进行初始化; 如果没有 hash 冲突就直接 CAS 无锁插入; 如果需要扩容,就先进行扩容; 如果存在 hash冲突,就加锁来保证线程安全,
    两种情况:一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入;
    如果该链表的数量大于阀值8,就要先转换成红黑树的结构,break 再一次进入循环如果添加成功就调用 addCount() 方法统计 size,并且检查是否需要扩容。

扩容方法 transfer()

  • 默认容量为 16,扩容时,容量变为原来的两倍。
    helpTransfer():调用多个工作线程一起帮助进行扩容,这样的效率就会更高。

get()方法

  • 计算 hash 值,定位到该 table 索引位置,如果是首结点符合就返回; 如果遇到扩容时,会调用标记正在扩容结点ForwardingNode.find()方法,查找该结点,匹配就返回; 以上都不符合的话,就往下遍历结点,匹配就返回,否则最后就返回null。

ConcurrentHashMap 的分段锁的实现原理

  • ConcurrentHashMap 成员变量使用 volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用 CAS 操作和 synchronized 结合实现赋值操作,多线程操作只会锁住当前操作索引的节点
1.3.4 LinkedHashMap

LinkedHashMap 怎么实现有序的?

  • LinkedHashMap 内部维护了一个单链表(双向链表),有头尾节点,同时 LinkedHashMap 节点 Entry 内部除了继承 HashMap 的 Node 属性,还有 before 和 after 用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。
1.3.5 TreeMap

TreeMap 怎么实现有序的

  • TreeMap 是按照 Key 的自然顺序或者 Comprator 的顺序进行排序,内部是通过红黑树来实现。所以要么 key 所属的类实现 Comparable 接口,或者自定义一个实现了 Comparator 接口的比较器,传给 TreeMap 用户 key 的比较。

2. 并发

多线程

2.1 并发、并行、多线程、同步、异步概念

  1. 并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
  2. 并行:在操作系统中,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。同一时刻
  3. 多线程:多线程是程序设计的逻辑层概念,它是进程中并发运行的一段代码。多线程可以实现线程间的切换执行。
  4. 异步:异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情。
  5. 异步和多线程并不是一个同等关系,异步是最终目的,多线程只是我们实现异步的一种手段。异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回而可以做其它的事情。实现异步可以采用多线程技术或则交给另外的进程来处理。

start 和 run 方法
调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法。

协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
协程多与线程进行比较

  1. 一个线程可以多个协程,一个进程也可以单独拥有多个协程,这样python中则能使用多核CPU。
  2. 线程进程都是同步机制,而协程则是异步
  3. 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态

2.3 多线程、线程池

多线程原理:

  • 同一时间内,CPU只能处理1条线程,只有1条线程在工作(执行);多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。

线程调度:

  • 分时调度
    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
  • 抢占式调度
    优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为 抢占式调度。 设置线程的优先级

创建线程的方法:

  • 一种是继承Thread类方式,
  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把 run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程
  • 一种是实现Runnable接口方式
  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正 的线程对象。
  3. 调用线程对象的start()方法来启动线程。

线程池

  • 其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源。

2.5 Volatile关键字

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到
主存中获取它的值,线程操作 volatile 变量都是直接操作主存

  • Volatile可以保证可见性和有序性,不能保证原子性
  • synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但
    缺点是synchronized是属于重量级操作,性能相对更低
  • CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性
  • 结合 CAS 和 volatile 可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下。
    因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
    但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

2.4 锁(线程同步)

线程同步的操作:

  1. 同步代码块。 2. 同步方法。 3. 锁机制。

锁的基本原理是,基于将多线程并行任务通过某一种机制实现线程的串行执行,从而达到线程安全性的目的

2.2.1 Synchornized
  1. Synchornized

原理*(基于monitor锁

  • synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁
  • 使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现。
  • 执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。
  • 执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。

synchromized缺陷
例子1:

  • 如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
      因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到

例子2:

  • 当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
      但是采用synchronized关键字来实现同步的话,就会导致一个问题:
    如果多个线程都只是进行读操作,当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

      因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。
      另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
      总的来说,也就是说Lock提供了比synchronized更多的功能
  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,
    我吃亏点再重试呗。
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁
    你们都别想改,我改完了解开锁,你们才有机会。
2.2.2 Synchornized和Lock区别

区别:

  1. Synchronized在线程发生异常时会自动释放锁,因此不会发生异常死锁。Lock异常时不会自动释放锁,所以需要在finally中实现释放锁。
  2. Lock是可以中断锁,Synchronized是非中断锁,必须等待线程执行完成释放锁。 Lock可以使用读锁提高多线程读效率。 synchronized会一直获取执行权限直到执行完毕,那lock能指定在一定时间内获取锁
  3. 一般情况下使用synchronized已经足够了,但是,每个线程在执行相关代码块时都要与其他线程同步确认是否可以执行代码。lock和semaphore就有了用武之地。lock可以帮我们实现尝试立刻获取锁,在指定时间内尝试获取锁,一直获取锁等操作,而semaphore信号量可以帮我们实现允许最多指定数量的线程获取锁。
2.2.4 ReentrantLock(AQS、CAS) 公平锁、非公平锁

ReentrantLock它是唯一一个实现了Lock接口的类。

  • 重入锁指的是 线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次;
  • 重入锁的设计目的
    比如调用demo方法获得了当前的对象锁,然后在这个方法中再去调用 demo2,demo2中的存在同一个实例锁,这个时候当前线程会因为无法获得 demo2的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死 锁

ReentrantLock的流程

  • ReentrantLock先通过CAS尝试获取锁, 如果此时锁已经被占用,该线程加入AQS队列并wait()
    当前驱线程的锁被释放,挂在CLH队列为首的线程就会被notify(),然后继续CAS尝试获取锁,此时:
  1. 非公平锁,如果有其他线程尝试lock(),有可能被其他刚好申请锁的线程抢占。
  2. 公平锁,只有在CLH队列头的线程才可以获取锁,新来的线程只能插入到队尾。
    (注:ReentrantLock默认是非公平锁,也可以指定为公平锁)

总结
0. 每一个ReentrantLock自身维护一个AQS队列记录申请锁的线程信息

  1. 通过大量CAS保证多个线程竞争锁的时候的并发安全;

  2. 可重入的功能是通过维护state变量来记录重入次数实现的。

  3. 公平锁需要维护队列,通过AQS队列的先后顺序获取锁,缺点是会造成大量线程上下文切换;

  4. 非公平锁可以直接抢占,所以效率更高;

多线程 竞争重入锁时,竞争失败的线程是如何实现阻塞以及被唤醒的呢?

  • 基于AQS(AbstractQueueSynchronizer抽象队列同步器)
  • AQS原理
    在AQS中,通过int类型的全局变量state来表示同步状态,即用state来表示锁
    (AQS使用一个FIFO的队列(也叫CLH队列,是CLH锁的一种变形),表示排队等待锁的线程)
    AQS内部维护一个state状态位,尝试加锁的时候通过CAS(CompareAndSwap)修改值,如果成功设置为1,并且把当前线程ID赋值,则代表加锁成功,一旦获取到锁,其他的线程将会被阻塞进入阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state重新置为0,同时当前线程ID置为空。

CAS原理

  • 原理:CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源时不会出现冲突,由于不会出现冲突自然不会阻塞其他线程。因此线程就不会出现阻塞停顿的状态。出现冲突时,无锁操作使用CAS(比较交换)来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。
  • CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:
  • 变量内存地址,V表示
  • 旧的预期值,A表示
  • 准备设置的新值,B表示

当执行CAS指令时,只有当V的值等于A时,才会用B去更新V的值,否则就不会执行更新操作。
CAS缺点
会导致ABA问题

  • ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。
  • Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
AQS实现共享锁和独占锁
  • 独占锁:
    独占锁是只有头节点获取锁,其余节点的线程继续等待,等待锁被释放后,才会唤醒下一个节点的线程;
    独占锁的同步状态state值在0和1之间切换,保证同一时间只能有一个线程是处于活动的,其他线程都被阻塞,参考ReentranLock。独占锁是一种悲观锁
  • 共享锁:
    共享锁是只要头节点获取锁成功,就在唤醒自身节点对应的线程的同时,继续唤醒AQS队列中的下一个节点的线程,每个节点在唤醒自身的同时还会唤醒下一个节点对应的线程,以实现共享状态的“向后传播”,从而实现共享功能。
    共享锁的同步状态state值在整数区间内(自定义实现),如果state值<0则阻塞,否则不阻塞。参考ReadWriteLock、Semphore、CountDownLautch等。共享锁是一种乐观锁,允许多个线程同时访问共享资源。
2.2.5 sychornized 的优化/升级机制

sychornized存在的问题:

  • Synchronized是基于底层操作系统的 Mutex Lock 实现的,每次获取锁和释放锁的操作都会带来用户态和内核态的切换,从而增加系统性能开销。
    因此,在锁竞争激烈的情况下,Synchronized同步锁在性能上就表现得非常糟糕,它也常被大家称为重量级锁
    到了 JDK1.5 版本,并发包中新增了 Lock 接口来实现锁功能,它提供了与 Synchronized 关键字类似的同步功能,只是在使用时需要显示获取锁和释放锁。
    单个线程重复申请锁的情况下,JDK1.5 版本的 Lock 性能要比 Synchronized 锁的性能好很多,也就是当时的 Synchronized 并不具备可重入锁的功能
  1. 优化机制 ×

优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁

锁粗化

  • 锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁。

锁消除

  • 即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程, 那么可以认为这段代码是线程安全的,不必要加锁。
  1. 升级机制

锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁。升级的过程就是从低到高,降级在一定条件也是有可能发生的。

  • 偏向锁就是通过对象头的偏向线程ID来对比,甚至都不需要CAS了,
  • 轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现,
  • 重量级锁则是除了拥有锁的线程其他全部阻塞

三种锁特点
Java虚拟机中synchronized关键字的实现,按照代价由高到低可以分为重量级锁、轻量锁和偏向锁三种。

  • 偏向锁
    只会在第一次请求锁时采用CAS操作并将锁对象的标记字段记录为当前线程地址。在此后的运行过程中,持有偏向锁的线程无需加锁操作。 针对的是锁仅会被同一线程持有的状况
  • 轻量级锁
    采用CAS操作,将锁对象标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。针对的是多个线程在不同时间段申请同一把锁的情况
  • 重量级锁 会阻塞、唤醒请求加锁的线程。 针对的是多个线程同时竞争同一把锁的情况
    JVM采用自适应自旋,来避免在面对非常小的同步代码块时,仍会被阻塞和唤醒的状况。

偏向锁

  • 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
  • 偏向锁是四种状态中最乐观的一种锁:从始至终只有一个线程请求某一把锁。
  • 这就好比你在私家庄园里装了个红绿灯,并且庄园里只有你在开车。偏向锁的做法便是在红绿灯处识别来车的车牌号。如果匹配到你的车牌号,那么直接亮绿灯。
  • 偏向锁的获取
  1. 当一个线程访问同步块并成功获取到锁时,会在对象头和栈帧中的锁记录字段里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,直接进入。
  2. 当线程访问同步块失败时,使用CAS竞争锁,并将偏向锁升级为轻量级锁。
  • 偏性锁的撤销(开销较大):
    偏向锁使用了一种等待竞争出现才释放锁的机制,所以当其他线程竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,并将锁膨胀为轻量级锁(持有偏向锁的线程依然存活的时候)。如果持有线程已经终止,则将锁对象的对象头设置为无锁状态。

轻量级锁
多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。针对这种情况,JVM采用了轻量级
锁,来避免线程的阻塞以及唤醒。

  • 加锁 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

  • 解锁 轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。 如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁

  • 因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

重量级锁

  • 重量级锁是JVM中为基础的锁实现。在这种状态下,JVM虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的 时候,唤醒这些线程。
  • 为了尽量避免昂贵的线程阻塞、唤醒操作,JVM会在线程进入阻塞状态之前,以及被唤醒之后竞争不到锁的情况 下,进入自旋状态,在处理器上空跑并且轮询锁是否被释放。如果此时锁恰好被释放了,那么当前线程便无须进入阻塞状态,而是直接获得这把锁。
2.2.6 读写锁

为什么需要读写锁?

  • 与传统锁不同的是读写锁的规则是可以共享读,但只能一个写,总结起来为:读读不互斥,读写互斥,写写互斥,
    而一般的独占锁是:读读互斥,读写互斥,写写互斥,而场景中往往读远远大于写,读写锁就是为了这种优化而创建出来的一种机制。
  • 读锁相当于一个共享锁,写锁相当于独占锁。

ReadWriteLock的实现原理

  • 在Java中ReadWriteLock的主要实现为ReentrantReadWriteLock,其提供了以下特性:
  1. 公平性选择:支持公平与非公平(默认)的锁获取方式,吞吐量非公平优先于公平。
  2. 可重入:读线程获取读锁之后可以再次获取读锁,写线程获取写锁之后可以再次获取写锁
  3. 可降级:写线程获取写锁之后,其还可以再次获取读锁,然后释放掉写锁,那么此时该线程是读锁状态,也就是降级操作。
  • ReentrantReadWriteLock也是通过AQS来实现锁的,但是ReentrantReadWriteLock有两把锁:读锁和写锁,它们保护的都是同一个资源,那么如何用一个共享变量来区分锁是写锁还是读锁呢?答案就是按位拆分
    由于state是int类型的变量,在内存中占用4个字节,也就是32位。将其拆分为两部分:高16位和低16位,其中高16位用来表示读锁状态,低16位用来表示写锁状态。当设置读锁成功时,就将高16位加1,释放读锁时,将高16位减1;当设置写锁成功时,就将低16位加1,释放写锁时,将第16位减1。

2.5 并发容器

2.3.1 阻塞队列blockingQueue

相关问题

  1. 如何设计一个生产者消费者队列? blockingQueue
  2. 如果加锁,生产操作会阻塞消费操作,怎么解决? linkedBlockingQueue
  1. blockingQueue

BlockingQueue
指的是一个阻塞队列。其主要用于生产者-消费者模式,也就是在多线程场景时生产者线程在队列头部添加元素,而消费者线程则在队列尾部消费元素,通过这种方式能够达到将任务的生产和消费进行隔离的目的。

  • BlockingQueue最典型的两个实现是ArrayBlockingQueue和LinkedBlockingQueue。
  • 添加和移除主要是通过add,offer,put和take,poll方法来进行的,而这些方法的效率是非常高的,因为其只需要在队列两端进行时间复杂度O(1)的操作,即使有多线程的竞争,但由于锁定时间非常短,因而通过多线程的偏向锁等特性,这种消耗是微乎其微的。
  • 但是这里可以看到,BlockingQueue还提供了其他的操作,主要包含计算剩余余量,移除指定对象,判断是否包含指定对象和将集合中元素移动到集合中。这些操作则是不建议经常使用的,因为在进行这些操作时,无论是ArrayBlockingQueue还是LinkedBlockingQueue,其都需要将整个队列锁定,然后对整个队列进行遍历,从而实现操作的目的,这将大大地减少队列的吞吐量。

尾部添加和头部删除的API

尝试往队列尾部添加元素,添加成功则返回true,添加失败则抛出IllegalStateException异常
   boolean add(E e);

   // 尝试往队列尾部添加元素,添加成功则返回true,添加失败则返回false
   boolean offer(E e);

   // 尝试往队列尾部添加元素,如果队列满了,则阻塞当前线程,直到其能够添加成功为止
   void put(E e) throws InterruptedException;

   // 尝试往队列尾部添加元素,如果队列满了,则最多等待指定时间,
   // 如果在等待过程中还是未添加成功,则返回false,如果在等待
   // 如果在等待过程中被中断,则抛出InterruptedException异常
   boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;

   // 尝试从队列头部取出元素,如果队列为空,则一直等待队列中有元素
   E take() throws InterruptedException;

   // 尝试从队列头部拉取元素,如果队列为空,则最多等待指定时间,
   // 如果等待过程中拉取到了新元素,则返回该元素,
   // 如果等待过程被中断,则抛出InterruptedException异常
   E poll(long timeout, TimeUnit unit) throws InterruptedException; ```
  1. ArrayBlockingQueue

ArrayBlockingQueue
通过一个循环数组的方式来实现存储元素的,这里takeIndex记录了当前可以取元素的索引位置,而putIndex则记录了下一个元素可以存储的位置。当队列满了时,takeIndex和putIndex将指向同一个元素,这里则可以通过count字段来判断当前是处于满状态还是空置状态。通过声明一个全局的锁来控制所有操作的控制权限,也就是说,对于ArrayBlockingQueue而言,其任何一个操作都是阻塞其他操作的。这里notEmpty和notFull则是由lock创建得来的,通过这两个分离的等待条件,可以实现队列两端线程添加和移除操作的分离

  • 这里enqueue()和dequeue()方法是入队和出队的核心方法。在enqueue()方法中,当成功入队之后,其会唤醒一个正在等待取出元素的线程;在dequeue()方法中,当成功出队之后,其会唤醒一个正在等待添加元素的线程

put()和take()操作中,首先都是通过while条件进行前置判断,对于put操作,如果队列满了,则在notFull中进行等待,对于take()操作,如果队列为空,则在notEmpty中进行等待,并且释放锁。在队列中有空闲空间或有元素时,才会继续执行put()或take()操作。

  1. LinkedBlockingQueue

LinkedBlockingQueue
其底层是通过一个单向链表实现的,由于单项链表需要有一个指向下一个节点的指针,因而其必须使用一个对象(这里是Node)来存储当前元素的值和下一个节点的索引。
为了实现阻塞的特性,LinkedBlockingQueue分别为队列头部和尾部声明了两个锁,并且创建了两个等待Condition。当往队列添加元素时,使用putLock锁定队列尾部,如果队列满了,则将该线程添加到notFull的Condition中,并且释放锁;当从队列中取元素时,使用takeLock锁定队列头部,如果队列为空,则将该线程添加到notEmpty的Condition中,并且释放锁。

  • notFull和notEmpty是两个condition对象。notEmpty用于控制队列中没有元素时阻塞尝试获取元素的线程。notFull用于控制队列满了时阻塞尝试往队列中添加元素的线程
  • 对于链表的入队和出队操作,其是非常简单的,这里仅仅只是单纯的入队和出队,并没有相关的锁的操作
  • put()和take()基本都是先判断前置条件是否成立,即队列未满或不为空,如果成立,当前线程才有权限进行入队和出队操作。在操作完成之后,当前线程还会唤醒正在等待尝试获取或取出元素的线程。

ArrayBlockingQueue和LinkedBlockingQueue的区别

  1. 两者底层数据结构不同,ArrayBlockingQueue是通过循环数组来实现的,而LinkedBlockingQueue是通过单向链表来实现的;
  2. 两者阻塞方式不同,ArrayBlockingQueue使用了一个全局锁来处理所有的操作,也就是说无论是队列头部还是尾部,只要一个线程获取到了锁,那么其他所有的线程将都会被阻塞,只不过由于锁定的时间非常短,因而这种消耗可以忽略不计;LinkedBlockingQueue为队列头部和尾部分别使用了两个不同的锁,在元素入队和出队操作时,两者几乎是互不干扰的;
  3. 两者初始化大小不同,ArrayBlockingQueue必须指定一个初始化大小,而LinkedBlockingQueue可以指定初始大小,也可以不指定,不指定时默认为Integer.MAX_VALUE。
2.3.2 非阻塞队列ConcurrentLinkedQueue

isEmpty()与size()==0的区别(时间复杂度)

  • 对于ArrayList、LinkedList,size()与isEmpty()的时间复杂度都是O(1)
  • 但是当Collection的实现类为ConcurrentLinkedQueue(或者NavigableMap、NavigableSet),我们可以看到,size()是将所有元素重新统计了一遍的,故时间复杂度为
    O(n)

3.JVM

双亲委派机制

双亲委派机制

3.1 JVM结构

JVM主要包括:程序计数器(Program Counter),Java堆(Heap),Java虚拟机栈(Stack),本地方法栈(Native Stack),方法区(Method Area)

3.2 GC

在这里插入图片描述

Java 中的引用类型

  • 强引用:发生 gc 的时候不会被回收。
  • 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
  • 弱引用:有用但不是必须的对象,在下一次GC时会被回收。
  • 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。

如何判断对象是否可以被回收?什么时候被回收?

  • 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。但是他有一个缺点是不能解决循环引用的问题。

  • 可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

可以作为GCRoots的对象包括下面几种:
(1) 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2) 方法区中的类静态属性引用的对象。
(3) 方法区中常量引用的对象。
(4) 本地方法栈中JNI(Native方法)引用的对象。

垃圾回收算法

JVM垃圾回收算法

  1. 标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
  2. 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
  3. 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
  4. 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

Java 堆内存被划分为新生代和年老代两部分

  • 新生代主要使用复制和标记-清除垃圾回收算法
  • 年老代主要使用标记-整理垃圾回收算法,因此 java 虚拟中针对新生代和年老代分别提供了多种不同的垃圾收集器
垃圾回收器

垃圾回收器

  • 回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,
    回收老年代的收集器包括Serial Old、Parallel Old、CMS
    回收整个Java堆的G1收集器
  1. Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  2. ParNew收集器 (复制算法):
    新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  3. Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
  4. Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
  5. Parallel Old收集器(标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
  6. CMS(Concurrent Mark Sweep)收集器(标记-清除算法):
    老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
  7. G1(Garbage First)收集器 (标记-整理算法):
    Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。
    此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
  • CMS 是英文 Concurrent Mark-Sweep的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM的参数加上 “-XX:+UseConcMarkSweepGC” 来指定使用 CMS 垃圾回收器。

  • CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。

CMS(Concurrent Mark Sweep)收集器,以获取最短回收停顿时间【也就是指Stop The World的停顿时间】为目标,多数应用于互联网站或者B/S系统的服务器端上。其中“Concurrent”并发是指垃圾收集的线程和用户执行的线程是可以同时执行的。
CMS是基于“标记-清除”算法实现的,整个过程分为4个步骤:

  1. 初始标记(CMS initial mark)。
  2. 并发标记(CMS concurrent mark)。
  3. 重新标记(CMS remark)。
  4. 并发清除(CMS concurrent sweep)。

注意
“标记”是指将存活的对象和要回收的对象都给标记出来,而“清除”是指清除掉将要回收的对象。

  • 初始标记重新标记这两个步骤仍然需要“Stop The World”。
  • 初始标记只是标记一下GCRoots能直接关联到的对象,速度很快。
  • 并发标记阶段【也就说明不会阻碍业务线程继续执行,因为它所以还会有下面要说的“重新标记”阶段了】就是进行GC Roots Tracing【啥意思?其实就是从GC Roots开始找到它能引用的所有其它对象】的过程。
  • 重新标记阶段则是为了修正并发标记期间因用户程序继续动作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
分代回收

为什么要进行分代回收?
因为:不同的对象,生命周期是不一样的。因此不同生命周期的对象采用不同的收集方式。可以提高垃圾回收的效率。

  • 在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分而治之的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。

如何分代?
在这里插入图片描述
虚拟机中的共划分为三个代:年轻代(Young Generation)、年老代(Old Generation)和持久代 (Permanent Generation)
其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

  • 年轻代
    所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
    年轻代分三个区:一个Eden区,两个Survivor区(一般而言)。
    大部分对象在Eden区中生成。
    当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。

  • 老年代 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的 都是一些生命周期较长的对象。

  • 持久代 用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或
    者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

  • Minor GC,也叫Young GC: 从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,也叫Young GC。因为Java对象大多具备朝生夕死的特征,所以MinorGC非常频繁,一般回收速度也比较快。一般采用复制算法。Minor GC触发条件:Eden区域满了
  • MajorGC:是清理老年代,Major GC发生过程常常伴随一次MinorGC
  • FullGCMajor GC+Minor GC共同进行的一整个过程,是清理整个堆空间(包括年轻代和老年代,这里不包含永久代,因为永久代在JDK7之前包含方法区,是一块与堆分离的区域;JDK7将静态变量从永久代移到堆中;JDK8则完全取消永久代,方法区存在元空间MetaSpace中,虽然与堆共享一块内存,逻辑上可以认为在堆中,但仍然与堆不相连)。Full GC的速度一般会比 Minor GC慢10倍以上。一般用的是标记整理和标记清除算法

Full GC触发条件:

  1. Minor GC时介绍中Survivor空间不足时,判断是否允许担保失败,如果不允许则进行Full GC。如果允许,并且每次晋升到老年代的对象平均大小>老年代最大可用连续内存空间,也会进行Full GC。
  2. MinorGC后存活的对象超过了老年代剩余空间
  3. 方法区内存不足时
  4. System.gc(),可用通过-XX:+DisableExplicitGC来禁止调用System.gc CMS
  5. GC异常,CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,会触发Full GC

分代垃圾回收的工作机制?

  • 举个栗子:
    Java对象的一生:我是一个java对象,我出生在Eden区,在Eden区有一些跟我一样的兄弟们,我们在Eden区中一起玩,每天都有新的兄弟进来。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,在这里生活非常不稳定。有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我15岁的时候(默认15岁),就被分配到年老代那边,在这里人很多,并且年龄都挺大的。在年老代里,我生活了很久,每次GC年龄就+1,然后被回收。
  • 分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
    新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1
  • 它的执行流程如下
  1. 在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。
  2. 进行GC,Eden区中所有存活的对象都会被复制到“To Survivor区”,仍存活的对象会根据他们的年龄值来决定去向。
  3. 年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中。清空 Eden 和 From Survivor 分区;
  4. 这时From Survivor 和 To Survivor 分区会互换角色,分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
  5. 每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
  6. 老生代当空间占用到达某个值之后就会触发全局垃圾收回 full GC,一般使用标记整理的执行算法。
  • 对象优先在 Eden 区分配
    多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次
    GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。
    Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;
    Major GC/Full GC 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC的速度通常会比 Minor GC 慢 10 倍以上。

  • 大对象直接进入老年代
    新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor
    区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。
    所谓大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来
    “安置” 它们。 虚拟机提供了一个XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(新生代采用的是复制算法)。

  • 长期存活对象将进入老年代
    虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在
    Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor
    区中每过一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。

GC调优

Stop the world的含义:不管选择哪种GC算法,stop-the-world都是不可避免的。

  • Stop-the-world意味着从应用中停下来并进入到GC执行过程中去。一旦Stop-the-world发生,除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。
  • GC调优通常就是为了改善stop-the-world的时间。

GC调优
JVM调优的本质:并不是为了显著的提升系统的性能,不是说调优过后,性能就能提升几倍或者十几倍,主要调的是稳定性。如果系统出现了频繁的垃圾回收,这个系统是不稳定的,所以就需要我们来进行jvm调优,调整垃圾回收的频次
一、GC调优原则

  1. 调优的原则 大多数的 java 应用不需要 GC 调优 大部分需要 GC 调优的的,不是参数问题,是代码问题 在实际使用中,分析 GC 情况优化代码比优化 GC 参数要多得多; GC 调优是最后的手段
  2. 调优的目的 GC 的时间够小 GC 的次数够少 发生 Full GC的周期足够的长,时间合理,最好是不发生
  3. 判断是否需要调优的指标 Minor GC 执行时间不到 50ms; Minor GC 执行不频繁,约 10 秒一次; Full GC
    执行时间不到 1s; Full GC 执行频率不算频繁,不低于 10 分钟 1 次;

二、GC调优步骤

  1. 监控gc状态 使用各种 JVM 工具,查看当前日志,分析当前 JVM 参数设置,并且分析当前堆内存快照和 gc 日志,根据实际的各区域内存划分和 GC 执行时间,觉得是否进行优化

  2. 分析结果,判断是否需要优化 如果各项参数设置合理,系统没有超时日志出现,GC 频率不高,GC 耗时不高,那么没有必要进行 GC优化;如果 GC 时间超过 1-3 秒,或者频繁 GC,则 必须优化;

  3. 调整GC类型和内存分配 如果内存分配过大或过小,或者采用的 GC 收集器比较慢,则应该优先调整这些参数,并且先找 1 台或几台机器进行 beta,然后比较优化过的机器和没有 优化的机器的性能对比,并有针对性的做出最后选择;

  4. 不断的分析和调整 通过不断的试验和试错,分析并找到最合适的参数

  5. 全面应用参数 如果找到了最合适的参数,则将这些参数应用到所有服务器,并进行后续跟踪。

三、调优工具
GC调优工具
在这里插入图片描述

  • jps:列出当前机器上正在运行的虚拟机进程,JPS 从操作系统的临时目录上去找
  • jstat:用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据
  • jinfo:查看和修改虚拟机的参数
  • jmap:用于生成堆转储快照
内存泄露

参考

概念:不再会被使用的对象的内存不能被回收,就是内存泄露
类型:

  1. static字段引起的内存泄露, 静态字段通常拥有与整个应用程序相匹配的生命周期。
  2. 常量字符串造成的内存泄露
  3. 未关闭的资源导致内存泄露
  4. equals()和hashCode()重写不合理

4. 反射

反射的作用:
Java的反射机制是在编译并不确定是哪个类被加载了,而是在程序运行的时候才加载、探知、自审。使用在编译期并不知道的类。这样的特点就是反射。

  • 假如有两个程序员,一个程序员在写程序的时候,需要使用第二个程序员所写的类,但第二个程序员并没完成他所写的类。那么第一个程序员的代码能否通过编译呢?这是不能通过编译的。利用Java反射的机制,就可以让第一个程序员在没有得到第二个程序员所写的类的时候,来完成自身代码的编译。
  • 要正确使用Java反射机制就得使用java.lang.Class这个类。它是Java反射机制的起源。当一个类被加载以后,Java虚拟机就会自动产生一个Class对象。通过这个Class对象我们就能获得加载到虚拟机当中这个Class对象对应的方法、成员以及构造方法的声明和定义等信息。

三、操作系统

1. 概述

操作系统的特征

并发:两个或多个时间在同一时间间隔内发生
引入进程的目的是使程序能并发执行。
并行是同一时刻

共享:系统中的资源可供内存中多个并发执行的进程共同使用。

  • 两种资源共享方式:
  1. 互斥共享方式:一段时间内置允许一个进程访问该资源。
    一段时间内只允许一个进程访问的资源成为临界资源或独占资源
  2. 同时访问方式:进程交替地对该资源进行访问,即“分时共享”。(例如磁盘)

并发和共享时操作系统两个最基本的特征,互为存在条件

虚拟:把一个物理实体变为若干个逻辑上的对应物。
异步: 在多道程序环境下,允许多个程序并发执行,但由于资源有限,进程的执行不是一贯到底,而是走走停停,以不可预知的速度向前推进。

操作系统的目标和功能

操作系统作为计算机系统资源的管理者

  1. 处理机管理
    处理机的分配和运行都以进程(或线程)为基本单位,因而对处理机的管理可归结为对进程的管理。进程管理的主要功能:进程控制、进程同步、进程通信、死锁处理、处理机调度等。
  2. 存储器管理:内存分配、地址映射、内存保护、共享和内存扩充。
  3. 文件管理
  4. 设备管理:完成用户的IO请求

操作系统作为用户与计算机硬件系统之间的接口
命令接口,程序接口

操作系统用作扩充器

内核态和用户态

操作系统的运行机制

  • 通常CPU执行两种不同性质的程序:一种是操作系统内核程序(内核态);另一种是用户自编程序或系统外层的应用程序(用户态
    核心态指令实际包括系统调用类指令和一些针对时钟、中断和原语的操作指令。
  • 为什么要分内核态和用户态:
    CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 – 用户态和内核态。
  • 中断和异常
    通过中断或异常实现用户态与核心态的切换

2. 进程管理

进程与线程

进程与线程对比
引入进程目的:更好地使多道程序并发执行,以提高资源利用率和系统吞吐量,增加并发程度。
引入线程目的:减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能。
区别:

  1. 线程是独立调度的基本单位,进程是拥有资源的基本单位。
  2. 线程之间的同步与通信非常容易,进程切换开销大。

PCB(进程控制块)是进程存在的唯一标志。

进程状态转换

  1. 运行状态
  2. 就绪状态:进程获得了除处理机之外的一切所需资源
  3. 阻塞状态:进程在等待某一时间而暂停运行,如等待某资源为可用或等待输入/输出完成。即使处理机调度空闲,该进程也不能运行。
  4. 创建状态
  5. 结束状态
    在这里插入图片描述

线程状态(6种)
线程的6种状态

  • New 表示线程被创建但尚未启动的状态:当我们用 new Thread() 新建一个线程时,如果线程没有开始运行 start() 方法,那么线程也就没有开始执行 run() 方法里面的代码,那么此时它的状态就是 New。而一旦线程调用了 start(),它的状态就会从 New 变成 Runnable,进入到图中绿色的方框
  • Runnable 可运行状态
    Runable 状态对应操作系统线程状态中的两种状态,分别是 Running 和 Ready,也就是说,Java 中处于 Runnable 状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源。
    所以,如果一个正在运行的线程是 Runnable 状态,当它运行到任务的一半时,执行该线程的 CPU 被调度去做其他事情,导致该线程暂时不运行,它的状态依然不变,还是 Runnable,因为它有可能随时被调度回来继续执行任务。
  • 阻塞状态
  1. Blocked 被阻塞状态
    从 Runnable 状态进入到 Blocked 状态:当进入到 synchronized 代码块中时未能获得相应的 monitor 锁
    从 Blocked 状态到 Runnable:当线程获得 monitor 锁,此时线程就会进入 Runnable 状体中参与 CPU 资源的抢夺
  2. Waiting 等待状态
    当线程中调用了没有设置 Timeout 参数的 Object.wait() 方法
    当线程调用了没有设置 Timeout 参数的 Thread.join() 方法
    当线程调用了 LockSupport.park() 方法

关于 LockSupport.park() 方法,这里说一下,我们通过上面知道 Blocked 是针对 synchronized monitor 锁的,但是在 Java 中实际是有很多其他锁的,比如 ReentrantLock 等,在这些锁中,如果线程没有获取到锁则会直接进入 Waiting 状态,其实这种本质上它就是执行了 LockSupport.park() 方法进入了Waiting 状态

  • BlockedWaiting 的区别
    Blocked 是在等待其他线程释放 monitor 锁
    Waiting 则是在等待某个条件,比如 join 的线程执行完毕,或者是 notify()/notifyAll() 。
  1. Timed Waiting 计时等待状态
    与 Waiting 状态非常相似,其中的区别只在于是否有时间的限制,在 Timed Waiting 状态时会等待超时,之后由系统唤醒,或者也可以提前被通知唤醒如 notify

以下情况会让线程进入 Timed Waiting 状态。
线程执行了设置了时间参数的 Thread.sleep(long millis) 方法;
线程执行了设置了时间参数的Object.wait(long timeout) 方法;
线程执行了设置了时间参数的 Thread.join(long millis) 方法;
线程执行了设置了时间参数的 LockSupport.parkNanos(long nanos) 方法和
LockSupport.parkUntil(long deadline) 方法。

  • Terminated 终止
    run() 方法执行完毕,线程正常退出。
    出现一个没有捕获的异常,终止了 run() 方法,最终导致意外终止。

线程状态间转换

  • Blocked 进入 Runnable:线程获得 monitor 锁
  • Waiting 进入 Runnable:只有当执行了 LockSupport.unpark(),或者 join 的线程运行结束,或者被中断时才可以进入 Runnable 状态
  • 如果通过其他线程调用 notify() 或 notifyAll()来唤醒它,则它会直接进入 Blocked 状态,这里大家可能会有疑问,不是应该直接进入 Runnable 吗?这里需要注意一点 ,因为唤醒 Waiting 线程的线程如果调用 notify() 或 notifyAll(),要求必须首先持有该 monitor 锁,这也就是我们说的wait()、notify 必须在 synchronized 代码块中。
  • 所以处于 Waiting 状态的线程被唤醒时拿不到该锁,就会进入 Blocked 状态,直到执行了 notify()/notifyAll() 的唤醒它的线程执行完毕并释放 monitor 锁,才可能轮到它去抢夺这把锁,如果它能抢到,就会从 Blocked 状态回到 Runnable 状态
  • 这里大家一定要注意这点,当我们通过 notify 唤醒时,是先进入阻塞状态的 ,再等抢夺到 monitor 锁后才会进入 Runnable 状态!
  • Timed Waiting 进入 Runnable
  1. 同样在 Timed Waiting 中执行 notify() 和 notifyAll() 也是一样的道理,它们会先进入 Blocked 状态,然后抢夺锁成功后,再回到 Runnable 状态。
  2. 但是对于 Timed Waiting 而言,它存在超时机制,也就是说如果超时时间到了那么就会系统自动直接拿到锁,或者当 join 的线程执行结束/调用了LockSupport.unpark()/被中断等情况都会直接进入 Runnable 状态,而不会经历 Blocked 状态

进程间的通信方式

  • 进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。
  • 每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信
  • IPC的方式通常有管道(包括无名管道和有名管道)、消息队列、信号量共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
  1. 匿名管道通信:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。【半双工是指在通信过程的任意时刻,信息既可由A传到B,又能由B传A,但只能有一个方向上的传输存在。】
  2. 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  3. 消息队列( message queue ) : 消息队列的本质其实是一个内核提供的链表,内核基于这个链表,实现了一个数据结构。存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  4. 信号量通信: 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  5. 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

处理机调度

  1. 先来先服务(FCFS)
  2. 短作业优先(SJF)
  3. 优先级调度算法
  4. 高响应比优先:响应比=(等待时间+要求服务时间)/要求服务时间
  5. 时间片轮转
  6. 多级反馈队列调度:时间片轮转+优先级调度

进程同步,管程

进程之间的制约关系:同步、互斥
临界资源:一次仅允许一个进程使用的资源
临界区互斥原则:空闲让进、忙则等待、有限等待、让权等待

生产者与消费者

死锁

定义:多个进程因竞争资源而造成的一种僵局,若无外力,这些进程无法向前推进
死锁产生原因:

  1. 系统资源的竞争
  2. 进程推进顺序非法
  3. 死锁产生的必要条件:
  • 互斥条件
  • 不可剥夺
  • 请求和保持
  • 循环等待

3. 内存管理(分页分段)

管理方式
连续分配

  • 单一连续分配
  • 固定分区分配
  • 动态分区分配:
    首次:按地址从小到大为序,分配第一个符合条件的分区
    最佳:按空间大小为序,…
    最坏: 空间从大到小…
    临近适应:与首次适应相似,从上次查完的结束为止开始查找。

非连续分配

  • 页式存储:内存分为固定的块,按物理结构划分,会有内部碎片
  • 段式存储:内存块的大小不固定,按逻辑结构划分,会有外部碎片。
  • 段页式存储:分页分段结合,有内部碎片。

内存碎片如何解决?

内存碎片分为内部碎片和外部碎片。

  1. 内存中划分为若干个固定的块(这些块大小可能相等也可能不相等),当一个程序或一个程序分解后的部分程序装进这些块后,在块里面不能完全占用的内存空间成为内部碎片
    解决方法:①采用可变分区分配②采用分段存储管理方式(一般采用这种方式)
  2. 采用可变分区分配或分段存储管理方式后,虽然分配的每一个块的大小和程序实际需要的空间一样大,但划分以后,内存中仍然有部分空间是剩余的,这些剩余的空间成为外部碎片
    解决方法:①采用单一连续分配②采用固定分区分配③采用分页存储管理方式④采用段页式存储管理方式(第③和第④是常用方法)

分页、分段、段页式

分页存储管理

  • 基本分页存储管理方式中,系统将一个进程的逻辑地址空间分成若干个大小相等的篇,称为页面或页
  • 相应地,将内存空间分成若干个与页面同样大小的块,称为物理块或页框
  • 在进程运行时,为了能在内存中找到每个页面对应的物理块,系统为每个进程建立了一张页面映射表,简称页表
  • 被浪费的空间称为页内碎片
  • CPU生成的每个地址分为两部分:页码+页偏移。
  • 页码作为页表的索引,页表包含每页所在物理内存的基地址。这个基地址与页偏移的组合就形成了物理内存地址,可发送到物理单元。

分段式存储

  • 分页系统虽然能较好地解决动态分区的碎片问题,却难以满足用户的某些需求(将自己的作业按逻辑关系分成若干段,然后通过段名和段内地址来访问相应的程序或数据,同时系统能以段为单位对程序和数据进行共享和保护,并要求能动态增长),因此引入了分段式存储管理方式
    在这里插入图片描述
    在这里插入图片描述

段页式存储
在段页式存储管理中,基本思想是:

  • 内存划分:按照分页式存储管理方案
  • 作业的管理:按照分段式存储管理进行分配

这种新的系统既具有分段系统的便于实现、分段可共享、易于保护、可动态链接等一系列优点,又能像分页系统那样,很好地解决内存的外部碎片问题

在这个地址变换中,我们经历了三次的寻址

  • 第一次访问是访问内存中的段表
  • 第二次访问是访问内存中的页表
  • 第三次访问是访问真正的地址

主要过程:

  • 首先根据逻辑地址得到段号、页号、页内偏移量
  • 然后判断段号是否越界
  • 不越界,则查询段表,找到相对应的段表项
  • 检查是否越界
  • 不越界,则根据页表存放块号、页号查询页表,找到对应的页表项
  • 计算出实际地址,访问实际地址

虚拟内存,页面置换算法

虚拟内存(VM)
为什么用虚拟内存?

  • 多道程序并发执行,共享主存,需要很多内存

局部性原理:

  1. 时间局部性:被访问过一次的存储器位置很可能在不远的将来会被再次访问原因:程序中存在大量循环操作。
  2. 空间局部性:如果一个存储器位置被访问了一次,那么程序很可能在不远的将来访问附近的一个存储器位置。
    原因:因为指令通常是顺序存放、顺序执行的,数据也一般以向量、数组、表等形式聚集存储。

页面置换算法

  1. 最佳置换:以后不用的
  2. 先进先出
  3. 最近最久未使用(LRU)
  4. 时钟算法

为什么要页面置换:

  • 在请求分页存储管理系统中,由于使用了虚拟存储管理技术,使得所有的进程页面不是一次性的全部调入内存,而是部分页面装入
    这就有可能出现以下的情况:要访问的页面不在内存,这时系统产生缺页中断,操作系统在处理缺页中断的时候,要把所需页面从外存调入到内存中。如果这时内存中有空闲块,就可以直接调入该页面,如果这时内存中没有空闲块,那么我们就先淘汰一个已经在内存中的页面,腾出空间,再把所需的页面装入,即进行页面置换。

虚拟存储器

  • 定义:基于局部性原理,程序装入时,可将程序的一部分装入内存,将其余部分留在外存,需要时再调入。
  • 特征:
  1. 多次性:无需在作业运行时一次性全部装入内存,允许被分成多次调入内存
  2. 对换性:无需在作业运行时一直常驻内存,允许作业运行时,进行换入换出
  3. 虚拟性:从逻辑上扩充内存。
  • 实现方式
    1. 请求分页存储管理
    2.请求分段存储管理
    3. 请求段页式存储管理

抖动:刚刚换出的页面马上又要换如内存,刚换出的又要换入。频繁发生缺页中断(抖动)
原因是:某个进程频繁访问的页面数目高于可用的物理页帧数目。

逻辑地址、物理地址

内存管理单元(MMU)是介于处理器和片外存储器之间的中间层。提供对虚拟地址(VA)向物理地址(PA)的转换。

4. 文件管理(磁盘)

磁盘调度算法

  • 先来先服务
  • 最短寻道时间
  • 扫描算法:磁头移动方向上,选择与磁头所在磁道最近的。
  • 循环扫描:

磁盘读写

磁盘读写时,涉及到磁盘上数据查找,地址一般由柱面号、盘面号、块号三者构成

  • 移动臂先根据柱面号移动到指定柱面,然后根据盘面号确定盘面的磁道,最后根据块号将制定的磁道段移动到磁头下,便可开始读写。
  • 整个过程主要有三部分时间消耗,查找时间+等待时间+传输时间
    即:定位柱面的耗时 + 将磁头移到指定磁道段的耗时 + 将数据传到内存的耗时
    整个磁盘IO最耗时的地方在查找时间,所以减少查找时间能大幅提升性能。

顺序读写相邻的磁块比起随机读写可以有效减少磁头的移动次数,从而顺序读写的性能高于随机读写。

局部性原理

局部性通常有两种不同的形式:时间局部性和空间局部性。

  • 时间局部性 在一个具有良好的时间局部性的程序中,被访问过一次的存储器位置很可能在不远的将来会被再次访问。
  • 空间局部性
    在一个具有良好空间局部性的程序中,如果一个存储器位置被访问了一次,那么程序很可能在不远的将来访问附近的一个存储器位置。

应用:缓存

  1. 一级缓存基本上都是内置在cpu的内部和cpu一个速度进行运行,能有效的提升cpu的工作效率。一级缓存越多,cpu的工作效率就会越来越高,是cpu的内部结构限制了一级缓存的容量大小,使一级缓存的容量都是很小的。
  2. 二级缓存主要作用是协调一级缓存和内存之间的工作效率。cpu首先用的是一级内存,当cpu的速度慢慢提升之后,一级缓存就不够cpu的使用量了,这就需要用到二级内存
  3. 三级缓存和一级缓存与二级缓存的关系差不多,是为了在读取二级缓存不够用的时候而设计的一种缓存手段,在有三级缓存cpu之中,只有大约百分之五的数据需要在内存中调取使用,这能提升cpu不少的效率,从而cpu能够高速的工作。

四、计网

1. OSI七层模型

在这里插入图片描述

物理层:

  • 传输单位是比特,
  • 任务是透明的传输比特流,
  • 功能是在物理媒体上为数据段设备透明的传输原始比特流

数据链路层:

  • 传输单位是帧,
  • 任务是将网络层传下来的IP数据报组装成帧。
  • 功能:
  1. 成帧
  2. 差错控制:循环冗余法
  3. 可靠传输: 数据链路层通常使用确认和超时重传两种机制来保证可靠传输
  4. 流量控制: 限制发送方的数据流量,使其发送速率不致超过接收方的接收能力
  5. 传输管理
  • 协议:SDLC\HDLS\PPP\STP和帧中继

网络层

  • 传输单位:数据报 - 关心的是通信子网的运行控制
  • 任务:把网路层协议的数据单元(分组)从源端传到目的端,为分组交换网上的不同主机提供通信服务。
  • 关键问题是对分组进行路由选择,并实现流量控制、拥塞控制和网际互联等功能。
  • 协议:IP、ICMP、IGMP、ARP、RARP

传输层:

  • 传输单位:报文段(TCP)或用户数据报(UDP)
  • 任务:负责主机中两个进程之间的通信
  • 功能:为端到端连接提供可靠服务;为端到端连接提供流量控制、差错控制、服务质量、数据传输管理等

会话层
表示层
应用层

2. TCP/IP模型

网络接口层
网际层
传输层

  • TCP:面向连接的,数据传输的单位是报文段,能够提供可靠的交付
  • UDP: 无连接的,数据传输单位是用户数据报,不保证提供可靠的交付,只能提供“尽最大努力交付”

应用层

  • 虚拟终端协议(Telnet)
  • 文件传输协议(FTP)
  • 域名解析服务(DNS)
  • 电子邮件协议(SMTP)
  • 超文本传输协议(HTTP)

3. 常见问题

3.1 TCP

3.1.1 TCP、UDP区别

区别:

  • TCP:面向连接的,数据传输的单位是报文段,能够提供可靠的交付
  • UDP: 无连接的,数据传输单位是用户数据报,不保证提供可靠的交付,只能提供“尽最大努力交付”
3.1.2 TCP确保可靠传输的方式

校验和
序列号
确认应答
超时重传
连接管理
流量控制
拥塞控制

基本的传输可靠性来源于确认重传机制,TCP的滑动窗口也是建立在确认重传基础上的

TCP重传机制

参考

  1. 超时重传,等待timeout

  2. 快速重传,如果发送方连续收到3次相同的ack,就重传,它的好处就是不用等timeout了再重传
    在这里插入图片描述

  3. SACK:
    这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。

  4. D-SACK:Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。

3.1.5 TCP滑动窗口(流量控制)

TCP滑动窗口(解决流量控制问题)

  • 在TCP中,窗口的大小是在TCP三次握手后协定的,并且窗口的大小并不是固定的,而是会随着网络的情况进行调整。
  • 机制:发送窗口收到接收端对于本段窗口内字节的 ACK 确认才会移动发送窗口的左边界。
    接收窗口只有在前面所有的段都确认的情况下才会移动左边界,当前面还有字节未接收但收到后面字节的情况下(乱序)窗口是不会移动的,并不对后续字节确认, 确保这段数据重传。
    可以根据滑动窗口的调整进行流量控制。
3.1.6 TCP拥塞控制

TCP拥塞控制

  • TCP传输的过程中,发送端开始发送数据的时候,如果刚开始就发送大量的数据,那么就可能造成一些问题。网络可能在开始的时候就很拥堵,如果给网络中在扔出大量数据,那么这个拥堵就会加剧。拥堵的加剧就会产生大量的丢包,就对大量的超时重传,严重影响传输。

  • 所以TCP引入了慢启动的机制,在开始发送数据时,先发送少量的数据探路。探清当前的网络状态如何,再决定多大的速度进行传输。这时候就引入一个叫做拥塞窗口的概念。发送刚开始定义拥塞窗口为1,每次收到ACK应答,拥塞窗口加 1。在发送数据之前,首先将拥塞窗口与接收端反馈的窗口大小比对,取较小的值作为实际发送的窗口。

  • 拥塞窗口的增长是指数级别的。慢启动的机制只是说明在开始的时候发送的少,发送的慢,但是增长的速度是非常快的。为了控制拥塞窗口的增长,不能使拥塞窗口单纯的加倍,设置一个拥塞窗口的阈值,当拥塞窗口大小超过阈值时,不能再按照指数来增长,而是线性的增长。在慢启动开始的时候,慢启动的阈值等于窗口的最大值,一旦造成网络拥塞,发生超时重传时,慢启动的阈值会为原来的一半(这里的原来指的是发生网络拥塞时拥塞窗口的大小),同时拥塞窗口重置为1

3.1.3 TCP 数据报文的结构

TCP 数据报文的结构

  1. 序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。

  2. 确认序号:Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=Seq+1。

  3. 标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:
    (A)URG:紧急指针(urgent pointer)有效。
    (B)ACK:确认序号有效。
    ©PSH:接收方应该尽快将这个报文交给应用层。
    (D)RST:重置连接。
    (E)SYN:发起一个新连接。连接建立时用于同步序号。
    (F)FIN:释放一个连接。

需要注意的是:
(A)不要将确认序号Ack与标志位中的ACK搞混了。
(B)确认方Ack=发起方Req+1,两端配对。

3.1.4 TCP粘包、拆包问题

TCP粘包问题

  1. 什么时候考虑粘包:
  • 如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题
  • 如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包
  1. 粘包出现原因:在流传输中出现,UDP不会出现粘包,因为它有消息边界(参考Windows 网络编程)
    1 发送端需要等缓冲区满才发送出去,多个包打包成一个,造成粘包
    2 接收方不及时接收缓冲区的包,造成多个包接收
  2. 避免粘包的措施:(先回答 在包前添加包长度)
  • 是对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;
  • 是对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;
  • 是由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。

TCP拆包
如果发送的数据包超过一次tcp报文所能传输的最大值时,就会将一个数据包拆成多个最大tcp长度的tcp报文分开传输,这就叫做拆包。
解决粘包拆包问题:比较通用的做法就是每次发送一个应用数据包前在前面加上四个字节的包长度值,指明这个应用包的真实长度

3.1.7 三次握手,四次挥手(SYN攻击)

三次握手
在这里插入图片描述

  • 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
  • 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
  • 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

为什么不能用两次握手进行连接?可能会产生死锁

  • 假定C给S发送一个连接请求,S收到了,并发 送了确认应答。
  • 按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。
  • 可是,若S的应答分组在传输中被丢失的情况下,C将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。
  • 在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。
  • 而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁

四次挥手
在这里插入图片描述

  1. 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。
    TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
  2. 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
  3. 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
  4. 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
  5. 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命,2MSL就是一个发送和一个回复所需的最大时间)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
    为什么要等待2msl:确保客户端发送的最后一个包可以给到服务器。
  6. 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

SYN泛洪攻击

  • Syn攻击就是 攻击客户端 在短时间内伪造大量不存在的IP地址,向服务器不断地发送syn包,服务器回复确认包,并等待客户的确认,由于源地址是不存在的,服务器需要不断的重发直 至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪。
  • Syn攻击是一个典型的DDOS攻击。检测SYN攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击

4. 浏览器输入url,会发生什么

在这里插入图片描述

  1. URL解析,根据域名查询域名的IP地址
  2. DNS解析
    查询浏览器缓存(浏览器会缓存之前拿到的DNS 2-30分钟时间),如果没有找到,
    检查系统缓存,检查hosts文件,这个文件保存了一些以前访问过的网站的域名和IP的数据。它就像是一个本地的数据库。如果找到就可以直接获取目标主机的IP地址了。没有找到的话,需要
    检查路由器缓存,路由器有自己的DNS缓存,可能就包括了这在查询的内容;如果没有,要
    查询ISP DNS 缓存:ISP服务商DNS缓存(本地服务器缓存)那里可能有相关的内容,如果还不行的话,需要,
    递归查询:从根域名服务器到顶级域名服务器再到极限域名服务器依次搜索对应目标域名的IP。
    在这里插入图片描述
  3. TCP连接(三次握手)
  4. 发送HTTP请求
  5. 服务器处理请求并返回HTTP报文
  6. 浏览器渲染页面

5. HTTP

HTTP相关

HTTP简介

  1. Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网
    (WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。

  2. HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。

  3. HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展。目前在WWW中使用的是HTTP/1.0的第六版,HTTP/1.1的规范化工作正在进行之中,而且HTTP-NG(Next
    Generation of HTTP)的建议已经提出。

  4. HTTP协议工作于客户端-服务端架构为上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。Web服务器根据接收到的请求后,向客户端发送响应信息。

HTTP1.0 , 1.1,2.0区别

  • HTTP1.0 无状态无连接
  • HTTP 1.1 :支持长连接
    默认开启Connection:keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点
    管道化使得请求能够“并行”传输;服务器必须按照客户端请求的先后顺序依次回送相应的结果,以保证客户端能够区分出每次请求的响应内容。即虽然HTTP1.1支持管道化,但是服务器也必须进行逐个响应的送回!!
  • HTTP 2.0:二进制分帧、多路复用
    多路复用:每一个request 都可以共享同一个连接,不同的请求可以混杂发送,实现真正的并行传输,基于二进制分帧(帧会 标记顺序)

HTTP和HTTPS区别,

  • HTTP协议以明文方式发送内容
  • HTTPS在HTTP的基础上加入了SSL协议
    在这里插入图片描述
    公钥和私钥是非对称加密
    使用同一个密钥,是对称加密

HTTP状态码
状态码大全
304 Not Modified 请求资源与本地缓存相同,未修改
403 Forbidden 禁止访问
502 Bad Gateway 网关无响应在这里插入图片描述

Session和cookie

端到端通信和点到点通信

物理层、数据链路层和网络层为点到点
传输层 为端到端
端到端可靠 点到点不可靠

五、 Linux常用命令

Shell命令
ls 目录或文件:列出指定目录下内容
pwd:显示当前工作目录
cd:进去指定目录
ps: 查看系统内进程信息

  • ps aux: 查看系统所有的程序数据
  • ps -ef 查看磁盘使用情况

mkdir 创建目录
rm 删除文件
cp 拷贝
mv 移动或重命名文件
ln 创建硬链接或软连接(需加-s)
df 查看系统挂在的磁盘情况

系统操作命令
top 显示进程信息
du 显示每个文件和目录的磁盘使用空间~~~文件的大小
df:显示磁盘分区上可以使用的磁盘空间

stat 显示文件元数据
touch 创建空文件;修改文件元数据

文本操作命令
cat 查看文件
more 在最后一行输出目前显示内容的百分比
head 取出前几行
cut 显示切割的行数据
sort 排序:字典序和数值序
wc 统计文件单词数-w,字节数-c,行数-l
sed 行编辑器
awk 把文件逐行读入,以空格和制表符作为默认分隔符将每行切片,切开的部分再进行各种分析处理。
vim 文本编辑
chmod 修改权限
grep 正则表达式

看端口是否占用

(1) netstat -an|grep 8080 (2) lsof -i:8080 区别:
1.netstat无权限控制,lsof有权限控制,只能看到本用户
2.losf能看到pid和用户,可以找到哪个进程占用了这个端口

六、框架

框架

IOC、AOP

  • IOC(Inversion Of Controll,控制反转)是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由给Spring框架来管理
    IOC在其他语言中也有应用,并非Spring特有。IOC容器是Spring用来实现IOC的载体,IOC容器实际上就是一个Map(key, value),Map中存放的是各种对象。
    将对象之间的相互依赖关系交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。在实际项目中一个Service类可能由几百甚至上千个类作为它的底层,假如我们需要实例化这个Service,可能要每次都搞清楚这个Service所有底层类的构造函数,这可能会把人逼疯。如果利用IOC的话,你只需要配置好,然后在需要的地方引用就行了,大大增加了项目的可维护性且降低了开发难度。
    Spring时代我们一般通过XML文件来配置Bean,后来开发人员觉得用XML文件来配置不太好,于是Spring Boot注解配置就慢慢开始流行起来。
  • AOP(Aspect-Oriented Programming,面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性
    Spring AOP是基于动态代理的,如果要代理的对象实现了某个接口,那么Spring AOP就会使用JDK动态代理去创建代理对象;而对于没有实现接口的对象,就无法使用JDK动态代理,转而使用CGlib动态代理生成一个被代理对象的子类来作为代理。

Spring中的bean的作用域有哪些?

1.singleton:唯一bean实例,Spring中的bean默认都是单例的。

2.prototype:每次请求都会创建一个新的bean实例。

3.request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。

4.session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。

5.global-session:全局session作用域,仅仅在基于Portlet的Web应用中才有意义,Spring5中已经没有了。Portlet是能够生成语义代码(例如HTML)片段的小型Java
Web插件。它们基于Portlet容器,可以像Servlet一样处理HTTP请求。但是与Servlet不同,每个Portlet都有不同的会话。

Spring中的bean生命周期?

在这里插入图片描述

RequestMapping 和 GetMapping 的不同之处在哪里?

  • RequestMapping 具有类属性的,可以进行 GET,POST,PUT 或者其它的注释中具有的请求方法。 GetMapping 是
  • GET 请求方法中的一个特例。它只是 ResquestMapping 的一个延伸,目的是为了提高清晰度。

SpringMVC原理

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Spring框架中的设计模式

  1. 工厂设计模式:Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象。

  2. 代理设计模式:Spring AOP功能的实现。

  3. 单例设计模式:Spring中的bean默认都是单例的。

  • 工厂设计模式:就是用来生产对象的,如果创建的时候直接new该对象,就会对该对象耦合严重,假如我们要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则,如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:解耦
  • 代理模式:又叫委托模式,是为某个对象提供一个代理对象,并且由代理对象控制对原对象的访问。代理模式通俗来讲就是我们生活中常见的中介
  • 单例模式:可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。
    单例设计模式实现

@Component和@Bean的区别是什么

  1. 作用对象不同。@Component注解作用于类,而@Bean注解作用于方法。

  2. @Component注解通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用@ComponentScan注解定义要扫描的路径)。@Bean注解通常是在标有该注解的方法中定义产生这个bean,告诉Spring这是某个类的实例,当我需要用它的时候还给我。

  3. @Bean注解比@Component注解的自定义性更强,而且很多地方只能通过@Bean注解来注册bean。比如当引用第三方库的类需要装配到Spring容器的时候,就只能通过@Bean注解来实现。

将⼀个类声明为Spring的 bean 的注解有哪些?

一般使用@Autowired注解自动装配bean,要想把类标识成可用于@Autowired注解自动装配的bean的类,采用以下注解可实现:

  • @Component:通用的注解,可标注任意类为Spring组件。如果一个Bean不知道属于哪个层,可以使用@Component注解标注。
  • @Repository:对应持久层即Dao层,主要用于数据库相关操作。
  • @Service:对应服务层,主要涉及一些复杂的逻辑,需要用到Dao层
  • @Controller:对应Spring MVC控制层,主要用户接受用户请求并调用Service层返回数据给前端页面。

MyBatis

1、#{}和${}的区别是什么?

在这里插入图片描述

2、通常一个XML映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?

在这里插入图片描述

SpringBoot

简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手

SpringBoot核心注解

启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组
合包含了以下 3 个注解:

  • @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。 -
  • @EnableAutoConfiguration(关键):可以帮助我们自动载入应用程序所需要的所有默认配置。打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能:@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
  • @ComponentScan:Spring组件扫描。

Spring Boot 自动配置原理是什么?

  • Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-
    INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,

  • 而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,

  • XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。

  • 一定要记得XxxxProperties类的含义是:封装配置文件中相关属性;XxxxAutoConfiguration类的含义是:自动配置类,目的是给容器中添加组件。

你如何理解 Spring Boot 配置加载顺序?

在 Spring Boot 里面,可以使用以下几种方式来加载配置。

  1. properties文件;
  2. YAML文件; k v结构 层次清晰
  3. 系统环境变量;
  4. 命令行参数;

等等……

  • 101
    点赞
  • 604
    收藏
    觉得还不错? 一键收藏
  • 23
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值