【面试复习】自用不定时更新

【面试复习】

Java SE基础

Java集合框架

在这里插入图片描述

集合面试题

面试题

java源码中常用的数据结构及设计模式

Collection和Collections的区别

  • Collection是一个集合接口(集合类的一个顶级接口),它提供了对集合对象进行基本操作的通用接口方法。

  • Collections是一个包装类,它包含有各种有关集合操作的静态多态方法。

  • Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式其直接继承接口有List与Set。

  • Collections,此类不能实例化,就像一个工具类,用于对集合中元素进行排序、搜索以及线程安全等各种操作,服务于Java的Collection框架。

List,Set,Map的区别

在这里插入图片描述

HashMap扩容流程

扩容机制

​ 默认情况下hashmap的容量为16,如果是用户通过构造方法指定了一个数字作为容量,那么hash会选择 大于该数字的第一个2的幂作为容量(3->4,9->16),通过指定初始化容量可以有效提高性能

​没有初始化设置的时候,就要进行自动扩容,扩大到原来的2倍

扩容条件:当HashMap中的元素个数size超过临界值时,就会自动进行扩容。

临界值=负载因子(0.75)*当前容量(默认是16),举个例子,当元素个数>12时,就要进行自动扩容

所以如果我们没有设置初始容量的大小,随着元素的不断增加,HashMap会进行多次扩容,都需要去建 hash表,是非常影响性能能

ConcurrentHashMap

JDK 1.7:

  1. 数据结构:ReentrantLock+Segment数组+HashEntry数组,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个个链表结构
  2. 元素查询:二次hash,第一次定位到Segment,第二次Hash定位到元素所在的链表的头部
  3. Segment分段锁,Segment继承了ReentrantLack,锁定操作的Segment,其他的Segment不受影响,并发度为Segment的个数,可以通过构造函数指定,数组扩容不会影响其他的Segment
  4. get方法不需要加锁,volatile保证可见性和有序性

JDK 1.8:

  1. 数据结构:synchronized+CAS+Node数组+红黑树,Node的val和next都用volatile修饰,保证可见性,查找,替换,赋值操作都是使用CAS
  2. :锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作,并发扩容
  3. 读操作无锁:
    Node的val和next都是用volatile修饰,读写线程对该变量互相可见
    数组用volatile修饰,保证扩容时被读线程感知

Java四大关键字作用域

在这里插入图片描述

抽象类和接口的区别

  • 抽象类可以存在普通成员函数,而接口中只能存在public abstract方法
  • 抽象类中的成员变量可以是各种类型的,而接口中只能是public static final类型的。
  • 抽象类只能继承一个,接口可以实现多个
  • 抽象类中可以有构造方法,而接口中不能有

全局变量和局部变量的区别

作用域不同:全局变量的作用域为整个程序,而局部变量的作用域为当前函数或循环等
内存存储方式不同:全局变量存储在全局数据区中,局部变量存储在栈区

生命期不同:全局变量的生命期和主程序一样,随程序的销毁而销毁,局部变量在函数内部或循环内部,随函数的退出或循环退出就不存在了

使用方式不同:全局变量在声明后程序的各个部分都可以用到,但是局部变量只能在局部使用。函数内部会优先使用局部变量再使用全局变量

String,StringBuffer,StringBuilder

  • String是底层结构是final修饰的,不可变,每次操作都会产生新的对象
  • StringBuffer和StringBuilder都是在原对象上进行操作的
  • StringBuffer是线程安全的,StringBuilder是线程不安全的
  • StringBuffer的方法通过synchronized修饰,所以安全
  • 性能:StringBuilder>StringBuffer>String
  • 场景:经常需要改变字符串内容时优先使用后面两个

final关键字

  • 修饰类:表示类不可以被继承
  • 修饰方法:表示方法不可以被子类覆盖
  • 修饰变量:表示变量一旦被赋值就不可以更改它的值

(1).修饰成员变量:
修饰静态变量:只能在静态初始化块指定初始值或者声明该类变量时指定初始值
修饰成员变量:可以在非静态初始化块,声明该变量或者构造器中执行初始值
(2).修饰局部变量
系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。可以在定义时指定默认值,也可以不指定默认值,但是在后面给变量赋值时只能赋值一次
(3).修饰基本数据类型和引用数据类型
基本数据类型:数值一旦初始化后就不能修改了
引用数据类型:初始化之后就不能再让指向另一个对象,但是引用的值是可以变的

static关键字

可以修饰成员变量,成员方法。

a. 被static修饰的成员称为静态成员,不属于某个具体的对象,是所有对象所共享的
不存储在某个对象的空间中,既可以通过对象访问,也可以通过类名访问,生命周期:随类的加载而创建,类的卸载而销毁。比如定义一个学生类,一个班里的学生其他属性不一定相同,但是教室是共用的。

b. 被static修饰的成员方法称为静态成员方法,是类的方法,不是某个对象所特有的,特性:不属于某个具体的对象,是类方法,没有隐藏的this引用参数,因此不能在静态方法中访问任何非静态变量,静态方法无法调用任何的非静态方法,因为非静态方法有this引用参数

c. static存在的意义:创建独立与具体对象的变量或方法,即使没有创建对象,也能使用属性和方法。可以形成静态代码块以优化程序的性能,类加载的时候,静态代码块只能执行一次,因此,很多的时候都会将一些只需要进行一次初始化的操作都放在static代码块中进行。

重载和重写

重载
在同一个类中,同名的方法有着不同的参数列表(参数类型不同,参数个数不同,参数顺序不同)则称为重载。与返回值类型无关。
重载(Overload)是一个类中多态性的一种表现

重写
发生在父类和子类之间
方法名,参数列表,返回值类型必须相同
子类方法访问修饰符的权限一定不能低于父类方法的访问修饰符
权限比较:(public>protected>default>private)

在这里插入图片描述

构造方法不能重写

构造方法需要和类名保持同名,而重写是要保证子类方法和父类方法同名,如果允许的话,子类中会存在和类名不同的构造方法,这个要求是矛盾 的。

序列化与反序列化

序列化

概念:把Java对象转换为字节序列的过程
作用:
在传递和保存对象时,保证对象的完整性和可传递性,对象转换为有序的字节流,以便在网络上传输和保存在本地
优点:

  • 将对象转为字节流存储到硬盘上,当JVM停机的话,字节流还会在硬盘上默默等待,等待下一次JVM的启动,把序列化的对象,通过反序列化为原来的对象,并且 序列化的二进制序列能够减少存储空间(永久性保存对象)
  • 序列化成字节流形式的对象可以进行网络传输(二进制形式),方便了网络传输
  • 通过序列化可以在进程间传递对象。

反序列化

概念:把字节序列转换为Java对象的过程

JVM

JVM内存管理

根据jvm的规范,JVM将内存分为了5个区域:

3.1 方法区
存储被虚拟机加载的类信息,静态变量,final定义的常量,即时编译器编译后的代码等数据。

3.2 程序计数器
是一块较小的内存空间,当前线程所执行的字节码的行号指示器,JVM通过改变程序计数器的值来选取下一条执行的字节码的指令。每一个线程都有一个独立的程序计数器。比如,线程1执行到某一行时,有优先级更高的线程2抢占式运行,当线程2运行完后,需要执行线程1,但是我们不可能从头开始重新执行,所以这块就用到了程序计数器,它标记了线程1运行的行数,所以直接从当前位置开始执行就行。

3.3 虚拟机栈(线程栈)
有线程执行的时候,就会从这个区拿出一部分空间,而且每个线程都对应着一个虚拟机栈,当这个线程内的方法执行时,都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。当方法被调用时,栈帧入栈,方法执行结束时,线程出栈。

3.4 本地方法栈
与虚拟机栈的作用相似,他们之间的区别在于虚拟机栈为虚拟机执行Java方法(字节码文件),而本地方法栈用于执行native方法的执行,存储了每个native方法调 用的状态。

3.5 堆
是理解Java GC机制的重要区域。在JVM中,堆区是最大的一块,由线程所共享,在虚拟机启动时创建。用来存储对象示例及数组值,几乎所有通过new创建的对象都在此区域分配。

JVM垃圾回收GC原理

垃圾回收算法主要采用的是分代收集算法【GC】。
GC是根据对象的存活周期的不同将内存划分为几块。一般是把java堆分成新生代和老年代。新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要复制少量存活对象即可完成收集。而老年代中因为对象存活率高、没有额外的空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来回收。

JVM内存分配及回收策略

JVM的内部结构直接贴图:
在这里插入图片描述

新生代和老年代的梗说,JVM的内存分配也是和GC保持一致的,具体分配如图:在这里插入图片描述

具体的回收策略如图:
在这里插入图片描述

判断是否是垃圾对象的算法

  1. 引用计数法:
    假设堆中每个对象都有一个引用计数器,当一个对象被创建并且初始化赋值后,该对象的计数器就设置为1,每当有一个地方引用它的时候,计数器++,反之当引用失效时,例如一个对象的某个引用超过了生命周期(出了作用域)或者被设置为一个新值时,该对象的计数器就- -,当某个对象的计数器计数为0时,就可以回收了。
    当一个对象被当作垃圾回收时,它引用的任何对象的计数器都要-1。
    优点:实现简单,对程序不被长时间打断的实时环境比较有利
    缺点:需要额外的空间来保存计数器,难以检测出对象之间的循环引用。

  2. 可达性分析法:
    将一系列的根对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,如果一个对象到根对象没有任何的引用链相连,那这个对象就不是可达的,也称之为不可达对象,可以解决循环引用的问题,不需要占用额外的空间。

总结策略就是:

  • 对象优先在Eden分配
  • 大对象直接进老年代
  • 长期存活的对象将进入老年代
  • 动态对象进行年龄判定再分代

垃圾回收算法

标记清除法

标记处需要回收的对象,再标记完成后统一回收掉被标记的对象,
优点:不需要进行对象的移动,并且仅对不存活的对象进行处理。
缺点:效率不高,标记清除后可能会产生大量的不连续的内存碎片。

标记整理算法

标记处需要回收的对象,再标记完成后统一回收掉被标记的对象,然后把有用的对象全部压缩到前面去
优点:比较简单,空闲区域的位置是可知的,也不会有碎片的问题了
缺点:GC暂停的时间会增长。因为你将所有的对象都移动到了一个新的地方,还得为他们更新引用地址。

复制算法

将内存按照容量分为大小相等的两块,每次只使用一块,当这一块的内存用户完之后,将活着的对象复制到另外一块,再把已经使用过的部分全部清理一遍。
优点: 标记阶段和复制阶段同时进行,每次只对一块内存进行回收,运行高效,实现简单,不会考虑内存碎片的出现
缺点:需要一块能容纳下所有存活对象的额外的内存空间

分代收集算法

将内存划分为新生代,老年代,永久代。新生代又被分为Eden和Survivor区,其中survivor由from区和to区组成,不同的对象生命周期是不一样的,因此,可以将不同的生命周期的对象进行分代,不同的代采取不同的回收算法进行垃圾回收,以便提高回收效率。

分代回收算法详细解释

所有新生成的对象都是放在新生代里面的,当Eden中的内存放满时,会触发一次Minor GC,将存活的对象复制到survivor0中,然后清空Eden区;当Eden区和survivor0区都存满的时候,将存活的对象都放到survivor1区中,然后清空Eden区和survivor0区,此时的话survivor0区就变成了空的,然后将0区和1区进行交换,保证1区为空。当1区不足以存放0区和E区的对象时,就将对象直接移动到老年代当中。当对象再survivor区中交换一次时,年龄就会+1,等到对象的年龄达到15时还没有被处理掉,就直接移动到老年代。如果老年代都满了的话,就会触发Full GC,将新生代,老年代中的垃圾对象都进行回收。

静态变量和实例变量的区别

静态变量是被static修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝;实例变量必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。在Java开发中,上下文类和工具类中通常会有大量的静态成员。

什么情况下会导致内存泄漏

  • 资源释放问题
    程序代码的问题,长期保持某些资源,如Context,Cursor,IO流的引用,资源得不到释放造成内存泄漏.

  • 对象内存过大
    保存了多个耗用内存过大的对象,如 Bitmap,XML文件,造成内存超出限制。

  • static关键字的使用问题static是java中的一个关键字,当用它修饰成员变量时,那么该变量就属于该类,而不是该类的实例。所以用static修饰的变量,它的生命周期是很长的,如果他用来引用一下资源耗费过多的实例(Context的情况最多),这时就要谨慎对待。

  • 线程导致内存溢出
    线程产生内存泄漏的主要原因是在于线程的生命周期不可控

怎么样避免发生内存泄露和溢出

1、尽早释放无用对象的引用

2、使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域

3、尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不参与垃圾回收

4、避免在循环中创建对象

5、开启大型文件或从数据库一次拿了太多的数据很容易造成内存溢出,所以在这些地方要大概计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。

什么是类加载机制

我们编写的 Java 文件都是以.java 为后缀的文件,编译器会将我们编写的.java 的文件编译成.class 文件,简单来说类加载机制就是jvm从文件系统将一系列的 class 文件z转化为二进制流加载 JVM 内存中并生成一个该类的Class对象,为后续程序运行提供资源的动作。JVM类加载机制主要采用的是双亲委派模型 在这里插入图片描述

  • 启动类加载器 Bootsrap ClassLoader
    它是最顶层的类加载器,是由C++编写而成, 已经内嵌到JVM中了。在JVM启动时会初始化该ClassLoader,它主要用来读取Java的核心类库JRE/lib/rt.jar中所有的class文件,
    如果需要将自己写的类加载器加载请求委派给引导类加载器,那直接使用null代替即可。
  • 扩展类加载器 Extension ClassLoader
    负责加载\lib\ext目中的jar包。
  • 应用程序类加载器 Application ClassLoader
    是类加载器ClassLoader.getSystemClassLoader()方法的返回值,因此称为系统类加载器,负责加载用户路径上指定的类库。一般情况下是默认的类加载器。
  • 自定义类加载器 Custom ClassLoader
    负责加载用户自定义的jar包

以上4种类加载器之间的这种层次关系称为类加载器的双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般 不会以继承的关系来实现,而是都是 用组合关系来复用父加载器的代码。

双亲委派的工作过程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。【PS:典型的啃老族,有问题先找老子,老子搞不定了才自己去尝试解决】

双亲委派机制的最大优点就是使得java类随着它的类加载器一起具备了一种带有优先级的层次关系。尤其是保证了基础类的统一性,保证了java程序的稳定运行。

类加载的流程

在这里插入图片描述

加载

a:加载的类的字节码文件以及二进制文件的来源
通过一个类的完整路径查找此类字节码文件(class 文件即二进制文件)。将二进制文件的静态存储结构转化为方法区的运行时数据结构,并利用二进制流文件创建一个Class对象,存储在 Java 堆中用于对方法区的数据结构引用的入口;
class 文件的来源:有一点需要注意的是类加载机制不仅可以从文件系统读取 class 文件,也可以通过网络获取,其他 jar 包或者其他程序生成,如 JSP 应用。

b:类加载器
讲到类加载不得不讲到类加载的顺序和类加载器。Java 中大概有四种类加载器,分别是:启动类加载器(Bootstrap ClassLoader),扩展类加载器(Extension ClassLoader),系统类加载器(System ClassLoader),自定义类加载器(Custom ClassLoader),依次属于继承关系(注意这里的继承不是 Java 类里面的 extends)

  • 启动类加载器(Bootstrap ClassLoader):主要负责加载存放在Java_Home/jre/lib下,或被-Xbootclasspath参数指定的路径下的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载),启动类加载器是无法被Java程序直接引用的。

  • 扩展类加载器(Extension ClassLoader):主要负责加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载Java_Home/jre/lib/ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

  • 系统类加载器(System ClassLoader):主要负责加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

  • 自定义类加载器(Custom ClassLoader:自己开发的类加载器

c:双亲委派
如果一个类加载器需要加载类,那么首先它会把这个类加载请求委派给父类加载器去完成,如果父类还有父类则接着委托,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。在这里插入图片描述

验证

验证的过程只要是保证 class 文件的安全性和正确性,确保加载了该 class 文件不会导致 JVM 出现任何异常,不会危害JVM 的自身安全。验证包括对文件格式的验证,元数据和字节码的验证。

准备

准备阶段是为类变量进行内存分配和初始化零值的过程。注意这时候分配的是类变量的内存,这些内存会在方法区中分配。此时不会分配实例变量的内存,因为实例变量是在实例化对象时一起创建在Java 堆中的。而且此时类变量是赋值为零值,即 int 类型的初值为 0,引用类型初值为 null,而不是代码中显示赋值的数值。

解析

将常量池的符号引用转化成直接引用。符号引用可以理解为只是个替代的标签,比如你此时要做一个计划,暂时还没有人选,你设定了个 A 去做这个事。然后等计划真的要落地的时候肯定要找到确定的人选,到时候就是小明去做一件事。
解析就是把 A(符号引用) 替换成小明(直接引用)。符号引用就是一个字面量,没有什么实质性的意义,只是一个代表。直接引用指的是一个真实引用,在内存中可以通过这个引用查找到目标。

初始化

初始化的阶段是类加载的最后一步,这个阶段主要是执行 java 代码,进行相关初始化的动作;这时候就执行一些静态代码块,为静态变量赋值,这里的赋值才是代码里面的赋值,准备阶段只是设置初始值占个坑。

数据结构

常见八大排序

红黑树和AVL树的区别

MySQL

常见面试题

常见面试题

1.索引

索引是一种特殊的文件,它们包含着对数据表里所有记录的引用指针。更通俗一点讲,索引就是一个目录,用来定位我们所需要的内容所处的位置。
索引是一种数据结构,底层实现通常采用的是B树及其变种的B+树。数据库索引是数据库管理系统中一个排序的数据结构,以协助快速查询,更新数据库表中数据。
索引是帮助mysql高效获取数据的排好序的数据结构
优点:大大加快数据的检索速度。在查询过程中使用优化隐藏器,提高系统的性能。
缺点:创建和维护比较浪费时间(时间方面),需要占据物理空间(空间方面)

a.索引的基本原理

用来快速寻找那些具有特定值的记录

把无序的数据变成了有序的查询

把创建出来的索引的内容进行排序

对排序结果生成倒排表

在倒排表的内容上加上数据地址链

在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据

b.索引的四种类型

类型特点
主键索引列不能重复,不能为空,每个表只能有一个主键
唯一索引索引列的所有值都只能出现一次,可以为空
普通索引值可以为空,没有唯一性的约束,即数可以重复
全文索引用来对大表的文本域进行索引

c.索引的数据结构

B+树,Hash

大多数需求为单条记录查询时,使用hash索引查询性能最快;其余大部分场景建议使用B+树索引

为什么用B+树呢?

相对于二叉树,层级更少,搜索效率更高
对于B-树,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针跟着减少,要同样保持大量数据,只能增加树的高度,导致性能降低

相对于Hash索引,B+树支持范围匹配及排序操作

用B+树而不用B树的理由:

  • B树只适合随机检索,而B+树同时支持随机检索和顺序检索
  • B+树空间利用率更高,可减少I/O操作次数,磁盘读写代价更低
  • B+树的查询效率更加的稳定一些
  • B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。B+树的叶子节点使用指针顺序来接在一起,只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的范围查询操作
  • 增加文件(节点)时,效率更高。因为B+树的叶子节点包含所有的关键字,并以有序的链表结构存储,可以提高增删效率。

Hash的特点:

  • 只能用于对等比较,不能进行范围查询
  • 无法利用索引完成排序操作,是无序的
  • 查询效率高,在没有hash冲突的情况下,只需要一次查询

B+树的特点:

  • 非叶子节点存储的是指针,不会保存数据
  • 所有的关键字都出现在叶子节点的链表中,并且是有序的

d.索引的实现原则

  • 针对数据量较大,且查询比较频繁的表建立索引

  • 针对常作为查询条件(where),排序(group by)操作的字段建立索引

  • 尽量选择区分度高的列作为索引,区分度越高,使用索引的效率越高

  • 如果是字符串类型的字段,字段的长度较长,可以针对于字段的特点,建立前缀索引

  • 尽量使用联合索引,减少单列索引

  • 控制索引的数量,并不是越多的越好,多的话代价就会越大,会影响增删改的效率

2. 事务

一个最小的不可再分的工作单元,通常一个事务对应一个完整的业务。

一个完整的事务需要批量的DML语句共同联合完成

事务只和DML语句有关,只有DML语句才有事务。

a.事务的四大特性(ACID)

-特性-特点
原子性(Atomicity)事务是不可分割的最小操作单元,要么全部成功,要么全部失败
一致性(Consistency)事务完成时,必须所有的数据都保持一致状态
隔离性(Isolation)数据库提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行
持久性(Durability)事务一旦提交或回滚,他对数据库中的改变就是永久的

b.并发事务问题

-问题-描述
脏读一个事务读到另外一个事务还没有提交的数据
不可重复读一个事务先后读取同一条数据,但是两次读取的结果不同
幻读一个事务按照条件查询数据时,没有对应的数据行,但是插入数据时,又发现这行数据又存在了

c.事务隔离级别?Mysql的默认隔离级别是什么?

为了达到事务的四大特性,数据库定义了四种不同的事务隔离级别;
由低到高依次为:

  • Read-uncommitted(读取未提交):允许读取尚未提交的数据变更,可能会导致脏读,幻读或者不可重复读
  • Read-committed(读取已提交):允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读和不可重复读可能还会发生
  • Repeatable-Read(可重复读):对同一字段的多次读取结果都是一致的,除非数据是本身事务自己所修改,可以阻止脏读和不可重复读,但是幻读可能还会又可能发生
  • Serializable(可串行化):最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样的事务之间就完全不可能产生干扰,该级别可以防止脏读,不可重复读,幻读

Mysql的默认隔离级别是Repeatable-Read(可重复读)

Oracle的默认隔离级别是Read-committed(读取已提交)

d.ACID靠什么保证的?

原子性(A):由undo log日志保证的,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql语句
一致性 (C):由其他三大特性进行保证,程序代码要保证业务上的一致性
隔离性(I):由MVCC来保证
持久性(D):由内存+redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,宕机(死机)的时候可以从redo log中恢复

3.锁

a.对MySQL的锁了解吗?

当数据库有并发事务的时候,可能产生的数据不一致,这时候需要一些机制来保证访问的次序,锁就是这样的机制

就像酒店的房子一样,如果可以随意进出,就可能出现好几个人抢夺一个房子的情况。而给房间加个锁,申请到房间的人才可以入住并且进去后将房子锁起来,等使用完后再供给别人使用。

b.隔离级别与锁的关系

在Read-uncommitted级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突

在Read-committed级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁

在Repeatable-Read级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才能释放共享锁

Serializable是限制性最高的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成

c.什么是死锁?怎么解决?

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶行循环的现象

常见的解决死锁的方法:

a.如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低产生死锁的概率
b.在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率
c.对于非常容易产生死锁的业务部分,可以尝试使用锁定颗粒度,通过表级锁定来减少死锁产生的概率
d.MySQL的锁有哪些,如何理解?
按锁粒度分类:
  • 行锁:锁某行数据,锁粒度最小,并发度最高
  • 表锁:锁整张表,锁粒度最大,并发度低
  • 间隙锁:锁的是一个区间
属性分类:
  • 共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务可以读,但是不能写

  • 排它锁:也就是写锁,一个事务给某行数据加了读锁,其他事务不能读,也不能写

其他:
  • 乐观锁
  • 悲观锁
e.数据库的乐观锁和悲观锁是什么?怎么实现的?
  • 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库的锁机制

  • 乐观锁:假定不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:使用版本号机制或CAS算法实现

4.MySQl慢查询该如何优化?

  • 检查是否走了索引,如果没有,则优化sql利用索引
  • 检查所利用的索引是否是最优索引
  • 检查所查字段是否都是必须的,是否查询了过多的字段,查出了多余数据
  • 检查表中的数据是否过多,是否应该进行分库分表
  • 检查数据库实例所在机器的性能设置是否太低,是否可以增加适当资源

5.sql的优化做过吗?有哪些策略?

  • 创建表的时候。应尽量建立主键,根据主键查询数据;
  • 大数据表删除,用truncate table代替delete。
  • 合理使用索引,组合索引的列顺序尽量与查询条件列顺序保持一致;
  • 对于数据操作频繁的表,索引需要定期重建,以减少失效的索引和碎片。查询尽量用确定的列名,少用*号。
  • 尽量少嵌套子查询,这种查询会消耗大量的CPU资源;
  • 对于有比较多or运算的查询,建议分成多个查询,用union all联结起来
  • 多表查询的查询语句中,选择最有效率的表名顺序(基于规则的优化器中有效)

6.MyISAM与InnoDB的区别

MyISAM:

  • 不支持事务,但是每次查询都是原子的
  • 支持表级锁,即每次操作都是对整个表加锁;
  • 存储表的总行数
  • 采用非聚簇索引,索引文件的数据域存储指向数据文件的指针,索引和数据是分离开的。辅索引与主索引基本一致,但是辅索引不用保证唯一性

InnoDB:

  • 支持ACID事务,支持事务的四种隔离级别
  • 支持行级锁和外键约束:因此可以支持写并发
  • 不存储总行数
  • 采用的是聚簇索引,数据文件和索引是绑定在一起的,必须要有主键

7.为什么建议InnoDB表必须建主键?并且推荐使用整形的自增主键?

推荐使用整形主键:整形的比较效率高,查找起来比较快一点,占用的空间比较小一点,节约空间。

自增ID可以保证每次插入时B+索引是从右边扩展的,可以避免B+树和频繁合并和分裂(对比使用UUID)。如果使用字符串主键和随机主键,会使得数据随机插入,效率比较差。

8.为什么非主键索引结构叶子节点存储的是主键值?

减少了出现行移动或者数据页分裂时二级索引的维护工作(当数据需要更新的时候,二级索引不需要修改,只需要修改聚簇索引,一个表只能有一个聚簇索引,其他的都是二级索引,这样只需要修改聚簇索引就可以了,不需要重新构建二级索引)

9.最左前缀原则

当一个SQL想要使用索引时,就一定要提供该索引所对应的字段中的最左边的字段,也就是排在最前面的字段,比如a,b,c三个字段建立了一个联合索引,那么在写一个sql时就一定要提供a字段的条件,这样才能用到联合索引。这是由于在建立a,b,c三个字段的联合索引时,底层的B+树是按照a,b,c三个字段从左到右去比较大小进行排序的。

10. 为什么不建议用 select*from… 来查找数据?

  • 浪费资源,降低效率

  • select* 用来获取表中所有的数据字段,但是我们实际上是不需要查询这么多数据的,这样做会浪费系统资源,降低查询效率

  • 可读性
    我们需要的是某几个数据字段,但是select*会查找出所有的数据,导致可读性降低

  • 程序变更问题
    加入我们有两个功能A,B同时使用这张表table,但是某一天要修改一下B的功能,但是要修改就必须要给table加某些字段,此时对A来说又多了几项不需要的字段,其实并没有什么用,白白浪费资源和时间。

网络

cookie和session

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

请简述TCP和UDP的区别

TCP和UDP都是OSI模型中运输层中的协议,

TCP—传输控制协议,提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。

UDP—用户数据报协议,是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。

  1. TCP 是面向连接的,UDP 是面向无连接的
  2. UDP程序结构较简单,占用资源少
  3. TCP 是面向字节流的,UDP 是基于数据报的
  4. TCP 保证数据正确性,UDP 可能丢包
  5. TCP 保证数据顺序,UDP 不保证

为什么TCP可靠而UDP不可靠

通过 TCP 连接传输的数据无差错,不丢失,不重复,且按顺序到达。

TCP 报文头里面的序号能使 TCP 的数据按序到达

报文头里面的确认序号能保证不丢包,累计确认及超时重传机制

TCP 拥有流量控制及拥塞控制的机制

参考这篇文章

TCP是如何保证可靠性传输的

校验和
在传输过程中,将发送的数据段都当作是一个16位的整数。将这些整数加起来,并且保证前面的进位不能丢弃,补在最后面,然后取反,得到校验和。
确认到达和序列号
TCP给每个字节的数据都进行了编号(序列号),传输过程中,每次接收方收到数据以后,都会对传输方进行确认应答,回复一个确认序列号,告诉发送方,接收到了那些数据,下次的数据从哪发。

超时重传
发送方发送完数据后,等待一段时间,在这个时间内,如果发送方没有接收到响应,就会对刚才的数据进行重新的发送

连接管理
三次握手四次挥手

流量控制
接收端在接收到数据后,对其进行处理。如果发送端的发送速度太快,导致接收端的接受缓冲区很快的填充满了。此时如果发送端仍旧发送数据,那么接下来发送的数据都会丢包,继而导致丢包的一系列连锁反应,超时重传呀什么的。而TCP根据接收端对数据的处理能力,决定发送端的发送速度,这个机制就是流量控制。

拥塞控制
TCP传输的过程中,发送端开始发送数据的时候,如果刚开始就发送大量的数据,那么就可能造成一些问题。所以TCP引入了慢启动的机制,在开始发送数据时,先发送少量的数据探路。探清当前的网络状态如何,再决定多大的速度进行传输。这时候就引入一个叫做拥塞窗口的概念

粘包/拆包的原因

发生TCP粘包或拆包有很多原因,现列出常见的几点:

  1. 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
  2. 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
  3. 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
  4. 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

粘包、拆包解决办法

TCP本身是面向流的,作为网络服务器,如何从这源源不断涌来的数据流中拆分出或者合并出有意义的信息呢?通常会有以下一些常用的方法:

  1. 发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
  2. 发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
  3. 可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

TCP三次握手

三次握手指的是:建立一个TCP连接时,需要客户端和服务器总共发送三个包。

三次握手的目的是:连接服务器的指定端口,建立TCP连接,并同步连接双方的序列号和确认号并交换TCP窗口大小。

第一次握手:建立连接,客户端向服务器发送请求报文段,将SYN置为1,客户端进入SYN_SEND状态,等待服务器确认

第二次握手:服务器接收到客户端的SYN 报文段,对这个SYN报文段进行确认;同时自己发送SYN请求信息,将SYN置为1;服务器将上述所有信息放入报文段(SYN+ACK)一起发送给客户端,此时服务器进入SYN_RECV状态

客户端接收到SYN+ACK报文段,向服务器发送ACK报文段,发送完毕后客户端和服务器都进入ESTABLISHED状态,完成三次握手

TCP四次挥手

第一次挥手:客户端数据传输完毕需要断开连接,发送报文段并停止再次发送数据,主动关闭TCP连接,进入FIN-WAIT-1状态

第二次挥手:服务器接收到客户端发送的报文段后,进入关闭等待状态,客户端到服务
器的连接释放,客户端收到服务器的确认后,进入FIN-WAIT-2状态,等待服务器发出连接释放的报文段

服务器的数据传输完毕后,向客户端发送连接释放报文段,服务器进入最后确认状态,等待客户端的确认

客户端收到服务器的连接释放报文段后,发出确认报文段,进入等待状态,经过等待时间后进入关闭状态,四次握手结束

从输入网址到显示网页的全过程

输入网址
DNS域名解析,将url解析为ip地址
客户端与服务器建立TCP连接
客户端向服务器发送HTTP请求
服务器处理客户端发来的请求
服务器响应请求,返回给浏览器一个响应
浏览器解析响应,展示HTML

输入url没有访问到网页的原因

DNS域名解析出问题
网络断了
服务器拒绝访问
请求或者响应在网络传输中途被劫走了

OSI七层模型

在这里插入图片描述

TCP/IP四层模型

在这里插入图片描述

HTTP 协议包括哪些请求

GET:对服务器资源的简单请求
POST:发送包含用户提交数据的请求
HEAD:类似于GET请求,不过返回的响应中没有具体内容,用于获取报头
PUT:用来传输文件,要求在请求报文的主体中包含文件内容,然后保存到请求的URI指定位置
DELETE:与PUT相反,用来删除文件,按照请求删除指定的资源
TRACE:让服务器将之前的请求通信环返回给客户端
CONNECT:与代理服务器通信时建立隧道,实现用隧道协议进行TCP通信

HTTP响应状态码

在这里插入图片描述

  • 200
    从客户端发来的请求在服务器端被正常处理了
  • 204
    服务器接收的请求已被成功处理,但在返回的响应报文中不包含实体的主体成分
  • 206
    客户端向服务器成功请求指定范围的实体内容
  • 301
    请求的资源已经被永久的移动到新的URI,以后新的请求应该使用新的URI代替
  • 302
    资源临时移动,客户端应继续使用原有的URI
  • 303
    请求对应的资源存在着另一个URI,应使用GET方法定向获取请求的资源
  • 302
    客户端发送附带条件的请求时,服务器允许访问资源,但是并没有返回任何实体
  • 400
    客户端请求的语法错误,服务器无法理解
  • 401
    请求需要有通过HTTP认证的认证信息
  • 403
    请求资源的房屋内被服务器拒绝了
  • 404
    服务器上无法找到请求的资源
  • 500
    服务器内部错误,无法完成请求
  • 503
    服务器暂时处于超负载或正在进行停机维护,现在无法处理请求

http与https

  • HTTP协议以明文方式发送内容,不提供任何方式的数据加密,HTTP协议不适合传输一些敏感信息
  • HTTPS在HTTP的基础上加入了SSL协议,SSL依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密
  • http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443
  • HTTPS协议握手阶段比较费时,会使页面的加载时间延长近50%,增加10%到20%的耗电;
  • HTTPS连接缓存不如HTTP高效,会增加数据开销和功耗,甚至已有的安全措施也会因此而受到影响;

GET和POST的区别

  • 语义不同:GET一般用于获取数据,POST一般用于提交数据

  • GET的body一般为空,需要传递的数据通过queryString传递,POST的queryString一般为空,需要传递的数据通过body传递

  • GET请求一般是幂等的,POST的请求一般是不幂等的。
    (幂等:如果多次请求的结果一样,就视为请求是幂等的。)

  • GET可以被缓存,POST不能被缓存

  • GET提交的数据有1024个字节的限制,POST无限制

计算机网络里面每一层的协议

在这里插入图片描述

多线程

创建线程的几种方式

  1. 通过显示继承一个 Thread 类的方式来实现
  2. 通过匿名内部类的方式继承 Thread 类
  3. 显式创建一个类,实现 Runnable 接口;然后把 Runnable实例 关联到一个 Thread 实例上
  4. 通过匿名内部类的方式,实现Runnable
  5. 使用 lambda 表达式,来指定线程执行的内容

代码示例:

public class ThreadDemo3 {
    //继承一个thread类
    static class MyThread extends Thread{
        	@Override
        	public void run() {
         	   System.out.println("Hello World, 我是一个线程");
       	    }
  	    }
    
    // Runnable 本质上就是描述了 一段要执行的任务代码是啥
    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            System.out.println("我是一个新线程~");
        }
    }
    
    public static void main(String[] args) {
        // 1.显式继承 Thread
        // 创建线程需要使用 Thread 类,来创建一个 Thread 的实例
        // 另一方面还需要给这个线程指定 要执行哪些指令/代码
        // 指定指令的方式有很多种,此处先用一种简单的,直接继承Thread类,
        // 重写 Thread 类中的 run 方法

        // 当 Thread 对象被创建出来的时候,内核中并没有随之产生一个线程(PCB)
        Thread t = new MyThread();
        // 执行这个 start 方法,才是真的创建出一个线程
        // 此时内核中才随之出现了一个 PCB,这个 PCB 就会对应让 CPU 来执行该线程的代码(上面的run方法中的逻辑)
        t.start();

        // 2.通过匿名内部类的方式,继承Thread来创建线程
        Thread t = new Thread(){
            @Override
            public void run() {

            }
        };
        t.start();

        // 3.显式创建一个类,实现 Runnable 接口
        // 然后把 Runnable实例 关联到一个 Thread 实例上
        Thread t = new Thread(new MyRunnable());
        t.start();

        // 4.通过匿名内部类的方式,实现Runnable
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("我是一个新线程~~");
            }
        };
        Thread t2 = new Thread(runnable);
        t2.start();

        // 5.使用 lambda 表达式,来指定线程执行的内容
        Thread t = new Thread(()->{
            System.out.println("我是一个新线程~~~");
        });
        t.start();
    }
}

无论是哪种方式,没有本质上的区别 (站在操作系统的角度),核心都是依靠Thread类,只不过指定线程执行的任务的方式有所差异

细节上有点差别(站在代码耦合性角度):
通过 Runnable / lambda 的方式来创建线程 和 继承 Thread 类相比,代码耦合性要更小一些,在写 Runnable / lambda 的时候 run 中没有涉及到任何 Thread 相关的内容,这就意味着,很容易把这个逻辑从多线程中剥离出来,去搭配其他的并发编程的方式来执行,当然也可以很容易的改成不并发的方式执行

线程和进程区别

  • 进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
  • 每个进程都有独立的代码和数据空间,程序之间的切换会有较大的开销;
  • 线程可以看做轻量级的进程,同一类线程共享代码和数据空间,线程之间切换的开销小。
  • 进程之间的资源是独立的,线程共享本进程的资源

多线程与多进程

  • 多进程:在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,多任务之间不会相互影响

  • 多线程:在一个程序中,有很多的操作是非常耗时的,如数据库读写操作,IO操作等,如果使用单线程,那么程序就必须等待这些操作执行完成之后才能执行其他操作。使用多线程,可以在将耗时任务放在后台继续执行的同时,同时执行其他操作。多线程是异步的,但这不代表多线程真的是几个线程是在同时进行,实际上是系统不断地在各个线程之间来回的切换。

在这里插入图片描述

线程池的工作原理

在这里插入图片描述

线程池的优势

总体来说,线程池有如下的优势:

(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

功能线程池

嫌上面使用线程池的方法太麻烦?其实Executors已经为我们封装好了 4 种常见的功能线程池,如下:

  1. 定长线程池(FixedThreadPool)
  2. 定时线程池(ScheduledThreadPool )
  3. 可缓存线程池(CachedThreadPool)
  4. 单线程化线程池(SingleThreadExecutor)

synchronized与Lock的区别

在这里插入图片描述

死锁的概念及条件

原文链接:死锁参考此篇

概念: 多个线程因争夺资源而造成的僵局

(1)互斥条件:指线程对所分配的资源进行排他性使用,即在一段时间内某资源只由一个线程占用。
(2)请求和保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不可剥夺条件:进程已获得的资源,在未使用之前,不能强行剥夺。
(4)循环等待条件:指在发生死锁时,必然存在一个进程资源循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求

死锁产生的原因

  1. 竞争不可抢占资源引起死锁
    通常系统中拥有的不可抢占资源,其数量不足以满足多个进程运行的需要,使得进程在运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可抢占资源的竞争 才可能产生死锁,对可抢占资源的竞争是不会引起死锁的。
  2. 竞争可消耗资源引起死锁
  3. 进程推进顺序不当引起死锁
    进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都会因为所需资源被占用而阻塞。
    信号量使用不当也会造成死锁。进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。

处理死锁的方法

  1. 预防死锁: 通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。
  2. 避免死锁: 在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。
  3. 检测死锁: 允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。
  4. 解除死锁: 当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。

预防死锁

  • 破坏“互斥”条件:
    就是在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是肯定不会发生的。但一般来说在所列的四个条件中,“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。

注意:互斥条件不能被破坏,否则会造成结果的不可再现性。

  • 破坏“占有并等待”条件:
    破坏“占有并等待”条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源。
    方法一:创建进程时,要求它申请所需的全部资源,系统或满足其所有要求,或什么也不给它。这是所谓的 “ 一次性分配”方案。
    方法二:要求每个进程提出新的资源申请前,释放它所占有的资源。这样,一个进程在需要资源S时,须先把它先前占有的资源R释放掉,然后才能提出对S的申请,即使它可能很快又要用到资源R。

  • 破坏“不可抢占”条件:
    破坏“不可抢占”条件就是允许对资源实行抢夺。
    方法一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
    方法二:如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,方法二才能预防死锁。

  • 破坏“循环等待”条件:
    破坏“循环等待”条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。

避免死锁

理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。所以,在系统设计、进程调度等方面注意如何让这四个必要条件不成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。

预防死锁和避免死锁的区别:
预防死锁是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现,而避免死锁则不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。避免死锁是在系统运行过程中注意避免死锁的最终发生。

常用避免死锁的方法
  • 有序资源分配法
    这种算法资源按某种规则系统中的所有资源统一编号(例如打印机为1、磁带机为2、磁盘为3、等等),申请时必须以上升的次序。系统要求申请进程:
      1、对它所必须使用的而且属于同一类的所有资源,必须一次申请完;
      2、在申请不同类资源时,必须按各类设备的编号依次申请。例如:进程PA,使用资源的顺序是R1,R2; 进程PB,使用资源的顺序是R2,R1;若采用动态分配有可能形成环路条件,造成死锁。
      采用有序资源分配法:R1的编号为1,R2的编号为2;
      PA:申请次序应是:R1,R2
      PB:申请次序应是:R1,R2
      这样就破坏了环路条件,避免了死锁的发生。

  • 银行家算法
    详见银行家算法.

常用避免死锁的技术
  1. 加锁顺序(线程按照一定的顺序加锁)
  2. 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  3. 死锁检测

加锁顺序
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。

如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。看下面这个例子:

Thread 1: 
lock A 
lock B 
Thread 2: 
wait for A 
lock C (when A locked) 
Thread 3: 
wait for A 
wait for B 
wait for C

如果一个线程(比如线程3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。

例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁C(译者注:获取锁A是获取锁C的必要条件)。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前,必须成功地对A加了锁。

按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(译者注:并对这些锁做适当的排序),但总有些时候是无法预知的。

加锁时限
另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(译者注:加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。

以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:

Thread 1 locks A 
Thread 2 locks B 
Thread 1 attempts to lock B but is blocked 
Thread 2 attempts to lock A but is blocked 
Thread 1’s lock attempt on B times out 
Thread 1 backs up and releases A as well 
Thread 1 waits randomly (e.g. 257 millis) before retrying. 
Thread 2’s lock attempt on A times out 
Thread 2 backs up and releases B as well 
Thread 2 waits randomly (e.g. 43 millis) before retrying.

在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁(除非线程2或者其它线程在线程1成功获得两个锁之前又获得其中的一些锁)。

需要注意的是,由于存在锁的超时,所以我们不能认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。

此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。
(译者注:超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试,导致新一轮的竞争,带来了新的问题。)

这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。写一个自定义锁类不复杂,但超出了本文的内容。后续的Java并发系列会涵盖自定义锁的内容。

死锁检测
死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。

当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。

下面是一幅关于四个线程(A,B,C和D)之间锁占有和请求的关系图。像这样的数据结构就可以被用来检测死锁。
在这里插入图片描述

检测死锁

一般来说,由于操作系统有并发,共享以及随机性等特点,通过预防和避免的手段达到排除死锁的目的是很困难的。这需要较大的系统开销,而且不能充分利用资源。为此,一种简便的方法是系统为进程分配资源时,不采取任何限制性措施,但是提供了检测和解脱死锁的手段:能发现死锁并从死锁状态中恢复出来。因此,在实际的操作系统中往往采用死锁的检测与恢复方法来排除死锁。
死锁检测与恢复是指系统设有专门的机构,当死锁发生时,该机构能够检测到死锁发生的位置和原因,并能通过外力破坏死锁发生的必要条件,从而使得并发进程从死锁状态中恢复出来。

这时进程P1占有资源R1而申请资源R2,进程P2占有资源R2而申请资源R1,按循环等待条件,进程和资源形成了环路,所以系统是死锁状态。进程P1,P2是参与死锁的进程。
下面我们再来看一看死锁检测算法。算法使用的数据结构是如下这些:
占有矩阵A:nm阶,其中n表示并发进程的个数,m表示系统的各类资源的个数,这个矩阵记录了每一个进程当前占有各个资源类中资源的个数。
申请矩阵R:n
m阶,其中n表示并发进程的个数,m表示系统的各类资源的个数,这个矩阵记录了每一个进程当前要完成工作需要申请的各个资源类中资源的个数。
空闲向量T:记录当前m个资源类中空闲资源的个数。
完成向量F:布尔型向量值为真(true)或假(false),记录当前n个并发进程能否进行完。为真即能进行完,为假则不能进行完。
临时向量W:开始时W:=T。

算法步骤:
(1)W:=T,
对于所有的i=1,2,…,n,
如果A[i]=0,则F[i]:=true;否则,F[i]:=false
(2)找满足下面条件的下标i:
F[i]:=false并且R[i]〈=W
如果不存在满足上面的条件i,则转到步骤(4)。
(3)W:=W+A[i]
F[i]:=true
转到步骤(2)
(4)如果存在i,F[i]:=false,则系统处于死锁状态,且Pi进程参与了死锁。什么时候进行死锁的检测取决于死锁发生的频率。如果死锁发生的频率高,那么死锁检测的频率也要相应提高,这样一方面可以提高系统资源的利用率,一方面可以避免更多的进程卷入死锁。如果进程申请资源不能满足就立刻进行检测,那么每当死锁形成时即能被发现,这和死锁避免的算法相近,只是系统的开销较大。为了减小死锁检测带来的系统开销,一般采取每隔一段时间进行一次死锁检测,或者在CPU的利用率降低到某一数值时,进行死锁的检测。

解除死锁

一旦检测出死锁,就应立即釆取相应的措施,以解除死锁。
死锁解除的主要方法有:

  1. 资源剥夺法。挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但应防止被挂起的进程长时间得不到资源,而处于资源匮乏的状态。
  2. 撤销进程法。强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源。撤销的原则可以按进程优先级和撤销进程代价的高低进行。
  3. 进程回退法。让一(多)个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。

并发,并行,串行区别

  1. 并发指两个或多个事件在同一时间间隔内发生,即交替做不同事的能力,多线程是并发的一种形式。
  2. 并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时进行,即同时做不同事的能力
  3. 串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务只能干等着。

并发的三大特性

  1. 原子性:在一次或者多次操作时,要么所有操作都被执行,要么所有操作都不执行
  2. 有序性:程序执行的顺序按照代码的先后顺序执行
  3. 可见性:一个线程对共享变量值的修改,能够及时的被其他线程看到

线程的生命周期,以及线程有哪些状态?

线程中通常有五种状态,创建,就绪,运行,阻塞,死亡

创建状态: 新创建了一个线程对象

就绪状态: 线程对象创建以后,其他线程执行该对象的start方法时,该线程就会处于一个就绪的状态,变得可运行,等待着cpu的使用权

运行状态: 处于就绪态的线程获得了cpu的使用权,处于运行态

阻塞态: 线程因为某种原因放弃了cpu的使用权,暂时停止运行。直到线程进入到就绪态才能够再次运行

阻塞又分为3种:

  • 等待阻塞: 运行的线程执行wait方法,该线程会释放占用的所有资源,JVM把线程放入到等待池里。进入这个状态后,线程是不能够自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒。
  • 同步阻塞: 运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入到锁池中。
  • 其他阻塞: 线程执行sleep方法或join方法,或者发出了IO请求,JVM将该线程置为阻塞状态。

死亡态: 线程执行完或者遇到了某种异常推出了run方法,该线程结束生命周期。

sleep,wait,yield,join

sleep:Thread的静态方法,将cpu的执行资格和执行权让出去,不再运行此线程,当定时时间过了后,参与cpu的调度,获取到cpu的使用权后就可以继续运行了。sleep不依赖同步器synchronized

wait:Object类的方法,当线程调用wait方法后,会将线程放入到等待池中,不会自动唤醒,得依靠notify,notifyAll方法。依赖同步器synchronized

yield:执行后线程会进入就绪状态,并且,马上释放cpu的使用权,但是保留了cpu的执行资格

join:执行后线程进入阻塞状态,在B中调用A的join,B线程会进入阻塞态,一直到A结束或者中断线程

对线程安全的理解

当多个线程访问同一个对象时,如果不用额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个线程是安全的。
(多个线程访问同一个对象跟单个线程执行的结果一样就是安全的)

主要的线程安全问题都是在堆中发生的,因为==堆是共享内存==,所有的线程都可以访问到该区域

说说你对守护线程的理解

守护线程:为所有非守护线程(用户线程)提供服务的线程;任何一个守护线程都是整个jvm中所有非守护线程的保姆

守护线程类似于整个进程的一个默默无闻的小喽啰;它的生死无关重要,却依赖整个进程而运行;哪天其他线程结束了,没有要执行的了,程序就结束了,理都不理守护线程,直接把它中断。

注意: 由于非守护线程的终止是自身无法控制的

守护线程的应用场景:

  • 来为其他服务提供服务支持的情况
  • 或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程使用

为什么使用线程池?解释一下线程池的参数?

使用原因:

  1. 降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗
  2. 提高响应速度;任务来了,直接由线程可用可执行,而不是先创建线程再执行
  3. 提高线程的课管理性;线程是稀缺资源,使用线程池可以统一的分配调优监控

参数解释:

corePoolSize :代表线程核心数,也是正常情况下创建工作的线程数,这些线程创建后而不会消除

maxinumPoolSize:代表的是最大线程数,与核心线程数相对应,表示最大允许被创建的线程数

keepAliveTime:空闲时间,超出核心线程之外的线程空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除

unit:上面这个空闲时间的单位

workQueue:任务队列,存放待执行的任务,假设核心线程已经全部被使用了,还有任务进来就放放入队列,等到队列放满时还有任务进来,此时就会开始创建新的线程

ThreadFactory:实际上是一个线程工厂,用来生产线程执行任务

Handler:任务拒绝策略,有两种情况,

1.当我们调用shutdown等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。

2.当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这时也会拒绝。

如何理解volatile关键字?

volatile关键字用来修饰对象的属性,在并发环境下可以保证这个属性的可见性,对于加了volatile关键字的属性,在对这个属性进行修改时,会直接将cpu高级缓存中的数据协会到主内存中,对这个变量的读取也会直接从主内存中读取,从而保证了可见性
底层是通过操作系统的内存屏障来实现的,由于使用了内存屏障,所以会禁止指令重排,也就保证了有序性

synchronized和lock都能进行加锁,那么区别是什么呢?

  1. 从语法上看
    synchronized是自动的加锁释放锁,而lock是我们进行显式(手动)来加 锁和释放锁(执行完,不管是否出现异常,都需要释放锁)。Lock相对就更加的灵活。

  2. lock提供了更多的获取锁的方式
    lock():和synchronized申请锁类似,申请失败就干等(无条件等)
    lockInterruptibly():可被中断的申请锁(申请失败等待时,可以被其它线程中断)
    tryLock():尝试获取锁(不阻塞),如果申请成功就加锁返回true,反之马上就返回false。
    tryLock(long timeout,TimeUnit unit):尝试获取锁,如果申请失败,是超时等待(等待一段时间),这段是时间过去后还没获取到锁,就返回false。

  3. 从效率上看,当线程冲突比较严重的时候,lock的性能要高很多
    synchronized在申请锁成功的线程释放锁以后,所有之前因为申请锁失败而阻塞的线程,都会再次竞争。
    lock是基于aqs来实现:aqs是一个双端队列,专门用来进行线程状态的管理;相当于:竞争锁失败的线程就放到队列中(入队),并设置状态(未获取到锁),释放锁以后,把队列中的线程引用拿出来,设置状态(获取到锁)

aqs提供了很多种方法,用来方便的实现独占锁/共享锁,公平锁/非公平锁lock就是基于aqs独占锁的方式来实现,提供了公平和非公平锁的设置。公平就相当于队列的先进先出,非公平锁就是我们的随机出队。

什么是单例模式?单例模式中饿汉模式与懒汉模式的区别

单例模式的概念:

内存中只会创建并且只创建一次对象的设计模式。
程序中多次使用同一个对象时,为了防止频繁的创建对象使得内存飙升,我们只创建一次这个对象,从而让所有需要调用的地方都共享这个对象。

单例模式的特点:

只能有一个实例
单例类必须自己创建自己的唯一实例
单例类必须给所有其他对象提供这一实例

饿汉模式:

在类加载的同时,就创建线程实例,后面直接使用即可

懒汉模式:

类加载的时候不会创建,第一次使用的时候才会创建实例

饿汉模式是线程安全的
而懒汉模式是线程不安全的,解决方法: 把创建实例的代码放在一个方法中,然后给这个方法加上一个synchronized锁,以保证线程安全。
懒汉模式的优化: 在加锁的基础上,使用双重if判定,降低锁的竞争频率;给instance加上volatile关键字,保证其可见性

怎么理解乐观锁和悲观锁的,具体怎么实现呢?

  • 悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁.
  • 乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突.
  • 悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就
    等待.

  • 乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突.

介绍下读写锁

  • 读写锁就是把读操作和写操作分别进行加锁.
  • 读锁和读锁之间不互斥.
  • 写锁和写锁之间互斥.
  • 写锁和读锁之间互斥.
  • 读写锁最主要用在 “频繁读, 不频繁写” 的场景中.

什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.

相比于挂起等待锁

优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用.

缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源.

synchronized 是可重入锁么?

是可重入锁.

可重入锁指的就是连续两次加锁不会导致死锁.

实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增.

讲解下你自己理解的 CAS 机制

全称 Compare and swap, 即 “比较并交换”. 相当于通过一个原子的操作, 同时完成 “读取内存, 比
较是否相等, 修改内存” 这三个步骤

本质上需要 CPU 指令的支撑.

ABA问题怎么解决?

给要修改的数据 引入版本号.

在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.

如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增;
如果发现当前版本号比之前读到的版本号大, 就认为操作失败.

什么是偏向锁?

偏向锁不是真的加锁, 而只是在锁的对象头中记录一个标记(记录该锁所属的线程). 如果没有其他线程参与竞争锁, 那么就不会真正执行加锁操作, 从而降低程序开销. 一旦真的涉及到其他的线程竞争, 再取消偏向锁状态, 进入轻量级锁状态.

synchronized 实现原理 是什么?

  1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
  2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
  3. 实现轻量级锁的时候大概率用到的自旋锁策略
  4. 是一种不公平锁
  5. 是一种可重入锁
  6. 不是读写锁

加锁工作过程
JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。

1) 偏向锁
第一个尝试加锁的线程, 优先进入偏向锁状态.
偏向锁不是真的 “加锁”, 只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程.

如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)

如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别
当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.

偏向锁本质上相当于 “延迟加锁” . 能不加锁就不加锁, 尽量来避免不必要的加锁开销.

但是该做的标记还是得做的, 否则无法区分何时需要真正加锁.

举个栗子理解偏向锁
假设男主是一个锁, 女主是一个线程. 如果只有这一个线程来使用这个锁, 那么男主女主即使不领证
结婚(避免了高成本操作), 也可以一直幸福的生活下去.
但是女配出现了, 也尝试竞争男主, 此时不管领证结婚这个操作成本多高, 女主也势必要把这个动作
完成了, 让女配死心.

2) 轻量级锁
随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).
此处的轻量级锁就是通过 CAS 来实现

  • 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
  • 如果更新成功, 则认为加锁成功
  • 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).
  • 自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.
  • 因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了.
  • 也就是所谓的 “自适应”

3) 重量级锁
如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁

此处的重量级锁就是指用到内核提供的 mutex .

  • 执行加锁操作, 先进入内核态.
  • 在内核态判定当前锁是否已经被占用
  • 如果该锁没有占用, 则加锁成功, 并切换回用户态.
  • 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
  • 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒这个线程, 尝试重新获取锁.

介绍下 Callable 是什么

Callable 是一个 interface . 相当于把线程封装了一个 “返回值”. 方便程序猿借助多线程的方式计算
结果.

Callable 和 Runnable 相对, 都是描述一个 “任务”. Callable 描述的是带有返回值的任务,
Runnable 描述的是不带返回值的任务.

Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为
Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
FutureTask 就可以负责这个等待结果出来的工作.

线程同步的方式有哪些?

synchronized, ReentrantLock, Semaphore 等都可以用于线程同步.

为什么有了 synchronized 还需要 juc 下的 lock? 以 juc 的 ReentrantLock 为例,

synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更
灵活,

synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时
间就放弃.

synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个
true 开启公平锁模式.

synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的
线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线
程.

信号量听说过么?之前都用在过哪些场景下?

  • 信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器.

  • 使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待, 直到前面的线程执行了 V 操作.

解释一下 ThreadPoolExecutor 构造方法的参数的含义

理解 ThreadPoolExecutor 构造方法的参数
把创建一个线程池想象成开个公司. 每个员工相当于一个线程.

  • corePoolSize: 正式员工的数量. (正式员工, 一旦录用, 永不辞退)
  • maximumPoolSize: 正式员工 + 临时工的数目. (临时工: 一段时间不干活, 就被辞退).
  • keepAliveTime: 临时工允许的空闲时间.
  • unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.
  • workQueue: 传递任务的阻塞队列
  • threadFactory: 创建线程的工厂, 参与具体的创建线程工作.
  • RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理.
    1.AbortPolicy(): 超过负荷, 直接抛出异常.
    2.CallerRunsPolicy(): 调用者负责处理
    3.DiscardOldestPolicy(): 丢弃队列中最老的任务.
    4.DiscardPolicy(): 丢弃新来的任务.

ConcurrentHashMap的读是否要加锁,为什么?

读操作没有加锁. 目的是为了进一步降低锁冲突的概率. 为了保证读到刚修改的数据, 搭配了
volatile 关键字.

介绍下 ConcurrentHashMap的锁分段技术?

这个是 Java1.7 中采取的技术. Java1.8 中已经不再使用了. 简单的说就是把若干个哈希桶分成一个"段" (Segment), 针对每个段分别加锁.
目的也是为了降低锁竞争的概率. 当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争.

ConcurrentHashMap在jdk1.8做了哪些优化?

取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对
象). 将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式. 当链表较长的时候(大于等于8 个元素)就转换成红黑树.

Hashtable和HashMap、ConcurrentHashMap 之间的区别?

HashMap: 线程不安全. key 允许为 null

Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.

ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用

CAS 机制. 优化了扩容方式. key 不允许为 null

谈谈 volatile关键字的用法?

volatile 能够保证内存可见性. 强制从主内存中读取数据. 此时如果有其他线程修改被 volatile 修饰
的变量, 可以第一时间读取到最新的值.

Java多线程是如何实现数据共享的?

JVM 把内存分成了这几个区域:
方法区, 堆区, 栈区, 程序计数器.
其中堆区这个内存区域是多个线程之间共享的.
只要把某个数据放到堆内存中, 就可以让多个线程都能访问到.

Java创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?

创建线程池主要有两种方式:
通过 Executors 工厂类创建. 创建方式比较简单, 但是定制能力有限.
通过 ThreadPoolExecutor 创建. 创建方式比较复杂, 但是定制能力强.
LinkedBlockingQueue 表示线程池的任务队列. 用户通过 submit / execute 向这个任务队列中添
加任务, 再由线程池中的工作线程来执行任务.

Java线程共有几种状态?状态之间怎么切换的?

NEW: 安排了工作, 还未开始行动. 新创建的线程, 还没有调用 start 方法时处在这个状态.
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作. 调用 start 方法之后, 并正在
CPU 上运行/在即将准备运行 的状态.
BLOCKED: 使用 synchronized 的时候, 如果锁被其他线程占用, 就会阻塞等待, 从而进入该状
态.
WAITING: 调用 wait 方法会进入该状态.
TIMED_WAITING: 调用 sleep 方法或者 wait(超时时间) 会进入该状态.
TERMINATED: 工作完成了. 当线程 run 方法执行完毕后, 会处于这个状态.

在多线程下,如果对一个数进行叠加,该怎么做?

使用 synchronized / ReentrantLock 加锁
使用 AtomInteger 原子操作.

Servlet是否是线程安全的?

Servlet 本身是工作在多线程环境下.
如果在 Servlet 中创建了某个成员变量, 此时如果有多个请求到达服务器, 服务器就会多线程进行
操作, 是可能出现线程不安全的情况的.

Thread和Runnable的区别和联系?

Thread 类描述了一个线程.
Runnable 描述了一个任务.
在创建线程的时候需要指定线程完成的任务, 可以直接重写 Thread 的 run 方法, 也可以使用
Runnable 来描述这个任务.

多次start一个线程会怎么样

第一次调用 start 可以成功调用.
后续再调用 start 会抛出 java.lang.IllegalThreadStateException 异常

有synchronized两个方法,两个线程分别同时用这个方法,请问会发生什么?

synchronized 加在非静态方法上, 相当于针对当前对象加锁.
如果这两个方法属于同一个实例:
线程1 能够获取到锁, 并执行方法. 线程2 会阻塞等待, 直到线程1 执行完毕, 释放锁, 线程2 获取到
锁之后才能执行方法内容.
如果这两个方法属于不同实例:
两者能并发执行, 互不干扰.

进程和线程的区别?

进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
进程是系统分配资源的最小单位,线程是系统调度的最小单位。

Linux

linux系统常用的命令

文件和目录
cd … 返回上一级目录
cd …/… 返回上两级目录
cd /home 进入home目录
cd 进入个人主目录
cd - 返回上次所在目录
pwd 显示当前工作路径
ls 查看目录中的文件
ls -a 查看目录中包括隐藏文件
mkdir dir1 创建一个叫dir1的文件夹
rm -f file1 删除file1文件
rmdir dir1 删除dir1目录
rm -rf dir1 删除dir1目录并删除内容
mv 文件1 文件2 将文件1改名文件2
mv 文件 目录 将文件移动到目录
cp -r test/ newtest 将test/文件夹下的所有文件复制到newtest
ln -s log.txt link2021 为log.txt创建软连接
ln log.txt link2021 创建硬链接
touch a.txt 修改时间戳

linux命令:top、free、netstat、ps grep、nslookup

top持续监听进程运行状态
top 命令的输出内容是动态的,默认每隔 3 秒刷新一次。命令的输出主要分为两部分:

  • 第一部分是前五行,显示的是整个系统的资源使用状况,我们就是通过这些输出来判断服务器的资源使用状态的;
  • 第二部分从第六行开始,显示的是系统中进程的信息;

free显示内存状态

netstat显示网络状态

  • ps命令将某个进程显示出来**
  • grep命令是查找,使用正则表达式搜索文本,并把匹配的行打印出来

nslookup
查询DNS的记录,查看域名解析是否正常,在网络故障的时候用来诊断网络问题

linux连接数据库

连接本地的mysql:mysql -uroot -p
远程:mysql -h localhost -uroot -p
在这里插入图片描述

测试技能

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

敲代码的布莱恩特

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值