C++/JAVA/测试开发岗位面试高频考点
5263
2021.04.23
2021.09.03
发布于 未知归属地
  1. Java:

    1. 1.深拷贝和浅拷贝
    2. 2.接口和抽象类的区别
    3. 3.java的内存是怎么分配的
    4. 4.java中的泛型是什么?类型擦除是什么?
    5. 5.Java中的反射是什么
    6. 6.序列化与反序列化
    7. 7.Object有哪些方法?
    8. 8.JVM内存模型
    9. 9.类加载机制
    10. 10.对象的创建和对象的布局
    11. 11.Java的四种引用(强引用、软引用、弱引用和虚引用)
    12. 12.内存泄露和内存溢出
    13. 13.List、Set和Map三者的区别和其底层数据结构
    14. 14.创建线程的四种方式
    15. 15.NIO、AIO和BIO
    16. 16.重写和重载
    17. 17.final/finally/finalize与static
    18. 18.String、StringBuffer和StringBuilder的区别
    19. 19.如果判断一个对象是否该被回收?
    20. 20.垃圾收集算法
    21. 21.Double与Float
    22. 22.垃圾收集器
    23. 23.线程池
    24. 24.线程同步和线程通讯
    25. 25.中断线程
    26. 26.Synchronized的用法
    27. 27.Synchronized的原理
    28. 28.Synchronized的四种状态
    29. 29.Synchronized与重入锁ReentrantLock的区别
    30. 30.锁优化
    31. 31.Java设计模式
    32. 32.HashMap
    33. 33.ConcurrentHashMap
    34. 34.int和Integer的区别
  2. C++

    1. 1.虚函数和纯虚函数
    2. 2.printf()函数的原理
    3. 3.全局变量、局部变量、静态全局变量和静态局部变量
    4. 4.C++11的新特性
    5. 5.c++中怎么定义常量?
    6. 6.左值与右值
    7. 7.指针常量、常量指针和常指针常量
    8. 8.C和C++的区别
    9. 9.多重继承
    10. 10.C++中new/delete和malloc/free的区别
    11. 11.智能指针
    12. 12.C++定义一个空类的时候,会生成哪些函数
    13. 13.内联函数Inline
    14. 14.强制类型转换
    15. 15.volatile关键字的作用
    16. 16.预编译
    17. 17.堆和栈的区别
    18. 18.重载、重写(覆盖)和隐藏
    19. 19.动态编译和静态编译
    20. 20.循环依赖
    21. 21.模板偏特化
    22. 22.struct和class
    23. 23.友元函数
    24. 24.运算符重载
    25. 25.explicit关键字
    26. 26.动态绑定与静态绑定
    27. 27.零拷贝技术
    28. 28.成员初始化列表
    29. 29.STL的基本组成
    30. 30.map和set的区别
    31. 31.C++20
    32. 32.vector的emplace_back和push_back方法
    33. 33.红黑树
    34. 34.排序的C++实现
    35. 35.引用、指针与数组
    36. 36.内存泄漏
  3. Mysql数据库

    1. 1.mysql数据库的四种隔离级别
    2. 2.mysql的常用存储引擎
    3. 3.mysql常用的索引类型和种类:
    4. 4.为什么mysql使用B+树做索引
    5. 5.什么样的列需要索引?
    6. 6.聚簇索引与非聚簇索引
    7. 7.慢查询优化
    8. 8.内连接和外连接
    9. 9.B树与B+树
    10. 10.数据库事务的四大特性
    11. 11.SQL的聚集(聚合)函数
    12. 12.数据库的死锁
    13. 13.关系型数据库和非关系型数据库的区别
  4. 计算机网络

    1. 1.TCP三次握手
    2. 2.TCP四次挥手
    3. 3.HTTP和HTTPS的区别
    4. 4.TCP流量控制与拥塞控制
    5. 5.OSI七层都是哪些?对应有哪些协议?
    6. 6.ICMP协议
    7. 7.网络字节序与大端小端
    8. 8.GET和POST的区别
    9. 9.HTTP状态码
    10. 10.DNS
    11. 11.HTTP1.0/1.1/2.0
    12. 12.路由器和交换机的区别
    13. 13.在浏览器输入URL到网页显示的过程
  5. 操作系统

    1. 1.僵尸进程与孤儿进程
    2. 2.虚拟内存和物理内存
    3. 3.分段和分页
    4. 4.线程间通信的手段
    5. 5.互斥锁和条件变量
    6. 6.内存屏障
    7. 7.线程、进程和协程
    8. 8.Linux常用命令
    9. 9.死锁
    10. 10.用户态和内核态
    11. 11.并行和并发
  6. 软件测试

    1. 1.五种测试
    2. 2.黑盒与白盒
    3. 3.开发和测试的结合
    4. 4.BUG的评测
    5. 5.PC网络障碍排查
    6. 6.测试用户界面登陆过程
    7. 7.吃鸡游戏的压力测试
    8. 8.自动化测试工具Selenium相关

Java:

1.深拷贝和浅拷贝

内存中有栈区和堆区,基本类型数据直接存在栈中,而引用类型(new出来的)是在堆中存储,在栈中保存堆中的地址。也就是说引用类型中在栈中存的不是数据,而是地址。赋值其实就是拷贝。
在基本类型数据赋值的时候,没有深浅拷贝的区别,因为直接赋予的是数据。
但在引用类型数据赋值的时候,实际上是把原来的地址复制给了新的,并没有实际复制其中的数据,所以这是一个浅拷贝(拷贝的深度不够),当使用新的变量操作地址中的值的时候,旧变量对应的值也会发生改变。Java中Object的clone方法默认是浅拷贝。
深拷贝会创造另外一个一模一样的对象,新对象和原来的对象不共享内存,修改新对象不会影响旧对象。

参考文章


2.接口和抽象类的区别

抽象类:被abstract关键字修饰。抽象方法也被abstract修饰,只有方法声明,没有方法体。

  • 抽象类不能被实例化,只能被继承
  • 抽象类可以有属性、方法和构造方法,但是构造方法不能用于实例化,主要用于被子类调用
  • 子类继承抽象类,必须实现抽象类抽象方法,否则子类必须也是抽象类
  • 抽象类中的抽象方法只能是public或protected

接口:被interface关键字修饰。

  • 接口可以包含变量和方法;变量隐式设定为public static final,方法被隐式设定为public abstract
  • 接口支持多继承,一个接口可以extends多个接口
  • 一个类可以实现多个接口
  • jdk1.8中增加了默认方法和静态方法:default/static

接口只能是功能的定义,而抽象类既可以为功能的定义也可以为功能的实现。
接口和抽象类都不能被实例化,接口的实现类和抽象类的子类只有实现了接口中/抽象类中的方法才能实例化。
实现接口的关键字是implements,继承抽象类的关键字是extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。
接口强调特定功能的实现,而抽象类强调所属关系。

参考文章


3.java的内存是怎么分配的

内存分配分为在栈上分配和在堆上分配,大多数都是引用类型,所以堆空间用的较多。
对象根据存活时间分为年轻代、年老代、永久代(方法区)。
年轻代:对象被创建时,首先分配在年轻代。年轻代有三个区域:Eden区,survivor 0区和survive 1区,Eden区大多数对象消亡速度很快,Eden是连续的内存空间,分配内存很快。Eden区满的时候执行Minor GC,清理消亡对象,将存活的对象放在survivor 0区中,每次执行Minor GC的时候,将剩余存活对象都放在非空的survivor区中,survivor区满之后,就会清理并转移到另一个survivor区,也就是说总有一个survivor区是空的。HotSpot虚拟机中默认切换15次之后,仍然存活的对象放在年老代中。
年老代:年老代的空间一般比年轻代大,存放更多的对象,年老代内存不足的时候,执行Major GC(Full GC),如果对象比较大的情况,可能直接放在老年代上。有可能出现老年代引用新生代对象的情况,java维护一个512 byte的块“card table”,记录引用映射,进行Minor GC的时候直接查card table就可以了。

参考文章


4.java中的泛型是什么?类型擦除是什么?

java源代码要运行,首先要经过编译器编译出字节码,字节码存储着能被JVM解释运行的指令。java的泛型在运行时,无法获得类型参数的真正类型,因为编译器编译生成的字节码不包括类型参数的具体类型。
泛型是java 1.5之后引入的,其本质是参数化类型,也就是说变量的类型是一个参数,在使用的时候再指定为具体类型,泛型可以用于类、接口和方法。

public class User<T> {
	
	private T name;
}//泛型实际上就是把类型当作参数传入了

而类型擦除机制使得Java的泛型实际上是伪泛型,类型参数只存在于编译期,运行时,JVM并不知道泛型的存在。

public class ErasedTypeEquivalence {
  public static void main(String[] args) {
    Class c1 = new ArrayList<String>().getClass();
    Class c2 = new ArrayList<Integer>().getClass();
 System.out.println(c1 == c2); //代码输出是true
  }
}

在C++、C#这些支持真泛型的语言中,它们代表着不同的类,但在JVM看来他们是同一个类。无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用 Object)替换。Java 编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。当具体的类型确定后,泛型提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。

参考文章


5.Java中的反射是什么

java反射就是把类中的各个成分映射成一个个java对象,在运行期间,对于任意一个类,都能够知道这个类的属性和方法,是一种动态获取信息、动态调用对象的方法。
反射实际上是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。
大白话说反射
优点:动态加载类,提高代码灵活度
缺点:降低性能,可能引起安全问题
我们使用的Spring/hibernate中使用了反射机制,在使用JDBC连接数据库使用class.forName()通过反射加载数据库的驱动程序。
Spring框架的IOC(动态加载管理bean)创建对象,AOP(动态代理)都和反射有关系。


6.序列化与反序列化

序列化:将Java对象转换成字节序列的过程。
反序列化:将字节序列转换成java对象。

  • serializable接口是可以进行序列化的标志性接口,仅仅是告诉JVM该类对象可以进行序列化。
  • 先让需要序列化的类实现serializable接口;序列化对象创建输出流ObjectOutputStream,然后调用writeObject()方法;反序列化对象创建输入流ObjectInputStream,然后调用readObject()方法,得到一个object对象。最后关闭流。

7.Object有哪些方法?
  • equals:比较对象是否相等,这里实质是比较地址是否相等。
  • wait:调用wait方法会导致线程阻塞,释放该对象的锁
  • notify:调用对象的notify方法会随机解除该对象阻塞的线程,该线程重新获取该对象的锁
  • notifyAll:唤醒所有正在等待对象的线程,全部进入锁池竞争获取锁
    wait,notify,notifyAll必须在synchronized方法块中使用。
  • toString:转换为字符串表示
  • getClass:返回对象运行时类,即反射机制。
  • hashCode: 对象在内存中的地址转换为int值。

8.JVM内存模型
  • 程序计数器(PC register):线程执行的字节码行号指示器,线程私有,唯一一个没有内存超出错误的区域。
  • Java虚拟机栈:每个线程创建时都会创建一个虚拟机栈,内部保存一个个栈帧,对应每一次方法调用。生命周期与线程相同。保存方法的局部变量和部分结果,参与方法的调用和返回。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflow异常;如果虚拟机栈可以动态扩展,当扩展到无法申请足够内存时抛出OutOfMemoryError异常。
  • 本地方法栈:与虚拟机栈类似,但只为native方法服务。
  • Java堆:线程共享内存,用来存放对象实例,是垃圾回收的主要区域。java堆可以处于物理上不连续的内存空间中,只要逻辑上连续就可以了,就类似于磁盘空间。如果在堆中没有内存完成实例分配,而且堆也无法再拓展的时候,将会抛出OutOfMemoryError的异常。
  • 方法区:是线程共享内存,它用于存储已被虚拟机加载的类信息等数据。它可以叫做永久代也可以是元空间,在jdk1.8之后,永久代的数据被分配到堆和元空间中,元空间存储类信息,字符串常量和运行时常量池放入堆中。方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。

JVM调优参数
(1) -Xms:初始化堆内存。默认为物理内存的六十四分之一
(2) -Xmx: 最大堆内存。默认为物理内存的四分之一
(3) -Xss:单个线程栈的大小
(4) -Xmn:设置新生代的大小
(5) -XX:MetaspaceSize:设置元空间大小
(6) -XX:SurvivorRatio:调节新生代eden和S0、S1的空间比例 默认为8:1:1

JVM性能监控工具
(1)jps -l:查看进程号
(2)jstack:java堆栈跟踪工具 查看死锁和cpu占用过高的代码
(3)jinfo -flag 查看运行的java程序参数属性的详情


9.类加载机制

类加载就是将类的数据从class文件加载到内存,并且进行校验解析和初始化,形成可以让虚拟机使用的java类型。
类的生命周期:加载,链接,初始化,使用,卸载。

  • 加载:通过类名获取二进制字节流(通过类加载器),把静态数据结构放在方法区,内存中生成对应class对象,作为访问入口。
  • 链接:确保当前字节流包含的信息符合虚拟机要求。正式分配内存,设置初始值(仅分配静态变量),虚拟机将常量池内的符号引用替换成直接引用。
  • 初始化:按照代码逻辑,赋予属性真正的初始值,初始化阶段就是执行类构造器方法的过程。
    类加载器:包括启动类加载器、扩展类加载器和应用程序类加载器。

10.对象的创建和对象的布局

对象创建的方法

  • 用new语句创建

  • 调用clone方法,需要实现cloneable接口

  • 反射:class的newInstance()

  • 反序列化:从文件中获取一个对象的二进制流,使用ObjectInputStream的readObject方法。

    对象创建的过程

  • 类加载检查:判断这个类是不是已经被加载链接初始化了。

  • 为对象分配内存:如果内存规整,虚拟机使用碰撞指针法(指针向空闲区前移对象大小的距离);如果不规整则使用空闲列表法。并发安全:虚拟机维护一个列表记录哪些内存块可用,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表内容。

  • 初始化分配的空间:所有属性初始化为零,保证对象实例字段在不赋值的时候可以直接用

  • 设置对象头信息

  • 执行构造方法初始化

    逃逸:方法体内创建的对象,方法体外被其他变量引用过。这样在方法执行完毕之后,该方法中创建的对象不能被GC回收。开启逃逸分析之后,如果对象的作用域仅在方法内,那对象可以创建在虚拟机栈上,随方法入栈创建,出栈销毁,减少GC回收压力。

    对象的内存布局:包含三部分:对象头,实例数据和对齐填充。

  • 对象头:运行时数据和类型指针。标记字段包含hashcode、GC分代年龄、锁状态标志、线程持有锁等信息;类元数据的指针:可以知道这个对象是哪个类的实例。

  • 实例数据:存储对象真正的数据,也包含父类的数据。

  • 对齐填充:保证对象大小是8字节的整数倍。


11.Java的四种引用(强引用、软引用、弱引用和虚引用)

在jdk1.2之前,Java对引用的定义很传统:如果reference类型的数据中存储的数值是另一块内存的起始地址,就称这块内存代表一个引用。

  • 强引用:Java中默认声明的引用为强引用,只要强引用存在,垃圾回收器永远不会回收被引用的对象,哪怕内存不足,JVM也只会抛出OOM错误,不会去回收。
 Object obj = new Object();
  • 软引用:用于描述一些非必需但仍有用的对象。内存足够的时候,软引用对象不会被回收,只有在内存不足的时候,系统会回收软引用对象,如果内存还是不够才会抛出OOM异常。这种特性使他往往用于实现缓存技术。在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。
  • 弱引用:弱引用的强度比软引用更弱。无论内存是否足够,只要JVM开始垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用java.lang.ref.WeakReference来表示弱引用。
  • 虚引用:最弱的引用关系。与其他几种引用不同,虚引用不会决定对象的生命周期,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,任何时期都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动,且必须与引用队列联合使用。当垃圾回收器准备回收一个对象的时候,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
    参考文章

12.内存泄露和内存溢出

内存泄漏:一个不再被线程所使用的对象或变量还在内存中占用空间。
内存溢出:程序无法申请到足够的内存。

内存泄漏的原因
1.长生命周期的对象持有短生命周期对象的引用。
2.连接未正常关闭。
3.变量作用域设置过大

避免内存泄漏
1.避免在循环中创建对象
2.没有用的对象尽早释放
3.慎用静态变量
4.字符串的拼接使用Stringbuffer/StringBuilder
5.增大xmx和xms的值

内存溢出的原因
1.加载数据过大
2.死循环或过多循环
3.启动参数中内存值设定过小

栈溢出
原因:递归深度过大、局部变量过大
解决:递归不要太深,局部变量改为静态变量

如果排查内存问题
1.JConsole:能看到内存用量的趋势,确定是否有问题
2.GC日志:能看到年轻代和老年代等区域配置是否合理
3.代码中打印内存使用量
4.分析dump文件:针对性的看到发生OOM时候的内存使用量和线程情况


13.List、Set和Map三者的区别和其底层数据结构
  • List:有序的对象
    (1)ArrayList:数组
    (2)Vector:数组
    (3)LinkedList:双向链表
  • Set:不允许重复的集合
    (1)HashSet(无序且唯一):基于HashMap
    (2)LinkedHashSet:基于HashMap
    (3)TreeSet(有序且唯一):基于红黑树
  • Map:使用键值对存储
    (1)HashMap:Jdk1.8之前HashMap由数组+链表组成,之后再链表长度大于阈值(默认8)时将链表转换为红黑树以减少搜索时间。
    (2)LinkedHashMap:继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。
    (3)HashTable:数组+链表组成,数组是HashMap的主体,链表为了解决哈希冲突
    (4)TreeMap:红黑树

ArrayList、LinkedList、Vector的区别

  • 存储结构:ArrayList和Vector是基于数组实现的,而LinkedList是基于双向链表实现的。
  • 线程安全性:ArrayList不具有线程安全性(ArrayList添加元素的操作不是原子操作,可能会出现一个线程的值覆盖另一个线程添加的值的问题),在单线程的环境中,LinkedList也是不安全的。Vector实现了线程安全,它大部分的关键字都包含synchronized,但效率低。
  • 扩容机制:ArrayList和Vector都是用数组来存储,容量不足的时候可以扩容,ArrayList扩容后的容量是之前的1.5倍,Vector默认是2倍。Vector可以设置扩容增量capacityIncrement。可变长度数组的原理是当元素个数超过数组长度时,产生一个新的数组,将原数组的数据复制到新数组,再将新元素添加到新数组中。
  • 增删改查效率:ArrayList和Vector中,从指定的位置检索一个对象,或在末尾插入删除一个元素时间复杂度都是O(1),但是在其他位置增加和删除对象的时间是O(n);LinkedList,插入删除任何位置的时间都是O(1),但是检索一个元素的时间是O(n)。

14.创建线程的四种方式
  • 继承Thread类,重写run方法,继承Thread类的线程类不能再继承其他父类。
  • 实现Runnable接口,重写run方法
  • 通过Callable接口和Future接口创建线程,执行call方法,有返回值可以抛异常
  • 线程池。前三种的线程如果创建关闭频繁的话会消耗系统资源影响性能,而使用线程池可以不用线程的时候放回线程池,用的时候再从线程池取。

15.NIO、AIO和BIO
  • BIO:传统的网络通讯模型,同步阻塞IO。服务器实现是一个连接一个线程,客户端有连接请求的时候,服务端就要启动一个线程去处理。线程数量可能会爆炸导致崩溃。适用于连接数目小且固定的架构。
  • NIO:同步非阻塞。服务器实现是一个请求一个线程,客户端发送的连接请求都会注册到多路复用器上,复用器轮询到连接有IO请求才启动线程。适用于连接数目多且连接比较短的架构,比如聊天服务器。
  • AIO:异步非阻塞。用户进程只需要发起一个IO操作然后立即返回,等IO操作真正完成之后,应用程序会得到IO操作完成的通知。适用于连接数目多且连接长的架构。

16.重写和重载
  • 重写(Override):重写是子类对父类允许访问的方法实现过程进行重新编写,返回值和形参都不能改变。重写的好处是子类可以根据特定需要,定义特定行为。异常范围可以减少,但是不能抛出新的或更广的异常。
class Animal{
   public void move(){
      System.out.println("动物可以移动");
   }
}
 
class Dog extends Animal{
   public void move(){
      System.out.println("狗可以跑和走");
   }
}
 
public class TestDog{
   public static void main(String args[]){
      Animal a = new Animal(); // Animal 对象
      Animal b = new Dog(); // Dog 对象
 
      a.move();// 执行 Animal 类的方法
      b.move();//执行 Dog 类的方法
   }
}

虽然b属于Animal类型,但是它运行的是Dog类的move方法。因为在编译阶段,只是检查参数的引用类型,运行时JVM指定对象的类型并运行该对象的方法。
方法重写规则
(1)参数列表和被重写方法的参数列表必须完全相同。
(2)访问权限不能比父类中被重写的方法访问权限更低。
(3)父类的成员方法只能被它的子类重写。
(4)声明为final的方法不能被重写;声明为static的方法不能被重写,但是能被再次声明。
(5)构造方法不能被重写。
(6)子类和父类在同一个包中,那么子类可以重写父类中没有声明为private和final的方法;如果不在同一个包中,子类只能重写父类声明为public和protected的非final方法。
当需要在子类中调用父类的被重写方法时,使用super关键字。

  • 重载(Overload):是在一个类里面,方法名字相同,参数不同的两个方法。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)必须有一个独一无二的参数类型列表。常用于构造器重载。
    重载规则
    (1)被重载的方法必须改变参数列表。
    (2)被重载的方法可以改变返回类型,可以改变访问修饰符,可以声明新的或更广的异常检查。
    (3)方法能够在同一个类中或者在一个子类中被重载。
public class Overloading {
    public int test(){
        System.out.println("test1");
        return 1;
    }
 
    public void test(int a){
        System.out.println("test2");
    }   
 
    //以下两个参数类型顺序不同
    public String test(int a,String s){
        System.out.println("test3");
        return "returntest3";
    }   
 
    public String test(String s,int a){
        System.out.println("test4");
        return "returntest4";
    }   
 
    public static void main(String[] args){
        Overloading o = new Overloading();
        System.out.println(o.test());
        o.test(1);
        System.out.println(o.test(1,"test3"));
        System.out.println(o.test("test4",1));
    }
}

方法重载和方法重写是java多态的不同表现。
参考文章


17.final/finally/finalize与static
  • final:java中的关键字,修饰符。如果一个类被声明为final,就意味着它不能再派生出新的子类,不能作为父类被继承。一个类不能被同时声明final和abstract抽象类。如果变量或方法被声明为final,就能保证它们在使用中不被改变,变量必须在声明时赋值,以后的引用中只读,被声明final的方法只能使用,不能重载。
  • finally:java的一种异常处理机制。java异常处理模型的最佳补充,finally结构使代码总会执行,而不管有无异常发生。使用finally可以维护对象的内部状态,清理非内存资源。在关闭数据库连接时,如果把数据库连接的close()方法放到finally中,就会减少出错的可能。
  • finalize:Java中的一个方法名,该方法是在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器确定这个对象没被引用的时候调用的。它在Object类中定义,因此所有类都继承了它。子类可以覆盖该方法来整理资源和清理。
  • static:static修饰的属性在编译器初始化,初始化之后能改变,final修饰的属性可以在编译器也可以在运行期初始化,但是不能被改变;static不能修饰局部变量,但是final可以。

18.String、StringBuffer和StringBuilder的区别
  • String是java编程中广泛使用的,但它的底层实现实际是一个final类型的字符数组,其中的值不可变,每次对String进行操作就会生成一个新对象,造成内存浪费。
private final char value[];
  • StringBuffer/StringBuilder:它们的底层是可变的字符数组,都继承AbstractStringBuilder抽象类,所以在进行频繁的字符串操作的时候,尽量使用这两个类,它们的区别是:StringBuilder是线程不安全的,但执行速度较快;StringBuffer线程安全,但执行速度慢。StringBuffer使用synchronized关键字进行同步锁。
    另外,String类型的比较,“==”是比较两个内存地址是否一样,而“equals”是比较两个字符串的值是不是一样的。
    参考文章

19.如果判断一个对象是否该被回收?
  • 引用计数算法:为对象增加一个引用计数器,当对象增加一个引用的时候+1,引用失效-1,引用计数为0的对象可以被回收。但是当两个对象循环引用的情况下,计数器永远不为0,因此JVM不使用引用计数算法。
  • 可达性分析算法:以GC Roots为起点开始搜索,可达的对象都是存活的,不可达的对象可以被回收,JVM使用该算法进行判断。GC Roots中包含:虚拟机栈中引用的对象、本地方法栈中引用的对象,方法区中静态成员或常量引用的对象。

20.垃圾收集算法
  • 标记-清除算法(Mark-Sweep)
    标记阶段:标记的过程实际上就是可达性分析算法过程,遍历GC Roots对象,可达的对象都做好标记,在对象的header中将其记录为可达。
    清除阶段:对堆进行遍历,如果发现有某个对象没有可达对象标记,则回收。
    缺点:两次遍历,效率低;GC运行时需要停止整个程序;产生大量的碎片,需要维护一个空闲列表。
  • 复制算法(Copying)
    对象在Survivor区每经历一次Minor GC,就将对象年龄+1,当对象年龄达到某个值时,对象复制到老年代,默认为15。JVM中Eden和Survivor区的默认比例为8:1:1,保证内存利用率为90%,如果每次回收有多于10%的对象存活,Survivor空间可能就不够用了,此时借用老年代空间。
    缺点:复制收集算法在对象存活率高的时候需要进行很多的复制操作,效率会变低,老年代一般不会用该算法。
  • 标记-整理算法
    第一阶段和标记-清楚算法一样,第二阶段将所有存活的对象压缩到内存的另一端,按顺序排放。之后,清理边界外所有的空间。
    缺点:效率不高,不仅要标记存活对象,还要整理所有存活对象的引用地址;移动过程中,要全程暂停用户应用程序。
  • 分代收集算法
    新生代:使用复制算法,因为大量对象需要回收。
    老年代:回收的对象很少,所以采用标记清除或者标记整理算法。
    **为什么会出现频繁的GC?**如果对象都比较小,生命周期都比较短,就需要频繁的GC,将这些对象从内存释放掉;频繁的GC还有可能是人为的,比如代码调用GC;headp比较小的时候,也肯定会发生频繁的GC。

21.Double与Float

java语言支持两种基本的浮点类型:float和double。32位浮点数float用1位表示符号,8位表示指数,用23位表示尾数;64位浮点数double用一位表示符号,11位表示指数,52位表示尾数。在表示超过23位的时候,float就会自动四舍五入,这就是float的精度限制,所以会出现double可以表示而float会不精确的情况,如果要将这两个浮点数进行转型,java提供了Float.doubleValue()和Double.floatValue()方法。使用这个方法在单精度转双精度的时候,会出现偏差。
浮点运算很少是精确的,只要超过精度表示范围就会产生误差。
解决方法:可以通过String结合BigDecimal或者通过使用long类型来转换。
参考文章


22.垃圾收集器

查看默认垃圾收集器:-XX:+PrintCommandLineFlags

  • Serial串行收集器:单线程收集器,只使用一个线程回收垃圾,需要停掉其他所有线程,Client模式下默认新生代垃圾收集器,新生代使用复制算法,老年代使用标记整理算法,Serial Old也作为CMS收集器的后备垃圾收集方案。JVM参数:-XX:+UseSerialGC
  • ParNew收集器:Serial的多线程版本,对应的JVM参数:-XX:+UseParNewGC。开启参数之后,会使用ParNew(新生代)复制算法+Serial Old(老年代)标记整理算法的组合,Java8之后不再推荐使用这种组合。
  • Parallel scavenge收集器:新生代和老年代都使用并行,Parallel scavenge收集器可以使用自适应调节策略,把基本的内存数据设置好,然后设定是更关注最大停顿时间或者更关注吞吐量,给虚拟机设立一个优化目标。JVM参数是:-XX:+UseParallelGC。新生代使用复制算法,老年代使用标记-整理算法。
  • CMS收集器:一种以获取最短回收停顿时间为目标的收集器。JVM参数:-XX:+UseConcMarkSweepGC。使用ParNew(新生代)+CMS(老年代)+Serial Old(后备)的收集器组合。优点是并发收集,停顿少。缺点是并发会造成CPU的压力,而且标记清除算法会产生大量空间碎片。
    (1)初始标记:标记GC Roots能直接关联到的对象,速度很快,需要停顿。
    (2)并发标记:进行GC Roots Trancing的过程,不需要停顿。
    (3)重新标记:修正并发标记期间因为用户程序继续运作而导致变动的那一部分对象重新进行标记,需要停顿。
    (4)并发清除:不需要停顿。
  • G1垃圾收集器:它使得Eden、Survivor和Tenured等内存区域不再连续,而变成一个个大小一样的region,每个region从1M到32M不等。它不再采用CMS的标记清理算法,G1整体上使用标记整理算法,局部上看是基于复制算法。JVM参数:-XX:+UseG1GC。
    降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可以预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片内。是因为G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的region。

另:JVM设置参数的方法(win10):环境变量中新建变量JAVA_OPTS,在里面设置。


23.线程池

我们使用线程的时候去创建一个线程,这种方法非常简便,但是会导致一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁的创建线程会大大降低系统效率。Java中引入了线程池来使得线程可以复用,执行完一个任务不会被立刻销毁,而是可以继续执行其他任务。
ThreadPoolExecutor类是线程池技术最核心的类:
其构造器中的参数意义

  • corePoolSize:核心池大小。在创建线程池之后,默认线程池中是没有线程的,除非调用prestartAllCoreThreads()或者prestartCoreThread()方法来预创建线程,就是没有任务到来之前先创建corePoolSize个线程。当线程池中的线程数目到达corePoolSize个之后,就会把到达的任务放到缓存序列中。
  • maximumPoolSize:非常重要的参数,表示线程池中最多能创建多少个线程。
  • keepAliveTime:表示线程没有任务执行时最多保持多久会终止。
  • unit:参数keepAliveTime的时间单位。
  • workQueue:阻塞队列,用来存储等待执行的任务,会对线程池的运行过程产生重大影响。有三个选择:ArrayBlockingQueue、LinkedBlockingQueue和SynchronousQueue,一般使用后两者。
  • threadFactory:线程工厂,主要用来创建线程。
  • handler:表示拒绝处理任务的策略,有四种取值:
    (1)ThreadPoolExecutor.AbortPolicy:丢弃任务抛出RejectedExecutionException异常;
    (2)ThreadPoolExecutor.DiscardPolicy:丢弃任务,不抛异常
    (3)ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复该过程)
    (4)ThreadPoolExecutor.CallRunsPolicy:由调用线程处理该任务

ThreadPoolExecutor类的方法

  • execute()和submit():都是提交任务,execute方法用于提交不需要返回值的任务,无法判断任务是不是被线程池执行成功;submit提交需要返回值的任务,线程池返回future类型的对象以判断是否执行成功,future对象具有的get()方法可以获取返回值。
  • shutdown()和shutdownNow():都是关闭线程池,他们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有正在执行或者暂停的线程,并返回等待执行任务的列表;shutdown只是将线程池的状态设置为SHUTDOWN,然后中断所有没有执行任务的线程。

如何合理分配线程池的大小:CPU密集型任务,一般公式为:最大线程数 = CPU核数+1;IO密集型的最大线程数 = CPU核数 * 2;

实现一个线程池:

public class Test {
     public static void main(String[] args) {   
         ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<Runnable>(5));
          
         for(int i=0;i<15;i++){
            MyTask myTask = new MyTask(i);
            executor.execute(myTask);
            System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
            executor.getQueue().size()+",已执行完别的任务数目:"+executor.getCompletedTaskCount());
         }
         executor.shutdown();
     }
}

线程池不允许使用Executors的静态方法创建,必须通过ThreadPoolExecutor。
**线程池的处理流程:**当线程池提交一个任务的时候:
(1)线程池判断核心线程池中的线程是不是都在执行任务,如果不是则创建一个新的工作线程执行任务,否则进入流程(2)
(2)线程池判断工作队列是否已满,如果没有满则将新提交的任务存储在这个任务队列中,如果工作队列满了,则进入流程(3)
(3)线程池判断池中的线程是否都处在工作状态,如果没有则创建一个新的工作线程来执行任务,如果已经满了就交给拒绝策略(handler)来处理任务。
参考文章

四种线程池:
(1)newCachedThreadPool 创建一个可以缓存的线程池。
(2)newFixedThreadPool 创建一个定长线程池,可以控制线程最大并发数。
(3)newScheduledThreadPool 创建一个定长线程池,支持定时和周期性任务执行。
(4)newSingleThreadExecutor 创建一个单线程化的线程池,他只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。

//可以缓存的线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); //需要指定长度
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);

详细实现代码


24.线程同步和线程通讯
  • 线程同步的五种方式:synchronized的关键字修饰方法、静态资源或者代码块;Lock(必须放在try-catch-finally中执行,finally释放锁以防止死锁);wait和notify,必须在synchronized范围内,被synchronized锁住的对象就是wait和notify的调用对象;CAS;信号量(Semaphore)。
  • 线程通讯的方式:
    (1)wait()、notify()、nofityAll():等待/通知机制。线程A调用了对象O的wait方法进入等待状态,另一个线程B调用了对象O的notify或notifyAll方法,线程A收到通知之后,从对象O的wait方法中返回执行后续操作。调用对象的wait方法会导致线程阻塞,释放该对象的锁;调用对象的notify方法会随机解除该对象阻塞的线程,该线程重新尝试获取该对象的锁;从wait方法返回的前提是获得了调用对象的锁;必须在synchronized块或方法中使用。
    (2)condition:Condition用await(),signal,singalAll方法代替wait和notify。notify只能随机唤醒一个线程,但是用condition可以唤醒指定线程。
    (3)管道
    (4)volatile
    (5)Thread.join:如果一个线程执行了Thread.join(),意味着当前线程A等待thread线程中止之后才从thread.join()返回。

25.中断线程
  • 调用一个线程的interrupt()方法来中断线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出InterruptedException,从而提前结束该线程。
  • 如果线程的run()执行一个死循环,并且没有执行sleep()等会抛出InterruptedException的操作,那么调用interrupt()方法无法使线程提前结束。但是调用interrupt方法会设置线程的中断标记,此时调用Thread.interrupted()或者Thread.currentThread().isInterrupted()方法会返回true。因此可以在循环体中使用interrupted()方法判断线程是否处于中断状态,从而提前结束线程。

26.Synchronized的用法

线程安全是Java并发编程中的重点,造成线程安全问题主要有两个原因:一是存在共享数据,二是存在多条线程共同操作共享数据。因此,当存在多个线程操作共享数据的时候,需要保证同一时刻有且只有线程在操作共享数据,其他线程必须等到该线程处理完才能进行,这种方式叫做互斥锁。Java中,关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或者某个代码块,同时它还可以保证一个线程(共享数据)的变化被其他线程所看到(可见性保证,完全可以替代Volatile功能)
synchronized是Java的关键字,是一种同步锁
Java的内置锁(synchronized):每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,退出同步代码块的时候会释放该锁。获得内置锁的唯一途径就是进入锁保护的同步代码块/方法。
Java的对象锁和类锁:在锁的概念上与内置锁一致,但对象锁是用于对象实例方法或对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。
Java中每个对象都有一把锁和两个队列,一个队列用于挂起未获得锁的线程,一个队列用于挂起条件不满足而等待的线程。synchronized实际上是一个加锁和释放锁的集成。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁,计数归零。线程第一次给对象加锁的时候,计数变成1。每当这个相同的线程在此对象上获得锁的时候,计数就会递增。每当任务离开一个synchronized方法,计数就会递减,为0的时候锁被完全释放。
Synchronized有三种应用方式:

  • 修饰一个实例方法:被修饰的方法称为实例同步方法,其作用范围是整个方法,锁定的事该方法所属的对象(调用该方法的对象)。所有需要获得该对象锁的操作都会对该对象加锁。
  public synchronized void method(){}
  //等同于
  public void method(){
    synchronized(this){
    }
  }

如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其他线程不能同时访问这个对象中任何一个synchronized方法。
当一个对象O1在不同的线程中执行这个同步方法的时候,会形成互斥。但是O1对象所属类的另一对象O2是可以调用这个被加了synchronized关键字的方法的。其他线程调用O2中的相同方法时不会造成同步阻塞。程序可能在这种情况下摆脱同步机制的控制,造成数据混乱。注意:
(1)synchronized关键字不会被继承:子类覆盖父类带synchronized方法的时候,必须也要给子类的这个方法显式的增加synchronized关键字。
(2)定义接口的时候不能使用synchronized关键字。
(3)构造方法不能使用synchronized关键字,但可以使用synchronized代码块完成同步。

  • 修饰一个静态方法:被修饰的方法被称为静态同步方法,其作用域是整个静态方法,锁是静态方法所属的类。
 public synchronized static void method(){}
  • 修饰代码块:被修饰的代码块被称为同步语句块。synchronized的括号中必须传入一个对象作为锁,作用范围是大括号中的代码,锁是synchronized括号中的内容,可以分为类锁和对象锁
//锁对象为实例对象
 public void method(Object o){
  synchronized(o){
   ...
  }
 }
//锁对象为类的Class对象 
 public class Demo{
   public static void method(){
      synchronized(Demo.class){
       ...
      }
   }
 }

27.Synchronized的原理

实际上是通过monitor(监视器)。Java中的同步代码块是使用monitorenter和monitorexit指令实现的,其中monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入同步代码块的结束位置。JVM保证这两个指令成对出现。
当执行monitorenter指令的时候,线程试图获取锁也就是获取monitor对象的所有权,当计数器为0的时候就可以成功获取,获取后将计数器加一。在执行monitorexit指令之后,将锁计数器减一,表明锁被释放。
synchronized修饰方法的时候,没有monitorenter和monitorexit指令,取而代之的是ACC_SYNCHRONIZED标识,这个标识指明这个方法是一个同步方法。


28.Synchronized的四种状态

无锁-->偏向锁-->轻量级锁-->重量级锁(过程不可逆)

  • 偏向锁:大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得;如果一个线程获得了锁,锁进入偏向模式,此时对象头的Mark Word结构也变为偏向锁结构。对象头在第十章节中提到过,另外这篇文章讲的更详细。当该线程再次请求锁的时候,只需要检查Mark Word锁标记为是否为偏向锁,以及当前线程ID是不是等于Mark Word的Thread Id即可,省去了大量有关锁申请的操作。偏向锁只适用于只有一个线程访问同步块的场景。
  • 轻量级锁:当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。适用于追求响应时间,同步快执行速度非常快的情况。
    代码在进入同步块的时候,如果同步对象锁状态是无锁,虚拟机首先在当前线程的栈帧中创建锁记录(Lock Record)空间,拷贝对象头的Mark Word复制到锁记录中。之后虚拟机使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record的owner指针指向对象的Mark Word。如果这个动作成功了,那么这个线程就有了该对象的锁,对象的锁标记为设置为“00”,说明处于轻量级锁定状态。如果这个动作失败了,JVM检查对象的Mark Word是否指向当前线程的栈帧,是则说明当前线程已经拥有了这个对象的锁,否则说明多个线程竞争锁。
    如果有两个以上的线程竞争同一个锁,轻量级锁不再有效,膨胀为重量级锁。
  • 重量级锁:多线程情况,线程阻塞响应时间缓慢,频繁的释放获取锁会带来巨大的性能损耗。适用于追求吞吐量,同步快执行速度较长的情景。

29.Synchronized与重入锁ReentrantLock的区别
  • 相对与ReentrantLock而言,synchronized锁是重量级的,而且是内置锁,意味着JVM可以对synchronized锁做优化。
  • 在synchronized锁上阻塞的线程是不可中断的,而ReentrantLock锁实现了可中断的阻塞。
  • synchronized锁释放是自动的,而ReentrantLock需要显式释放(在try-finally块中释放)
  • 线程在竞争synchronized锁的时候是非公平的:如果synchronized锁被线程A占有,线程B请求失败,被放入队列中,线程C此时来请求锁,恰好A在此时释放了,线程C会跳过队列中等待的线程B直接获得这个锁。但是ReentrantLock可以实现锁的公平性。
  • synchronized锁是读写和读读都互斥,ReentrankWriteLock分为读锁和写锁,读锁可以同时被多个线程持有,适合于读多写少的并发场景。
  • ReentrantLock只能锁代码块,但是synchronized可以锁方法和类。ReentrantLock可以知道线程有没有拿到锁,但是synchronized不行。

有关synchronized的参考文章


30.锁优化

28章节中,我们提到过重量级锁,在重量级锁中,JVM会阻塞未获取到锁的线程,在锁被释放的时候唤醒这些线程,阻塞和唤醒依赖于操作系统,需要从用户态切换到内核态,开销很大。monitor调用了OS底层的互斥量(mutex),切换成本很高。因此JVM引入了自旋的概念。

  • 自旋锁与自适应自旋锁,CAS实现:
    自旋锁:很多情况下,共享数据的锁定状态持续时间短,切换线程不值得;通过让线程执行忙循环等待锁的释放,不让出CPU,缺点是如果锁被其他线程长时间占用,带来很多开销。
    自适应自旋锁:自旋的次数不固定,由前一次在同一个锁上的自旋时间和锁的拥有者状态来决定。
    优点:自旋锁不会使线程状态发生改变,一直处于用户态,不会使线程阻塞,执行速度快。
    CAS(Compare And Swap) 乐观锁与悲观锁:synchronized操作就是悲观锁,这种情况线程一旦得到锁,其他需要锁的线程就挂起的情况是悲观锁;CAS操作实际上是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果失败了就重试,直到成功为止。悲观在认为程序中的并发情况严重,乐观在于并发情况不那么严重,可以多次尝试。
  • 锁消除:虚拟机在即时编译器运行时,对一些代码上要求同步而被检测到实际不可能存在共享数据竞争的锁进行消除。依据是:JVM会判断一段程序中的同步明显不会逃逸出去从而被其他线程访问,JVM就把它们当作栈上的数据对待,认为这些数据是线程独有的。
  • 锁粗化:在加同步锁的时候,我们尽量的把同步块的作用范围限制到尽量小的范围。但是如果存在一连串的操作都对同一个对象反复加锁解锁,甚至加锁出现在循环体内,即使没有线程竞争,频繁的进行互斥同步也会导致消耗。
public static String test04(String s1, String s2, String s3) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        sb.append(s3);
        return sb.toString();
    }

上述连续的append操作就属于这类情况,jvm检测到一连串操作都是对同一个对象加锁,就会把锁同步范围扩展(粗化)到整个一系列操作的外部,使得一连串append操作只需要加一次锁就可以了。


31.Java设计模式

设计模式是一套被反复使用,多数人知晓的,经过分类编目的,代码设计经验的总结。使用设计模式是为了可重用代码,让代码更容易被他人理解。实际上就是在某些场景下,针对某类问题的某种通用的解决方案
设计模式分为三类:
(1)创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程。包括单例模式、简单工厂、抽象工厂等。
(2)结构型模式:把类和对象结合在一起形成一个更大的结构。包括适配器模式、组合模式、装饰模式等。
(3)行为型模式:类和对象如何交互、及划分责任和算法。包括模板模式、解释器模式、观察者模式等。

单例模式:属于创建型模式,主要有三种写法:懒汉式、饿汉式和登记式。
单例模式的特点
(1)单例类只能有一个实例
(2)单例类必须自己创建自己的唯一实例
(3)单例类必须给所有其他对象提供这一实例

懒汉式:在第一次调用的时候就实例化自己。

  public class Singleton{
    private Singleton(){}
    private static Singleton single = null;
    //静态工厂方法
    private static Singleton getInstance(){
      if(single == null) single = new Singleton();
    }
    return single;
  }

懒汉式并不考虑线程安全问题,所以他是线程不安全的,并发情况下很可能出现多个Singleton实例,要实现线程安全,有以下三个方式:

  • 在getInstance方法上加同步关键字:在并发环境下,多个一起进入getInstance里,因为还没有实例化单例模式,single都是null,就会创建多个Singleton实例化对象,破坏了单例模式想要的结果。我们可以在getInstance方法上加synchronized锁。
 public static synchronized Singleton getInstance(){
   if(single == null) single = new Singleton();
   return single;
 }
  • 双重校验锁定:
 public static Singleton getInstance(){
  if(singleton == null){
    synchronized (Singleton.class){
       if(singleton == null) singleton = new Singleton();
    }
  }
  return singleton;
 }

双重校验锁定的单例仍然需要再加上volatile确保线程安全。

  • 静态同步类:即实现了线程安全,又避免了同步带来的性能影响。
 public class Singleton{
   private static class LazyHolder{
     private static final Singleton INSTANCE = new Singleton();
   }
   private Singleton(){}
   public static final Singleton getInstance(){
     return LazyHolder.INSTANCE;
   }
 }

饿汉式:饿汉式在类创建的同时就已经创建好了一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。

  public class Singleton1{
    private Singleton1(){}
    private static final Singleton1 single = new Singleton1();
    //静态工厂方法
    public static Singleton1 getInstance(){
       return single;
    }
  }

饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例已经存在了;而懒汉比较懒,只有用户调用getInstance的时候,才会初始化这个实例。


32.HashMap

HashMap是怎么实现的:HashMap是用数组+链表+红黑树实现的,当添加一个元素的时候,首先计算元素key的hash值,并根据hash值决定插入数组中的位置,但是可能存在其他元素已经被放在数组同一位置了,就是我们所说的哈希冲突,这个时候使用链表来解决,当链表长度过长的时候,将链表转换成红黑树来提高搜索效率。
HashMap的特点

  • 数组初始容量是16,容量以2的次方扩充,一是为了提高性能使用足够大的数组,二是为了能使用位运算替代模运算。
  • 数组是否需要扩充取决于负载因子,如果当前元素个数为数组容量的0.75时,就会扩充数组。这个0.75就是默认的负载因子。负载因子的选择:如果负载因子很大,那么扩容发生的概率就会降低,但是发生hash冲突的概率就会变大,使得链表变长,可能会影响性能。但是负载因子小的时候,扩容会过于频繁,会占用更多的空间。
  • 为了解决碰撞,数组中的元素是单向链表类型,如果链表长度到达一个阈值的时候(>=8),会将链表转换成红黑树提高性能。当链表长度缩小到另一个阈值的时候(<=6),又会将红黑树转换成单链表。
    jdk1.7时多线程头插法会造成死循环:有两个线程分别进行扩容操作,线程A执行完Entry<K,V> next = e.next后挂起;线程B完整的执行完扩容流程;线程A唤醒,继续之前的往下运行,while循环执行三次之后可能会形成环形链表。因此使用头插法会改变链表节点原有的顺序,如果使用尾插法就不会出现链表成环的问题。
    HashMap和HashTable的区别:HashMap不是线程安全的,允许有null的键和值,是HashTable的轻量级实现;HashTable是线程安全的,方法由synchronize修饰,不允许有null的键和值,效率较HashMap更低。
    参考文章

33.ConcurrentHashMap

如何求size

  • JDK8推荐使用mappingCount方法,这个方法返回值是long型。
  • 没有并发的情况下,使用volatile修饰的baseCount变量就足够了
  • 并发时,CAS修改baseCount失败后会使用CounterCell类,通常会创建一个对象,对象的volatile value属性是1。在计算size的时候,会将baseCount和CounterCell数组中的元素和value累加,得到总的大小。
    ConcurrentHashMap是如何实现的
  • JDK1.7中,整个ConcurrentHashMap由一个个Segment组成,Segment代表“部分”或者“一段”的意思,所以我们把它描述为分段锁。我们可以把每一个Segment看成一个小的HashMap,其内部结构是相同的。
  • JDK1.8中放弃了分段锁的设计,使用的是Node数组+CAS+Synchronized来保证线程的安全性。

34.int和Integer的区别
  • Integer是int的包装类,int是java的一种基本数据类型
  • Integer变量必须实例化后才能使用,而int变量不需要
  • Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int是直接存储数据值。
  • Integer的默认值是null,int的默认值是0
    在32位环境下,Integer对象占用内存16字节,在64位环境下更大。

C++


1.虚函数和纯虚函数

virtual关键字实现虚函数,“虚”在推迟联编上,一个类函数的调用并不是编译时确定的,而是在运行时确定的,编写代码的时候并不知道被调用的是基类的函数还是派生类的函数,所以被定义为虚函数。
c++中的虚函数主要用于实现多态,就是用父类的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。
虚函数是通过虚函数表来实现的,主要存储了一个类的虚函数的地址表,当我们用父类指针来操作子类的时候,虚函数表指明了实际所调用的函数。虚函数表中存储着该类所有虚函数对应的函数指针。虚函数表是全局共享的,在编译时构造完成。虚函数表位于只读数据段(内存模型中的常量区);而虚函数位于代码段(内存模型中的代码区)。
一般情况下,虚函数表一个类只能有一个;在多继承情况下,有几个基类就有几个虚函数表指针
纯虚函数是在基类中声明的虚函数,在基类中没有定义,但要求任何派生类都必须定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型之后加“=0”

  virtual void function1() = 0

含有纯虚函数的类被称为抽象类,它不能生成对象,只能创建他派生类的实例。而虚函数必须被实现。
C++中基类使用虚析构函数是为了防止内存泄漏,具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放;假设基类中采用的是非虚的析构函数,当删除基类指针指向的派生类对象时,就不会触发动态绑定,因而只会调用基类的析构函数,不会调用派生类的析构函数。在这种情况下,派生类中申请的空间得不到释放从而产生内存泄漏。
构造函数不能是虚函数,构造函数在调用时还不存在子类父类的概念,不存在动态绑定;但在构造函数中可以调用虚函数。
静态成员函数不能为虚函数。


2.printf()函数的原理

printf实际上是可变参数表函数。
c/c++中对函数参数的扫描是从后向前的,函数参数压入堆栈来给函数传参,堆栈是先入后出。printf第一个被找到的参数就是字符指针(被双引号扩起来的),函数通过字符串里控制参数的个数来判断参数的个数和数据类型,通过这些算出数据需要堆栈指针的偏移量。
参考文章

C++和C语言中,printf调用遵循**_cdecl**调用规则
cdecl规则:

  • 参数从右向左依次入栈
  • 调用者负责清理堆栈
  • 参数的数量类型不会导致编译阶段的错误

3.全局变量、局部变量、静态全局变量和静态局部变量

全局变量:具有全局作用域,只需要在一个源文件中定义,就可以作用于所有的源文件,其他不包含全局变量定义的源文件需要用extern关键字再次声明这个全局变量。
静态全局变量:区别在于如果程序包含多个文件,它作用于它定义的文件里,不能作用于其他文件,被static修饰过的变量只有文件作用域
局部变量:具有局部作用域,它在程序运行期间不是一直存在,而是只是在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,占用的内存撤回。
静态局部变量:具有局部作用域,全局变量对所有函数都可见,而静态局部变量只对定义自己的函数体始终可见。它只被初始化一次,从被初始化到程序运行结束都存在。

局部变量在中分配空间,其他三种在静态存储区分配空间。
参考文章

**局部变量可以和全局变量重名吗?**这是一个作用域的问题,一个声明将一个变量名引入一个作用域;局部变量的作用域是从声明的一点开始,直到这个声明的所在块结束为止。全局变量的作用域从声明的那一点开始,直到这个声明所在的文件结束。与全局变量重名的局部变量可以屏蔽全局变量


4.C++11的新特性
  • auto关键字:使用auto的时候,编译器会根据上下文情况,确定auto变量的真正的类型。auto在c++14中可以做返回值,但是它做返回值的时候,只能用于定义函数不能用于声明函数。
  • nullptr:
class Test
{
public:
    void TestWork(int index)
    {
        std::cout << "TestWork 1" << std::endl;
    }
    void TestWork(int * index)
    {
        std::cout << "TestWork 2" << std::endl;
    }
};

int main()
{
    Test test;
    test.TestWork(NULL);
    test.TestWork(nullptr);
}
/*运行结果是
TestWork 1
TestWork 2
*/

我们本来想调用第二个函数,但是NULL在c++中代表空指针,会导致错误的调用,但是nullptr解决了这个问题。

  • for循环:
    C++11之后支持像C#中foreach那样的用法
int main()
{
    int numbers[] = { 1,2,3,4,5 };
    std::cout << "numbers:" << std::endl;
    for (auto number : numbers)
    {
        std::cout << number << std::endl;
    }
}
  • STL容器:array:相对于数组,增加了迭代器等函数
  • STL容器:forward_list:与list的区别在于它是单向链表,在插入和删除频繁的场景中,使用list和forward_list要比vector、array和deque效率高很多。
  • STL容器:unordered_map:与map用法相似,但是原理有很大差别,map使用的底层数据结构是平衡二叉树(红黑树),而unordered_map使用的是哈希表,查找效率很高,但存储效率低。使用挂链表的方式解决哈希冲突。
  • STL容器:unordered_set:与set不同的地方是插入的时候不会自动排序,底层也是使用哈希表的方式结构。
  • 多线程中增加了thread线程类,atomic原子数据类型。原子数据类型不会发生数据竞争,能直接用于多线程不必用户对其进行添加互斥资源锁。
    参考文章

5.c++中怎么定义常量?
  • 使用#define预处理器
  • 使用const关键字
    常量,就是在程序执行期间不会改变的变量,常量可以是任意类型的值,定义之后值不能修改。
    const修饰指针指向的内容,则内容为不可变量;修饰指针则指针为不可变量。
    注:const在函数后面加,代表这个函数不修改类对象
 class Myclass{
  public:
   int GetData(int Id) const;
 }

参考文章


6.左值与右值

C++中,左值是一个指向内存的变量,右值并不指向内存中的任何东西,右值通常只是暂时的,左值可以理解为右值的容器。可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值。

 int x = 66; //true
 66 = x;     //error

C++中赋值需要一个左值作为他的左操作数。同样我们不可以使用&66这样的作为右值,但是&y可以,66是一个常量一个右值,并没有具体的内存位置。
左值可以转换为右值,通过加减乘除等操作;但右值不可以被转换为左值。
参考文章
左值引用

int a = 10;
int &b = a;  // 定义一个左值引用变量
b = 20;      // 通过左值引用修改引用内存的值

左值引用在汇编层面其实和普通的指针是一样的;定义引用变量必须初始化,因为引用其实就是一个别名,需要告诉编译器定义的是谁的引用。

int &var = 10;  //error
const int &var = 10;  //pass

这个操作称为常引用,因为此时内存上已经存在临时变量保存10,实际上是取了该临时变量的地址,左值引用要求右操作数必须能够取地址。
右值引用
C++11中加入了右值引用。右值引用的格式:类型 && 引用名 = 右值表达式;

int &&var = 10;

在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。
带右值引用参数的拷贝构造和赋值重载函数,又叫移动构造函数和移动赋值函数,这里的移动指的是把临时量的资源移动给了当前对象,临时对象就不持有资源,为nullptr了,实际上没有进行任何的数据移动,没发生任何的内存开辟和数据拷贝。


7.指针常量、常量指针和常指针常量
  • 指针常量:如果在定义指针的时候,指针变量前用const修饰,被定义的指针变量就变成了一个指针类型的常量,指针类型的常量简称为指针常量。指针变量的值不能修改,所以一定要在定义的时候给定初值(指向一个确定存在的地方)。指针变量指向的值可以修改。
 int * const p = 123;
//修改值 *p = 23;
  • 常量指针:指向常量的指针变量。可以修改指针指向的位置,但不能修改指针指向的内容。
 const int *p = 123;
 //修改值 p = NULL;
  • 常指针常量:指针不能改变,指针指向的值也不能改变。
const 数据类型 * const 指针变量=变量名;
数据类型 const  *const 指针变量=变量名;

参考文章


8.C和C++的区别
  • C++和C不仅仅是面向对象和面向过程语言的区别那么简单,经过多次更新,现代C++应该已经包容了多种编程范式(面向过程面向对象泛型编程函数式编程)。C语言也可以实现面向对象编程,比如Linux图形界面GNOME。
  • C++虽然比C功能多,但大多数都是在编译器层面上进行改进,其实影响的是编译速度,而不是影响运行速度。
  • C++的标准库比较丰富,几乎可以说是C的超集
  • C++的namespace解决重名问题

9.多重继承

常规情况下,一个类只有一个基类,C++支持多重继承,一个类可以同时继承多个类。
优点:对象可以调用多个基类的接口。
缺点:多继承的类,如果调用基类的基类的方法,容易出现二义性。
参考文章


10.C++中new/delete和malloc/free的区别
  • new delete是c++中的操作符,malloc和free是标准库函数。
  • malloc free是库函数而不是运算符,不在编译器控制范围之内,不能够自动调用构造函数和析构函数。而NEW在为对象申请分配内存空间时,可以自动调用构造函数,同时也可以完成对对象的初始化。同理,delete也可以自动调用析构函数。而malloc只是做一件事,只是为变量分配了内存,同理,free也只是释放变量的内存。
  • new返回的是指定类型的指针,并且可以自动计算所申请内存的大小。而malloc需要我们计算申请内存的大小,并且在返回时强行转换为实际类型的指针。
p = new int;
p = (int *) malloc (sizeof(int));

11.智能指针

智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,当函数结束时会自动释放。
智能指针的种类

  • auto_ptr:不支持赋值和复制,不能被放入到容器中。
  • unique_ptr:不支持赋值和复制,直接赋值会编译出错。move()方法可以将所有权转移,之前的指针称为无效指针
  unique_ptr<int> p3 = move(p1); //p1无效 转移到p3
  • shared_ptr:基于引用计数的智能指针,可以任意赋值,直到内存的引用计数为0的时候,这个内存会被释放。
  • weak_ptr:弱引用,引用计数的问题是互相引用导致成环,两个指针指向的内存都无法释放。可以用weak_ptr或者手动打破循环来释放。weak_ptr只引用,不计数,如果一块内存被shared_ptr和wear_ptr同时引用,当所有shared_ptr析构之后,不管有没有weak_ptr引用,内存都会被释放。所以它不能保证它指向的内存一定有效,在使用前要先检查是否是空指针。

参考文章


12.C++定义一个空类的时候,会生成哪些函数

会生成默认构造函数、默认拷贝构造函数、默认析构函数、默认赋值运算符、取地址运算符和常量取地址运算符。

class Empty{
public:
  Empty(); //构造函数
  Empty(const Empty&); //拷贝构造函数
  ~Empty(); //析构函数
  Empty& operator=(const Empty&); //赋值运算符
  Empty* operator&(); //取地址运算符
  const Empty* operator&() const; //常量取地址运算符
}

参考文章


13.内联函数Inline

函数的存在意义是避免将相同代码重写,但会带来程序运行时间上的开销。函数调用在执行的时候,首先在栈中为形参和局部变量分配存储空间,然后将实参的值复制给形参,将函数的返回地址放入栈中,最后才跳转到函数内部执行。函数执行return的时候,需要从栈中回收形参和局部变量的空间,从栈中取出返回地址。
inline内联函数就是为了解决函数调用开销,当编译器处理调用内联函数的语句时,不会将该语句编译成函数调用的指令,而是直接将整个函数题的代码插入调用语句处。内联函数中的代码应该是简单而执行速度快的语句,不能使用循环、switch和递归语句。
inline和define的区别

  • 内联函数会在编译时展开,而宏是由预处理器对宏展开
  • 内联函数会检查参数类型,但是宏不会检查,内联函数更安全
  • 宏不是函数,inline是函数

14.强制类型转换
  • static_cast关键字:static_cast用于数据类型的强制转换。采用static_cast进行强制类型转换的时候,想要转换成的类型放到尖括号中,待转换的变量或表达式放在圆括号中。需要的头文件是cstblib。
  static_cast<类型说明符>(变量或表达式);

static_cast的应用场景主要包括:基本数据类型之间的转换、空指针转换成目标类型的空指针,任何类型的表达式转换成void类型。

  • const_cast关键字:const_cast主要用于去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或者引用。
  const_cast<type_id>(expression);
  • reinterpret_cast关键字:主要用于改变指针或引用的类型、将指针或引用转换成一个足够长度的整型、将整型转换为指针或引用类型。reinterpret_cast可以将指针或引用转换为一个足够长度的整形,此中的足够长度具体长度需要多少则取决于操作系统,如果是32位的操作系统,就需要4个字节及以上的整型,如果是64位的操作系统则需要8个字节及以上的整型。

  • dynamic_cast关键字:其他三种都是编译时完成,而它是在运行时处理,运行时进行类型检查;不能用于内置的基本数据类型的强制转换;转换成功返回指向类的指针或引用,失败则返回NULL;使用dynamic_cast进行转换,基类中一定要有虚函数,否则编译不通过。
    类中只有存在虚函数,说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。


15.volatile关键字的作用

volatile与const关键字对应,用来修饰变量。编译器对volatile修饰的变量,当要读取这个变量的时候,任何情况下都会从内存中读取而不是从寄存器缓存中读取。可以防止在共享的空间发生读取的错误,保证其可见性,不保证原子性。
volatile的底层原理参考操作系统中的内存屏障


16.预编译

预编译又叫预处理,是做些代码文本的替换工作。处理以#开头的指令,比如拷贝#include包含的文件代码,#define宏定义的替换,#if/#endif/#else/#elif条件编译等,实际上就是为编译做预备工作。
C/C++源程序到可执行文件的阶段:
源代码->编译预处理->编译->汇编程序->链接程序->可执行文件。

**有关inlude:**include<>和include " " 有什么区别?include<>会先到c标准库中寻找文件,include " "会先从当前目录中找文件。include语句一般用来包含标准头文件,因为这些头文件一般不会被修改。非标准的头文件一般存在当前目录下,可以经常修改它们。


17.堆和栈的区别

一个由C/C++编译的程序占用的内存分为几个部分:

  • 栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量等。操作方式类似于数据结构中的栈。
  • 堆区(heap):一般由程序员分配释放,如果程序员不释放,程序结束之后由OS回收,和数据结构中的堆不同,分配方式更类似于链表。C中的malloc函数和C++的new运算符都是申请堆内存,并指明大小。
p1 = (char *)malloc(10);
p2 = new char[10];
//注意p1、p2本身在栈中
  • 全局区(静态区):全局变量和静态变量的值存在一起,程序结束后系统释放。
  • 文字常量区:保存常量字符串。
  • 程序代码区:存放函数体的二进制代码。
    堆和栈空间的申请
    栈:只要栈的剩余空间大于申请空间,系统就分配内存,否则报栈溢出错误。
    堆:OS中有一个记录空闲内存地址的链表,系统收到程序申请之后,遍历该链表寻找第一个空间大于所申请空间的堆节点用于分配。对于大多数系统,会在这块内存空间的首地址记录本次分配的大小,方便delete正确释放空间。
    申请大小限制
    栈:向低地址扩展的数据结构,是一块连续的内存区域,在windows栈的大小是2M。
    堆:向高地址扩展的数据结构,不连续的内存。堆的大小受限于计算机系统中有效的虚拟内存。堆的空间更大。
    堆和栈中的存储内容
    栈:在函数调用时,第一个进栈的是主函数调用的下一条可执行语句的地址,然后是函数的各个参数,在大多数C编译器中,参数从右向左入栈,然后是函数中的局部变量。静态变量不入栈。
    堆:一般在堆的头部用一个字节来存放堆的大小,具体内容由程序员安排。

18.重载、重写(覆盖)和隐藏
  • 重载:在同一作用域中,同名函数的形参不同的时候,构成函数重载。类的静态成员函数与普通成员函数可以形成重载;函数重载发生在同一作用域。操作符也可以重载。
  • 隐藏:指不同作用域中定义的同名函数构成隐藏(不要求函数返回值和函数参数类型相同)。比如派生类成员函数隐藏与自己同名的基类成员函数、类成员函数隐藏全局外部函数。隐藏的实质是在查找函数时,名字查找先于类型检查。
class HideA{
public:
	void hidefunc(){
		cout << "HideA function" << endl;
	}
};

class HideB : public HideA{
public:
	void hidefunc()	{
		cout << "HideB function" << endl;
	}

	void usehidefunc()	{
		//隐藏基类函数hidefunc,使用外部函数时要加作用域
		hidefunc();
		HideA::hidefunc();
	}
};
  • 重写(覆盖):派生类中与基类同返回值类型、同名和同参数的虚函数重定义,构成虚函数覆盖,也叫虚函数重写。
    协变:只要原来的返回类型是指向类的指针或引用,新的返回类型是指向派生类的指针或引用,覆盖的方法就可以改变返回类型。
class Base{
public:
    virtual A& show()    {
        cout<<"In Base"<<endl;
        return *(new A);
    }
};

class Derived : public Base{
public:
     //返回值协变,构成虚函数重写
     B& show(){
        cout<<"In Derived"<<endl;
        return *(new B);
    }
};

参考文章


19.动态编译和静态编译
  • 静态编译在编译时,把所有模块都编译进可执行文件,当启动这个可执行文件的时候,所有模块都能成功加载;这样做代码的装载速度快,执行速度也比较快。代价是程序体积会变大,如果静态库需要更新,程序需要重新编译。每有一个应用程序使用,就会被装载一次,浪费内存。
  • 动态编译是将应用程序需要的模块都编译成动态链接库,启动程序的时候,这些模块不会被加载,运行时用到哪个模块就调用哪个。优点是多个应用程序可以使用一个动态库,而不需要在磁盘上存储多个拷贝。缺点是运行时加载可能会影响程序的前期执行性能。

20.循环依赖

如果存在两个类A、B使得A类中含有B类的对象且B类中包含A类的对象,则称A、B之间存在循环依赖。

class A{
  public:
    B b;
};
class B{
  public:
    A a;
};

如果存在这样的循环依赖就会在编译时报错,无法给两个类分配具体的空间。
解决循环依赖的方法
(1)使用指针代替变量声明,这样编译的时候就不会报错,指针类型就是4个字节(在32位系统下所有类型的指针是4字节,但是在64位系统下指针是8字节),编译器已经知道了AB类所占内存的大小。

class A{
  public:
    B *b;
};
class B{
  public:
    A *a;
};

(2)如果AB相互包含说明这两个类的耦合度很高,可以为它们声明一个类,然后使用派生,将A、B声明为这个类的子类。


21.模板偏特化

模板是泛型编程的基础,是以一种独立于任何特定类型的方式编写代码。

//函数模版
template<typename type>
ret-type func-name(parameter list){//函数主体}
 
 //类模版
template<class type>
class class-name{}

在代码中包含函数模板,它只是生成函数的方案。模板特化是指函数模板或类模板实例化为特殊的类型,通过模板特化可以定制特定的模板参数下的函数模板或类模板的实现。

 template<typename T>
 bool compare(T a,T b) return a<b;

像上面的代码是一个比较同类型的的值的函数模板,一般的类型如int、float都可以用上述函数模板进行比较,但字符数组类型如何比较,比较字符串首地址是行不通的,这个时候就要用特化函数模板。

 template<>
 bool compare(const char* s1.const char* s2){
   return strcmp(s1,s2) < 0;
 }

这个版本的函数模板定义template<>,参数为空,相当于用const char* 实例化了。
特化分全特化和部分特化两种,全特化指模板参数为空。

//部分特化
template<typename T>
void testfun(T param1,int){}

我们也可以用特化禁止某些条件下的模板调用。

 template<typename T>
 void testfun(int a,T b) = delete;

类模板特化与函数模板特化类似:

template<typename T>
class Test<int , T>{
}

参考文章


22.struct和class

C++中的struct和class基本是通用的,但有几个细节不同。

  • 使用class的时候,类中的成员默认都是private属性的;使用struct的时候,结构体中的成员默认是public属性的。
  • class继承默认是private继承,而struct继承默认是public继承。
  • class可以使用模板,但是struct不能。

23.友元函数

类的友元函数式定义在类外部,但有权访问类的所有私有成员和保护成员。友元函数并不属于成员函数,友元可以是一个函数,也可以是一个类(友元类),在这种情况下,整个类及其所有成员都是友元。
如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用friend关键字。

  class Box
{
   double width;
public:
   double length;
   friend void printWidth( Box box );
   void setWidth( double wid );
};

如果要声明类B的所有成员函数作为类A的友元,需要在A的定义中声明:

 friend class B;

**友元函数的参数:**因为友元函数没有this指针,所以参数有三种情况:

  • 访问非static成员时,需要对象做参数;
  • 访问static成员或全局变量时,不需要对象做参数;
  • 如果做参数的对象时全局对象,则不需要对象做参数。
class INTEGER
{
    friend void Print(const INTEGER& obj);//声明友元函数
};

void Print(const INTEGER& obj)
{
    //函数体
}
24.运算符重载

重载的运算符是带有特殊名称的函数,函数名是由关键字operator和其后要重载的运算符符号所构成的,与其他函数一样,重载运算符有一个返回类型和一个参数列表。

Box operator+(const Box&);{
    Box box;
    box.length = this->length + b.length;
    box.breadth = this->breadth + b.breadth;
    box.height = this->height + b.height;
    return box;
}

以上例子重载了 ‘+’运算符,用于把两个Box对象相加。
不能重载的运算符

  • 成员访问运算符:"."
  • 成员指针访问运算符: “.” , “->
  • 域运算符:"::"
  • 长度运算符: "sizeof"
  • 条件运算符: "?:"
  • 预处理符: "#"

能够重载的运算符和运算符重载实例


25.explicit关键字

explicit关键字的作用就是防止类构造函数的隐式自动转换。
explicit关键字只对有一个参数的类构造函数有效,如果类构造函数大于等于两个时,是不会产生隐式格式转换的。但是当除了第一个参数以外,其他的参数都有默认值的时候,参数大于两个也会让explicit有效。

Class Demo{
  public:
    Demo(); //无参数,不能格式转换
    Demo(double a); //可以格式转换
    Demo(int a,double b); //两个参数且无默认值,不能格式转换
    Demo(int a,int b=10,double c=1.6); //可以进行转换
};
  
  //隐式格式转换的例子
  Demo test;
  test = 10.2; //这里实际上是把10.2隐式转换为Demo类型

使用explicit的情况:

 class Demo{
  public:
    Demo();
    explicit Demo(double a);
 };

此时,就不能进行隐式转换,但是还是可以进行显式类型转换,如:

  Demo test;
  test = Demo(12.2);
  //或者
  test = (Demo)12.2;

26.动态绑定与静态绑定

绑定(Binding)是指将变量和函数名转换为地址的过程。
对象的静态类型指的是对象在声明时采用的类型,是在编译时决定的;动态类型是指目前所指对象的类型,是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。
静态绑定绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;动态绑定绑定的是动态类型,对应的函数或属性依赖于对象的动态类型,发生在运行期。
非虚函数一般都是静态绑定,虚函数都是动态绑定,如此才能实现多态


27.零拷贝技术

零拷贝主要的任务是避免CPU将数据从一块拷贝到另一块存储,利用零拷贝技术,避免CPU做大量的数据拷贝任务,或者让别的组件来完成这一类简单的数据传输任务,让系统资源的利用更加有效。

  • 使用直接I/O的数据传输
  • 利用mmap():在Linux中,减少拷贝次数的一种方法是调用mmap()来代替调用read,如:
  tmp_buf = mmap(file,len);
  write(socket,tmp_buf,len);

28.成员初始化列表

调用构造函数时,对象将在括号中的代码执行前被创建,因此无法在括号内初始化对象的常量成员。如

Queue::Queue(int qs)
{
    front = rear = NULL;
    items = 0;
    qsize =qs;    // not acceptable! qsize是一个常量成员
}

因此C++提供了成员初始化列表来完成上述工作。它由逗号分隔的初始化列表组成(前有冒号),位于参数列表的右括号后,函数体左括号之前。初始化器为数据成员名(初值)。初值可以是常量、可以是构造函数参数列表中的参数。

Queue::Queue(int qs) : qsize(qs), items(0){
  front = rear = NULL;
}

只有构造函数可以使用初始化列表语法。
对于本身就是类对象的成员来说,使用成员初始化列表的效率更高。
被声明为引用的类成员必须使用这种语法。
数据成员初始化的顺序与它们在类中声明的顺序相同,与初始化器排列顺序无关。
成员初始化列表使用的括号方式也可以用于常规初始化。

  int games = 162;
  //等同于
  int gams(162);

29.STL的基本组成

STL是由容器、算法、迭代器、函数对象、适配器和内存分配器这六部分组成。其中后面四个部分主要为容器和算法服务。

  • 容器:一些封装数据结构的模版类,如vector向量容器和list列表容器。
  • 算法:STL提供了约100个多数据结构算法,被设计成一个个的模板函数,其中大部分算法包含在头文件algorithm中,少部分位于numeric中。
  • 迭代器:对容器中数据的读写操作可以通过迭代器完成。
  • 函数对象:如果一个类将()运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(仿函数)。
  • 适配器:可以使一个类的接口适配成用户指定的形式。
  • 内存分配器:为容器类模板提供自定义的内存申请和释放功能。

C++标准中共有13个STL头文件


30.map和set的区别

map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。他们的区别在于:

  • map中的元素是key-value键值对,Set与之相对的就是关键字的简单集合,每个元素只包含一个关键字。
  • set的迭代器是const的,不允许修改元素的值;map允许修改value但不允许修改key;因为map和set都是根据关键字排序来保证有序性的,如果允许修改key的话,首先要删除键,调节平衡再插入修改后的键,严重破坏了结构和iterator的指向。
  • map支持下标操作,但Set不支持下标操作,但find方法查找效率很高。
    Set容器能够实现去重+排序

31.C++20
  • 新增模块(Modules):没有头文件,声明实现可以分离,可以显式指定导出哪些类和函数,模块之间名称可以相同,只处理一次,编译更快。对应新的关键字import和export。
//创建模块
export module cppcon;
namespace CppCon{
  auto GetWelcomeHelper() {return "welcome";}
  export auto GetWelcome(){
     return GetWelcomeHelper();
  }
}
//引用模块
import cppcon;
int main(){
  cout<< CppCon::GetWelcome();
}
  • Ranges:代表一串元素或者一串元素中的一段,类似于begin/end对,可以防止begin/end不配对。
  vector<int> data{12,23,34};
  sort(begin(data),end(data));
  sort(data);//使用 ranges
  • 协程(Coroutines):是一个函数,可以简化异步IO、延迟计算等问题
    (1)co_wait关键字:挂起协程
    (2)co_return关键字:从协程返回
    (3)co_yield:弹出一个值,挂起协程,下一次调用继续协程的运行
    (4)for co_await:循环体
  • 原子智能指针(Atomic)
  • ...

32.vector的emplace_back和push_back方法
  • push_back要在调用处构造一个类,传递进push_back参数内再拷贝进缓冲区;emplace_back在内部直接将构造class的参数转发构造到缓冲区。
  v.push_back(Class(10));
  //等价于
  v.emplace_back(10);

push_back比emplace_back多了一次拷贝操作,因此emplace_back的效率更高


33.红黑树

红黑树的特性:是一棵特殊的排序二叉平衡树。

  • 每个节点只能是红色或者黑色。
  • 根节点一定为黑色
  • 每个叶子结点都是黑色的空节点
  • 从根节点到叶子结点,不会出现连续的红色节点。
  • 从任何一个节点到叶子结点,每条路径上都有相同数目的黑色节点。
    相比于AVL的优势:在查询性能上,平均时间复杂度都是O(logN),但是平衡二叉树要比红黑树稍好,红黑树比AVL树会不平衡最多一层。
  • 插入删除上红黑树比AVL要好得多。平衡树要求每个节点的左子树和右子树高度差不超过1。导致删除和插入节点的时候,很容易破坏平衡树的这个性质,我们都需要左旋和右旋进行调整。红黑树不会像平衡树一样容易被破坏规则,因此不需要频繁调整。
  • 如果插入一个节点导致了树的不平衡,AVL和红黑树都最多只需要一个2次旋转操作,都是O(1);但是在删除节点时,AVL需要维护从根节点到被删节点路径上所有节点的平衡性,需要旋转的量级是o(logN),红黑树最多需要3次旋转。
  • 多读少写的情况下用AVL,频繁的插入删除用红黑树。

34.排序的C++实现
  • 冒泡排序:用循环使数组元素按顺序两两比较,通过数值互换将比较大的向下沉。时间复杂度最好是O(n),最坏是O(n^2),空间复杂度O(1),是一种稳定的排序。

  • 选择排序:现将n个数中最小的数与第一位相换,再将2~n个数中最小的与第二位相换...一共比较n-1轮。时间复杂度O(n^2),空间复杂度O(1),是一种不稳定的排序。

  • 插入排序:为元素寻找插入点,然后把这个点的所有元素依次后移;对于排序完成度越高的数组,插入排序的效率越高,也可以使用二分插入法。时间复杂度最好为O(n),最差为O(n^2),空间复杂度是O(1),是一种稳定的排序。

  • 快速排序:快速排序的每一次遍历都会把要排序的数据分割成两部分,其中一部分的所有数据都比另外一部分小;再递归的对这两部分数据再进行快速排序,直到整个数据序列有序。本质上看快速排序是简单排序基础上的递归分治法。最优时间复杂度O(nlog2n),最差时间复杂度O(n^2),空间复杂度是O(logn),是一种不稳定的排序。

  • 堆排序:利用大顶堆/小顶堆的性质来进行排序的算法:子结点的键值总是小于/大于他的父结点。堆排序的时间复杂度就是O(log2n),空间复杂度是O(1),是一种不稳定的排序。

排序方式平均时间复杂度最差时间复杂度最好时间复杂度空间复杂度稳定性
插入排序O()O()O()O(1)稳定
希尔排序O()O(1)不稳定
冒泡排序O()O()O()O(1)稳定
快速排序O()O()O()O()不稳定
选择排序O()O()O()O(1)不稳定
堆排序O()O()O()O(1)不稳定
归并排序O()O()O()O(N)稳定
基数排序O(d(n+r))O(d(n+r))O(d(n+r))O(r)稳定

35.引用、指针与数组

指针和引用的区别:

  • 指针是一个变量,存储的是地址;引用和原来的变量实质是一个东西,是一个别名。
  • 可以有多级指针,但只能有一级引用。
  • 指针可以为空,但引用不能为NULL且定义时必须初始化
  • 指针在初始化之后可以改变指向,但是引用在初始化之后不能改变。
  • sizeof指针得到的是指针的大小,sizeof引用得到的是引用所指向变量的大小
  • 引用的本质其实是一个指针
  • 参考本站另一篇文章

**指针和数组的区别:**数组要么在静态存储区被创建(全局数组),要么在栈上被创建。数组名对应着(不是指向)一块内存,其地址与容量在生命周期内保持不变,只有数组的内容能够发生改变。指针可以随时指向任意类型的内存块,它的特征是可变,所以我们常用指针来操作动态内存。

  • 数组保存数据,而指针保存地址
  • 数组直接访问数据;指针间接访问数据,先取得指针的内容,然后以它为地址取得数据
  • 数组用于存储数目固定且类型相同的数据;指针通常用于动态数据结构
  • 数组编译器自动分配和删除;指针动态的分配和删除
  • 相关文章

36.内存泄漏

内存泄漏场景

  • 堆内存溢出:堆内存指的是程序运行中根据需要分配,通过malloc,new等从堆中分配的一块内存,完成后必须调用对应的free或者delete删去。如果程序设计错误导致这一块内存没有被释放,那么这一块内存就会产生Heap Leak,这是最常见的内存泄漏;
  • 系统资源泄露:主要指程序使用系统分配到的资源如Bitmap、handle、socket等没有使用相应的函数释放掉,导致系统资源的浪费或导致系统性能降低,运行不稳定。

检测内存泄露的工具

  • memwatch:检测多次调用free和free错误地址的问题;检测内存访问的上越界和下越界;检测对野指针的写操作。
$ g++ -g -DMEMWATCH -DMW_STDIO  memwatch/memwatch.c example07.cpp
$ ./a.out

目录下出现memwatch.log日志文件,有泄露检测结果和越界检测结果。


Mysql数据库

1.mysql数据库的四种隔离级别

数据库的事务操作实际上是一组原子性的操作,要么全部操作成功,要么全部操作失败。
并行事务的四个问题:
(1)更新丢失:和别的事务读到相同的东西,各自写,自己的写被覆盖了。
(2)脏读:读到别的事务未提交的数据。
(3)不可重复读:两次读之间有别的事务修改。
(4)幻读:两次读之间有别的事务增删。
四种问题生动的例子
隔离级别

  • 读未提交(READ UNCOMMITTED):事务还没有提交,别的事务可以看到他其中修改的数据,这样就会造成脏读。这种隔离级别尽量不要使用,可能造成脏读、不可重复读和幻读
  • 提交读(READ COMMITTED):大多数数据库系统的默认隔离级别是提交读,只能看到已经完成的事务的结果,正在执行的事务是不能被其他事务看到的,会造成读取旧数据的现象。可能造成不可重复读和幻读。
  • 可重复读(REPEATABLE READ):解决了脏读问题,保证每行记录的结果是一致的。但无法解决幻读,指某个事务在读取某个范围的数据,但是另一个事务又向这个范围的数据去插入数据,导致多次读取每次数据行数不一致。
  • SERIALIZABLE(可串行化):最高隔离级别,强制事务串行执行,避免四种问题,但是它加了大量锁,导致大量请求超时,性能会比较低下。适合并发量小的情况。

2.mysql的常用存储引擎

存储引擎是数据库的核心,对于mysql来说,存储引擎是以插件形式运行的。mysql5之后,支持的存储引擎有十几个,但是常用的只有几种,默认使用InnoDB

  • MyISAM:每个MyISAM在磁盘上存储三个文件:
    (1)frm文件:存储表的定义数据
    (2)MYD文件:存放表具体记录的数据
    (3)MYI文件:存储索引。仅保存记录所在页的指针,索引的结构是B+树。
    支持数据类型有三种:
    (1)静态固定长度表:存储速度非常快,容易发生缓存,表发生损坏后容易修复。缺点是占空间。这是默认存储格式。
    (2)动态可变列表:节省空间,出错恢复麻烦。
    (3)压缩表:数据文件发生错误的时候,可以使用check table工具来检查,可以使用repair table恢复。
    MyISAM不支持事务,这意味着他的存储速度更快。
  • InnoDB:默认数据库引擎。支持事务
    (1)可以自动增长列(auto_increment)
    (2)支持事务。默认隔离级别为可重复读,通过MVCC(并发版本控制)来实现。
    (3)使用的锁粒度为行级锁,可以支持更高的并发。
    (4)支持外键约束,增加表的耦合度,降低了表的查询速度。
    (5)配合热备工具可以实现在线热备份。
    (6)存在缓冲管理,通过缓冲池,将索引和数据全部缓存起来,加快查询速度。
    (7)表实际是聚簇表,所有数据按照主键来组织。数据和索引放在一起,都位于B+树的叶子结点上。
  • Memory:将数据存在内存里,提高数据的访问速度,每一个表实际上和一个磁盘文件关联。
    (1)支持的数据类型有限制,不支持TEXT和BLOB类型,字符串类型的数据只支持固定长度的行,varchar会被自动存储为char。
    (2)默认使用hash索引
    (3)支持的锁粒度为表级锁,所以不适合访问量比较大的情况
    (4)如果一个内部表很大,会转换为磁盘表
    (5)由于数据存放在内存中,一旦服务器出现故障,数据都会丢失。
    (6)查询的时候,如果有用到临时表,表中若有BLOB、TEXT类型的字段,那么这个临时表就会转换为MyISAM类型的表,性能会急剧降低。

3.mysql常用的索引类型和种类:

索引类型

  • FULLTEXT:全文索引,只有MyISAM支持,可以在CREATE TABLE,ALTER TABLE,CREATE INDEX使用,不过目前只有CHAR、VARCHAR,TEXT列上可以创建全文索引。
  • HASH:哈希的唯一性和类似键值对形式,很适合做索引,可以实现一次定位。不需要像树形索引逐层查找,因此有很高的效率。但只有在“=”和“in”条件下高效,但范围查询,排序效率依然不高。
  • BTREE:按算法存储到二叉树中。
  • RTREE:很少使用,仅支持geometry数据类型,只有MyISAM、BDb、InnoDb、NDb、Archive支持,适用于范围查找。
    索引种类
  • 普通索引:加速查询
  • 唯一索引:加速查询+列值唯一(可以null)
  • 主键索引:加速查询+列值唯一(不能null)+表中只有一个
  • 组合索引:多列值组成一个索引,专门用于组合搜索
  • 全文索引:对文本内容进行分词再搜索

4.为什么mysql使用B+树做索引

mysql到查询效率取决于磁盘IO次数。一般来说索引非常大,尤其是关系型数据库数据量大的索引能达到亿级别,所以为了减少内存占用,索引存在磁盘上。
B+/B-树的特点是每层节点数目多,层数少,就是为了减少磁盘IO次数,但是B-树的每个节点都有data域(指针),无疑增加了节点大小,而B+树除了叶子结点,其他节点不存储数据。
而AVL树(平衡二叉树)和RB-TREE红黑树(平衡的二叉排序树)基本都是存储在内存中才采用的数据结构,大规模数据存储的时候红黑树会因为树的深度过大而导致IO过于频繁。根据磁盘查找存取的次数往往由树的高度决定。
详细的优劣比较


5.什么样的列需要索引?

如果一个表的数据量很小,就几百条数据,也没什么增长量是没有必要建索引的。每张表上也不要建立太多的索引,会影响数据增删改的性能,耗费更多的磁盘空间。

  • 主键索引:每张表必备
  • 选择性高,重复读低的列(比如身份证号列)
  • 经常出现在查询中的列(where条件中的列)
  • 多表关联查询时作为关联条件的列
  • 值会频繁变化的列不适合建立索引。

6.聚簇索引与非聚簇索引
  • 聚簇索引:InnoDB数据库使用聚簇索引。聚簇索引是顺序结构和数据存储物理结构一致的索引,一个表的聚簇索引只能是唯一的一条。比如在图书馆借书,先去电脑中查书名来定位藏书位置,一目了然,因为藏书的结构和图书室的位置,书架和书本的摆放顺序、书籍的编号都是按照从小到大/从大到小的顺序摆放的,所以很容易找到。这就类似于聚簇索引的功效了。
  • 非聚簇索引:MyISAM使用非聚簇索引。非聚簇索引记录的物理顺序和逻辑顺序没有必然的关联,与数据的存储物理结构没有关系,一个表对应的非聚簇索引可以有很多条。比如去商业性质的图书城,书的摆放往往是按照书上架的先后顺序摆放的,即使有位置,可能摆放编号也不是按照顺序来的。
    聚簇索引和非聚簇索引文章引用

7.慢查询优化

Mysql中的慢查询,全名是慢查询日志,是mysql提供的一种日志记录,用来记录在Mysql中响应时间超过阈值的语句。具体环境中,运行时间超过long_query_time值的语句,就会被记录到慢查询日志中,默认值是10,单位是秒(s),默认情况下,mysql不启动慢查询日志,需要手动设置参数。

  • 索引没起作用的情况:
    (1)使用LIKE关键字的查询语句,如果匹配字符串的第一个字符是‘%’,索引不会起作用。
    (2)使用多列索引的查询语句。对于多列索引,只有查询条件使用了这些字段中的第一个字段的时候,索引才会被使用。
  • 优化数据库结构:
    (1)将字段很多的表分解成多个表
    (2)增加中间表,把经常需要联合查询的表数据放在中间表中。
  • 分解关联查询:将一个大的查询分解成多个小查询。
  • 优化LIMIT分页:系统中需要分页的操作通常使用limit加偏移量的方法实现,同时加上合适的order by子句,如果有对应的索引,通常效率会不错,否则数据库可能需要做大量的文件排序操作。优化方法是尽可能使用索引覆盖扫描
    慢查询相关知乎高赞答案

8.内连接和外连接
  • 内连接查询:组合两个表中的记录,返回关联字段相符的记录,也就是返回两个表的交集。关键字是inner join on。
 select * from a_table a inner join b_table b on a.a_id = b.b_id;
  • 左连接(外连接)查询:left join是left outer join的简写,它的全称是左外连接,是外连接中的一种。左外连接,左表的记录将会全部被表示出来,而右表只会显示符合搜索条件的记录。关键字 left join on。
 select * from a_table a left join b_table b ON a.a_id = b.b_id;
  • 右连接(外连接)查询:与左外连接相反,右连接,左表只会显示符合搜索条件的记录,而右表的记录将会全部表示出来。关键字right join on。
 select * from a_table a right join b_table b ON a.a_id=b.b_id;

9.B树与B+树
  • B树:是特殊的平衡N叉树。
    (1)每个节点最多有m个分支,如果是根节点且不是叶子结点,分枝数最少为2个;非根非叶子结点分枝数最少为m/2个。
    (2)有n个分支的节点,就有n-1个关键字,关键字互不相等。
    (3)关键字把子节点划分成一个个区间
    (4)所有的叶子结点处于同一层
    (5)新加节点和删除节点可能会进行树节点的合并和分裂。
    B树的每一个节点都包含key和value,如果经常访问的元素离根节点很近,访问会很迅速。
  • B+树
    (1)有k个子节点的节点必然有k个关键码。
    (2)非叶子结点只有索引作用,跟数据有关的信息均存放在叶子结点上
    (3)树的所有叶子结点构成一个有序链表,可以按照关键码的排序次序遍历所有记录。
    B+树的内部节点没有指向关键字具体信息的指针,所以内部节点更小;如果把所有同一内部节点的关键字存在同一个盘块中,那么盘块所能容纳的关键字数量也就更多,一次性读入内存中需要查找的关键字也就更多,减少了IO读写次数。
    B+树的叶子结点是相连的,因此对整棵树的遍历只需要一次线性遍历叶子结点就可以了。便于区间搜索和查找

10.数据库事务的四大特性
  • 原子性Atomicity:事务包含的所有操作要么全部成功,要么全部失败回滚。
  • 一致性Consistency:事务必须使数据库从一个一致性状态变换到另一个一致性状态。
  • 隔离性Isolation:多个用户并发访问数据库时,比如操作同一张表,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,要互相隔离。
  • 持久性Durability:一个事务一旦被提交了,那么对数据库中的数据的改变是永久性的,即使在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

11.SQL的聚集(聚合)函数

聚合函数就是对一组值进行运算并返回单一值的函数。除了count(*)以外,聚合函数都会忽略NULL值,聚合函数通常与select语句的group by字句一起使用。所有的聚合函数都是确定性函数,即每次使用一组特定的输入值调用聚合函数时,他们的返回值是相同的。

  • 统计总数\记录数:count(*) 包括空值;count()不包括空值
  • 求某一列的平均数:avg(),如果想把null当作0,可以使用avg(IsNull(score,0))
  • 求和:sum()
  • 求最大值:max()
  • 求最小值:min()

12.数据库的死锁

一.数据库死锁的现象:
程序在执行的过程中,点击确定和保存,程序没有响应,但也没有报错
二.死锁的原理:
当对于数据库某个表的某一列做更新或删除等操作,执行完毕后该条语句不提交,另一条对于这一列数据做更新操作的语句在执行时就会处于等待状态,此时的现象是这条语句一直在执行,但一直没有执行成功,也没有报错。
三.死锁的定位:
通过检查数据库表,能够检查出哪一条语句被死锁,产生死锁的机器是哪一台。

1)用dba用户执行以下语句
select username,lockwait,status,machine,program from v$session where sid in
(select session_id from v$locked_object)
如果有输出的结果,则说明有死锁,且能看到死锁的机器是哪一台。字段说明:
Username:死锁语句所用的数据库用户;
Lockwait:死锁的状态,如果有内容表示被死锁。
Status: 状态,active表示被死锁
Machine: 死锁语句所在的机器。
Program: 产生死锁的语句主要来自哪个应用程序。
2)用dba用户执行以下语句,可以查看到被死锁的语句。
select sql_text from v$sql where hash_value in
(select sql_hash_value from v$session where sid in
(select session_id from v$locked_object))

四.死锁的解决方法
一般情况下,只要将产生死锁的语句提交就可以了,但是在实际的执行过程中,用户可能不知道产生死锁的语句是哪一句。

参考文档


13.关系型数据库和非关系型数据库的区别
  • 数据存储结构不同:关系型数据库都有固定的表结构,不容易进行扩展,而非关系型数据库的存储机制就有很多了,比如基于文档、基于Key-Value键值对、基于图的等,对于数据格式十分灵活。如果业务的数据结构不是固定的或者变动很大的,非关系型数据库是一个好的选择
  • 可扩展性:传统的关系型数据库给人一种横向扩展难,不好对数据进行分片的印象;而一些非关系型数据库则原生就支持数据的水平扩展(如mongoDB的sharding机制)
  • 数据一致性:非关系型数据库一般强调的是数据最终一致性,而不没有像ACID一样强调数据的强一致性,从非关系型数据库中读到的有可能还是处于一个中间态的数据,因此如果业务对数据一致性要求很高,非关系型数据库并不是一个很好的选择

计算机网络

1.TCP三次握手

为了能让数据达到目标,TCP建立连接采用三次握手策略。发送端首先发送一个SYN(synchronize)标志的数据包给接收方。接收端收到后,回传一个带有SYN/ACK(acknowledgement)标志的数据包以示传达确认信息。SYN标志的意义是发送方到接收方的通道没问题,ACK的意义是验证接收方到发送方到通道没问题。最后,发送端回传一个ACK标志的数据包,代表握手结束。
在握手某个阶段莫名中断,TCP协议会再次用相同的顺序发送相同的数据包

  • 为什么要三次握手?最主要的目的是双方确认自己与对方的发送和接受都是正常的
    (1)第一次握手,发送方无法确认任何信息,接收方确认发送方的发送正常,接受方的接收正常;
    (2)第二次握手,发送方确认两方的发送都是正常的,接受方不能确认更多信息
    (3)第三次握手,发送方无法确认更多信息,接收方确认到双方的接受都是正常的
  • 为什么不能两次握手?主要防止已经失效的连接请求报文又传送到了服务器,产生错误,客户端发送的第一个连接请求可能没有丢失,只是因为在网络节点中滞留时间过长。
    (1)由于TCP客户端没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这个报文,此后客户端和服务器经过两次握手建立连接,传输数据然后关闭连接。
    (2)此前滞留的连接请求最终还是到达了服务器,本身这个报文应该失效,但是两次握手的机制会让客户端和服务器又创建了连接,导致不必要的错误和资源浪费
    (3)如果是三次握手,就算是失效报文重新传送过来了,服务器接收到了失效报文并回复确认报文,但是客户端不会再次确认,由于服务器收不到确认,就知道客户端并没有请求连接了。
  • 为什么三次握手返回时ack的值是seq加一?
    假如对方接收到数据,比如seq = 1000,TCP payload = 1000,数据第一个字节编号是1000,最后一个是1999,回应一个确认报文,确认号是2000意味着编号前2000的字节接受完成,准备接受2000之后更多的数据。
    实际是确认收到的序列,并告诉发送端,下次发送的序列号从哪开始。
  • SYN洪范攻击怎么解决?
    (1)SYN是利用TCP协议缺陷,发送大量伪造的TCP连接请求,常用假冒的IP和IP段发来海量的第一个握手包(SYN包),被攻击的服务器回应了SYN+ACK包,因为对方是假冒的IP,所以永远也收不到包也不会回应服务器第三个握手包,导致服务器保持大量的SYN_RECV状态的半连接,使正常合法的syn排不上队列,使正常业务受到影响。
    (2)检测洪范攻击:服务器上有大量的半连接状态,且IP地址都是随机的。可以使用netstat命令查看。
    (3)解决方法:除了缩短超时时间、增加最大半连接数和过滤网关保护以外,还有SYN cookies技术。
    (4)SYN Cookies技术:当服务器接收到SYN报文的时候,不直接为TCP分配资源,而是打开一个半开的套接字,接着使用SYN报文段的源ID,目的ID,端口号和加密函数生成一个cookie,并把cookie作为序列号响应给客户端。如果客户端是正常连接,会返回一个确认字段为cookie+1的报文段,服务器根据原来的信息计算出一个结果,如果结果+1等于确认字段的值,则证明这个是刚刚请求连接的客户端,这时候才会为TCP分配资源。
  • 三次握手的最后一次回复丢失会发生什么?
    如果最后一次ACK在网络中丢失,服务端的状态会保持为半连接,根据TCP的超时重传机制依次等待3、6、12秒后重新发送SYN+ACK包。如果重发指定次数之后还是没有收到ACK应答,服务端自动关闭这个连接。但是此时客户端认为连接已经建立,如果客户端向服务器发送数据,服务端会返回RST包(RESET)响应,客户端就知道第三次握手失败了。

2.TCP四次挥手

主动断开方为A,被动断开方为B。
A发送一个FIN用来关闭A与B的数据传送。B收到这个FIN,返回一个ACK报文,确认序号为收到的序号+1,和SYN一样,一个FIN占用一个序号。B关闭与A的连接,发送一个FIN给A。A发回ACK确认报文,并将确认序号设置为收到序号+1。

  • 为什么连接是三次握手,断开却是四次挥手?
    (1)建立连接的时候,服务器在listen状态下,收到SYN报文之后,把ACK和SYN放在一个报文里发送给客户端。
    (2)关闭连接时,服务器收到对方的FIN报文,仅仅表示对方不发送数据,但还可以接受数据,而自己的数据不一定都发送完全了,所以服务器此时可以立刻关闭,也可以继续发送数据给对方之后再发送FIN报文告诉对方同意关闭。因此,服务器的FIN和ACK是分开发送的,导致多了一次。
  • 为什么TCP挥手每两次之间有一个wait等待时间?
    主动关闭的一方调用完close之后进入wait状态,如果这个时候因为网络突然断掉、被动关闭的一方宕机等原因,导致主动关闭方不能收到被动关闭的一方发来的FIN,就需要一个定时器,如果定时器超时还是没收到被动方发来的FIN,就直接释放这个链接。
  • 为什么客户端最后还要等待2MSL?为什么还有个TIME-WAIT的时间等待?
    (1)保证客户端发送的最后一个ACK报文能到达服务器,因为ACK报文可能丢失,服务器已经发送了FIN+ACK报文,请求断开,此时客户端却没有回应,于是服务器就会重新发送一次,而客户端就会在这个2MSL时间段内收到这个重传报文,给出回应。
    (2)MSL是最大报文生存时间,一个MSL是30秒,2MSL是60秒
    (3)防止已经失效的连接请求报文段重新出现在连接中,客户端发送完ACK报文后,在这个时间内可以使本连接持续时间内产生的所有报文段都从网络中消失,保证新的连接中不会出现旧连接的请求报文。
  • TIME-WAIT状态过多会产生什么后果?
    短时间过多的短连接,会消耗服务器的资源/消耗客户端的端口,服务器会导致部分客户端连接不上,客户端可能会无法发起新的连接。
    在高并发短连接的TCP服务器上,可能会出现大量socket处在TIME_WAIT状态,如果并发量持续很高,部分客户端就会显示连接不上。高并发会让服务器短时间内占用大量端口,但是端口范围是0~65536,是有限的。短连接表示业务处理和传输数据的时间远小于TIMEWAIT超时的时间都连接。
    解决方法:负载均衡;设置SO_REUSEADDR套接字选项避免TIME_WAIT状态;强制关闭,发送RST越过TIMEWAIT状态,直接CLOSE。
  • 服务器出现了大量的CLOSE_WAIT如何解决?
    一般是程序出了问题。对方的socket已经关闭连接,而我方忙于读或写没有及时关闭连接,需要检查释放资源的代码或者处理请求的线程配置。

3.HTTP和HTTPS的区别
  • HTTP:超文本传输协议是互联网上应用最为广泛的网络协议。设计HTTP最初的目的是为了提供一种发布和接受HTML页面的方法,使浏览器更加高效。HTTP协议是以明文方式发送信息的,如果黑客截取了Web浏览器和服务器之间的传输报文,就可以直接获得其中的信息。
    原理是客户端的浏览器首先要通过网络与服务器建立连接,该连接是通过TCP来完成的。建立连接之后,客户端发送一个请求给服务器,请求方式的格式是统一资源标志符(URL)、协议版本号,后边是MIME信息包括请求修饰符、客户机信息和许可内容。服务器接到请求之后,给予相应的响应信息,包括信息的协议版本号、一个成功或错误的代码,后面是MIME信息包括服务器信息、实体信息和可能的内容。
  • HTTPS:是以安全为目标的HTTP通道,其安全基础是SSL协议。SSL协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。HTTPS的设计目标是数据保密性、数据完整性和身份校验安全性。
    HTTP和HTTPS的区别
    (1)HTTPS协议需要用到CA认证证书,包含了公钥、公钥拥有者名称、数字签名、有效期、授权中心名称、序列号等信息。
    (2)HTTP是超文本传输协议,信息是明文传输。HTTPS则是具有安全性的SSL加密传输协议。
    (3)HTTP和HTTPS采用的完全不同的连接方式,用的端口也不一样,HTTP是80,HTTPS是443。
    (4)HTTP的连接简单且无状态。(无状态是指数据包的发送、传输和接受都是相互独立的,通信双方都不长久的维持对方的任何信息。)
    HTTP的响应报文包括状态行、响应头部和对应数据。其中状态行包括了HTTP版本号,状态码信息。
    详细区别

4.TCP流量控制与拥塞控制
  • 流量控制:发送方不能一直随意发送数据给接收方,要考虑接收方的处理能力。TCP连接的每一方都有固定大小的缓存空间,TCP的接收端只允许发送端发送接收端缓冲区能够接纳的数据。TCP使用的流量控制协议就是可变大小的滑动窗口协议。
    滑动窗口:窗口是缓存的一部分,用来暂时存放字节流,发送方和接收方各有一个窗口,接收方通过TCP报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其他信息设置自己的窗口大小。
  • 拥塞控制:如果网络中出现拥塞,分组就会丢失,此时发送方会继续重传,使得网络拥塞更加严重。因此出现拥塞时,应该控制发送方的速率。TCP主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快恢复和快重传。发送方维护一个叫做拥塞窗口的状态变量。
    慢开始和拥塞避免:发送的最初执行慢开始算法,使得拥塞窗口为1,发送方只能发送一个报文段;收到确认之后,拥塞窗口大小加倍。设置一个慢开始门限,当拥塞窗口的大小大于慢开始门限时,进入拥塞避免算法,每个轮次只将拥塞窗口+1,如果出现了超时,就让慢开始门限等于当前拥塞窗口大小的二分之一,并重新开始执行慢开始。
    快重传和快恢复:在接收方,要求每次接收到报文段都应该对最后一个已经收到的有序报文段进行确认。发送方如果收到三个重复的确认,就说明下一个报文段是丢失的,此时执行快重传算法,立即重传下一个报文段,在这种情况下只是丢失个别报文段,而不是网络拥塞。此时执行快恢复算法,令慢开始门限等于拥塞窗口大小的二分之一,并让拥塞窗口等于新的慢开始门限,并进入拥塞避免算法。

5.OSI七层都是哪些?对应有哪些协议?
  • 物理层:以二进制数据形式在物理媒体上传输数据。IEEE802.2
  • 数据链路层:传输有地址的帧,错误检测功能。SLIP、PPP、ARP、RARP、CSLIP
  • 网络层:为数据包选择路由。IP、ICMP、RIP、OSPF、IGMP
  • 传输层:提供端对端的接口。TCP、UDP
  • 会话层:解除或者建立与其他节点的关系。
  • 表示层:数据格式化、加密以及代码转换。
  • 应用层:文件传输、文件服务、电子邮件和虚拟终端。HTTP、FTP、TFTP、SNMP、SMTP、DNS、Telnet

6.ICMP协议

ICMP是网络层协议,往往需要一个简单的测试来验证网络是否畅通,IP协议并不能提供可靠传输,如果丢包了,IP协议并不能通知传输层是否丢包以及丢包的原因。
ICMP协议的主要功能:

  • 确认IP包是否成功到达目标地址
  • 通知在发送过程中IP包被丢弃的原因
    要注意的是:ICMP是基于IP协议工作的,因此它属于网络层协议。
    ICMP的报文格式:ICMP报文包含在IP数据报中,一个ICMP报文包括IP报头、ICMP报头和ICMP报文。当IP报头中的协议字段为1时,说明这是一个ICMP报文。
    ICMP大概分为两类:一类用来诊断查询,一类通知出错原因
    常见的ICMP报文
  • 相应请求:ping操作中包含了回应请求(类型8)和应答报文(类型0),一台主机向一个节点发送一个类型字段值为8的ICMP报文,如果途中没有异常(如果没有被路由丢弃,目标不回应ICMP或者传输失败),则目标返回类型字段值为0的ICMP报文,说明这台主机存在。
  • 目标不可达、源抑制和超时报文:这三种报文的格式是一样的。
    (1)目标不可达报文(类型3)在路由器或者主机不能传递数据时使用。常见的不可到达类型还包括网络不可到达(类型0),主机不可到达(类型1)和协议不可到达(类型2)。
    (2)源抑制符文(类型4):充当一个控制流量的角色,通知主机减少数据报流量。
    (3)无连接方式网络的问题是数据报会丢失,或者长时间在网络中游荡,会触发ICMP超时报文的产生。超时报文(类型为11)
  • 时间戳请求:时间戳请求报文(类型为13),时间戳应答报文(类型14)用于测试两台主机之间数据包来回一次的传输时间。
    另外,traceroute的功能也是基于ICMP协议实现的。

ICMP详解


7.网络字节序与大端小端

字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序。在跨平台以及网络程序中字节序才是一个应该被考虑的问题。字节序有两种:小端(Little-Endian)和大端(Big-Endian)。
小端就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端
大端是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端
TCP/IP各层协议将字节序定义为大端,因此TCP/IP协议中使用的字节序称之为网络字节序。而通常我们说的主机序遵循小端规则。所以当两台主机之间要通过TCP/IP协议进行通信的时候,就需要调用相应的函数进行主机序和网络序(大小端)切换。网络字节序就是所见即所得的顺序。

  • 大端的优点是首先提取高位字节,总是可以先看看偏移位置0的字节确定这个数字是正数还是负数。这个数值是以他们被打印出来的顺序存放的,所以从二进制到十进制的函数特别有效。
  • 小端的优点:提取一个、两个、四个或者更长字节数据的汇编指令以与其他所有格式相同的方式。
    大小端与网络字节序详解

8.GET和POST的区别

GET和POST是HTTP请求的两种基本方法:

  • GET在浏览器回退时是无效的,但POST会再次提交
  • GET产生的URL地址可以被Bookmark,但Post不可以
  • GET请求会被浏览器主动cache,而Post不会,除非手动设置
  • GET请求只能进行URL编码,但是POST支持多种编码方式
  • GET请求在URL传送的参数有长度限制,但POST没有
  • 对参数的数据类型,GET只接受ASCII字符,但POST没有限制。
  • GET比POST更不安全,因为参数直接暴露在URL上,而POST放在Request Body中。

9.HTTP状态码
  • 1XX:指示信息,表示请求正在处理
    (1)100:Continue,客户端继续处理请求。
    (2)101:Switching Protocol,切换协议,服务器根据客户端的请求切换到更高级的协议
  • 2XX:成功,表示请求已经被成功处理
    (1)200:OK,请求成功,请求所希望的响应头或数据体将随此响应返回。
    (2)201:Created,请求已实现,并且有一个新的资源已经依据需求而建立
    (3)202:Accepted,请求已接受,还未处理成功。
  • 3XX:重定向:要完成的请求需要进行附加操作
    (1)300:Multiple Choices,多种选择,被请求的资源有一系列可以选择的回应地址。
    (2)301:Moved Permanently,永久移动,请求的资源已被永久地移动到新URI,返回信息会包含新的URI
    (3)302:Found,临时移动,资源只是临时被移动,客户端应继续使用原有URI。
  • 4XX:客户端错误:请求有语法错误或者请求无法实现,服务器无法处理请求
    (1)400:Bad Request,客户端请求的语法错误,服务器无法理解
    (2)401:Unauthorized,当前请求需要用户验证
    (3)403:Forbidden,服务器理解请求,但是拒绝执行
    (4)404:Not Found,请求失败,请求所希望得到的资源没有在服务器端上被发现。
    (5)408:Request Time-Out,请求超时。服务端等待客户端发送的请求时间过长
  • 5XX:服务端错误:服务器处理请求出现错误
    (1)500:Internal Server,服务器遇到了未曾料到的错误,导致他无法处理请求
    (2)501:Not Implemented,服务器不支持当前请求所需要的某个功能
    (3)502:Bad Gateway,网关错误
    (4)503:Service Unavailable,临时服务器维护或者过载,服务器当前无法处理请求
    (5)504:GateWay Time-out,网关超时
    (6)505:Http Version not supported,服务器不支持当前请求中的HTTP版本

10.DNS

DNS(Domain Name System)域名系统是一种组织成域层次结构的计算机和网络服务命名系统,用于TCP/IP网络。
DNS的作用:通常我们有两种方式识别主机:通过IP地址或主机名。人们喜欢便于记忆的主机名表示,而路由器使用定长的、有层次结构的IP地址。为了满足不同的需要,我们需要一种将主机名转换为IP地址的目录服务,域名系统作为将域名和IP地址相互映射的一个分布式数据库,能够让人们更好的访问互联网。
DNS的层次结构从上到下依次为根域名服务器、顶级域名服务器和权威域名服务器。
DNS的工作原理
(1)客户机提出域名解析请求,并将该请求发送给本地的域名服务器。
(2)当本地的域名服务器收到请求后,就先查询本地的缓存,如果有该纪录项,则本地的域名服务器就直接把查询的结果返回。
(3)如果本地的缓存中没有该纪录,则本地域名服务器就直接把请求发给根域名服务器,然后根域名服务器再返回给本地域名服务器一个所查询域(根的子域) 的主域名服务器的地址。
(4)本地服务器再向上一步返回的域名服务器发送请求,然后接受请求的服务器查询自己的缓存,如果没有该纪录,则返回相关的下级的域名服务器的地址。
(5)重复第四步,直到找到正确的纪录。
(6)本地域名服务器把返回的结果保存到缓存,以备下一次使用,同时还将结果返回给客户机。


11.HTTP1.0/1.1/2.0

HTTP1.0在1996年引入,1.1在1999年引入,2.0在2015年引入.
HTTP1.0:

  • HTTP1.0只提供最基本的认证,用户名和密码都没有加密
  • 只支持短链接,每次发送数据都要经过三次握手和四次挥手.
  • 不支持断点续传,只要发送就会发送全部数据
  • 认为每台计算机都只能绑定一个IP地址,不支持虚拟网络
    HTTP1.1:
  • HTTP1.1使用了摘要算法进行身份验证
  • 默认使用长连接(通过心跳保持):只需要建立一次连接,传输多次数据,传输完成之后只需要一次切断即可.
  • 支持断点续传:通过请求头的Range实现
  • 使用虚拟网络:在一台物理计算机上可以存在多个虚拟主机,共享一个ip地址
    HTTP2.0:
  • 头部压缩:使用HPACK算法进行压缩
  • 二进制格式
  • 强化安全,支持HTTPS
  • 多路复用:一个连接上可以有多个请求

12.路由器和交换机的区别
  • 外形上:交换机通常端口比较多,路由器端口少体型小
  • 工作层次:交换机工作在数据链路层,实现数据帧的转发;路由器工作在网络层,实现网络互联。
  • 数据的转发对象:**交换机是根据MAC地址转发数据帧;而路由器是根据IP地址转发数据报。**IP地址决定最终数据要到达某一台主机,而MAC地址是决定下一跳要交给哪一台设备。ip地址是软件实现的,可以描述主机所在的网络,而MAC地址是硬件实现的,每一个网卡在出场的时候都会把全世界唯一的MAC地址固化在ROM中。
  • 分工不同:**交换机主要是用于组建局域网,而路由器则负责让主机连接外网。**多台主机可以通过网线连接到交换机,组建成局域网,但是此时交换机组建的内网是不能连接外网的,这时候就需要路由器。
  • 冲突域和广播域:**交换机分割冲突域,但不分割广播域,而路由器分割广播域。**由交换机连接的网段,仍属于同一个广播域,广播数据包会在交换机连接的所有网段上传播,这时会导致广播风暴和安全漏洞,而连接在路由器上的网段会被分配给不同的广播域,路由器不会转发广播数据。

13.在浏览器输入URL到网页显示的过程
  • 在客户端浏览器中输入网址URL
  • 发送到DNS(域名解析服务器)获得域名对应的IP地址
  • 客户端浏览器和WEB服务器建立TCP连接
  • 客户端浏览器向WEB服务器发送HTTP/HTTPS请求
  • WEB服务器响应HTTP/HTTPS请求,返回指定的URL数据或错误信息
  • 客户端浏览器下载数据,解析HTML源文件,解析的过程中实现对页面的排版,解析完成后,在浏览器中显示基础页面。
    参考文章

操作系统

参考leetcode讨论区

1.僵尸进程与孤儿进程
  • 僵尸进程是指它的父进程没有等待(调用wait/waitpid)。如果子进程先结束而父进程后结束,即子进程结束后,父进程还在继续运行但是没有调用wait/waitpid那么子进程就会成为僵尸进程;如果父进程先结束就不会产生僵尸进程。在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件、占用的内存。但是仍然会保留一些信息(如进程号、pid、退出状态和运行时间等)。这些保留的信息直到进程通过调用wait/waitpid时才会释放。这样就会导致如果没有调用wait/waitpid的话,那么保留的信息就不会释放。比如进程号就会被一直占用,所以如果产生大量的僵尸进程,将导致系统没有可用的进程号而导致系统不能创建线程。
  • 孤儿进程:一个父进程退出,而它的一个或者多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将会被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。孤儿进程只是没有父进程的进程,管理孤儿进程的任务就落到了init进程身上,所以孤儿进程不会产生什么实质危害。

2.虚拟内存和物理内存

虚拟内存使得应用程序以为它拥有一个连续的地址空间,实际上,它通常被分割成多个物理内存碎片,还以一部分存储在外部磁盘存储器上,在需要的时候进行数据交换。
虚拟内存可以让程序可以拥有超过系统物理内存大小的可用内存空间。虚拟内存让每个进程拥有一片连续完整的内存空间。
局部性原理表现在两个方面:

  • 时间局部性:如果程序中的某条指令一旦执行,不久之后该指令可能再次执行;如果某数据被访问过,不久之后该数据可能再次被访问。
  • 空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也会被访问。
    操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成很多块。每一块称为一页,这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都在物理内存中。当程序引用到不在物理内存的页时,会将缺失的部分从磁盘装入物理内存。
    页面置换算法
  • OPT页面置换算法(最佳页面置换算法):所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。
  • FIFO(First In First Out)先入先出页面置换算法:总是淘汰最先进入内存的页面。
  • LRU(最近最久未使用页面置换算法):将最近最久没有使用的页面换出。
  • LFU(最少使用页面置换算法):该置换算法选择在之前时间是用最少的页面作为淘汰页

3.分段和分页

内存管理机制

  • 块式管理:将内存分为几个固定大小的块,每个块只包含一个进程
  • 页式管理:把主存分为大小相等且固定的一页一页的形式,页较小,相对相比于块式管理的划分力度更大,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址。
  • 段式管理:页式管理虽然提高了内存利用率。但是页式管理其中的页并无任何实际意义。段页管理把主存分为一段段的,最重要的是段有实际意义,每个段定义了一组逻辑信息。段式管理通过段表对应逻辑地址和物理地址。
  • 段页式管理:段页式管理机制结合了段式管理和页式管理的优点。就是把主存先分成若干段,每个段又分成若干页。
    分段和分页的共同点:分页机制和分段机制都是为了提高内存利用率,减少内存碎片,页和段都是离散存储的,所以两者都是离散分配内存的方式。但是每个页和段中的内存是连续的。
    分段和分页的不同点:页的大小是固定的,又操作系统决定;而段的大小不固定,取决于当前运行的程序。分页仅仅是满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段和数据段。

4.线程间通信的手段
  • 全局变量:由于多个线程之间可能修改全局变量,所以全局变量最好声明为volatile
  • 使用消息实现通信:在Windows程序设计中,每一个线程都可以拥有自己的消息队列,因此可以采用消息进程线程间通信sendMessage、postMessage
  • 使用事件CEvent类实现线程间通信:Event对象有两种状态:有信号和无信号,线程可以监视处于有信号状态的事件,以便在适当的时候执行对事件的操作。

5.互斥锁和条件变量

线程同步的常见方法:互斥锁、条件变量、读写锁和信号量

  • 互斥锁:本质是一个特殊的全局变量,拥有lock和unlock两种状态。unlock的互斥锁可以由某个线程获得,一旦获得,这个互斥锁会变成lock状态,此后只有该线程有权利打开锁,其他线程想要获得互斥锁必须等到互斥锁打开。
  • 条件变量:互斥量不是万能的,比如某个线程正在等待共享数据内某个条件出现,可能需要重复对数据对象进行加锁解锁(轮询),但是这样轮询非常耗费时间和资源,不适合互斥锁。条件变量是:当线程在等待满足某些条件时使线程进入睡眠状态,一旦条件满足,就唤醒因等待满足特定条件而睡眠的线程
    条件变量通过运行线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,往往和互斥锁一起使用。使用时,条件变量被用来阻塞某个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化,一旦某个线程改变了条件变量,它就会通知相应的条件变量唤醒一个或多个正因此被阻塞的线程,这些线程将重新锁定互斥锁并且重新测试条件是否满足。
    虚假唤醒
    (1)当我们使用互斥量和条件变量进行多线程同步的时候可能会产生虚假唤醒现象。
    (2)在多核处理器下,pthread_cond_signal可以会激活多于一个线程的可能,当一个线程调用pthread_cond_singal()后,多个调用pthread_cond_wait()和pthread_cond_timedwait()的线程返回。这种效应就是虚假唤醒。
    (3)当消费线程和生产线程都为多个的情况下才有可能出现虚假唤醒。

6.内存屏障

内存屏障是一个CPU指令。它的作用是确保一些特定操作执行的顺序和影响一些数据的可见性。编译器和CPU在保证输出结果一致的情况下对指令重排序,插入内存屏障是告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。
内存屏障的另一个作用是强制更新一次不同CPU的缓存,例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个CPU执行的。
可见性会让我们想到JAVA的关键字volatile。如果你使用了volatile,java内存模型会在写操作后插入一个写屏障指令,读操作前插入一个读屏障指令。


7.线程、进程和协程

线程和进程的区别:

  • 进程是资源分配的最小单位,线程是任务执行的最小单位。
  • 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常消耗资源。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
  • 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式IPC进行。如果处理好同步和互斥是编写多线程程序的难点。
  • 多进程程序更加健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另一个进程造成影响。
    进程的调度算法有哪些
  • 先来先去服务
  • 时间片轮转法
  • 短作业优先
  • 多级反馈队列调度算法
  • 优先级调度

协程是什么?:协程是更轻量级的线程,用于解决线程间切换和进程间切换的通病(对内核开销过大),协程各个状态(阻塞、运行)的切换是由程序控制,而不是内核控制,减少了资源消耗。


8.Linux常用命令

linux下的常用命令:
(1)ls命令:list缩写,查看linux文件夹包含的文件等等
(2)cd命令:切换当前目录
(3)pwd命令:查看当前工作目录路径
(4)rm命令/rmdir命令:删除文件/文件夹
(5)mv命令:移动文件或修改文件名
(6)cp命令:将原文件复制到目标文件,或将多个原文件复制到目标目录
(7)cat命令:显示、创建文件
(8)head命令:显示档案的开头
显示1.log的前20行:head 1.log -n 20
显示2.log的后10行:head -n -10 2.log
(9)which/whereis命令:搜索某个文件
(10)find命令:搜索符合条件的文件
查找48小时内修改的文件:find -atime -2
在当前目录查找以.log结尾的文件: find ./ -name '*.log'
查找文件名为a.txt的文件: find -name 'a.txt'
(11)chmod/chown命令:赋权
(12)df/du命令:显示磁盘使用情况
(13)grep命令:文本搜索命令,支持正则表达式
查找指定的进程:ps -ef | grep svn
查找当前文件夹中哪些文件包含某字段:grep -rl 'string' *
文本中查找包含x的内容行 grep -E 'x' test.txt
-c 计算符合条件的列数
-n 显示匹配条件的行数
(14)wc命令:统计指定的文件中字节数、字数、行数并将其统计结果输出
(15)ps命令:查看当前运行的进程状态
(16)kill命令:结束某个线程
(17)free命令:显示系统内存使用情况
(18)显示xx端口是否被占用:lsof -i:xx
可以查询到pid,并使用kill -9 pid的方式关闭该线程
根据进程名杀死进程:kill -9 $(pidof 进程名)
检查tomcat进程占用端口号情况:ps -aux|grep tomcat
检查8080端口被哪个进程占用:netstat -apn | grep 8080
(19)df:检查文件系统的磁盘空间占用情况
du: 检查每个文件和目录占用磁盘空间的情况
(20)统计一个文件中重复的行和重复次数:cat a.txt | uniq -c


9.死锁

什么是死锁?:指多个进程在运行过程中因资源争夺而造成的一种僵局,当进程处于这种僵持状态的时候,若无外力作用,他们都无法继续推进。
死锁产生的四个必要条件

  • 互斥条件:进程要求对所分配的资源进行排他性控制,即在某一段时间内某资源仅为一进程所占用。
  • 请求和保持条件:当进程因请求资源而阻塞时,对已有的资源保持不放
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放
  • 环路等待条件:在发生死锁的时候,必然存在一个进程-资源的环形链
    预防死锁:
  • 资源一次性分配
  • 只要有一个资源得不到分配,也不给这个进程分配其他的资源
  • 可剥夺资源:当某进程获得了部分资源,但得不到其他资源,则释放已占有的资源
  • 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按照编号递增的顺序请求资源,释放则相反.
    避免死锁:银行家算法
    检测死锁:
  • 为每一个进程和每个资源指定一个唯一的号码
  • 建立资源分配表和进程等待表
    解除死锁:
    当发现进程有死锁的时候,应该将其从死锁的状态中脱离出来:
  • 剥夺资源:从其他进程剥夺足够数量的资源给死锁进程
  • 撤销进程:撤销代价最小的进程,直到有足够的资源可用.
    死锁检测:
  • JStack:jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。 Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。
  • JConsole:Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。

10.用户态和内核态
  • 内核态:特殊的软件程序,负责控制计算机的硬件资源,例如协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序运行。
  • 用户态:提供应用程序运行的空间。为了让应用程序访问内核管理的资源如CPU和内存,内核必须提供一组通用的访问接口,这些接口叫做系统调用
    实际上内核态和用户态也是linux的两种不同的权限等级。
    用户态切换到内核态的方法:可以通过系统调用、外设中断和异常。

11.并行和并发
  • 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
  • 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
    (1) 并行是两个及以上事件在同一时刻发生的情况;而并发是指两个或多个事件在同一时刻间隔发生。
    (2) 并行是在不同实体上的多个事件;并发是在同一实体上的多个事件
    (3) 并行是在多台处理器上同时处理多个任务(Hadoop集群),并发是在一台处理器上“同时”处理多个任务。
    参考文章

软件测试

测试部分内容参考

1.五种测试
  • 单元测试:完成最小的软件设计单元的验证工作,目标是确保模块被正确的编码,使用过程设计描述作为指南,对重要的控制路径进行测试来发现模块中的错误,通常是白盒测试,对代码风格和规范、程序设计和结构以及业务逻辑等进行静态测试。
  • 集成测试(SIT):通过测试发现与模块接口有关的问题。目标是把通过了单元测试的模块拿来,构造一个在设计中所描述的程序结构,避免一次性的集成,采用增量集成。一般采用黑盒测试与白盒测试相结合的方法
  • 系统测试:基于系统整体需求说明书黑盒测试,应该覆盖系统所有联合的部件。系统测试是针对整个产品系统进行的测试,目的是验证系统是否满足了需求规格的定义,找出与需求规格不相符合或与之矛盾的地方。系统测试的对象不仅仅是需要测试的产品系统的软件,还包括软件所依赖的硬件甚至包括某些数据、支持软件和接口等。因此,必须将系统中的软件和各种依赖的资源结合起来,在系统实际运行环境下进行测试。系统测试是最重要的一步测试
  • 回归测试:回归测试是值发生修改之后重新测试先前的测试用例以保证修改的正确性。理论上,软件产生新版本,都需要回归测试来验证以前发现和修复的错误是否在新版本中再次出现。回归测试的意义就在于验证以前出现过但已经修复好的缺陷不再重新出现
  • 验收测试(UAT):验收测试是指系统开发生命周期方法论的一个阶段,这时相关的用户或独立测试人员根据测试计划和结果对系统进行测试和接收。它是确定产品是否满足合同或用户所规定需求的测试

2.黑盒与白盒
  • 黑盒测试:也称功能测试或者数据驱动测试。它是已知产品所具有的功能情况下,测试来检查每个功能是否能正常使用。测试时把程序看成一个不能打开的黑盒子,在不考虑系统内部结构和特性的情况下,在程序接口进行测试。它只检查程序功能是否按照规格需求说明书的规定正常使用,保持外部信息(数据库/文件)的完整性。
    黑盒法是穷举输入测试,只有把所有可能的输入都作为测试情况使用,才能查出程序中所有的错误。实际上测试情况有无穷多个,因此不仅要测试合法输入,还要对那些不合法但是可能的输入进行测试。
    黑盒测试方法
    (1)等价类划分:将系统的输入域划分为若干部分,从每个部分中选取少量代表性数据进行测试。等价类可以划分为有效等价类和无效等价类。
    (2)边界值分析法:大多数错误发生在输入输出的边界上。边界值分析法通过优先选择不同等价类间的边界值覆盖有效等价类和无效等价类来更有效的进行测试。常见的边界值有:对16bit整数而言的32767和-32768、屏幕上光标的最左上、最右下的位置、报表的第一行和最后一行、数组元素的第一个和最后一个等。
    (3)正交试验法:从大量的实验点中挑选出适量的、有代表性的点。
    (4)状态迁移法:对一个状态在给定的条件内能够产生需要的状态变化,有没有出现不可达的状态或非法状态。
    (5)流程分析法:针对测试场景类型属于流程测试场景。
    (6)输入域测试法:针对输入的测试,主要考虑极端、中间范围和特殊值。
    (7)输出域测试法:对输出域进行等价类和边界值划分,确定要覆盖的输出域样点,反推应该输入的值。
    (8)判定表分析法:分析和表达多种输入条件下系统执行不同动作的工具。
    (9)因果图法:绘制因果图描述系统输入输出之间的因果和约束关系。
    (10)错误猜测法:针对系统对于错误操作时的操作处理进行猜测。
    (11)异常分析法:针对系统有可能存在的异常进行分析。
    设计测试用例:一般分为四个等级:基本功能核心业务流程、非基本核心功能,边界值、交互操作和中断等和异常测试内容。
  • 白盒测试:也称结构测试或逻辑驱动测试。是针对被测单元内部如何进行工作的测试。它根据程序的控制结构设计测试用例,主要用于软件和程序验证。白盒测试检查程序内部逻辑结构,对所有的逻辑路径进行测试。但穷举路径无法检查出程序本身是否违反了设计规范,不可能检查出程序因为遗漏路径而出错,发现不了一些与数据相关的错误。
    常用的白盒测试方法
    (1)静态测试:不运行程序的测试,包括代码检查、静态结构分析、代码质量度量和文档测试等等,可以由人工进行,也可以借助软件工具(Fxcop)自动进行。
    (2)动态测试:需要执行代码,包括功能确认和接口测试、覆盖率分析、性能分析和内存分析等。
    六种白盒测试的逻辑覆盖标准(发现错误的能力由弱到强)
    (1)语句覆盖:每条语句至少执行一次
    (2)判定覆盖:每个判定的每个分支至少执行一次
    (3)条件覆盖:每个判定的每个条件应取到各种可能的值
    (4)判定/条件覆盖:同时满足判定覆盖和条件覆盖
    (5)条件组合覆盖:每个判定各个条件的每一种组合至少出现一次。
    (6)路径覆盖:程序中每一条可能的路径至少覆盖一次。

3.开发和测试的结合

开发和测试应该按照W模型的方式进行结合,测试和开发同步进行,尽早发现软件缺陷,降低软件开发的成本。
W模型强调的是测试伴随着整个软件开发周期,测试的对象不仅是程序,需求和设计同样需要测试。
W模型:
用户需求-->需求分析与系统设计-->概要设计-->详细设计-->编码-->集成-->实施-->交付
验收测试设计-->确认与系统测试设计-->集成测试设计-->单元测试设计-->单元测试-->集成测试-->确认测试与系统测试-->验收测试
因此,测试的相关规范流程是:
需求测试-->概要设计测试-->详细设计测试-->单元测试-->集成测试-->系统测试-->验收测试


4.BUG的评测

Bug有优先级(priority)和严重程度(severity)两个重要属性,测试人员在提交bug的时候,只定义严重程度,将优先级交给leader定义。
Severity的评级
(1)blocker:系统无法执行,崩溃或严重资源不足,应用模块无法启动或异常退出,无法测试,造成系统不稳定。常见的问题有花屏、内存泄漏、用户数据丢失损坏、模块无法启动或异常退出、严重的数值计算错误、功能设计与需求严重不符等。
(2)critical:影响系统功能或操作,主要功能存在缺陷,但不会影响到系统的稳定性。常见的问题有功能未实现、功能有误、系统刷新错误、数据通讯错误等。
(3)major:界面、性能缺陷和兼容性问题。常见的问题有:操作界面错误、边界条件错误、提示信息错误、系统未优化和兼容性问题。
(4)minor:易用性或建议问题。
Priority的评级:根据急需解决的程度从高到低分别为immediate、urgent、high、normal和low。


5.PC网络障碍排查
  • 排除接触障碍:确保网线和网卡是可以正常使用的。
  • 使用ipconfig查看计算机的网络参数
  • 使用ping命令测试网络的连通性:首先ping 127.0.0.1本机环回地址,如果四个数据包的丢包率为0,则判断正常,若显示超时,则说明本机网卡的安装或者TCP/IP协议有问题。在环回地址能被ping通的情况下,继续ping本地的ip地址,如果失败则说明网卡驱动程序有问题。ping网关如果通的话,说明本机网络连接正常,如果不成功可能是网关设备问题或者网络参数设置有误。

6.测试用户界面登陆过程
  • 功能测试:
    1.输入正确的用户名和密码,点击提交是否能登陆,是否能跳转到正确的页面
    2.输入错误的用户名或密码,验证登录是否失败并给出相应提示
    3.用户名和密码过短和过长或者包含其他非法字符的提示处理
    4.是否能记住用户名
    5.用户名和密码前后有空格的情况
    6.密码是否做密文处理
    7.输入密码的时候,大写是否开启有没有提示
  • 界面测试:
    1.布局是否合理,按钮和输入框是否整齐
    2.界面的设计风格是否和UI设计风格统一
    3.界面中的文字简洁易懂,没有错别字
  • 性能测试:
    1.页面的加载时间是否在用户可接受的范围内/需求要求的时间内。
    2.输入正确的用户名和密码后,跳转时间是否在要求的时间内。
    3.模拟大量用户同时登陆,检查在一定压力下是否能正常跳转。
  • 安全性测试:
    1.登陆成功生成的cookie是否是httponly
    2.用户名和密码在发送给web服务器的时候是否加密
    3.用户名和密码的验证应该在服务器端验证而不是在客户端。
    4.用户名和密码的输入框应该屏蔽SQL注入攻击且禁止输入脚本防止XSS攻击
    5.是否有错误登陆的次数限制以防止暴力破解
    6.是否支持多个用户在同一机器上登陆
    7.同一用户是否能在多台机器上登陆
  • 兼容性测试:
    1.不同浏览器/不同版本下是否能显示正常且功能正常
    2.不同的平台是否能正常工作
    3.移动设备上是否能正常工作
    4.不同的分辨率情况下显示是否正常

7.吃鸡游戏的压力测试
  • 压力测试的内容:
    1.服务器硬件:硬盘的读写、内存、CPU
    2.网络压力:最大长连接数、流量,每秒建立的连接数和实际处理能力
    3.数据库:每秒事务数,每秒锁等待数,平均延迟时间
    4.多线程的最优线程数

8.自动化测试工具Selenium相关
  • Selenium驱动浏览器使用的是什么协议?JsonWireProtocol
  • Selenium工具都包含哪些组件:Selenium IDE、WebDriver和Selenium Grid
  • 在Selenium中定位网页元素有几种方式:ID、Name、ClassName、LinkText、PartialLinkText、TagName、Xpath、Css Selector
  • WebDriver启动常见浏览器的语句?
 WebDriver driver = new FirefoxDriver();
 WebDriver driver = new ChromeDriver();
 WebDriver driver = new InternetExplorerDriver();
  • 如何判断一个元素在页面中是显示出来的?使用WebElement类中的isDisplayed()方法。
  • 如何选中下拉列表中的下拉选项:selectByVisibleText、selectByValue、selectByIndex
  • Selenium如何处理Web弹窗和Js弹窗:需要使用driver.switchTo.alert();
    alert.accept()相当于点击弹窗的OK,alert.dismiss()相当于点击弹窗的cancel。
  • Selenium中常见的时间等待有哪几种?Thread.Sleep强制固定等待、Implicit Wait 用于全局的隐式等待、Explicit Wait等待具体某个元素某个状态。
    操作原理:
  • Selenium是怎么实现对应操作的:RemoteConnection这个类里面定义了所有的selenium操作需要的接口地址(这些接口地址全都封装在浏览器驱动程序中),那么所有的浏览器操作就是通过访问这些接口来实现的。通过execute方法调用_request方法通过urilib3标准库向服务器发送对应操作请求地址,进而实现了浏览器各种操作。**打开浏览器和操作浏览器实现各种操作是怎么关联的?**打开浏览器也是发送请求,请求会返回一个sessionId,后面操作的各种接口地址,接口地址中也包含了一个$sessionId,实际上就是通过这个sessionId做关联的。
  • 因此,Selenium工作的过程:
    1.selenium client(py编写的自动化测试脚本)初始化一个service服务,通过webdriver启动浏览器驱动程序chromedriver.exe
    2.通过RemoteWebDriver向浏览器驱动程序发送http请求,浏览器驱动程序解析请求,打开浏览器,并获得sessionId,如果再次对浏览器操作需要携带该id
    3.打开浏览器,绑定特定的端口,把启动后的浏览器作为Webdriver的remote server
    4.打开浏览器后,所有selenium的操作都是通过RemoteConnection链接到remote server,然后使用execute方法调用_request方法通过urilib3向remote server发送请求。
    5.浏览器通过请求的内容执行动作,再把执行动作的结果通过浏览器驱动程序返回给测试脚本。
评论 (0)