本文将笔者在面试中遇到的真实题目与牛客网和CSDN等地的面经汇总成集,以供参考。
持续更新中…
坦克小游戏项目,主要是对于面向对象的综合应用,结合使用了多线程、事件监听、接口、容器等技术…
1.抽象类中的方法既可以有抽象方法,也可以有普通方法,但是接口中只能包含抽象方法
2.抽象类需要使用abstract关键字来修饰,而接口使用interface关键字来修饰
3.子类使用extends关键字来继承抽象类,使用implement关键字来实现接口
4.子类继承抽象类的时候必须要实现所有的抽象方法,普通方法可以不重写,而接口中的所有方法必须实现
5.抽象类中可以定义成员变量,而接口中只能定义静态常量
6.抽象类在子类实现的时候时单继承,为接口实现时为多继承
7.抽象类和接口都不能实例化,但是抽象类中可以有构造方法,而接口中不能有构造方法
8.抽象类中可以实现接口,并且不实现接口中的方法,而接口只能继承接口,不能实现接口
传送门: 接口和抽象类的区别.1
C++中可以实现多继承,而Java只能实现单继承。在Java中,如果想要实现继承多个父类,需要使用接口技术。
进程: 是操作系统资源分配的基本单位,每个进程都有独立的代码和数据空间(程序上下文)。由于进程比较重量级,占据独立的内存,且进程间主要通过管道(Pipe)、命名管道(FIFO)、消息队列(Message)、信号量(Semaphore)、共享内存(Shared Memory)和套接字(Socket)进行通信,所以程序之间切换会有较大的开销,但是相对比较稳定和安全。
线程: 是处理器任务调度和执行的基本单位,属于进程中的一个任务执行单元,可以称之为轻量级进程。线程间通信主要通过共享内存,因此线程之间的上下文切换负担要比进程小得多,但是相对进程而言不够稳定,容易丢失数据。
联系: 通常,一个进程中可以包含多个线程,这些线程共享进程的堆和方法区资源,但每个线程都拥有自己私有的程序计数器、虚拟机栈和本地方法栈。进程中的多线程可以并发执行。一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃将会导致整个进程都死掉。因此多进程要比多线程健壮。
注意1: 程序计数器为什么是私有的?
因为程序计数器主要是用于线程切换后能恢复到正确的执行位置。
注意2: 虚拟机栈和本地方法栈为什么是私有的?
因为虚拟机栈中的每一个方法都是一个栈帧,每个栈帧中存储着当前方法的局部变量表、操作数栈、动态链接(常量池引用)和返回值地址等信息,这些信息属于方法内部的局部信息,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈应该是线程私有的。同理,本地方法栈中存储着虚拟机使用到的Native方法,也应该是线程私有的。
协程: 是一种用户态的轻量级线程,协程的调度不由操作系统内核管理,而是完全由程序员所控制,也就是在用户态执行。协程的特点在于它在执行过程中只有一个线程参与执行,因此相比多线程,协程之间的切换不涉及任何系统调用或者阻塞调用,同时也不需要多线程的锁机制,因此具有极高的执行效率。
要理解是什么是“用户态的线程”,必然就要先理解什么是“内核态的线程”。 内核态的线程是由操作系统来进行调度的,在切换线程上下文时,要先保存上一个线程的上下文,然后执行下一个线程,当条件满足时,切换回上一个线程,并恢复上下文。 协程也是如此,只不过,用户态的线程不是由操作系统来调度的,而是由程序员来调度的,是在用户态的。
yield这个关键字就是用来产生中断, 并保存当前的上下文的,比如说程序的一段代码是访问远程服务器,那这个时候CPU就是空闲的,就用yield让出CPU,接着执行下一段的代码,如果下一段代码还是访问除CPU以外的其它资源,还可以调用yield让出CPU。 继续往下执行,这样就可以用同步的方式写异步的代码了。
使用场景: 协程适用于IO阻塞且需要大量并发的场景,当发生IO阻塞,由协程的调度器进行调度,通过将数据流yield掉,并且记录当前栈上的数据,阻塞完后立刻再通过线程恢复协程栈,并把阻塞的结果放到这个线程上去运行。
容器的线程不安全: 指的是当多个线程同时对非线程安全的集合容器进行增删改查的时候,由于容器对于线程的不同步,而导致集合中元素的数据一致性受到破坏。例如某一个线程在读取集合中的某个数据时,这一数据被另一个线程修改了,而第一个线程并没有获取到同步的数据从而引发错误。
容器的线程安全: 指的是在容器在多线程访问的情况下,其内部集合做了同步处理,从而能够保证数据一致性。
具体实现: 对集合容器内部的方法进行加锁,以map为例,hashMap由于内部完全没有锁,所以它是线程不安全的容器,而hashTable则是直接将整个Map对象加上了synchronized锁,所以他是线程安全的。但需要注意的是,由于hashTable内部存在着大量的重量级锁,因此其效率非常低,目前基本上已经被淘汰了,取而代之的是较细粒度的synchronousMap(只在方法中加synchronized代码块,不加在整个方法上)和concurrentHashMap。
此外,线程安全的集合包括:Vector、HashTable、synchronousMap、concurrentHashMap等。
线程不安全的集合包括:ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等。
乐观锁: 在操作数据时认为别人不会修改数据,因此不加锁,在更新数据时采用比较判断的方式进行操作,如果当前数据被修改过,则放弃操作,否则就执行操作。典型的乐观锁有CAS机制,它的操作过程包括compare 和 set,在操作数据时,先去比较待修改对象是否为它自身所持有的对象,然后比较该对象的数据是否等于期望数据,如果都为是,那就将该对象数据修改为新的数据。采用CAS操作实现乐观锁的类有java.util.concurrent.atomic包下的原子操作类,如AtomicInteger、AtomicLong等。但是对于CAS操作,容易产生ABA问题,如果修改操作的对象是引用数据类型,需要加版本号机制来解决。
版本号机制: 在表中加一个 version 字段;
当读取数据时,连同这个 version 字段一起读出;
数据每更新一次就将此值加一;
当提交更新时,判断数据库表中对应记录的当前版本号是否与之前取出来的版本号一致,如果一致则可以直接更新,如果不一致则表示是过期数据需要重试或者做其它操作。
悲观锁: 在操作数据时总是认为别人会修改数据,因此需要加锁,在更新数据时,会锁住已经抢到的资源,待操作执行完毕才释放。典型的悲观锁实现方式为synchronized关键字(锁方法或代码块)、ReentrantLock独占锁和MySQL中的排他锁(锁数据)。synchronized是一个重量级锁,在多线程访问共享数据时同一时刻只能由单个线程抢到资源去执行,效率v较低,同时,在线程切换的过程中还会涉及到操作系统内核态与用户态的切换,这些操作会消耗额外的资源。
但是随着JVM对synchronized的锁升级,synchronized锁的性能也得到了进一步的优化,目前效率提高了不少。
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。 同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。 所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。
怎么等待呢?执行一段无意义的循环即可(自旋)。 自旋等待不能替代阻塞,先不说对处理器数量的要求,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。 如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。 所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。
自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整; 如果通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。
适应自旋锁:
JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。 所谓自适应就意味着自旋的次数不再是固定的,它由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
它怎么做呢? 线程如果自旋成功了,那么下次自旋的次数会更多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。
答案传送门: Java中的锁优化.2
一般多线程执行的任务类型可以分为 CPU 密集型和 I/O 密集型,根据不同的任务类型,我们计算线程数的方法也不一样。
CPU 密集型任务: 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型任务: 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
在平常的应用场景中,我们常常遇不到这两种极端情况,那么碰上一些常规的业务操作,比如,通过一个线程池实现向用户定时推送消息的业务,我们又该如何设置线程池的数量呢?此时我们可以参考以下公式来计算线程数:
WT:线程等待时间; ST:线程时间运行时间
综合来看,我们可以根据自己的业务场景,从“N+1”和“2N”两个公式中选出一个适合的,计算出一个大概的线程数量,之后通过实际压测,逐渐往“增大线程数量”和“减小线程数量”这两个方向调整,然后观察整体的处理时间变化,最终确定一个具体的线程数量。在此前提下,我们再增大线程池队列,通过队列将来不及处理的线程缓存起来。在设置缓存队列时,我们要尽量使用一个有界队列,以防因队列过大而导致的内存溢出问题。
传送门: 如何设置线程池参数大小?3
略。
JDK1.7采用数组加链表的方式,在1.8之后采用数组加链表加红黑树的方式。底层通过数组存储对象节点,当对象的hashCode发生冲突,就采用链地址法,把新增对象节点挂在当前地址的节点下面。当链表长度过长时,增删改查的效率会降低,因此规定当链表长度大于8时,将链表转换成红黑树结构。
因为红黑树是一种特化的平衡二叉搜索树,其查询过程类似于二分查找,只与树的高度有关,因此查询的事件复杂度可以做到O(log N)。
PS: 在插入和删除时通过特定操作保持二叉搜索树的相对平衡(红黑树与AVL树不同,AVL是绝对平衡),从而也能获得较高的性能。
关于红黑树:红黑树.4
解决hash冲突有四种方式:开放定址法、再哈希法、链地址法和建立公共溢出区。
开放地址法: 这种方法也称再散列法,其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。例如线性探测再散列的方式是通过将元素放入冲突地址的下一个地址,如果仍然冲突,就继续往下找,一直到查找到空地址为止。
再哈希法: 这种方法是同时构造多个不同的哈希函数。
链地址法: 这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
建立公共溢出区: 这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
传送门:Hash冲突的四种解决办法.5
值传递。
传送门:Java是引用传递还是值传递?6
传送门:TCP和UDP的区别.7
传送门:MySQL SELECT:数据表查询语句.8
通常来讲,建堆过程有两种做法,一种是经典的自上而下式,一种是自下而上式。对于自上而下式建堆,主要用于用户数据是一个一个给出的情况,比如下一题的从某些元素中找出其中最大或最小的几个元素(留在下题中讲);而对于自下而上式,则适用于本题中用户一股脑把所有数据都给出的情况,也就是已经有了一个无序数组,需要用堆排序来调整的情况。
由于面试官没有具体说明排序顺序,在面试过程中我们可以跟面试官进行一次确认,这里我们假定按照从小到大的顺序排。
现在有一个含一万个元素的数组,要对他进行升序排序,我们的整体思路是:先从后往前遍历数组一次,将它调整为大根堆,然后再将数组的头部元素跟最后一个元素互换,再将前面的数组个数-1个元素调整为大根堆,然后将头部元素跟倒数第二个元素互换,再将前面数组长度-2个元素调整为大根堆,循环去操作这个互换元素、调整堆结构的过程,最终就能够将数组排好序。具体过程如下:
// heapSort
public static void heapSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
// 从后往前(也就是从下往上)将数组调成大根堆堆的过程,时间复杂度可以收敛到 O(N)
for(int i = arr.length - 1; i >= 0; i++){
heapify(arr, i, arr.length);
}
// 从循环中跳出来之后,数组arr就已经被调整成了大根堆
// 维护一个堆大小的变量heapSize, 这个heapSize初始大小为整个数组的长度
// 后面会在每一次堆的头部元素和堆的尾部元素交换之后做减1操作
int heapSize = arr.length;
swap(arr, 0, --heapSize); // 这里将交换和减1操作合并在一起写
// 只要需要维护的堆还在,就一直循环下面while()中的操作
// 该操作具体含义:此时原数组中最后一个位置已经是把大根堆堆顶元素换过来了
// 他就是整个数组中的最大值,然后这最后一个位置的元素就不需要再动了
// 然后把前面的元素再调成大根堆,再拿堆顶元素放到数组的倒数第二个位置
// 这样循环下去,数组就会按照从此到大排好序了
// 时间复杂度O(N * logN)
while (heapSize > 0){
// 这里的heapify就是在调整heapSize减小了一个之后的堆结构
heapify(arr, 0, heapSize);
// 而堆大小之所以减1,就是因为堆的最后一个位置在交换后被占了,这个位置不能动了,所以堆就变小了
swap(arr, 0, --heapSize);
}
}
// 来看看heapify的真面目
// 传入参数:要操作的数组arr、当前要进行heapify的节点位置、堆的大小
// heapify过程时间复杂度O(logN)
public static void heapify(int[] arr, int index, int heapSize){
int left = 2 * index + 1; // 左孩子
// 左孩子存在,就一直循环
while (left < heapSize){
// 找出左右孩子中最大的那个,用变量largest表示
int largest = (left + 1) < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
// 这里largest做了重定向操作,比较原来的largest和index位置上的数谁大,谁大largest就重新指向谁
largest = arr[largest] > arr[index] ? largest : index;
// 如果经过重定向,largest指向的index,说明index位置的数比两个孩子都大,此时就不需要往下看了,直接break即可
if (largest == index){
break;
}
// 到这里没有break的话,说明它的某个孩子大于index位置的数,那就把index位置的数和largest位置的数交换
swap(arr, largest, index);
// 交换后index来到它的这个孩子的位置
index = largest;
// 再把此时index的左孩子指向2 * index + 1,然后重复循环
left = 2 * index + 1;
}
}
public static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
TIME_WAIT是在TCP中,主动调用close()发起断开连接请求的一方,在发送了最后一个ACK包之后所进入的状态,在这个状态下,主动方会保持一个2MSL的等待时间后才断开连接回到起始CLOSED状态。其中的MSL是Maximum Segment Lifetime,最大报文生存寿命,指一个数据包在网络中的最大生存时间。之前要保持2MSL的等待时间,有以下两方面原因:
1.实现TCP全双工连接的可靠释放
假如主动发起关闭的一方(通常来说是客户端)在最后一次挥手过程中发送的ACK包丢失了,那么由于TCP的重传机制,被动关闭一方(通常是服务端)会再次发送FIN包,以提醒客户端。如果没有这个2MSL时间,那么就算客户端最后的ACK包丢失,它也无法收到服务端的重传消息,也就不能实现TCP的可靠性传输了,同时,由于客户端在发完ACK之后就早早地断开了,这时服务端重发FIN包之后,会收到客户端传来的RST包响应,服务端会认为出现了异常。因此,客户端在发送完最后一个ACK包后,要有一个2MSL的等待时间,当发生丢包后,能够在这个时间段内重新收到服务端的FIC包,然后再给服务端重发一次ACK包,再次经过2MSL时间,如果没有收到服务端的FIN,就认为此次断开连接成功,回到CLOSED状态。
2.使旧数据包在网络中因过期而消失
我们知道,TCP连接由一个四元组(local_ip, local_port, remote_ip, remote_port)唯一标识,假如此时因为某些原因,TCP连接断开了,接着又很快以相同的四元组建立了连接,这时因为出现过终端,两次的四元组实际上已经属于两个不同的连接了,但是对于TCP协议栈来说,它无法感知这样的中断,就会认为第二次的四元组依然是上一次的连接,倘或此时上一次连接中的旧数据包没有在网络中消失,而由客户端再次发送给了服务端,这时服务端的TCP传输层会把它当成是正常的数据来接收并向上传送到应用层,从而引起数据混乱。因此为了避免这种情况的发生,需要在一次连接结束时设置这样一个大于MSL的时间,来让本次连接过程中的所有数据包失效。
但是,在高并发短连接的TCP服务器上,当服务器处理完请求后立刻主动正常关闭连接,这种情况下会出现大量socket处于TIME_WAIT状态,如果客户端的并发量持续很高,此时又没有空闲的端口可供TCP连接,部分客户端就会显示连接不上,从而影响服务器的响应请求。因此,如果我们想避免TIME_WAIT状态,可以设置SO_REUSERADDR套接字选项来通知内核,告知它,如果端口忙但TCP连接又位于TIME_WAIT状态的话,可以重用端口。
传送门:面试总结之time_wait状态产生的原因,危害,如何避免.9
略。
(写代码之前和面试官简单做了交流,确认输入输出等等需求。写代码大概用了二十分钟,写完之后讲了每一步的思路,并说明现场写的代码边界条件可能不严谨,面试官表示理解。写代码加回答问题总计大概三十分钟)
与上述腾讯第9题过程类似。
按照腾讯第8题的堆排序过程进行,但是不同的是,在将调整好的大根堆在数组中进行首尾互换时,只需要进行 m 次即可,然后逆序遍历这个调整了 m 次的数组,将他们依次填在需要返回的数组中。时间复杂度最大是 O(m * logN),因为第一次将数组调整为大根堆时通过从下往上建堆的方式能做到 O(N),然后首尾元素互换过程,由于只需要调整 m 次,因此能够做到 O(m * logN),因此总体的时间复杂度就能做到 O(m * logN)。
如果采用小根堆结构也能找到最大的 m 个元素,但是时间复杂度就会上升到 O(N * logN)。
如果内存装不下,就要考虑将数据分文件存放,这里我们需要用到三个数组,一个叫 heapSortArr,大小为 N,用来存放加载进来的 N 个数并通过堆排序找到最大的 m 个数,第二个叫 resArr,大小为 m,用来存放排完序后最大的 m 个数,第三个叫 tempArr,大小为 2m,用来对上一个文件中找到的 m 个数和这次选出的 m 个数做比较,选出这 2m 个数中最大的 m 个数,然后将两组数比较后选出的最大的 m 个数重新放回 resArr 中。
具体求解过程为:先加载进来一份文件( N 个数)放在数组 heapSortArr 中,用堆排序选出最大的 m 个数,将这 m 个数存在数组 resArr 中,时间复杂度 O(m * logN);再加载进来另一份文件放在 heapSortArr 中,堆排序选出最大的 m 个数 ( O(m * logN) ),将这 m 个数和 arr1 中的 m 个数放进 tempArr 中,通过堆排序选出最大的 m 个数 ( O(m * logm) ),把这 m 个数放进数组 resArr 中,然后后续过程就一直循环这些加载、选数、比较的过程,最终便能选出所有数据中最大的 m 个数。整体时间复杂度和总数据量以及分出的文件个数有关,如果总数据量为 sum,分成 k 份,每份 N 个数,那总体的时间复杂度就是 ( k * m * logN )。
「堆内存相关」
-Xms :设置初始堆的大小
-Xmx :设置最大堆的大小
-Xmn :设置年轻代大小,相当于同时配置 -XX:NewSize和 -XX:MaxNewSize 为一样的值
-Xss :每个线程的堆栈大小
-XX:NewSize :设置年轻代大小(for 1.3/1.4)
-XX:MaxNewSize :年轻代最大值(for 1.3/1.4)
-XX:NewRatio :年轻代与年老代的比值(除去持久代)
-XX:SurvivorRatio :Eden区与Survivor区的的比值
-XX:PretenureSizeThreshold :当创建的对象超过指定大小时,直接把对象分配在老年代
-XX:MaxTenuringThreshold :设定对象在Survivor复制的最大年龄阈值,超过阈值转移到老年代
「垃圾收集器相关」
-XX:+UseParallelGC :选择垃圾收集器为并行收集器
-XX:ParallelGCThreads=20 :配置并行收集器的线程数
-XX:+UseConcMarkSweepGC :设置年老代为并发收集
-XX:CMSFullGCsBeforeCompaction=5 :由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行5次GC以后对内存空间进行压缩、整理
-XX:+UseCMSCompactAtFullCollection :打开对年老代的压缩。可能会影响性能,但是可以消除碎片
「辅助信息相关」
-XX:+PrintGCDetails :打印GC详细信息
-XX:+HeapDumpOnOutOfMemoryError :让JVM在发生内存溢出的时候自动生成内存快照,用于排查问题
-XX:+DisableExplicitGC禁止系统System.gc() :防止手动误触发FGC造成问题.
-XX:+PrintTLAB :查看TLAB空间的使用情况
TCP/IP分层模型(TCP/IP Layering Model)被称作因特网分层模型(Internet Layering Model)、因特网参考模型(Internet Reference Model)。下图表示了TCP/IP分层模型的四层。
TCP/IP协议被组织成四个概念层,其中有三层对应于ISO参考模型中的相应层。ICP/IP协议族并不包含物理层和数据链路层,因此它不能独立完成整个计算机网络系统的功能,必须与许多其他的协议协同工作。
TCP/IP分层模型的四个协议层分别完成以下的功能:
第一层 网络接口层
网络接口层包括用于协作IP数据在已有网络介质上传输的协议。
协议:ARP,RARP
第二层 网间层
网间层对应于OSI七层参考模型的网络层。负责数据的包装、寻址和路由。同时还包含网间控制报文协议(Internet Control Message Protocol,ICMP)用来提供网络诊断信息。
协议:本层包含IP协议、RIP协议(Routing Information Protocol,路由信息协议),ICMP协议。
第三层 传输层
传输层对应于OSI七层参考模型的传输层,它提供两种端到端的通信服务。
其中TCP协议(Transmission Control Protocol)提供可靠的数据流运输服务,UDP协议(Use Datagram Protocol)提供不可靠的用户数据报服务。
第四层 应用层
应用层对应于OSI七层参考模型的应用层和表达层。
因特网的应用层协议包括Finger、Whois、FTP(文件传输协议)、Gopher、HTTP(超文本传输协议)、Telent(远程终端协议)、SMTP(简单邮件传送协议)、IRC(因特网中继会话)、NNTP(网络新闻传输协议)等。
此外,OSI七层参考模型如下:
OSI(Open System Interconnection)开放系统互连参考模型是国际标准化组织(ISO)制定的一个用于计算机或通信系统间互联的标准体系。
应用层: 各种应用程序协议,比如 HTTP、HTTPS、FTP、SOCKS 安全套接字协议、DNS 域名系统、GDP 网关发现协议等等。
表示层: 加密解密、转换翻译、压缩解压缩,比如 LPP 轻量级表示协议。
会话层: 不同机器上的用户建立和管理会话,比如 SSL 安全套接字层协议、TLS 传输层安全协议、RPC 远程过程调用协议等等。
传输层: 接受上一层的数据,在必要的时候对数据进行分割,并将这些数据交给网络层,保证这些数据段有效到达对端,比如 TCP 传输控制协议、UDP 数据报协议。
网络层: 控制子网的运行:逻辑编址、分组传输、路由选择,比如IP、IPV6、SLIP 等等。
数据链路层: 物理寻址,同时将原始比特流转变为逻辑传输路线,比如 XTP 压缩传输协议、PPTP 点对点隧道协议等等。
物理层: 机械、电子、定时接口通信信道上的原始比特流传输,比如 IEEE802.2 等等
面向过程就是在处理某件事的时候,将每个步骤逐步罗列,待每一步都执行完了,整件事才能完成。而面向对象的思想就是抽取出某类事的共性,将其归纳为一类,在这一类事物中定义好所有的属性和方法,然后通过这个类的实例,就能够直接调用这些共性。相对面向过程来说,面向对象的封装继承和多态操作起来就更加方便。
数据结构有数组、链表、栈、堆、树、Map等。
HashMap在JDK1.7之前采用数组加链表的形式实现,在1.8之后又引入了红黑树。底层通过数组存储对象节点,当对象的hashCode发生冲突,就采用链地址法,把新增对象节点挂在当前地址的节点下面。当链表长度过长时,增删改查的效率会降低,因此规定当链表长度大于8时,将链表转换成红黑树结构。
红黑树必须满足是二叉搜索树,红黑树的头结点和叶子节点都要求是黑色的,并且任意父子节点之间不能同为红色。从一个节点到该节点的叶子结点的所有路径上包含的黑节点数量相等(这是平衡关键)。
由于红黑树属于二叉搜索树,所以它的查询时间复杂度取决于树的高度,为O(logN)。
分为三个层面:
在单线程的情况下操作是线程安全的,在多线程访问时就不是线程安全的,因为i++操作底层是通过三个操作来完成的:
这三个操作对应过来就是先用一个变量记录 i 的值,然后对 i 做加 1 操作,最后再把加完 1 的值给记录的变量。这三个操作在volatile的修饰下不能保证其为原子性操作,因此在多线程访问的情况下不是线程安全的。
还可以用CAS操作保证同步。CAS全称叫作Compare and Swap,在调用CAS的方法时会有三个参数,分别是期望值,当前值和修改后的新值。在多线程访问的情况下,某个线程会一直去比较当前值和要修改的值是否一样,如果一样就把他修改成新的值,如果不一样就不修改,继续比较。所以是一定要比较的。
如果发现当前值不是要修改的期望值,那CAS操作就会一直轮询,直到条件匹配做了修改为止。
可重入锁的意思是指当某一个线程获得了某一把锁之后,还可以再次获得同一把锁而不会出现死锁。可重入锁可以是公平的也可以是非公平的。
TCP保证可靠性的方式:10
HTTP和HTTPS的区别:11
HTTPS的安全性保证:12
HTTPS是在HTTP的基础上增加了SSL的加密算法。
在传输数据时,首先服务端会先将自己的公钥拿给第三方的CA机构,CA会用自己的私钥对服务端的公钥签名,生成证书。服务端将CA签名的证书发送给浏览器,浏览器通过内嵌的CA机构的公钥对证书解密,得到服务端的公钥,这一过程采用非对称加密的方式。
然后浏览器会随机生成一个密码信息,通过解密得到的服务端公钥对这个随机密钥进行加密,然后发送给服务端,服务端通过自己的私钥可以解密这个密文,从而拿到浏览器随机生成的密钥。
最后浏览器个服务器双方通过这个密钥,对需要发送的数据进行对称加密。由此便保证了数据传输的安全性。
由于非对称加密的安全性更高,所以采用非对称加密的方式对密钥进行加密;但是在传输数据时,由于非对称加密的效率较低,所以采用对称加密的方式对数据进行加密。
参考文章1: 接口和抽象类的区别 ↩︎
参考文章2: Java中的锁优化 ↩︎
参考文章3: 如何设置线程池参数大小? ↩︎
参考文章4: 红黑树 ↩︎
参考文章5: Hash冲突的四种解决办法 ↩︎
参考文章6: Java是引用传递还是值传递? ↩︎
参考文章7: TCP和UDP的区别 ↩︎
参考文章8: MySQL SELECT:数据表查询语句 ↩︎
参考文章9: 面试总结之time_wait状态产生的原因,危害,如何避免 ↩︎
参考文章10: TCP保证可靠性的方式 ↩︎
参考文章11: HTTP和HTTPS的区别 ↩︎
参考文章12: HTTPS的安全性保证 ↩︎