一、数据结构
1. 排序算法
经典排序算法
快排
2. 结构
堆和栈区别
堆
- Java的堆是一个运行时数据区,类的对象从堆中分配空间。这些对象通过new等指令建立,通过垃圾回收器来销毁。
- 堆的优势是可以动态地分配内存空间,需要多少内存空间不必事先告诉编译器,因为它是在运行时动态分配的。但缺点是,由于需要在运行时动态分配内存,所以存取速度较慢。
栈
- 栈中主要存放一些基本数据类型的变量(byte,short,int,long,float,double,boolean,char)和对象的引用。
- 栈的优势是,存取速度比堆快,栈数据可以共享。但缺点是,存放在栈中的数据占用多少内存空间需要在编译时确定下来,缺乏灵活性。
外部排序 在内存中进行的排序称为内部排序,而在许多实际应用中,经常需要对大文件进行排序,因为文件中的记录很多,信息量庞大,无法将整个文件拷贝进内存进行排序。因此,需要将带排序的记录存储在外存上,排序时再把数据一部分一部分的调入内存进行排序,在排序中需要多次进行内外存的交互,对外存文件中的记录进行排序后的结果仍然被放到原有文件中。这种排序方法就称外部排序。
贪心算法和动态规划的区别 动态规划:重叠子问题 最优子结构(自下而上),每步所做选择依赖于子问题,本质是穷举法,可以保证结果最佳,复杂度高。 贪心算法:贪心选择 最优子结构(自上而下),仅在当前状态下做出最好选择。不能保证解最佳,复杂度低。 动态规划三要素:重叠子问题,最优子结构,状态转移方程
数组与链表
遍历一遍,哪个快?
- 数组,数组开辟的空间是连续的,读入了缓存
- 而链表的节点很可能是分散的,极端情况下,一次访存就有可能引起一次缓存失效
两者区别:
- 在内存中,数组是一块连续的区域,需要预留空间,使用前先申请占内存的大小;而链表可以在内存中的任何地方,不要求连续,扩展方便。
- 数组插入和删除效率低,插入数据时,这个位置后面的数据在内存中都要后移,删除时,都要前移。但是随机读取的效率高。
- 链表增加和删除数据容易,查找效率低,因为不具有随机访问性。
二、Java基础
python深拷贝、浅拷贝
- copy浅拷贝,没有拷贝子对象,所以原始数据改变,子对象会改变
- 深拷贝,包含对象里面的自对象的拷贝,所以原始对象的改变不会造成深拷贝里任何子元素的改变
0 基础问题
1. 抽象类和接口的区别
(1)接口只有方法定义,不能有方法的实现,而抽象类可以有定义与实现,方法可在抽象类中实现。 (2)实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。(java只支持单继承)
接口:
- 接口的使用,它不能创建对象,但是可以被实现( implements ,类似于被继承)。一个实现接口的类(可以看做 是接口的子类),需要实现接口中所有的抽象方法,创建该类对象,就可以调用方法了,否则它必须是一个抽象 类。
2. final关键字、内部类、static
final: 不可改变。可以用于修饰类、方法和变量。
- 类:被修饰的类,不能被继承。
- 方法:被修饰的方法,不能被重写。
- 变量:被修饰的变量,不能被重新赋值。
内部类:
- 内部类可以直接访问外部类的成员,包括私有成员。
- 外部类要访问内部类的成员,必须要建立内部类的对象。
static
- 可以用来修饰的成员变量和成员方法,被修饰的成员是属于类的,而不是单单是属 于某个对象的。也就是说,既然属于类,就可以不靠创建对象来调用了。
- 静态方法只能访问静态成员。
- 静态方法可以直接访问类变量和静态方法。
- 静态方法不能直接访问普通成员变量或成员方法。反之,成员方法可以直接访问类变量或静态方法。
- 静态方法中,不能使用this关键字。
3. 多态
多态: 是指同一行为,具有多个不同表现形式。
- 继承或者实现【二选一】
- 方法的重写【意义体现:不重写,无意义】
- 父类引用指向子类对象【格式体现】
引用类型转换
- 向上转型:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。当父类引用指向一个子类对象时,便是向上转型。
- 向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。(解决了:不能调用子类拥有,而父类没有的方法)
重写、重载
方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。
- 方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
- 方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
- 方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
4. 成员变量和局部变量
在类中的位置不同 重点
- 成员变量:类中,方法外
- 局部变量:方法中或者
方法声明上(形式参数) 作用范围不一样 重点 –
- 成员变量:类中
- 局部变量:方法中
初始化值的不同 重点
- 成员变量:有默认值
- 局部变量:没有默认值。必须先定义,赋值,最后使用
在内存中的位置不同 了解
- 成员变量:堆内存
- 局部变量:栈内存
生命周期不同 了解
- 成员变量:随着对象的创建而存在,随着对象的消失而消失
- 局部变量:随着方法的调用而存在,随着方法的调用完毕而消失
5. String类、StringBuilder类
String类:
- 字符串是常量,它们的值在创建之后不能更改
- 字符串的底层是一个被final修饰的数组,不能改变,是一个常量。
StringBuilder类:
- 字符串缓冲区,可以提高字符串的操作效率(看成一个可变长度的字符串)。底层也是一个数组,但没有被final修饰,可以改变长度。
6. Java集合
6.1 HashSet
元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果 equals 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。
1. HashMap(数组 链表 红黑树)
HashMap结构
HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。
- capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
- loadFactor:负载因子,默认为 0.75。
- threshold:扩容的阈值,等于 capacity * loadFactor
- hash运算的过程其实就是对目标元素的Key进行hashcode,再对Map的容量进行取模
- 数组默认容量:16(为了提升取模的效率,使用位运算代替了取模运算,这就要求Map的容量一定得是2的幂)
- 数组扩容:大于12时(12=默认容量16*装载因子0.75),扩容成原容量的2倍,table长度大于64时,链表才会升级为红黑树、
- put操作:链表长度大于8,会转为红黑树。
- java7查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)
- java8的HashMap在java7的基础上增加了红黑树这种数据结构,使得在桶里面查找数据的复杂度从O(n)降到O(logn),当然还有一些其他的优化,比如resize的优化等。
1.1 put和get方法
put() 方法:
- 调用 hash(K) 方法计算 K 的 hash 值,然后结合数组长度,计算得数组下标;
- 调整数组大小(当容器中的元素个数大于 capacity * loadfactor 时,容器会进行扩容resize 为 2n);
- 如果 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() 方法:
- 调用 hash(K) 方法(计算 K 的 hash 值)从而获取该键值所在链表的数组下标;
- 顺序遍历链表,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。
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作为原始数组,这样也会有问题。
1.3.1 hashMap与hashTable的区别
- HashMap 是线程不安全的,HashTable 是线程安全的;
- 由于线程安全,所以 HashTable 的效率比不上 HashMap;
- HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而 HashTable不允许;
- HashMap默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍 1;
- 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。
1.3.4 StringBuilder和StringBuffer
StringBuilder是线程不安全的,StringBuffer是线程安全的。如果只是在单线程中使用字符串缓冲区,则StringBuilder的效率会高些,但是当多线程访问时,最好使用StringBuffer。
HashMap & HashTable , ArrayList & Vector , StringBuilder & StringBuffer
- ArrayList和LinkedList:区别
- 面向接口编程、get和set时间复杂度(o(1)),在jdk1.8中是线程安全的吗(不,为什么);
2. 并发
2.1 并发、并行、多线程、同步、异步概念
- 并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
- 并行:在操作系统中,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。同一时刻
- 多线程:多线程是程序设计的逻辑层概念,它是进程中并发运行的一段代码。多线程可以实现线程间的切换执行。
- 异步:异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情。
- 异步和多线程并不是一个同等关系,异步是最终目的,多线程只是我们实现异步的一种手段。异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回而可以做其它的事情。实现异步可以采用多线程技术或则交给另外的进程来处理。
start 和 run 方法: 调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法。
2.2 进程、线程、协程
PCB(进程控制块)是进程存在的唯一标志。
进程状态转换
- 运行状态
- 就绪状态:进程获得了除处理机之外的一切所需资源
- 阻塞状态:进程在等待某一时间而暂停运行,如等待某资源为可用或等待输入/输出完成。即使处理机调度空闲,该进程也不能运行。
- 创建状态
- 结束状态
线程状态
进程与线程对比
- 引入进程目的:更好地使多道程序并发执行,以提高资源利用率和系统吞吐量,增加并发程度。
- 引入线程目的:减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能。 区别:
- 线程是独立调度的基本单位,进程是拥有资源的基本单位。
- 线程之间的同步与通信非常容易,进程切换开销大。
协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。 协程多与线程进行比较
- 一个线程可以多个协程,一个进程也可以单独拥有多个协程,这样python中则能使用多核CPU。
- 线程进程都是同步机制,而协程则是异步
- 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态
2.3 多线程、线程池
多线程原理:
- 同一时间内,CPU只能处理1条线程,只有1条线程在工作(执行);多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。
线程调度:
- 分时调度 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 抢占式调度 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为 抢占式调度。 设置线程的优先级
创建线程的方法:
- 一种是继承Thread类方式,
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把 run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
- 一种是实现Runnable接口方式
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正 的线程对象。
- 调用线程对象的start()方法来启动线程。
线程池
- 其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源。
2.4 锁(线程同步)
线程同步的操作:
- 同步代码块。 2. 同步方法。 3. 锁机制。
2.2.1 Synchornized
- Synchornized
原理
- synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁
- 使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现。
- 执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器 1。此时其他竞争锁的线程则会进入等待队列中。
- 执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。
synchromized缺陷 例子1:
- 如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。 因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
例子2:
- 当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。 但是采用synchronized关键字来实现同步的话,就会导致一个问题: 如果多个线程都只是进行读操作,当一个线程在进行读操作时,其他线程只能等待无法进行读操作。 因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。 另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。 总的来说,也就是说Lock提供了比synchronized更多的功能。
2.2.2 Synchornized和Lock(CAS)区别
区别:
- Synchronized在线程发生异常时会自动释放锁,因此不会发生异常死锁。Lock异常时不会自动释放锁,所以需要在finally中实现释放锁。
- Lock是可以中断锁,Synchronized是非中断锁,必须等待线程执行完成释放锁。 Lock可以使用读锁提高多线程读效率。 synchronized会一直获取执行权限直到执行完毕,那lock能指定在一定时间内获取锁
- 一般情况下使用synchronized已经足够了,但是,每个线程在执行相关代码块时都要与其他线程同步确认是否可以执行代码。lock和semaphore就有了用武之地。lock可以帮我们实现尝试立刻获取锁,在指定时间内尝试获取锁,一直获取锁等操作,而semaphore信号量可以帮我们实现允许最多指定数量的线程获取锁。
2.2.3 Lock
原理 Lock:底层是CAS乐观锁,依赖AbstractQueuedSynchronizer类,把所有的请求线程构成一个CLH队列。而对该队列的操作均通过Lock-Free(CAS)操作。
2.2.4 ReentrantLock(AQS、CAS)
ReentrantLock基于AQS(AbstractQueueSynchronizer抽象队列同步器)
- AQS原理: AQS内部维护一个state状态位,尝试加锁的时候通过CAS(CompareAndSwap)修改值,如果成功设置为1,并且把当前线程ID赋值,则代表加锁成功,一旦获取到锁,其他的线程将会被阻塞进入阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state重新置为0,同时当前线程ID置为空。
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来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
ReentrantLock和synchronized区别
2.2.5 锁的优化/升级机制
- 优化机制
优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁
- 升级机制
锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁。升级的过程就是从低到高,降级在一定条件也是有可能发生的。
- 偏向锁就是通过对象头的偏向线程ID来对比,甚至都不需要CAS了,
- 轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现,
- 重量级锁则是除了拥有锁的线程其他全部阻塞
2.5 并发容器
2.3.1 阻塞队列blockingQueue
相关问题
- 如何设计一个生产者消费者队列? blockingQueue
- 如果加锁,生产操作会阻塞消费操作,怎么解决? linkedBlockingQueue
- 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; ```
- ArrayBlockingQueue
ArrayBlockingQueue 通过一个循环数组的方式来实现存储元素的,这里
takeIndex
记录了当前可以取元素的索引位置,而putIndex
则记录了下一个元素可以存储的位置。当队列满了时,takeIndex和putIndex将指向同一个元素,这里则可以通过count
字段来判断当前是处于满状态还是空置状态。通过声明一个全局的锁来控制所有操作的控制权限,也就是说,对于ArrayBlockingQueue而言,其任何一个操作都是阻塞其他操作的。这里notEmpty和notFull则是由lock
创建得来的,通过这两个分离的等待条件,可以实现队列两端线程添加和移除操作的分离
- 这里enqueue()和dequeue()方法是入队和出队的核心方法。在enqueue()方法中,当成功入队之后,其会唤醒一个正在等待取出元素的线程;在dequeue()方法中,当成功出队之后,其会唤醒一个正在等待添加元素的线程
在put()和take()
操作中,首先都是通过while条件进行前置判断,对于put操作,如果队列满了,则在notFull中进行等待,对于take()操作,如果队列为空,则在notEmpty中进行等待,并且释放锁。在队列中有空闲空间或有元素时,才会继续执行put()或take()操作。
- LinkedBlockingQueue
LinkedBlockingQueue 其底层是通过一个单向链表实现的,由于单项链表需要有一个指向下一个节点的指针,因而其必须使用一个对象(这里是Node)来存储当前元素的值和下一个节点的索引。 为了实现阻塞的特性,LinkedBlockingQueue分别为队列头部和尾部声明了两个锁,并且创建了两个等待Condition。当往队列添加元素时,使用
putLock
锁定队列尾部,如果队列满了,则将该线程添加到notFull的Condition中,并且释放锁;当从队列中取元素时,使用takeLock
锁定队列头部,如果队列为空,则将该线程添加到notEmpty的Condition中,并且释放锁。
- notFull和notEmpty是两个condition对象。
notEmpty
用于控制队列中没有元素时阻塞尝试获取元素的线程。notFull
用于控制队列满了时阻塞尝试往队列中添加元素的线程 - 对于链表的入队和出队操作,其是非常简单的,这里仅仅只是单纯的入队和出队,并没有相关的锁的操作
put()和take()
基本都是先判断前置条件是否成立,即队列未满或不为空,如果成立,当前线程才有权限进行入队和出队操作。在操作完成之后,当前线程还会唤醒正在等待尝试获取或取出元素的线程。
ArrayBlockingQueue和LinkedBlockingQueue的区别
区别
- 两者底层数据结构不同,ArrayBlockingQueue是通过循环数组来实现的,而LinkedBlockingQueue是通过单向链表来实现的;
- 两者阻塞方式不同,ArrayBlockingQueue使用了一个全局锁来处理所有的操作,也就是说无论是队列头部还是尾部,只要一个线程获取到了锁,那么其他所有的线程将都会被阻塞,只不过由于锁定的时间非常短,因而这种消耗可以忽略不计;LinkedBlockingQueue为队列头部和尾部分别使用了两个不同的锁,在元素入队和出队操作时,两者几乎是互不干扰的;
- 两者初始化大小不同,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 GC
3.1.1 内存泄露
参考
概念:不再会被使用的对象的内存不能被回收,就是内存泄露 类型:
- static字段引起的内存泄露, 静态字段通常拥有与整个应用程序相匹配的生命周期。
- 常量字符串造成的内存泄露
- 未关闭的资源导致内存泄露
- equals()和hashCode()重写不合理
堆内存和栈内存
4. 反射
反射的作用: Java的反射机制是在编译并不确定是哪个类被加载了,而是在程序运行的时候才加载、探知、自审。使用在编译期并不知道的类。这样的特点就是反射。
- 假如有两个程序员,一个程序员在写程序的时候,需要使用第二个程序员所写的类,但第二个程序员并没完成他所写的类。那么第一个程序员的代码能否通过编译呢?这是不能通过编译的。利用Java反射的机制,就可以让第一个程序员在没有得到第二个程序员所写的类的时候,来完成自身代码的编译。
- 要正确使用Java反射机制就得使用
java.lang.Class
这个类。它是Java反射机制的起源。当一个类被加载以后,Java虚拟机就会自动产生一个Class对象。通过这个Class对象我们就能获得加载到虚拟机当中这个Class对象对应的方法、成员以及构造方法的声明和定义等信息。 - java怎么变成可执行文件,在命令行里怎么运行java文件;java类加载器工作机制
- java集合类
- java I/0
- concurrenthashmap:如何实现线程安全;concurrent包了解?;
- java泛型:为什么是伪泛型,泛型的好处,int可作为泛型吗
- 静态和动态:区别,动态底层怎么实现
- 线程,进程:区别,run,start区别,两次start同一个线程会怎么样;介绍线程模型;violate及其原理,是线程安全?;violate和synchronized区别;线程池原理;核心线程和非核心线程区别
- CAS:怎么解决ABA问题;CountDownLatch和cyclicBarrier区别
- JVM:GC调优
- 同步异步
同步
三、操作系统
1. 概述
操作系统的特征
并发:两个或多个时间在同一时间间隔内发生 引入进程的目的是使程序能并发执行。 并行是同一时刻
共享:系统中的资源可供内存中多个并发执行的进程共同使用。
- 两种资源共享方式:
- 互斥共享方式:一段时间内置允许一个进程访问该资源。 一段时间内只允许一个进程访问的资源成为临界资源或独占资源。
- 同时访问方式:进程交替地对该资源进行访问,即“分时共享”。(例如磁盘)
并发和共享时操作系统两个最基本的特征,互为存在条件
虚拟:把一个物理实体变为若干个逻辑上的对应物。 异步: 在多道程序环境下,允许多个程序并发执行,但由于资源有限,进程的执行不是一贯到底,而是走走停停,以不可预知的速度向前推进。
操作系统的目标和功能
操作系统作为计算机系统资源的管理者
- 处理机管理 处理机的分配和运行都以进程(或线程)为基本单位,因而对处理机的管理可归结为对进程的管理。进程管理的主要功能:进程控制、进程同步、进程通信、死锁处理、处理机调度等。
- 存储器管理:内存分配、地址映射、内存保护、共享和内存扩充。
- 文件管理
- 设备管理:完成用户的IO请求
操作系统作为用户与计算机硬件系统之间的接口 命令接口,程序接口
操作系统用作扩充器
操作系统的运行环境
操作系统的运行机制
- 通常CPU执行两种不同性质的程序:一种是操作系统内核程序(内核态);另一种是用户自编程序或系统外层的应用程序(用户态) 核心态指令实际包括系统调用类指令和一些针对时钟、中断和原语的操作指令。
中断和异常
- 通过中断或异常实现用户态与核心态的切换
2. 进程管理
进程与线程
PCB(进程控制块)是进程存在的唯一标志。
进程状态转换
- 运行状态
- 就绪状态:进程获得了除处理机之外的一切所需资源
- 阻塞状态:进程在等待某一时间而暂停运行,如等待某资源为可用或等待输入/输出完成。即使处理机调度空闲,该进程也不能运行。
- 创建状态
- 结束状态
进程与线程对比 引入进程目的:更好地使多道程序并发执行,以提高资源利用率和系统吞吐量,增加并发程度。 引入线程目的:减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能。 区别:
- 线程是独立调度的基本单位,进程是拥有资源的基本单位。
- 线程之间的同步与通信非常容易,进程切换开销大。
处理机调度
- 先来先服务(FCFS)
- 短作业优先(SJF)
- 优先级调度算法
- 高响应比优先:响应比=(等待时间 要求服务时间)/要求服务时间
- 时间片轮转
- 多级反馈队列调度:时间片轮转 优先级调度
进程同步,管程
进程之间的制约关系:同步、互斥 临界资源:一次仅允许一个进程使用的资源 临界区互斥原则:空闲让进、忙则等待、有限等待、让权等待
生产者与消费者
死锁
定义:多个进程因竞争资源而造成的一种僵局,若无外力,这些进程无法向前推进 死锁产生原因:
- 系统资源的晶振
- 进程推进顺序非法
- 死锁产生的必要条件:
- 互斥条件
- 不可剥夺
- 请求和保持
- 循环等待
3. 内存管理(分页分段)
管理方式 连续分配:
- 单一连续分配
- 固定分区分配
- 动态分区分配: 首次:按地址从小到大为序,分配第一个符合条件的分区 最佳:按空间大小为序,… 最坏: 空间从大到小… 临近适应:与首次适应相似,从上次查完的结束为止开始查找。
非连续分配:
- 页式存储:内存分为固定的块,按物理结构划分,会有内部碎片
- 段式存储:内存块的大小不固定,按逻辑结构划分,会有外部碎片。
- 段页式存储:分页分段结合,有内部碎片。
内存碎片如何解决?
内存碎片分为内部碎片和外部碎片。
- 内存中划分为若干个固定的块(这些块大小可能相等也可能不相等),当一个程序或一个程序分解后的部分程序装进这些块后,在块里面不能完全占用的内存空间成为内部碎片。 解决方法:①采用可变分区分配②采用分段存储管理方式(一般采用这种方式)
- 采用可变分区分配或分段存储管理方式后,虽然分配的每一个块的大小和程序实际需要的空间一样大,但划分以后,内存中仍然有部分空间是剩余的,这些剩余的空间成为外部碎片。 解决方法:①采用单一连续分配②采用固定分区分配③采用分页存储管理方式④采用段页式存储管理方式(第③和第④是常用方法)
分页、分段、段页式
分页存储管理
- 基本分页存储管理方式中,系统将一个进程的逻辑地址空间分成若干个大小相等的篇,称为页面或页。
- 相应地,将内存空间分成若干个与页面同样大小的块,称为物理块或页框。
- 在进程运行时,为了能在内存中找到每个页面对应的物理块,系统为每个进程建立了一张页面映射表,简称页表
- 被浪费的空间称为页内碎片
- CPU生成的每个地址分为两部分:页码 页偏移。
- 页码作为页表的索引,页表包含每页所在物理内存的基地址。这个基地址与页偏移的组合就形成了物理内存地址,可发送到物理单元。
分段式存储
- 分页系统虽然能较好地解决动态分区的碎片问题,却难以满足用户的某些需求(将自己的作业按逻辑关系分成若干段,然后通过段名和段内地址来访问相应的程序或数据,同时系统能以段为单位对程序和数据进行共享和保护,并要求能动态增长),因此引入了分段式存储管理方式
段页式存储 在段页式存储管理中,基本思想是:
- 内存划分:按照分页式存储管理方案
- 作业的管理:按照分段式存储管理进行分配
这种新的系统既具有分段系统的便于实现、分段可共享、易于保护、可动态链接等一系列优点,又能像分页系统那样,很好地解决内存的外部碎片问题 在这个地址变换中,我们经历了三次的寻址
- 第一次访问是访问内存中的段表
- 第二次访问是访问内存中的页表
- 第三次访问是访问真正的地址
主要过程:
- 首先根据逻辑地址得到段号、页号、页内偏移量
- 然后判断段号是否越界
- 不越界,则查询段表,找到相对应的段表项
- 检查是否越界
- 不越界,则根据页表存放块号、页号查询页表,找到对应的页表项
- 计算出实际地址,访问实际地址
虚拟内存 为什么用虚拟内存?
- 多道程序并发执行,共享主存,需要很多内存
局部性原理:
- 时间局部性:被访问过一次的存储器位置很可能在不远的将来会被再次访问。原因:程序中存在大量循环操作。
- 空间局部性:如果一个存储器位置被访问了一次,那么程序很可能在不远的将来访问附近的一个存储器位置。原因:因为指令通常是顺序存放、顺序执行的,数据也一般以向量、数组、表等形式聚集存储。
页面置换算法:
- 最佳置换:以后不用的
- 先进先出
- 最近最久未使用(LRU)
- 时钟算法
虚拟存储器
- 定义:基于局部性原理,程序装入时,可将程序的一部分装入内存,将其余部分留在外存,需要时再调入。
- 特征:
- 多次性:无需在作业运行时一次性全部装入内存,允许被分成多次调入内存
- 对换性:无需在作业运行时一直常驻内存,允许作业运行时,进行换入换出
- 虚拟性:从逻辑上扩充内存。
- 实现方式: 1. 请求分页存储管理 2.请求分段存储管理 3. 请求段页式存储管理
抖动:刚刚换出的页面马上又要换如内存,刚换出的又要换入。频繁发生缺页中断(抖动)原因是:某个进程频繁访问的页面数目高于可用的物理页帧数目。
逻辑地址、物理地址
映射:MMU
4. 文件管理(磁盘)
磁盘调度算法
- 先来先服务
- 最短寻道时间
- 扫描算法:磁头移动方向上,选择与磁头所在磁道最近的。
- 循环扫描:
四、计网
1. OSI七层模型
物理层:
- 传输单位是比特,
- 任务是透明的传输比特流,
- 功能是在物理媒体上为数据段设备透明的传输原始比特流
数据链路层:
- 传输单位是帧,
- 任务是将网络层传下来的IP数据报组装成帧。
- 功能:
- 成帧
- 差错控制:循环冗余法
- 可靠传输: 数据链路层通常使用确认和超时重传两种机制来保证可靠传输
- 流量控制: 限制发送方的数据流量,使其发送速率不致超过接收方的接收能力
- 传输管理
- 协议:SDLCHDLSPPPSTP和帧中继
网络层
- 传输单位:数据报 – 关心的是通信子网的运行控制
- 任务:把网路层协议的数据单元(分组)从源端传到目的端,为分组交换网上的不同主机提供通信服务。
- 关键问题是对分组进行路由选择,并实现流量控制、拥塞控制和网际互联等功能。
- 协议: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重传机制
参考
- 超时重传,等待timeout
- 快速重传,如果发送方连续收到3次相同的ack,就重传,它的好处就是不用等timeout了再重传
- SACK: 这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
- D-SACK:Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。
3.1.3 TCP 数据报文的结构
TCP 数据报文的结构
- 序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
- 确认序号:Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=Seq 1。
- 标志位:共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粘包问题
- 什么时候考虑粘包:
- 如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题
- 如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包
- 粘包出现原因:在流传输中出现,UDP不会出现粘包,因为它有消息边界(参考Windows 网络编程) 1 发送端需要等缓冲区满才发送出去,多个包打包成一个,造成粘包 2 接收方不及时接收缓冲区的包,造成多个包接收
- 避免粘包的措施:(先回答 在包前添加包长度)
- 是对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;
- 是对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;
- 是由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。
TCP拆包 如果发送的数据包超过一次tcp报文所能传输的最大值时,就会将一个数据包拆成多个最大tcp长度的tcp报文分开传输,这就叫做拆包。 解决粘包拆包问题:比较通用的做法就是每次发送一个应用数据包前在前面加上四个字节的包长度值,指明这个应用包的真实长度
3.1.5 TCP滑动窗口(流量控制)
TCP滑动窗口(解决流量控制问题)
- 在TCP中,窗口的大小是在TCP三次握手后协定的,并且窗口的大小并不是固定的,而是会随着网络的情况进行调整。
- 机制:发送窗口收到接收端对于本段窗口内字节的 ACK 确认才会移动发送窗口的左边界。 接收窗口只有在前面所有的段都确认的情况下才会移动左边界,当前面还有字节未接收但收到后面字节的情况下(乱序)窗口是不会移动的,并不对后续字节确认, 确保这段数据重传。 可以根据滑动窗口的调整进行流量控制。
3.1.6 TCP拥塞控制
TCP拥塞控制
- TCP传输的过程中,发送端开始发送数据的时候,如果刚开始就发送大量的数据,那么就可能造成一些问题。网络可能在开始的时候就很拥堵,如果给网络中在扔出大量数据,那么这个拥堵就会加剧。拥堵的加剧就会产生大量的丢包,就对大量的超时重传,严重影响传输。
- 所以TCP引入了慢启动的机制,在开始发送数据时,先发送少量的数据探路。探清当前的网络状态如何,再决定多大的速度进行传输。这时候就引入一个叫做拥塞窗口的概念。发送刚开始定义拥塞窗口为1,每次收到ACK应答,拥塞窗口加 1。在发送数据之前,首先将拥塞窗口与接收端反馈的窗口大小比对,取较小的值作为实际发送的窗口。
- 拥塞窗口的增长是指数级别的。慢启动的机制只是说明在开始的时候发送的少,发送的慢,但是增长的速度是非常快的。为了控制拥塞窗口的增长,不能使拥塞窗口单纯的加倍,设置一个拥塞窗口的阈值,当拥塞窗口大小超过阈值时,不能再按照指数来增长,而是线性的增长。在慢启动开始的时候,慢启动的阈值等于窗口的最大值,一旦造成网络拥塞,发生超时重传时,慢启动的阈值会为原来的一半(这里的原来指的是发生网络拥塞时拥塞窗口的大小),同时拥塞窗口重置为1。
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在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。
四次挥手
- 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
- 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u 1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
- 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
- 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u 1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
- 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w 1,而自己的序列号是seq=u 1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命,2MSL就是一个发送和一个回复所需的最大时间)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。 为什么要等待2msl:确保客户端发送的最后一个包可以给到服务器。
- 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
SYN泛洪攻击
- Syn攻击就是 攻击客户端 在短时间内伪造大量不存在的IP地址,向服务器不断地发送syn包,服务器回复确认包,并等待客户的确认,由于源地址是不存在的,服务器需要不断的重发直 至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪。
- Syn攻击是一个典型的DDOS攻击。检测SYN攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击
4. 浏览器输入url,会发生什么
- URL解析,根据域名查询域名的IP地址
- DNS解析 查询浏览器缓存(浏览器会缓存之前拿到的DNS 2-30分钟时间),如果没有找到, 检查系统缓存,检查hosts文件,这个文件保存了一些以前访问过的网站的域名和IP的数据。它就像是一个本地的数据库。如果找到就可以直接获取目标主机的IP地址了。没有找到的话,需要 检查路由器缓存,路由器有自己的DNS缓存,可能就包括了这在查询的内容;如果没有,要 查询ISP DNS 缓存:ISP服务商DNS缓存(本地服务器缓存)那里可能有相关的内容,如果还不行的话,需要, 递归查询:从根域名服务器到顶级域名服务器再到极限域名服务器依次搜索对应目标域名的IP。
- TCP连接(三次握手)
- 发送HTTP请求
- 服务器处理请求并返回HTTP报文
- 浏览器渲染页面
5. HTTP
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状态码
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和用户,可以找到哪个进程占用了这个端口
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/100171.html原文链接: