mysql、jvm、基础

jvm

java 内存区域

线程私有:程序计数器、虚拟机栈、本地方法栈

线程共享:堆和方法区

程序计数器: 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

虚拟机栈: 每个方法在执行的同时都会创建一个线帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。

本地方法栈: 与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的(也就是字节码),而本地方法栈是为虚拟机调用 Native 方法服务的。

堆: Java 堆是 JVM 中内存最大的一块,是被所有线程共享的,在虚拟机启动时候创建,Java 堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。如果在堆中没有内存完成实例分配,并且堆不可以再扩展时,将会抛出OutOfMemoryError。

方法区: 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。当方法无法满足内存分配需求时会抛出 OutOfMemoryError 异常。

JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了,取而代之是元空间,元空间使用的是直接内存。

为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢

1、整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
-XX:MaxMetaspaceSize 标志设置最大元空间大小

2、元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
Java classes在Java hotspot VM内部表示为类元数据。

方法区和永久代的关系

《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。

java对象的创建过程

栈:先进先出,物理地址分配是连续的。存储的是局部变量,定义在方法中都是局部变量。线程私有,生命周期和线程相同。

堆:堆对于整个应用都是共享的。存放的是对象和数组,new建立的对象都在堆中,堆中存放的是实体。

int[] arr = new int[3];

new一个数组的时候,在堆中开辟一个空间,分配一个连续的二进制地址,存放数组实体,数组有一个索引,数组实体在堆中赋初始,声明的arr变量存放在栈中,并且指向之前分配的物理地址,arr引用了堆中的实体。

java创建对象的过程

类加载、分配内存、初始化零值、设置对象头、执行init方法

  • 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
  • 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
  • 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值
  • 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。
  • 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但其实是一个半初始化状态,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行init方法,调用构造方法之后赋初始值,这样一个真正可用的对象才算完全产生出来。

怎么判断对象是否可以被回收

1、引用计数法

为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。

2、可达性分析

通过一系列的“GC roots” 对象作为起点搜索。如果在“GCroots”和一个对象之间没有可达路径,则称该对象是不可达的 。

GC roots:Java 虚拟机栈中的引用对象、本地方法栈中 (既一般说的 Native 方法)引用的对象、方法区中常量的引用对象

垃圾回收算法

Mark-Sweep(标记清除):执行分两阶段,第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。

Copying(复制算法):可以解决内存碎片问题,复制算法是将内存分为大小相同的两块,当这一块使用完了,就把当前存活的对象复制到另一块,然后一次性清空当前区块。此算法的缺点是只能利用一半的内存空间。

Mark-Compact(标记压缩):分为两个阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

垃圾回收器

新生代:

  • serial :单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。
  • Parallel Scavenge:复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。如果虚拟机总共运行100分钟,其中垃圾花掉1分钟,吞吐量就是99%。
  • ParNew: Serial收集器的多线程版本,也需要stop the world,复制算法。

老年代:

  • SerialOld:是Serial收集器的老年代版本,单线程收集器,使用标记整理算法。
  • ParallelOld:是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。
  • ConcurrentMarkSweep(CMS、标记-清除算法):是一种以获得最短回收停顿时间为目标的收集器

CMS:从名字上可以看出来是使用的标记清除算法。是获取最短回收停顿时间为目标的收集器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。

CMS既然是MarkSweep,就一定会有碎片化的问题,碎片到达一定程度,CMS的老年代分配对象分配不下的时候,使用SerialOld 进行老年代回收

CMS 收集器工作过程

  • 初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,但是STW
  • 并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
  • 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致有一部分没有被标记到,浮动垃圾,只能在下次GC 的时候进行清除。仍然需要STW
  • 并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。

由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作, 所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。

G1: G1回收的范围是整个Java堆(包括新生代,老年代)。
是一种兼顾吞吐量和停顿时间的收集器。

标记整体算法,所以没有内存碎片

G1运作步骤:

  • 初始标记:只是标记一下GC Roots能直接关联到的对象
  • 并发标记:是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段时耗时较长,但可与用户程序并发执行。
  • 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。
  • 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

分代垃圾回收器是怎么工作的

分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。

新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。

当 Eden 区的空间满了, Java虚拟机会触发一次 Minor GC。它的执行流程如下:

  • 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
  • 清空 Eden 和 From Survivor 分区;
  • From Survivor 和 To Survivor 分区交换;

每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。

老生代主要存放应用程序中生命周期长的内存对象。老年代的对象比较稳定,所以 Major GC不会频繁执行。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 Major GC进行垃圾回收腾出空间。一般使用标记整理的执行算法。

以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

为什么要分代

这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

新生代与老年代 的比例的值为 1:2 ,可以通过参数 –XX:NewRatio 配置。

Eden : from : to = 8 : 1 : 1 ( 可以通过参数
–XX:SurvivorRatio 来设定)。

为什么要分代

新生代由于对象朝生夕死,每次垃圾回收时只有少量对象需要被回收,采用复制算法。

老年代的对象比较稳定。老生代的特点是每次垃圾回收时只有少量对象需要被回收,所以采用标记整理算法。

因此可以根据不同区域选择不同的算法来提高效率。

为什么要分为Eden和Survivor?为什么要设置两个Survivor区?

  • 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
  • Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
  • 设置两个Survivor区最大的好处就是解决了碎片化(复制算法),刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)

对象分配规则

  • 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  • 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,达到阀值对象进入老年区。

类加载

1、JVM类加载机制

  • 加载:根据查找路径找到相应的 class 文件然后导入;
  • 验证:检查加载的 class 文件的正确性;
  • 准备:给类中的静态变量分配内存空间;
  • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
  • 初始化:对静态变量和静态代码块执行初始化工作。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。

2、类加载器(4种类型)

类加载器 就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。

  • BootstrapClassLoader:启动类类加载器,用来加载java核心类库,无法被java程序直接引用。
  • ExtClassLoader:拓展类类加载器,它用来加载 Java 的扩展库。
  • AppClassLoader:应用程序类类加载器,它主要加载应用程序ClassPath下的类(包含jar包中的类)。它是java应用程序默认的类加载器。
  • 用户自定义类加载器:用户根据自定义需求,自由的定制加载的逻辑,继承AppClassLoader,仅仅覆盖findClass()即将继续遵守双亲委派模型。
  • ThreadContextClassLoader:线程上下文加载器,它不是一个新的类型,更像一个类加载器的角色,ThreadContextClassLoader可以是上述类加载器的任意一种,但往往是AppClassLoader

3、双亲委派模型

当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。

4、为什么需要双亲委派模型?

如果没有双亲委派,那么用户可以自己定义一个java.lang.String的同名类,并把它放到ClassPath中,那么类的唯一性将无法保证,所以双亲委派模型主要是防止内存中出现多份同样的字节码。

5、怎么打破双亲委派模型?

重写loadClass破坏双亲委派模型:默认的loadClass()方法: 先判断这个类是不是已经被当前层的类加载器加载过了,如果没有加载过就将该类委派给父类加载器,如果父类无法加载再向下传递,回来由自己来进行加载,重写了这个方法以后就能自定义使用什么加载器了,也可以自定义加载委派机制,也就打破了双亲委派模型。

场景:

1、JDBC破坏双亲委派模型例子

原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。例如,MySQL的mysql-connector-.jar中的Driver类具体实现的。

原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的,在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-.jar中的Driver类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载。

这个时候就引入线程上下文件类加载器(Thread Context ClassLoader,。有了这个东西之后,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。破坏了双亲委派机制。

2、Tomcat 的类加载器是怎么设计的?

Tomcat是个web容器, 那么它要解决什么问题:

  • 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
  • 部署在同一个web容器相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。

Tomcat 如何实现自己独特的类加载机制:

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;

双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载。

tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。

JVM参数

标准: - 开头,所有的HotSpot都支持
非标准:-X 开头,特定版本HotSpot支持特定命令
不稳定:-XX 开头,下个版本可能取消

-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen

-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小

-Xmx:512 设置最大堆内存为 512 M-Xms:215 初始堆内存为 215 M-Xmn2g: 设置年轻代大小为2g。
-XX:+UseConcMarkSweepGC: 设置年老代为并发收集。
-XX:+PrintGCDetails:打印 gc 详细信息。 jstack pid命令查看当前java进程的堆栈状态

jstat,是用于监视虚拟机运行时状态信息的命令,
它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。

jstack,用于生成java虚拟机当前时刻的线程快照。
info,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数

对象在内存中的存储布局

对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。

Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。

实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。

对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

Object object = new Object()在内存中占用多少字节?

8(mark word)+8(class point没有压缩(类型指针))+(没有成员变量0)+(不需要对齐0)=16个字节;

User {int id;String name}
User user=new User(); 占多少个字节

8(mark word)+4(class point开启压缩)+8(int 4个字节,string 4个字节的成员变量)+4(padding)=24个字节

对象头

在markword中记录有synchronized锁的信息,GC标记信息
markword一共有64位,8个字节

用于存储对象自身的运行时数据,例如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、等信息。Mark Word占用一个机器码,在32位和64位的JVM中,这部分数据分别为32bit和64bit。

mysql

三大范式

1、第一范式

每列都是不可再分的最小数据单元(确保每列原子性)

address 列为 “中国北京” ,不符合原子性,需要拆分为两列,country 中国,city 北京。

2、第二范式

在第一范式的基础上,规定表中的非主键列不存在对主键列的部分依赖,即第二范式只要求每个表只描述一件事情。

Order表中包含订单信息,也包含产品信息,需要拆分为两个表(订单表,产品表)。

3、第三范式

满足第一范式和第二范式,并且表中的列不存在对非主键列的传递依赖。

订单表中做为主键的订单编号,顾客姓名依赖于非主键列的顾客编号,就需要将该列去掉。

存储引擎

myisam引擎是5.1版本之前的默认引擎,不支持事务和行级锁,所以一般用于有大量查询少量插入的场景来使用,而且myisam不支持外键,并且索引和数据是分开存储的。因为不支持事务,所以最大的缺陷就是崩溃后无法安全恢复。

MySQL 5.5版本后默认的存储引擎为InnoDB。而InnoDB 支持行级锁和表级锁,默认为行级锁。并且支持事务。索引和数据存储在一起。
innodb索引的叶子节点直接存放数据,而myisam存放地址。

索引数据结构

MySQL索引使用的数据结构主要有B+Tree索引 和 哈希索引 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择B+Tree索引。

不同的存储引擎的实现方式是不同的。

  • MyISAM: B+Tree叶节点的data域存放的是磁盘地址。在索引检索的时候,则取出磁盘地址,然后以磁盘地址读取相应的数据记录。这被称为“非聚簇索引”。
  • InnoDB: 其数据文件本身就是索引文件。其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,这被称为“聚簇索引”。而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。辅助索引查询的时候则需要先取出主键的值,再走一遍主索引。

B+树插入一个数据

无论怎么插入,B+树都能保持平衡。但是为了平衡,新插入键值时可能要做大量的拆分页(split)操作。因为B+树结构主要用于磁盘,页的拆分意味着磁盘的操作,总所周知磁盘操作效率是非常低的,所以应该尽量减少页的拆分操作。因此,B+树同样提供了类似平衡二叉树的旋转(Rotation)操作。

旋转
旋转发生在Leaf Page已经满,但其左右兄弟节点没有满的情况下。这时B+树并不会急于去做拆分页的操作,而是将记录移到所在页的兄弟节点上。在通常情况下,左兄弟节点会被首先用来检查做旋转操作。

为什么推荐使用主键自增

如果不是主键自增,插入数据时会导致频繁页分裂

mysql InnoDB 引擎底层数据结构是 B+ 树,所谓的索引其实就是一颗 B+树 ,mysql 中的数据都是按顺序保存在 B+ 树上的(所以说索引本身是有序的)

然后 mysql 在底层又是以数据页为单位来存储数据的,一个数据页大小默认为 16k,也就是说如果一个数据页存满了,mysql 就会去申请一个新的数据页来存储数据。

如果主键为自增 id 的话,mysql 在写满一个数据页的时候,直接申请另一个新数据页接着写就可以了。

如果主键是非自增 id,为了确保索引有序,mysql 就需要将每次插入的数据都放到合适的位置上。

什么是事务

事务是逻辑上的一组操作,要么都执行,要么都不执行。

事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。

事务的四大特性

  • 原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  • 一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
  • 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,数据库是独立的;
  • 持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

那ACID靠什么保证的呢?

  • A原子性底层就是通过undo log实现的。undo log主要记录了数据的逻辑变化,比如一条INSERT语句,对应一条DELETE的undo log,这样在发生错误时,就能回滚到事务之前的数据状态。

  • C一致性一般由代码层面来保证

  • I隔离性由MVCC来保证

  • D持久性由内存+redo log来保证,mysql修改数据同时在redo log记录这次操作,事务提交的时候通过redo log刷盘,宕机的时候可以从redo log恢复

事务的隔离级别

  • READ-UNCOMMITTED(读取未提交): 最低的隔离级别,可能会读到其他事务未提交的数据,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交): 要求读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读): mysql的默认级别。对同一字段的多次读取结果都是一致的,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化): 最高的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改。
幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。

什么是幻读,什么是MVCC

1、幻读: 同一事务下,连续执行两次同样的SQL可能得到不同的结果,第二次的SQL可能返回了不存在的行。

一个例子:child 表只有id为90和102的记录 ,执行这样一条查询语句:

select * from child where id > 100 for update;

如果此时没有锁定90到102这个范围的话,另一个线程可能会成功插入一条id为101的数据,那么再次执行这个查询,就会出现一条id为101的记录,这就是幻读问题。

产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB只好引入新的锁,也就是间隙锁(Gap Lock)。

2、MVCC,MVCC叫做多版本并发控制,它的工作机制是另一个事务读取一个事务时,读取这个事务的快照。

MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读

我们每行数实际上隐藏了两列,创建时间版本号,过期(删除)时间版本号,每开始一个新的事务,版本号都会自动递增。

事务读数据的原则:

  • 读版本号小于等于当前版本的数据(意思就是读不到在当前事务之后修改的数据 避免了不可重复读)
  • 读删除事务版本号大于等于当前版本的数据(意思就是如果这条数据在之后的事务里删了,当前事务也不能再读了)

mvcc 是通过 readview+undolog 来实现

日志

  • redo log (重做日志):为了最大程度的避免数据写入时,因为 IO 瓶颈造成的性能问题,MySQL 采用了这样一种缓存机制,先将数据写入内存中,再批量把内存中的数据统一刷回磁盘。为了避免将数据刷回磁盘过程中,因为掉电或系统故障带来的数据丢失问题,InnoDB 采用 redo log 来解决此问题。持久性也是用redo log来保证的。
  • undo log(回滚日志) :用于存储日志被修改前的值,从而保证如果修改出现异常,可以使用 undo log 日志来实现回滚操作。undo log是逻辑日志,可以认为当 delete一条记录时,undo log 中会记录一条对应的 insert 记录。
  • bin log(二进制日志):是一个二进制文件,主要记录所有数据库表结构变更。

1、按照锁的粒度分类

MyISAM和InnoDB存储引擎使用的锁:

  • MyISAM采用表级锁
  • InnoDB支持行级锁表级锁,默认为行级锁

表级锁: MySQL中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,资源消耗也比较少,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低

行级锁: MySQL中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能减少数据库操作的冲突。由于加锁粒度最小,并发度高,但加锁的开销也最大,会出现死锁。
InnoDB支持的行级锁,包括如下几种。

  • Record Lock: 对索引项加锁,锁定符合条件的行。
  • Gap Lock: 对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁),不包含索引项本身。其他事务不能在锁范围内插入数据,这样就防止了别的事务新增行(幻读)。
  • Next-key Lock: 锁定索引项本身和索引范围。即Record Lock和Gap Lock的结合。可解决幻读问题。

主键id为5、10、15、20,间隙所会锁定(5,10],(10,15],(15,20]。

A: select * from table where id = 11 for update;
B: insert into user value(12,12,12)

当有如下事务A和事务B时,事务A会对数据库表增加(10,15]这个区间锁,这时insert id = 12 的数据的时候就会因为区间锁(10,15]而被锁住无法执行。

2、按照是否可写分类

表级锁和行级锁可以进一步划分为共享锁(s)和排他锁(X)

  • 共享锁又被称为读锁,如果一个事务对数据对象加共享锁,则这个事务只能读,其它事务只能再对这个对象加共享锁,其他用户可以并发读取数据,但任何事务都不能获取数据上的排他锁,直到已释放所有共享锁。
  • 排它锁又称为写锁,若事务T对数据对象A加上写锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。

两者之间的区别:
共享锁(S锁):如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不 能加排他锁。获取共享锁的事务只能读数据,不能修改数据。

排他锁(X锁):如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获取排他锁的事务既能读数据,又能修改数据。

联合索引,覆盖索引

开启慢查询日志,Explain会生成SQL的分析结果,看有没有索引失效等问题,当使用索引列进行运算、函数操作都会导致无法使用索引。

1.联合索引

场景:检查记录,根据某位检查人员的 user_id 显示最近的20条检查记录,按照时间顺序返回。

select * from table where user_id = "xxxx" orderby check_time limit 20;

当前假设建立了单索引 (user_id)、。

如果使用user_id 的单索引,那么因为需要额外的一次排序操作才能完成查询,需要对 check_time 进行orderby排序操作。
因为索引user_id 中的 check_time 是未排序的,那么explain的extra列就是 using where & using filesort

当我们使用联合索引时(user_id, check_time)
但是 MySQL 优化器使用的是 (user_id,check_time)这个联合索引,当 userid 匹配好了之后,在这个联合索引中,check_time已经排好序了, 直接根据 联合索引取出数据,无须再对check_time做一次额外的排序。
在explain的extra列就是 using where & using index

最左匹配

最左匹配原则也叫最左前缀原则,指的是索引以最左边为起点任何连续的索引都能匹配上,当遇到范围查询(>、<、between、like)就会停止匹配。

select * from s1 where name='egon'; #可以
select * from s1 where name='egon' and email='asdf'; #可以
select * from s1 where email='alex@oldboy.com'; #不可以
select * from s1 where email='asdf' and name='egon'; #可以(优化器)

2.覆盖索引

  • 避免 Innodb 表进行索引的二次查询、减少回表次数: 对于 Innodb 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询 ,减少了 IO 操作,可以提升查询效率。

  • MySQL5.6支持索引下推,在磁盘上查找数据的时候进行数据筛选,尽量减少回表的次数(减少IO操作),提升性能。explain 看执行计划,extra列显示 using index condition

  • 如果存储介质使用机械硬盘(很怕随机读写,有很大的磁盘寻址开销,磁头的旋转和定位数据耗费较长时间),可以把MRR(Multi-Range Read Optimization)打开,MRR 通过把「随机磁盘读」,转化为「顺序磁盘读」,从而提高了索引查询的性能。
    在查询辅助索引时,根据查询得到的结果,在回表之前把 ID 读到一个数据缓冲池中,并且排序,使得随机读变成了顺序读。
    索引本身就是为了减少磁盘 IO,加快查询,而 MRR,则是把索引减少磁盘 IO 的作用,进一步放大。
    如果启用了该特性,在 Extra 列上看到 using index condition 和 using MRR 选项

使用场景

需要查询一条检查记录具体的违法问题,检查项目状态表中,用recordId来查询具体的违法行为,或者查询限期整改或者现场整改标识,

select id,checkItem where recordId='121212';

只会走辅助索引,extra列:using index condition,说明需要再次通过id值扫描聚集索引获取checkItem字段。效率会降低,

但是当我们建立了联合索引(recorId,checkItem),会直接命中索引,不需要回表。Using index

3.普通索引

场景:写多读少、唯一性要求不那么高/业务代码可以保证唯一性的前提下

索引和数据都时保存在一个ibd中, 表数据更新的同时也会更新对应的表的索引数据,很可能会产生大量的物理读。

InnoDB 会将这些写操作缓存在 change buffer 中,减少读磁盘,语句的执行速度会得到明显的提升,等到真正读到那个数据页,做一个合并的操作。

这样可以提高写入的速度。

4.索引失效的原因

  • 当使用索引列进行查询的时候尽量不要使用表达式,把计算放到业务层而不是数据库层
  • 数据类型出现了隐式转换,varchar不加单引号的话可能会自动转换为int型,使索引无效,产生全表扫描。
  • 联合索引没有遵循最左前缀原则
  • 在索引字段上使用not,<>,!=。不等于操作符是永远不会用到索引的,因此对它的处理只会产生全表扫描。

5.创建索引

  • 尽量选择区分度高的列作为索引。区分度最高的放在联合索引的最左侧(区分度=列中不同值的数量/列的总行数)

  • 使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引)

  • 尽可能的考虑建立联合索引而不是单列索引,因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。

  • 尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO 性能也就越好)

PRIMARY KEY
UNIQUE INDEX
INDEX 
CREATE INDEX index_name ON table_name (column_name)

ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` )
//创建索引
alter table projectfile drop index s2123;//删除索引


优化

MySQL Explain 字段总结

  • id:选择标识符
  • select_type:表示查询的类型。
  • table:输出结果集的表 -
  • partitions:匹配的分区
  • type:表示表的连接类型
  • possible_keys:表示查询时,可能使用的索引
  • key:表示实际使用的索引
  • ==key_len:索引字段的长度 ==
  • ref:列与索引的比较
  • ==rows:扫描出的行数(估算的行数) ==
  • filtered:按表条件过滤的行百分比
  • Extra:执行情况的描述和说明

1、using index 使用覆盖索引的时候就会出现,不需要回表查询;
2、using where 在查找使用索引的情况下,需要回表去查询所需的数据;
3、using index condition:MySQL5.6支持索引下推,在磁盘上查找数据的时候进行数据筛选,尽量减少回表的次数(减少IO操作)。查找使用了索引,但是需要回表查询数据; 在5.6版本后加入的新特性(Index Condition Pushdown)
4、using index & using where:覆盖索引。查找使用了索引,但是需要的数据都在索引列中能找到,所以不需要回表查询数据。

项目组遇到的难题:(http://www.woshipm.com/zhichang/1662321.html)

一条sql的执行过程

MySQL 主要分为 Server 层和引擎层,Server 层主要包括连接器、查询缓存、分析器、优化器、执行器

  • 客户端先通过连接器连接到 MySQL 服务器;
  • 连接器权限验证通过之后,先查询是否有查询缓存,如果有缓存(之前执行过此语句)则直接返回缓存数据,如果没有缓存则进入分析器;
  • 分析器会对查询语句进行语法分析和词法分析,判断 SQL 语法是否正确,如果查询语法错误会直接返回给客户端错误信息,如果语法正确则进入优化器;
  • 优化器是对查询语句进行优化处理,例如一个表里面有多个索引,优化器会判别哪个索引性能更好;
  • 执行器:当选择了执行方案后,MySQL 就准备开始执行了,首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会去调用引擎的接口,返回接口执行的结果。

SQL注入

SQL注入是一种将SQL代码添加到用户的输入参数中的攻击,之后再将这些参数传递给后台的sql服务器加以解析和执行。恶意代码就将被执行。

解决方法:使用正则表达式过滤传入的参数、字符串过滤

分区分表

当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:

1、垂直分区
简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。
根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表。

垂直拆分的优点: 可以使得列数据变小,在查询减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;

2、水平分区

水平拆分是指数据表行的拆分,保持数据表结构不变,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。

分库分表之后,id 主键如何处理?
利用 redis 生成 id : 性能比较好,灵活方便,不依赖于数据库。

delect、truncate 、drop

DELETE语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作。

TRUNCATE TABLE 则一次性地从表中删除所有的数据并不把单独的删除操作记录记入日志保存,删除行是不能恢复的。

TRUNCATE 和DELETE只删除数据, DROP则删除整个表(结构和数据)。

数据库连接池

JDBC数据库连接池的必要性

在使用开发基于数据库的web程序时,传统的模式基本是按以下步骤:

  • 在主程序(如servlet、beans)中建立数据库连接
  • 进行sql操作
  • 断开数据库连接

这种模式开发,存在的问题:

  • 普通的JDBC数据库连接使用 DriverManager 来获取,每次向数据库建立连接的时候都要将 Connection 加载到内存中,再验证用户名和密码(得花费0.05s~1s的时间)
  • 对于每一次数据库连接,使用完后都得断开。 否则,如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄漏,最终将导致重启数据库。
  • 这种开发不能控制被创建的连接对象数,系统资源会被毫无顾及的分配出去,如连接过多,也可能导致内存泄漏,服务器崩溃。

数据库连接池的基本思想

就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。

数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个

数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数来设定的。无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量。连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。

Druid(德鲁伊)数据库连接池

基础

JVM跨平台

.java文件(源代码)经过javac编译之后变成 .class文件(Jvm可以理解的Java字节码)- JVM 机器可执行的二进制机器码

Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。

什么是面向对象?

面向过程就是分析出实现需求所需要的步骤,通过函数(方法)一步一步实现这些步骤,接着依次调用即可。不易维护、扩展。

面向对象是把整个需求按照特点、功能划分,将这些存在共性的部分封装成类(类实例化后才是对象),规定与其他对象之间的关系。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。

面向对象的特点:封装,继承,多态

1、封装

封装隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。

2、继承

继承是使用已存在的类的定义作为基础建立新类,新类的定义可以增加新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码

  • 子类拥有父类非 private 的属性和方法
  • 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法。

3、多态:

1.运行时多态

一个引用变量到底会指向哪个类的实例对象,只有在程序运行期间才能确定。父类类型的引用可以指向子类的对象。

同一个事件发生在不同的对象上会产生不同的结果。

Animal a = new Cat();  // 向上转型  
a.eat();               // 调用的是 Cat 的 eat
Cat c = (Cat)a;        // 向下转型  
c.work();              // 调用的是 Cat 的 work

在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。

2.编译时多态

方法重载都是编译时多态。根据实际参数的数据类型、个数和次序,Java 在编译时能够确定执行重载方法中的哪一个。

方法重写表现出两种多态性,当对象引用本类实例时,为编译时多态,否则为运行时多态。

如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写 (Overriding)。

如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载(Overloading)。

重写、重载

1、重写

重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。
重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。

声明为 final 的方法不能被重写。
子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。

//当需要在子类中调用父类的被重写方法时,要使用 super 关键字。
class Animal{
   public void move(){
      System.out.println("动物可以移动");
   }
}
 
class Dog extends Animal{
   public void move(){
      super.move(); // 应用super类的方法
      System.out.println("狗可以跑和走");
   }
}
 
public class TestDog{
   public static void main(String args[]){
 
      Animal b = new Dog(); // Dog 对象
      b.move(); //执行 Dog类的方法
 
   }
}

2、重载

重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。

每个重载的方法都必须有一个独一无二的参数类型列表。

最常用的地方就是构造器的重载。

重载规则:

  • 被重载的方法必须改变参数列表(参数个数或类型不一样);
  • 被重载的方法可以改变返回类型;
  • 被重载的方法可以改变访问修饰符;
  • 被重载的方法可以声明新的或更广的检查异常;
  • 无法以返回值类型作为重载函数的区分标准

组合、继承

组合和继承是面向对象中两种代码复用的方式

组合是指在新类中创建原有类的对象,重复利用已有类的功能。

继承是面向对象的主要特性之一,它允许设计人员根据其他类的实现来定义一个类的实现。组合和继承都允许在新的类中设置子对象,只是组合是显示的,而继承是隐式的。

不要轻易地使用继承,不要单纯的为了实现代码的重用而使用继承,因为过多使用继承会破坏代码的可维护性,当父类被修改时,会影响到所有继承自他的子类,从而增加程序的维护难度和成本。

8大基本数据类型

1 byte(字节) =  = 8bits(位)//一个汉字占两个字节
//整型
byte 1字节  //-128 - 127
short 2字节  //-2^15 - 2^15-1
int 4字节	//-2^31 
long 8字节  //-2^63
//浮点型,不能用来表示精确的值,货币
flot 4字节 //0.0f  float f1 = 234.5f
double 8字节 //0.0d 
//字符型
char 2字节 //char letter = 'A';  最小值是 \u0000(十进制等效值为 0)
//布尔
boolean 1

Object

public final native Class<?> getClass();
public native int hashCode();
public boolean equals(Object obj) {
    return (this == obj);
}
protected native Object clone() throws;
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout)
public final void wait() throws InterruptedException {
    wait(0);
}
protected void finalize() throws Throwable { }

装箱拆箱

如果要生成一个数值为10的Integer对象

Integer i = new Integer(10);

有了自动装箱,如果要生成一个数值为10的Integer对象,只需要这样就可以了:Integer i = 10;这个过程中会自动根据数值创建对应的 Integer对象,这就是装箱

拆箱就是自动将包装器类型转换为基本数据类型:

Integer i = 10;  //装箱
int n = i;   //拆箱

装箱:自动将基本数据类型转换为包装器类型;装箱的时候自动调用的是Integer的valueOf(int) 方法。
拆箱:就是自动将包装器类型转换为基本数据类型。而在拆箱的时候自动调用的是Integer的intValue方法。

Integer.valueOf(int)的缓存-128-127

public class Main {
    public static void main(String[] args) {
         
        Integer i1 = 100;
        Integer i2 = 100;
        Integer i3 = 200;
        Integer i4 = 200;
        
        System.out.println(i1==i2);//true,Integer。valueOf缓存的原因
        System.out.println(i3==i4);//false

	  Integer a = 1;
      Integer b = 2;
      Integer c = 3;
      System.out.println(c==(a+b));//true,解析见下 1
      System.out.println(c.equals(a+b));//true,解析见下 2
    }
}

public static Integer valueOf(int i) {
    if(i >= -128 && i <= IntegerCache.high)
        return IntegerCache.cache[i + 128];
    else
        return new Integer(i);
}

1、当 "=="运算符的两个操作数都是 包装器类型的引用,则是比较指向的是否是同一个对象,而如果其中有一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)

2、对于c.equals(a+b) 会先触发自动拆箱过程,再触发自动装箱过程,也就是说a+b,会先各自调用intValue方法,得到了加法运算后的数值之后,便调用Integer.valueOf方法,再进行equals比较。

== 与 equals

== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型比较的是值,引用数据类型比较的是内存地址)。

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

  • 类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
  • 类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
public class test1 {
    public static void main(String[] args) {
        String a = new String("ab"); // a 为一个引用
        String b = new String("ab"); // b为另一个引用,对象的内容一样
        String aa = "ab"; // 放在常量池中
        String bb = "ab"; // 从常量池中查找
        
        System.out.println(aa == bb);//true
      	System.out.println(a == b);// false,非同一对象
        System.out.println(a.equals(b));// true 
    }
}
  • String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
  • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

hashCode 与 equals

public native int hashCode();

hashCode() 的作用是获取哈希码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。

Object 的 hashcode 方法是本地方法,也就是用 c 语言实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。

我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode?

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较.
1、如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。
2、但是如果发现有相同 hashcode 值的对象,这时会调用 equals() 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

为什么重写 equals 时必须重写 hashCode 方法?

如果两个对象相等,则 hashcode 一定也是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true。
但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。
因此,重写equals()方法,必须重写hashCode()方法,以保证equals方法相等时两个对象hashcode返回相同的值。

hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?
我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。

10 public class EqualsTest2{
11 
12     public static void main(String[] args) {
13         // 新建2个相同内容的Person对象,
14         // 再用equals比较它们是否相等
15         Person p1 = new Person("eee", 100);
16         Person p2 = new Person("eee", 100);
17         System.out.printf("%s\n", p1.equals(p2));//true
		   //这是判断p1和p2的内容是否相等。因为Person覆盖equals()方法,而这个equals()是用来判断p1和p2的内容是否相等,恰恰p1和p2的内容又相等;因此,返回true。
		   System.out.printf("p1==p2 : %s\n", p1==p2);
		   // 这是判断p1和p2是否是同一个对象。由于它们是各自新建的两个Person对象;因此,返回false。	
18     }
19 
20     /**
21      * @desc Person类。
22      */
23     private static class Person {
24         int age;
25         String name;
26 
27         public Person(String name, int age) {
28             this.name = name;
29             this.age = age;
30         }
31 
32         public String toString() {
33             return name + " - " +age;
34         }
35 
36         /** 
37          * @desc 覆盖equals方法 
38          */  
39         @Override
40         public boolean equals(Object obj){  
41             if(obj == null){  
42                 return false;  
43             }  
44               
45             //如果是同一个对象返回true,反之返回false  
46             if(this == obj){  
47                 return true;  
48             }  
49               
50             //判断是否类型相同  
51             if(this.getClass() != obj.getClass()){  
52                 return false;  
53             }  
54               
55             Person person = (Person)obj;  
56             return name.equals(person.name) && age==person.age;  
57         } 
58     }
59 }

重写了Person的equals()函数:当两个Person对象的 name 和 age 都相等,则返回true。

没有重写equals方法时, 我们通过 p1.equals(p2) 来“比较p1和p2是否相等时”。实际上,调用的Object.java的equals()方法,即调用的 (p1==p2) 。它是比较“p1和p2是否是同一个对象”。
而由 p1 和 p2 的定义可知,它们虽然内容相同;但它们是两个不同的对象!因此,返回结果是false。

什么是字符串常量池?

字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串。

在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。

String、StringBuilder、StringBuffer

String 类中使用 final 关键字修饰字符数组来保存字符串,

private final char value[]

所以 String 对象是不可变的。也就可以理解为常量,线程安全。

String str1 = "abcd";//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";
String str2 = new String("abcd");//堆中创建一个新的对象
String str3 = new String("abcd");//堆中创建一个新的对象
System.out.println(str1==str2);//false
System.out.println(str2==str3);//false

  • 第一种方式是在常量池中拿对象;
  • 第二种方式是直接在堆内存空间创建一个新的对象。

只要使用 new 方法,便需要创建新的对象。

在这里插入图片描述
String.intern() 是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7 之前的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,

String s1 = new String(“abc”);这句话创建了几个字符串对象?

将创建 1 或 2 个字符串。如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。如果池中没有字符串常量“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。

StringBuilder、StringBuffer

而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。

AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

性能

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

操作少量的数据: 适用 String
单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

线程和进程

进程: 系统中运行的一个程序,每个进程都拥有独立的地址空间,一个进程无法访问另一个进程资源,如果想让一个进程访问另一个进程的资源,需要使用进程间通信

  • 匿名管道:半双工,只能在父子进程间通信
  • 有名管道: 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道可以在不同进程间通信
  • 共享内存 :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁。可以说这是最有用的进程间通信方式。

线程: 一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享堆和方法区的资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。

线程调度有抢占式调度、协同式调度

  • 抢占式调度:指的是每条线程执行的时间、线程的切换都由系统控制,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
  • 协同式调度:指某一线程执行完后主动通知系统切换到另一线程上执行,线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

线程的状态

  • 新建(NEW):新创建了一个线程对象。
  • 可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
  • 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片 ,执行程序代码。
  • 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。
  • 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

在这里插入图片描述
线程的创建

  • 继承 Thread 类,重写 run 方法

  • 实现 Runnable 接口,实现 run 方法

  • 实现 Callable 接口,实现 call 方法

线程通信

多个线程通过synchronized关键字这种方式来实现线程间的通信。这种方式,本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。

这里用到了Object类的 wait() 和 notify() 方法。
当条件未满足时,线程A调用wait() 放弃CPU,并进入阻塞状态。
当条件满足时,线程B调用 notify()通知 线程A,所谓通知线程A,就是唤醒线程A,并让它进入可运行状态。

线程池

池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

ThreadPoolExecutor 重要的参数:

  • corePoolSize : 当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其它空闲的基本线程能够执行新任务也会创建,等到需要执行的任务数大于线程池基本大小时就不再创建。
  • maximumPoolSize : 允许创建的最大线程数,如果队列满了,并且已创建的线程数小于最大线程数,则线程池会创建新的线程执行任务。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中
  • keepAliveTime: 当线程池的工作线程空闲后,保持存活的时间,增大时间可以提高线程的利用率。
  • unit : keepAliveTime 参数的时间单位。
  • threadFactory :创建线程的工厂,可以给每个创建出来的线程设置名字。
  • handler :饱和策略

饱和策略:1、抛出异常拒绝新任务的处理
2、由调用线程处理该任务。
3、不处理新任务,直接丢弃掉。
4、丢弃最早的未处理的任务请求。

线程池的工作过程

1、提交任务后,线程池先判断线程数是否达到了核心线程数(corePoolSize)。如果未达 到线程数,则创建核心线程处理任务;否则,就执行下一步;

2、接着线程池判断任务队列是否满了。如果没满,则将任务添加到任务队列中;否则,执 行下一步;

3、接着因为任务队列满了,线程池就判断线程数是否达到了最大线程数。如果未达到,则 创建非核心线程处理任务;否则,就执行饱和策略,默认会抛出RejectedExecutionException异常。

执行 execute()方法和 submit()方法的区别

execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;

submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值。
get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

产生异常后

发生异常的线程被回收,重新填充一个新的线程。
使用submit()提交线程,Future 的 get()这个方法使外部线程阻塞,直到submit的线程执行完毕或异常退出。

Executors 返回线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的最大线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

关闭线程池

shutdown:不会立即终止线程池,而是要等所有任务队列中的任务都执行完后才会终止。

shutdownNow():线程池的状态立刻变成 STOP 状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,执行此方法会返回未执行的任务。

wait() sleep()

sleep() 方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。

wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程

  • 存在类的不同:sleep() 来自 Thread,wait() 来自 Object。
  • 释放锁:sleep() 不释放锁;wait() 释放锁。
  • 用法不同:sleep() 时间到会自动恢复;wait() 可以使用

yield()方法

Thread.yield()方法作用是:暂停当前正在执行的线程对象(及放弃当前拥有的cup资源),并执行其他线程。yield()做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其
他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。
但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

结论:
yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状
态转到可运行状态,但有可能没有效果。

值传递和引用传递有什么区别

值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。

引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。

值传递和引用传递最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。

假设B复制了A,修改A的时候,看B是否发生变化:
如果B跟着也变了,说明是引用传递(修改堆内存中的同一个值)
如果B没有改变,说明是值传递(修改堆内存中的不同的值)

队列

LinkedBlockingQueue 是一个由链表实现的有界阻塞队列

ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变。内部的阻塞队列是通过重入锁 ReenterLock 和 Condition 条件队列实现的。线程安全的

DelayQueue 是一个支持延时获取元素的无界阻塞队列,队列中的元素必须实现Delayed 接口,在创建元素时可以指定延迟时间,只有到达了延迟的时间之后,才能获取到该元素。

PriorityQueue一个基于优先级堆的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。优先级队列不允许使用 null 元素。

final 关键字

final 关键字,意思是最终的、不可修改的,最见不得变化 ,用来修饰类、方法和变量,具有以下特点:

  1. final 修饰的类不能被继承,final 类中的所有成员方法都会被隐式的指定为 final 方法;
  2. final 修饰的方法不能被重写
  3. final 修饰的变量是常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。
  4. 类中所有的 private 方法都隐式地指定为 final

static、final、this、super关键字

  1. 修饰成员变量和成员方法: 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被 static 声明的成员变量属于静态成员变量静态变量 存放在 Java 内存区域的方法区。方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    调用格式:类名.静态变量名 类名.静态方法名()
  2. 静态代码块: 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次。
  3. 静态内部类(static 修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非 static 成员变量和方法。

final 关键字

final 关键字,意思是最终的、不可修改的,用来修饰类、方法和变量,具有以下特点:

  1. final 修饰的类不能被继承,final 类中的所有成员方法都会被隐式的指定为 final 方法;
  2. final 修饰的方法不能被重写
  3. final 修饰的变量是常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。
  4. 类中所有的 private 方法都隐式地指定为 final

this 关键字

this 关键字用于引用类的当前实例。 例如:

//this.employees.length:访问类 Manager 的当前实例的变量。
//this.report():调用类 Manager 的当前实例的方法。
class Manager {
    Employees[] employees;

    void manageEmployees() {
        int totalEmp = this.employees.length;
        System.out.println("Total employees: " + totalEmp);
        this.report();
    }

    void report() { }
}

super 关键字

super 关键字用于从子类访问父类的变量和方法。

public class Super {
    protected int number;

    protected showNumber() {
        System.out.println("number = " + number);
    }
}

public class Sub extends Super {
    void bar() {
        super.number = 10;
        super.showNumber();
    }
}

在构造器中使用 super() 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。

在静态方法中可以使用 this 或 super 吗?为什么?

在静态方法中不能使用 this 或 super,因为 this 和 super 指代的都是需要被创建出来的对象,而静态方法在类加载的时候就已经创建了,所以没办法在静态方法中使用 this 或 super。

IO模型

为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space)和内核空间(Kernel space ) 。

从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。

当应用程序发起 I/O 调用后,会经历两个步骤:

  1. 内核等待 I/O 设备准备好数据
  2. 内核将数据从内核空间拷贝到用户空间。

Java 中 3 种常见 IO 模型

BIO (Blocking I/O):同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。

应用程序发起 read 调用后,会一直阻塞,直到在内核把数据拷贝到用户空间。
在这里插入图片描述

NIO (Non-blocking/New I/O):NIO 是一种==同步非阻塞的 I/O ==模型,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。Java NIO使我们可以进行非阻塞IO操作。比如说, 单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后, 线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一 些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。JDK 的 NIO 底层由 epoll 实现。

通常来说 NIO 中的所有 IO 都是从 Channel(通道) 开始的。

  • 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。

  • 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。

在这里插入图片描述

同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。

相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

I/O 多路复用模型

IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用, read调用的过程(数据从内核空间->用户空间)还是阻塞的。

IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。
在这里插入图片描述

AIO (Asynchronous I/O): 异步非阻塞IO模型,异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回 ,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

在这里插入图片描述

在这里插入图片描述

内存泄露和内存溢出的场景

内存泄漏:内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。

Java 内存泄漏的根本原因是长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是 Java 中内存泄漏的发生场景。

内存溢出:指程序运行过程中无法申请到足够的内存而导致的一种错误。

堆溢出,主要原因是创建的对象太大,堆内存空间分配不下。解决方法:
1、手动设置 JVM Heap(堆)的大小。 -Xms -Xmx
2、检查程序,看是否有死循环或不必要地重复创建大量对象。

栈溢出: java.lang.StackOverflowError : Thread Stack space:线程的方法嵌套调用层次太多(如递归调用),以致于把栈区溢出了。
解决方法:
1、修改程序。
2、通过 -Xss: 来设置每个线程的 Stack 大小即可。

Java 反射?

Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;这种动态调用对象的方法的功能称为 Java 语言的反射机制。

优点:

  • 运行期类型的判断,动态加载类,提高代码灵活度。

缺点:

  • 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 Java 代码要慢很多。
  • 安全问题,让我们可以动态操作改变类的属性同时也增加了类的安全隐患。

使用场景:

1、数据库连接池,也会使用反射调用不同类型的数据库驱动,代码如下所示:

String url = "jdbc:mysql://127.0.0.1:3306/mydb";
String username = "root";
String password = "root";
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(url, username, password);

2、动态代理的实现也依赖反射。

静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)

相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,

静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 Method 来调用指定的方法

public class DebugInvocationHandler implements InvocationHandler {
    /**
     * 代理类中的真实对象
     */
    private final Object target;

    public DebugInvocationHandler(Object target) {
        this.target = target;
    }


    public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
        System.out.println("before method " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("after method " + method.getName());
        return result;
    }
}

接口和抽象类

抽象类: 使用关键字 abstract 修饰的类。没有包含足够的信息来描绘一个具体的对象,封装子类中重复定义的内容,为其他子类提供一个公共的类型。抽象方法仅有声明没有方法体。

由于Java不支持多继承,子类不能够继承多个类,但可以实现多个接口。因此你就可以使用接口来解决它。

接口: 用interface 关键字来声明,接口方法默认修饰符是public,当你实现这个接口时,你就需要实现这个接口的方法。

区别:

  • 子类使用 extends 关键字来继承抽象类,一个子类只能继承一个抽象类。
  • 子类使用 implements 关键字来实现接口, 一个子类可以实现多个接口

当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。

面向接口编程的好处

面向接口编程就是先把业务逻辑线提取出来,作为接口,业务具体实现通过该接口的实现类来完成。 当需求变化时,只需编写该业务逻辑的新的实现类就可以完成需求,不需要改写现有代码,减少对系统的影响。

  • 降低程序的耦合性
  • 易于程序的扩展
  • 有利于程序的维护

内部类

  1. 静态内部类:定义在类内部的静态类被称为静态内部类,
  • 与内部成员类的创建方式 new Outer().new Inner() 不同,静态成员内部类可使用 new OuterClass.InnerClass() 的方式进行创建。
  • 内部类可以访问外部类中的所有静态属性和方法
  • 不能从静态成员内部类中访问非静态外部类对象
  • 可以通过“外部类.内部类”的方式直接访问;
  1. 成员内部类
    定义在类内部的非静态类被称为成员内部类,成员内部类不能定义静态方法和变量
  • 成员内部类可直接访问外部类,Outer.this.xxx
  • 外部成员类要访问内部类,必须先建立成员内部类对象;new Inner().xxx
  • 内部成员类可以使用任意访问修饰符
  1. 局部内部类
    定义在方法中的类叫做局部内部类,当一个类只需要在某个方法中使用某个特定的类时,可以通过局部内部类来实现。
  • 局部内部类不能使用任何访问修饰符
  • 局部类如果在方法中,可以直接使用方法中的变量
  1. 匿名内部类

通过实现一个接口的方式直接定义并使用的类,匿名内部类没有class关键字,这是因为匿名内部类直接使用new生成一个对象的引用。(new Thread(){}).start()。

  • 匿名内部类必须继承一个父类或者实现一个接口
  • 匿名内部类不能定义任何静态成员和方法
  • 匿名内部类中的方法不能是抽象的

Lambda 表达式是 JDK8 的一个新特性,可以取代大部分的匿名内部类,写出更优雅的 Java 代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。

语法形式为 () -> {},其中 () 用来描述参数列表,{} 用来描述方法体,-> 为 lambda运算符 ,读作(goes to)。

使用场景:
1、我们以往都是通过创建 Thread 对象,然后通过匿名内部类重写 run() 方法,一提到匿名内部类我们就应该想到可以使用 lambda 表达式来简化线程的创建过程。

 Thread t = new Thread(() -> {
      for (int i = 0; i < 10; i++) {
        System.out.println(2 + ":" + i);
      }
    });
  	t.start();

2、集合内元素的排序

在以前我们若要为集合内的元素排序,就必须调用 sort 方法,传入比较器匿名内部类重写 compare 方法,我们现在可以使用 lambda 表达式来简化代码。

ArrayList<Item> list = new ArrayList<>();
        list.add(new Item(13, "背心", 7.80));
        list.add(new Item(11, "半袖", 37.80));
        list.add(new Item(14, "风衣", 139.80));
        list.add(new Item(12, "秋裤", 55.33));

        /*
        list.sort(new Comparator<Item>() {
            @Override
            public int compare(Item o1, Item o2) {
                return o1.getId()  - o2.getId();
            }
        });
        */

        list.sort((o1, o2) -> o1.getId() - o2.getId());

        System.out.println(list);
/**
 * precondition interface is functional interface
 */
public class Lambda {

    //3.static inner class
    static class Like2 implements ILike {
        @Override
        public void lambda() {
            System.out.println("i like lambda2");
        }
    }

    public static void main(String[] args) {
        ILike like = new Like();
        like.lambda();

        like = new Like2();
        like.lambda();

        //4.local inner class
        class Like3 implements ILike {
            @Override
            public void lambda() {
                System.out.println("i like lambda3");
            }
        }
        like = new Like3();
        like.lambda();

        //5.anonymous inner class,no class name,must use interface or parent class.
        like=new ILike(){
            @Override
            public void lambda() {
                System.out.println("i like lambda4");
            }
        };
        like.lambda();


        //6.lambda
        like= () -> System.out.println("i like lambda5");
        like.lambda();
    }
}

//1.define a functional interface(only one abstract method)
interface ILike {
    void lambda();
}

//2.implements class
class Like implements ILike {

    @Override
    public void lambda() {
        System.out.println("i like lambda");
    }
}

session 和 cookie 有什么区别?

  • 由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session
    典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户,并且跟踪用户
  • Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息。Cookie其实还可以用在,登陆过一个网站,下次登录的时候不想再次输入账号了,怎么办?这个信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。

说一下 session 的工作原理?

其实session是一个存在服务器上的类似于一个散列表格的文件。里面存有我们需要的信息,在我们需要用的时候可以从里面取出来。类似于一个大号的map吧,里面的键存储的是用户的sessionid,用户向服务器发送请求的时候会在cookie种带上这个sessionid。这时就可以从中取出对应的值了。

因为Session是用Session ID来确定当前对话所对应的服务器Session,而Session ID是通过Cookie来传递的,禁用Cookie相当于失去了Session ID,也就得不到Session了。

序列化和反序列化

序列化:实现java.io.Serializable接口,序列化只能保存对象的状态,即成员变量,所以静态变量不会被序列化,Transient关键字可以阻止该变量被序列化。

  • 序列化指把应用层的对象或数据结构转换成一段连续的二进制
  • 反序列化指把二进制串转换成应用层的对象或数据结构

notify()/notifyAll()直接唤醒。

interrupt,interrupted与isInterrupted方法的区别? 如何停止一个正在运行的线程

1、interrupt()方法的作用实际上是:在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞状态。

2、interrupted()调用的是currentThread().isInterrupted(true)方法,即说明是返回当前 线程的是否已经中断的状态值,而且有清理中断状态的机制。测试当前线程是否已经中断,线程的中断状态由该方法清除。即如果连续两次调用该方法, 则第二次调用将返回false(在第一次调用已清除flag后以及第二次调用检查中断状态之前, 当前线程再次中断的情况除外)所以,interrupted()方法具有清除状态flag的功能

3、isInterrupted()调用的是isInterrupted(false)方法,意思是返回线程是否已经中断的状态,它没有清理中断状态的机制。

interrupt() 方法用于中断线程。调用该方法的线程的状态为将被置为"中断"状态。

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中 断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作。

注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态位并做处理。

interrupted() 检测当前线程是否已经中断,是则返回true,否则false,并清除中断状态。换言之,如果该方法被连续调用两次,第二次必将返回false,除非在第一次与第二次的瞬间线程再次被中断。如果中断调用时线程已经不处于活动状态,则返回false。

isInterrupted() 检测当前线程是否已经中断,是则返回true,否则false。中断状态不受该方法的影响。如果中断调用时线程已经不处于活动状态,则返回false。

在java中有以下3种方法可以终止正在运行的线程:

使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都 是过期作废的方法

使用interrupt()方法中断线程

泛型

泛型本质上是类型参数化,解决了不确定对象的类型问题。

安全:不用担心程序运行过程中出现类型转换的错误。
可读性高:编码阶段就明确的知道集合中元素的类型。

泛型是通过类型擦除来实现的,在编译阶段采用泛型时加上的类型参数,会被编译器在编译时去掉。这个过程就是类型擦除。

类型擦除指的是编译器在编译时,会擦除了所有类型相关的信息。

比如 List<.String> 在编译后就会统一成List 类型,这样做的目的就是确保能和 Java 5 之前的版本(二进制类库)进行兼容。

无参构造

Java 程序在执行子类的构造方法之前,如果没有用 super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super()来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。

什么是快速失败、什么是安全失败?

快速失败(fail-fast)

是 Java 集合的一种错误检测机制。在使用迭代器对集合进行遍历的时候,我们在多线程下操作非安全失败(fail-safe)的集合类可能就会触发 fail-fast 机制,导致抛出ConcurrentModificationException 异常。另外,在单线程下,如果在遍历过程中对集合对象的内容进行了修改的话也会触发 fail-fast 机制。

举个例子:多线程下,如果线程 1 正在对集合进行遍历,此时线程 2 对集合进行删除操作,会导致线程 1 抛出异常。

安全失败(fail-safe)

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。所以,在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException。

io流

1、按流向分类
输入流
输出流
2、按处理数据不同分类

字节流:二进制,可以处理一切文件
在这里插入图片描述

异常

在这里插入图片描述
1.所有的异常都是从Throwable继承而来的,是所有异常的共同祖先。
2.Error是错误
对于所有的编译时期的错误以及系统错误都是通过Error抛出的。类定义错误(NoClassDefFoundError)、虚拟机运行错误。
3.Exception 程序本身可以处理的异常
异常和错误的区别是,异常是可以被处理的,而错误是没法处理的。
4.Checked Exception
可检查的异常,所有checked exception都是需要在代码中处理的。比如IOException,或者一些自定义的异常。除了RuntimeException及其子类以外,都是checked exception。
5.Unchecked Exception
RuntimeException及其子类都是unchecked exception。比如NPE空指针异常,除数为0的算数异常ArithmeticException等等,这种异常是运行时发生,无法预先捕捉处理的。Error也是unchecked exception,也是无法预先处理的。

linux

常用命令

通过进程id查看占用的端口,通过端口号查看占用的进程 id?

netstat -nap | grep 进程id

pwd:显示当前所在位置

kill -9 进程的 pid : 杀死进程(-9 表示强制终止)。

怎么判断一个主机是不是开放某个端口?

telnet IP 地址 端口
telnet 127.0.0.1 3389

touch test1.txt test2.txt 同时创建一个文件

tar -czvf filename 压缩
tar -xzvf filename.tar.gz 解压

grep -n mystr myfile 在文件 myfile 中查找包含字符串 mystr的行

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

德玛西亚!!

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

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

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

打赏作者

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

抵扣说明:

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

余额充值