阿里-测试开发面经(十)
点击蓝字关注我们,获取更多面经
短连接
连接->传输数据->关闭连接
比如HTTP是无状态的的短链接,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。
具体就是 浏览器client发起并建立TCP连接 -> client发送HttpRequest报文 -> server接收到报文->server handle并发送HttpResponse报文给前端,发送完毕之后立即调用socket.close方法->client接收response报文->client最终会收到server端断开TCP连接的信号->client 端断开TCP连接,具体就是调用close方法。
也可以这样说:短连接是指SOCKET连接后,发送接收完数据后马上断开连接。因为连接后接收了数据就断开了,所以每次数据接受处理不会有联系。这也是HTTP协议无状态的原因之一。
长连接
连接->传输数据->保持连接 -> 传输数据-> ………..->直到一方关闭连接,多是客户端关闭连接。长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差。HTTP在短链接和长连接上的选择:HTTP是无状态的 ,也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。如果客户端浏览器访问的某个HTML或其他类型的 Web页中包含有其他的Web资源,如JavaScript文件、图像文件、CSS文件等;当浏览器每遇到这样一个Web资源,就会建立一个HTTP会话
HTTP1.1和HTTP1.0相比较而言,最大的区别就是增加了持久连接支持(貌似最新的HTTP1.1 可以显示的指定 keep-alive),但还是无状态的,或者说是不可以信任的。
如果浏览器或者服务器在其头信息加入了这行代码 Connection:keep-alive TCP连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了带宽。
实现长连接要客户端和服务端都支持长连接。
优缺点
tcp长连接优缺点:长连接可以省去较多的tcp建立/关闭的操作,减少浪费,节省时间,对于频繁请求资源的客户,较适用于长连接;client和server如果长时间不关闭的话,会存在一个问题,随着客户的越来越多,server早晚会有扛不住的一天,这时需要采取一些策略,如关闭一些长时间不读写操作的连接,这样可以避免一些恶意连接导致server端服务受损,如果条件再允许,就可以以客户端为颗粒度,限制每个客户端的最大连接数
tcp短连接优缺点:短连接对于服务器来说较为简单,存在的连接都是有用的连接,不需要额外的控制,但如果客户端连接频繁,会在tcp的建立和关闭上浪费时间。
public class Singleton {
//没有volatile线程可能访问到的是一个没有初始化的对象
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) { //静态方法的同步锁为类的class对象,即Singleton.class
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
注:通过第8行判断的每个线程会依次获得锁进入临界区,所以进入临界区后还要再判断一次单例类是否已被其它线程实例化,以避免多次实例化。由于双重加锁实现仅在实例化单例类时需要加锁,所以会带来性能上的提升。另外需要注意的是双重加锁要对 instance 域加上 volatile 修饰符。由于 synchronized 并不是对 instance 实例进行加锁(因为现在还并没有实例),所以线程在执行完第11行修改 instance 的值后,应该将修改后的 instance 立即写入主存(main memory),而不是暂时存在寄存器或者高速缓冲区(caches)中,以保证新的值对其它线程可见,避免其他线程进行初始化。
volatile关键字的作用:
实例化对象的那行代码( instance = new Singleton();),实际上可以分解成以下三个步骤:
1.分配内存空间
2.初始化对象
3.将对象指向刚分配的内存空间
但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:
1.分配内存空间
2.将对象指向刚分配的内存空间
3.初始化对象
现在考虑重排序后,两个线程发生了以下调用:
Time Thread A Thread B
T1 检查到uniqueSingleton为空
T2 获取锁
T3 再次检查到uniqueSingleton为空
T4 为uniqueSingleton分配内存空间
T5 将uniqueSingleton指向内存空间
T6 检查到uniqueSingleton不为空
T7 访问uniqueSingleton(此时对象还未完成初始化)
T8 初始化uniqueSingleton
在这种情况下,T7时刻线程B对uniqueSingleton的访问,访问的是一个初始化未完成的对象。
解决:使用volatile关键字修饰instance对象,使用了volatile关键字后,重排序被禁止,上述编译器为了性能优化进行的重排序行为就不允许发生了。
定义
多态的定义简单来说就是使一条语句有多种状态。
实现方式
多态的实现方式分为三块:重载、重写、重定义。下面我们来谈一谈他们各自的实现方式和实现原理。
重载
实现方式
重载是在同一作用域内(不管是模块内还是类内,只要是在同一作用域内),具有相同函数名,不同的形参个数或者形参类型。返回值可以相同也可以不同(在函数名、形参个数、形参类型都相同而返回值类型不同的情况下无法构成重载,编译器报错。这个道理很简单,在函数调用的时候是不看返回值类型的)。
实现原理
重载是一种静态多态,即在编译的时候确定的。C++实现重载的方式是跟编译器有关,编译过后C++的函数名会发生改变,会带有形参个数、类型以及返回值类型的信息(虽然带有返回值类型但是返回值类型不能区分这个函数),所以编译器能够区分具有不同形参个数或者类型以及相同函数名的函数。插一句,在C语言中编译器编译过后函数名中不会带有形参个数以及类型的信息,因此C语言没有重载的特性。由此带来麻烦的一点是如果想要在C++中调用C语言的库,需要特殊的操作(extern “C”{})。库中的函数经过C编译器编译的话会生成不带有形参信息的函数名,而用C++的编译器编译过后会生成带有形参信息的函数名,因此将会找不到这个函数。extern “C”{}的作用是使在这个作用域中的语句用C编译器编译,这样就不会出错。这也是一种语言兼容性的问题。
重写
实现方式
重写是在不同作用域内(一个在父类一个在子类),函数名、形参个数、形参类型、返回值类型都相同并且父类中带有virtual关键字(换言之子类中带不带virtual都没有关系)。有一种特殊的情况:函数返回值类型可以不同但是必须是指针或者引用,并且两个虚函数的返回值之间必须要构成父子类关系。这种情况称之为协变,也是一种重写。引入协变的好处是为了避免危险的类型转换。
实现原理
重写是一种动态多态,即在运行时确定的。C++实现重写的方式也跟编译器有关,编译器在实例化一个具有虚函数的类时会生成一个vptr指针(这就是为什么静态函数、友元函数不能声明为虚函数,因为它们不实例化也可以调用,而虚函数必须要实例化,这也是为什么构造函数不能声明为虚函数,因为你要调用虚函数必须得要有vptr指针,而构造函数此时还没有被调用,内存中还不存在vptr指针,逻辑上矛盾了)。vptr指针在类的内存空间中占最低地址的四字节。vptr指针指向的空间称为虚函数表,vptr指针指向其表头,在虚函数表里面按声明顺序存放了虚函数的函数指针,如果在子类中重写了,在子类的内存空间中也会产生一个vptr指针,同时会把父类的虚函数表copy一下当做自己的,然后如果在子类中重新声明了虚函数,会按声明顺序接在父类的虚函数函数指针下。而子类中重写的虚函数则会替换掉虚函数表中原先父类的虚函数函数指针。重点来了,在调用虚函数时,不管调用他的是父类的指针、引用还是子类的指针、引用,他都不管,只看他所指向或者引用的对象的类型(这也称为动态联编),如果是父类的对象,那就调用父类里面的vptr指针然后找到相应的虚函数,如果是子类的对象,那就调用子类里面的vptr指针然后找到相应的虚函数。当然这样子的过程相比静态多态而言,时间和空间上的开销都多了(这也是为什么内联函数为什么不能声明为虚函数,因为这和内联函数加快执行速度的初衷相矛盾)。
重定义
实现方式
重定义是在不同作用域内的(一个在父类一个在子类),只要函数名相同,且不构成重写,均称之为重定义
实现原理
重定义的实现原理跟继承树中函数的寻找方式有关,他会从当前对象的类作用域内开始查找同名的函数,如果没有找到就一直向上查找直到基类为止。如果找到一个同名的函数就停止。这也就说明他不管函数的形参类型或者个数是不是一样,只要函数名一样,他就认为是找到了,如果这时候形参类型或者个数不一致,编译器就会报错。多重继承的查找,如果在同一层内出现一样的函数声明那么编译器会报错不知道调用哪一个函数,这类问题也叫钻石继承问题。钻石问题的解决方案可以通过虚继承来实现,这样就不会存在多个一样的函数声明。
更多面经
扫描二维码
获取更多面经
扶摇就业