当前位置:   article > 正文

JAVA 面试大全_java面试是什么

java面试是什么

Java相关

基础&进阶篇
1.什么是Java
Java是一门面向对象的高级编程语言,不仅吸收了C++语言的各种优点,比如继承了C++语言面向对象的
技术核心。还摒弃了C++里难以理解的多继承、指针等概念,,同时也增加了垃圾回收机制,释放掉不
被使用的内存空间,解决了管理内存空间的烦恼。
因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地
实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 。
2. Java的特点有哪些
Java 语言是一种分布式的面向对象语言,具有面向对象、平台无关性、简单性、解释执行、多线程、安
全性等很多特点,下面针对这些特点进行逐一介绍。

  1. 面向对象
    Java 是一种面向对象的语言,它对对象中的类、对象、继承、封装、多态、接口、包等均有很好的支
    持。为了简单起见,Java 只支持类之间的单继承,但是可以使用接口来实现多继承。使用 Java 语言开发
    程序,需要采用面向对象的思想设计程序和编写代码。
  2. 平台无关性
    平台无关性的具体表现在于,Java 是“一次编写,到处运行(Write Once,Run any Where)”的语言,
    因此采用 Java 语言编写的程序具有很好的可移植性,而保证这一点的正是 Java 的虚拟机机制。在引入
    虚拟机之后,Java 语言在不同的平台上运行不需要重新编译。
    Java 语言使用 Java 虚拟机机制屏蔽了具体平台的相关信息,使得 Java 语言编译的程序只需生成虚拟机
    上的目标代码,就可以在多种平台上不加修改地运行。
  3. 简单性
    Java 语言的语法与 C 语言和 C++ 语言很相近,使得很多程序员学起来很容易。对 Java 来说,它舍弃了
    很多 C++ 中难以理解的特性,如操作符的重载和多继承等,而且 Java 语言不使用指针,加入了垃圾回
    收机制,解决了程序员需要管理内存的问题,使编程变得更加简单。
  4. 解释执行
    Java 程序在 Java 平台运行时会被编译成字节码文件,然后可以在有 Java 环境的操作系统上运行。在运
    行文件时,Java 的解释器对这些字节码进行解释执行,执行过程中需要加入的类在连接阶段被载入到运
    行环境中。
  5. 多线程
    Java 语言是多线程的,这也是 Java 语言的一大特性,它必须由 Thread 类和它的子类来创建。Java 支持
    多个线程同时执行,并提供多线程之间的同步机制。任何一个线程都有自己的 run() 方法,要执行的方
    法就写在 run() 方法体内。
  6. 分布式
    Java 语言支持 Internet 应用的开发,在 Java 的基本应用编程接口中就有一个网络应用编程接口,它提
    供了网络应用编程的类库,包括 URL、URLConnection、Socket 等。Java 的 RIM 机制也是开发分布式
    应用的重要手段。
  7. 健壮性
    Java 的强类型机制、异常处理、垃圾回收机制等都是 Java 健壮性的重要保证。对指针的丢弃是 Java 的
    一大进步。另外,Java 的异常机制也是健壮性的一大体现。
  8. 高性能
    Java 的高性能主要是相对其他高级脚本语言来说的,随着 JIT(Just in Time)的发展,Java 的运行速度
    也越来越高。
  9. 安全性
    Java 通常被用在网络环境中,为此,Java 提供了一个安全机制以防止恶意代码的攻击。除了 Java 语言
    具有许多的安全特性以外,Java 还对通过网络下载的类增加一个安全防范机制,分配不同的名字空间以
    防替代本地的同名类,并包含安全管理机制。
    Java 语言的众多特性使其在众多的编程语言中占有较大的市场份额,Java 语言对对象的支持和强大的
    API 使得编程工作变得更加容易和快捷,大大降低了程序的开发成本。Java 的“一次编写,到处执行”正是
    它吸引众多商家和编程人员的一大优势。
  10. JDK和JRE和JVM的区别
  11. JDK
    JDK(Java SE Development Kit),Java标准的开发包,提供了编译、运行Java程序所需要的各种工具
    和资源,包括了Java编译器、Java运行时环境、以及常用的Java类库等。
  12. JRE
    JRE(Java Runtime Environment),Java运行时环境,用于解释执行Java的字节码文件。普通用户只
    需要安装JRE来运行Java程序即可,而作为一名程序员必须安装JDK,来编译、调试程序。
  13. JVM
    JVM(Java Virtual Mechinal),Java虚拟机,是JRE的一部分。它是整个Java实现跨平台的核心,负责
    解释执行字节码文件,是可运行Java字节码文件的虚拟计算机。所有平台上的JVM向编译器提供相同的
    接口,而编译器只需要面向虚拟机,生成虚拟机能识别的代码,然后由虚拟机来解释执行。
    当使用Java编译器编译Java程序时,生成的是与平台无关的字节码,这些字节码只面向JVM。也就是说
    JVM是运行Java字节码的虚拟机。
    不同平台的JVM是不同的,但是他们都提供了相同的接口。JVM是Java程序跨平台的关键部分,只要为不
    同平台实现了相同的虚拟机,编译后的Java字节码就可以在该平台上运行。
    为什么要采用字节码:
    在 Java 中,JVM 可以理解的代码就叫做 字节码 (即Java源代码经过虚拟机编译器编译后扩展名为
    .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,
    在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。
    所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无
    须重新编译便可在多种不同操作系统的计算机上运行。
    什么是跨平台:
    所谓跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上运行。
    实现原理:Java程序是通过java虚拟机在系统平台上运行的,只要该系统可以安装相应的java虚拟
    机,该系统就可以运行java程序。
    Java 程序从源代码到运行需要三步:
  14. 总结
  15. JDK 用于开发,JRE 用于运行java程序 ;如果只是运行Java程序,可以只安装JRE,无序安装JDK。
  16. JDk包含JRE,JDK 和 JRE 中都包含 JVM。 3. JVM 是 Java 编程语言的核心并且具有平台独立性。
  17. Oracle JDK 和 OpenJDK 的对比
    Oracle JDK版本将每三年发布一次,而OpenJDK版本每三个月发布一次;
    OpenJDK 是一个参考模型并且是完全开源的,而Oracle JDK是OpenJDK的一个实现,并不是完全
    开源的;
    在这里插入图片描述

Oracle JDK 比 OpenJDK 更稳定。OpenJDK和Oracle JDK的代码几乎相同,但Oracle JDK有更多的
类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择Oracle JDK,因为它经过了
彻底的测试和稳定。某些情况下,有些人提到在使用OpenJDK 可能会遇到了许多应用程序崩溃的
问题,但是,只需切换到Oracle JDK就可以解决问题;
在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的性能;
Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来
获取最新版本;
Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。
5. Java有哪些数据类型
Java中有 8 种基本数据类型,分别为:
6 种数字类型 (四个整数形,两个浮点型):byte、short、int、long、float、double
1 种字符类型:char
1 种布尔型:boolean。
byte:
byte 数据类型是8位、有符号的,以二进制补码表示的整数;
最小值是 -128(-2^7);
最大值是 127(2^7-1);
默认值是 0;
byte 类型用在大型数组中节约空间,主要代替整数,因为 byte 变量占用的空间只有 int 类型的四
分之一;
例子:byte a = 100,byte b = -50。
short:
short 数据类型是 16 位、有符号的以二进制补码表示的整数
最小值是 -32768(-2^15);
最大值是 32767(2^15 - 1);
Short 数据类型也可以像 byte 那样节省空间。一个short变量是int型变量所占空间的二分之一;
默认值是 0;
例子:short s = 1000,short r = -20000。
int:
int 数据类型是32位、有符号的以二进制补码表示的整数;
最小值是 -2,147,483,648(-2^31);
最大值是 2,147,483,647(2^31 - 1);
一般地整型变量默认为 int 类型;
默认值是 0 ;
例子:int a = 100000, int b = -200000。
long:
注意:Java 里使用 long 类型的数据一定要在数值后面加上 L,否则将作为整型解析
long 数据类型是 64 位、有符号的以二进制补码表示的整数;
最小值是 -9,223,372,036,854,775,808(-2^63);
最大值是 9,223,372,036,854,775,807(2^63 -1);
这种类型主要使用在需要比较大整数的系统上;
默认值是 0L;
例子: long a = 100000L,Long b = -200000L。
"L"理论上不分大小写,但是若写成"l"容易与数字"1"混淆,不容易分辩。所以最好大写。
float:
float 数据类型是单精度、32位、符合IEEE 754标准的浮点数;
float 在储存大型浮点数组的时候可节省内存空间;
默认值是 0.0f;
浮点数不能用来表示精确的值,如货币;
例子:float f1 = 234.5f。
double:
double 数据类型是双精度、64 位、符合IEEE 754标准的浮点数;
浮点数的默认类型为double类型;
double类型同样不能表示精确的值,如货币;
默认值是 0.0d;
例子:double d1 = 123.4。
char:
char类型是一个单一的 16 位 Unicode 字符;
最小值是 \u0000(即为 0);
最大值是 \uffff(即为 65535);
char 数据类型可以储存任何字符;
例子:char letter = ‘A’;(单引号)
boolean:
boolean数据类型表示一位的信息;
只有两个取值:true 和 false;
这种类型只作为一种标志来记录 true/false 情况;
默认值是 false;
例子:boolean one = true。
这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、
Character、Boolean
类型名 称 字节、位数 最小值 最大值 默认值 例子 byte字 节 1字节,8位 -128(-2^7) 127(2^7-1) 0 byte a = 100,byte b = -50 short短 整型 2字节,16位 -32768(-2^15) 32767(2^15 - 1) 0 short s = 1000, short r = -20000 int整形 4字节,32位 -2,147,483,648(-2^31) 2,147,483,647(2^31 - 1) 0 int a = 100000, int b = -200000 lang长 整型 8字节,64位 -9,223,372,036,854,775,808(-2^63) 9,223,372,036,854,775,807(2^63 -1) 0L long a = 100000L, Long b = -200000L double 双精度 8字节,64位 double类型同样不能表示精确的 值,如货币 0.0d double d1 = 123.4 float单 精度 4字节,32位 在储存大型浮点数组的时候可节省内存 空间 不同统计精准的货币值 0.0f float f1 = 234.5f
char字 符 2字节,16位 \u0000(即为0) \uffff(即为65,535) 可以储存任何字符
char letter = ‘A’; boolean 布尔 返回true和 false两个值 这种类型只作为一种标志来记录 true/false 情况; 只有两个取值:true 和 false; false boolean one = true
6. Java中引用数据类型有哪些,它们与基本数据类型有什么区别?
引用数据类型分3种:类,接口,数组;
简单来说,只要不是基本数据类型.都是引用数据类型。 那他们有什么不同呢?
1、从概念方面来说
1,基本数据类型:变量名指向具体的数值
2,引用数据类型:变量名不是指向具体的数值,而是指向存数据的内存地址,.也及时hash值 2、从内存的构建方面来说(内存中,有堆内存和栈内存两者)
1,基本数据类型:被创建时,在栈内存中会被划分出一定的内存,并将数值存储在该内存中.
2,引用数据类型:被创建时,首先会在栈内存中分配一块空间,然后在堆内存中也会分配一块具体的空间用来
存储数据的具体信息,即hash值,然后由栈中引用指向堆中的对象地址.
举个例子
由上图可知,基本数据类型中会存在两个相同的1,而引用型类型就不会存在相同的数据。
假如"hello"的引用地址是xxxxx1,声明str变量并其赋值"hello"实际上就是让str变量引用了"hello"的内
存地址,这个内存地址就存储在堆内存中,是不会改变的,当再次声明变量str1也是赋值为"hello"时,
此时就会在堆内存中查询是否有"hello"这个地址,如果堆内存中已经存在这个地址了,就不会再次创建
了,而是让str1变量也指向xxxxx1这个地址,如果没有的话,就会重新创建一个地址给str1变量。
7. 从使用方面来说
1,基本数据类型:判断数据是否相等,用和!=判断。
2,引用数据类型:判断数据是否相等,用equals()方法,和!=是比较数值的。而equals()方法是比较内存
地址的。
补充:数据类型选择的原则
如果要表示整数就使用int,表示小数就使用double;
如果要描述日期时间数字或者表示文件(或内存)大小用long;
如果要实现内容传递或者编码转换使用byte;
如果要实现逻辑的控制,可以使用booleam;
如果要使用中文,使用char避免中文乱码;
如果按照保存范围:byte < int < long < double;
//基本数据类型作为方法参数被调用 public class Main{ public static void main(String[] args){ //基本数据类型 int i = 1; int j = 1; double d = 1.2; //引用数据类型 String str = “Hello”; String str1= “Hello”; } }
8. Java中的自动装箱与拆箱
什么是自动装箱拆箱?
从下面的代码中就可以看到装箱和拆箱的过程
装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
在Java SE5之前,自动装箱要这样写:Integer i = newInteger( 10``);
对于Java的自动装箱和拆箱,我们看看源码编译后的class文件,其实装箱调用包装类的valueOf方法,
拆箱调用的是Integer.Value方法,下面就是变编译后的代码:
常见面试一:
这段代码输出什么?
//自动装箱 Integer total = 99; //自定拆箱 int totalprim = total;
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); System.out.println(i3
i4); } }
答案是: true false
为什么会出现这样的结果?输出结果表明i1和i2指向的是同一个对象,而i3和i4指向的是不同的对象。此
时只需一看源码便知究竟,下面这段代码是Integer的valueOf方法的具体实现:
public static Integer valueOf(int i) { if(i >= -128 && i <= IntegerCache.high) return IntegerCache.cache[i + 128]; elsereturn new Integer(i); } private static class IntegerCache { static final int high; static final Integer cache[]; static { final int low = -128; // high value may be configured by property int h = 127; if (integerCacheHighPropValue != null) { // Use Long.decode here to avoid invoking methods that // require Integer’s autoboxing cache to be initialized int i = Long.decode(integerCacheHighPropValue).intValue(); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - -low); }high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); }
基本类型 包装器类型
boolean Boolean
char Character
int Integer
byte Byte
short Short
long Long
float Float
double Double
从这2段代码可以看出,在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,便返
回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个新的Integer对象。
上面的代码中i1和i2的数值为100,因此会直接从cache中取已经存在的对象,所以i1和i2指向的是同一
个对象,而i3和i4则是分别指向不同的对象。
常见面试二:
输出结果为:
原因很简单,在某个范围内的整型数值的个数是有限的,而浮点数却不是。
9. 为什么要有包装类型?
让基本数据类型也具有对象的特征
private IntegerCache() {} } public class Main { public static void main(String[] args) { Double i1 = 100.0; Double i2 = 100.0; Double i3 = 200.0; Double i4 = 200.0; System.out.println(i1i2); System.out.println(i3i4); } }false false
为了让基本类型也具有对象的特征,就出现了包装类型(如我们在使用集合类型Collection时就一定要使
用包装类型而非基本类型)因为容器都是装object的,这是就需要这些基本类型的包装器类了。
自动装箱: new Integer(6); ,底层调用: Integer.valueOf(6)
自动拆箱: int i = new Integer(6); ,底层调用 i.intValue(); 方法实现。
答案在下面这段代码中找:
二者的区别:

  1. 声明方式不同:基本类型不使用new关键字,而包装类型需要使用new关键字来在堆中分配存储空
    间; 2. 存储方式及位置不同:基本类型是直接将变量值存储在栈中,而包装类型是将对象放在堆中,然后
    通过引用来使用;
  2. 初始值不同:基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null; 4. 使用方式不同:基本类型直接赋值直接使用就好,而包装类型在集合如Collection、Map时会使用
    到。
  3. a=a+b与a+=b有什么区别吗? += 操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型,而
    a=a+b则不会自动进行类型转换.如:
    以下代码是否有错,有的话怎么改?
    有错误.short类型在进行运算时会自动提升为int类型,也就是说 s1+1 的运算结果是int类型,而s1是short
    类型,此时编译器会报错.
    正确写法:
    Integer i = 6; Integer j = 6; System.out.println(i==j); public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }byte a = 127; byte b = 127; b = a + b; // 报编译错误:cannot convert from int to byte b += a; short s1= 1; s1 = s1 + 1; short s1= 1; s1 += 1;
    += 操作符会对右边的表达式结果强转匹配左边的数据类型,所以没错.
  4. 能将 int 强制转换为 byte 类型的变量吗?如果该值大于 byte 类
    型的范围,将会出现什么现象?
    我们可以做强制转换,但是 Java 中 int 是 32 位的,而 byte 是 8 位的,所以,如果强制转化,int 类型
    的高 24 位将会被丢弃,因为byte 类型的范围是从 -128 到 127
  5. Java程序是如何执行的
    我们日常的工作中都使用开发工具(IntelliJ IDEA 或 Eclipse 等)可以很方便的调试程序,或者是通过打
    包工具把项目打包成 jar 包或者 war 包,放入 Tomcat 等 Web 容器中就可以正常运行了,但你有没有想
    过 Java 程序内部是如何执行的?其实不论是在开发工具中运行还是在 Tomcat 中运行,Java 程序的执行
    流程基本都是相同的,它的执行流程如下:
    先把 Java 代码编译成字节码,也就是把 .java 类型的文件编译成 .class 类型的文件。这个过程的大
    致执行流程:Java 源代码 -> 词法分析器 -> 语法分析器 -> 语义分析器 -> 字符码生成器 -> 最终生成
    字节码,其中任何一个节点执行失败就会造成编译失败;
    把 class 文件放置到 Java 虚拟机,这个虚拟机通常指的是 Oracle 官方自带的 Hotspot JVM;
    Java 虚拟机使用类加载器(Class Loader)装载 class 文件;
    类加载完成之后,会进行字节码效验,字节码效验通过之后 JVM 解释器会把字节码翻译成机器码交
    由操作系统执行。但不是所有代码都是解释执行的,JVM 对此做了优化,比如,以 Hotspot 虚拟机
    来说,它本身提供了 JIT(Just In Time)也就是我们通常所说的动态编译器,它能够在运行时将热
    点代码编译为机器码,这个时候字节码就变成了编译执行。Java 程序执行流程图如下:
  6. final 在 Java 中有什么作用?
    final作为Java中的关键字可以用于三个地方。用于修饰类、类属性和类方法。
    特征:凡是引用final关键字的地方皆不可修改!
    (1)修饰类:表示该类不能被继承;
    (2)修饰方法:表示方法不能被重写;
    (3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。
  7. final有哪些用法?
    final也是很多面试喜欢问的地方,但我觉得这个问题很无聊,通常能回答下以下5点就不错了: 被final修饰的类不可以被继承
    被final修饰的方法不可以被重写
    被final修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变. 被final修饰的方法,JVM会尝试将其内联,以提高运行效率
    被final修饰的常量,在编译阶段会存入常量池中.
    除此之外,编译器对final域要遵守的两个重排序规则更好:
    在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间
    不能重排序 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序.
  8. static都有哪些用法?
    所有的人都知道static关键字这两个基本的用法:静态变量和静态方法.也就是被static所修饰的变量/方法
    都属于类的静态资源,类实例所共享.
    除了静态变量和静态方法之外,static也用于静态块,多用于初始化操作:
    此外static也多用于修饰内部类,此时称之为静态内部类.
    最后一种用法就是静态导包,即 import static .import static是在JDK 1.5之后引入的新特性,可以用来指
    定导入某个类中的静态资源,并且不需要使用类名,可以直接使用资源名,比如: public calss PreCache{ static{ //执行相关操作 } }import static java.lang.Math.*; public class Test{ public static void main(String[] args){ //System.out.println(Math.sin(20));传统做法 System.out.println(sin(20)); } }
    关键词 修饰物 影响
    final 变量 分配到常量池中,程序不可改变其值
    final 方法 子类中将不能被重写
    final 类 不能被继承
    static 变量 分配在内存堆上,引用都会指向这一个地址而不会重新分配内存
    static 方法块 虚拟机优先加载
    static 类 可以直接通过类来调用而不需要new
  9. static和final区别
  10. 为什么有些java类要实现Serializable接口
    为了网络进行传输或者持久化
    什么是序列化
    将对象的状态信息转换为可以存储或传输的形式的过程
    除了实现Serializable接口还有什么序列化方式
    Json序列化
    FastJson序列化
    ProtoBuff序列化
  11. 什么是java序列化,如何实现java序列化?或者请解释
    Serializable接口的作用。
    我们有时候将一个java对象变成字节流的形式传出去或者从一个字节流中恢复成一个java对象,例如,
    要将java对象存储到硬盘或者传送给网络上的其他计算机,这个过程我们可以自己写代码去把一个java
    对象变成某个格式的字节流再传输。
    但是,jre本身就提供了这种支持,我们可以调用 OutputStream 的 writeObject 方法来做,如果要让
    java帮我们做,要被传输的对象必须实现 serializable 接口,这样,javac编译时就会进行特殊处理,
    编译的类才可以被 writeObject 方法操作,这就是所谓的序列化。需要被序列化的类必须实现
    Serializable 接口,该接口是一个mini接口,其中没有需要实现方法,implements Serializable只是
    为了标注该对象是可被序列化的。
    例如,在web开发中,如果对象被保存在了Session中,tomcat在重启时要把Session对象序列化到硬
    盘,这个对象就必须实现Serializable接口。如果对象要经过分布式系统进行网络传输,被传输的对象就
    必须实现Serializable接口。
  12. 什么是内部类?内部类的作用
    内部类的定义
    将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。
    内部类的作用:
    1、成员内部类 成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态
    成员)。 当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问
    的是成员内部类的成员。
    2、局部内部类 局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局
    部内部类的访问仅限于方法内或者该作用域内。
    3、匿名内部类 匿名内部类就是没有名字的内部类
    4、静态内部类 指被声明为static的内部类,他可以不依赖内部类而实例,而通常的内部类需要实例化外
    部类,从而实例化。静态内部类不可以有与外部类有相同的类名。不能访问外部类的普通成员变量,但
    是可以访问静态成员变量和静态方法(包括私有类型) 一个 静态内部类去掉static 就是成员内部类,他
    可以自由的引用外部类的属性和方法,无论是静态还是非静态。但是不可以有静态属性和方法
  13. Excption与Error包结构
    Java可抛出(Throwable)的结构分为三种类型:被检查的异常(CheckedException),运行时异常
    (RuntimeException),错误(Error)。 1、运行时异常
    定义:RuntimeException及其子类都被称为运行时异常。
    特点:Java编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明
    抛出它",也"没有用try-catch语句捕获它",还是会编译通过。例如,除数为零时产生的
    ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail-fast机制产生的
    ConcurrentModificationException异常(java.util包下面的所有的集合类都是快速失败的,“快速失败”
    也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有
    可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线
    程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而
    不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异
    常,从而产生fail-fast机制,这个错叫并发修改异常。Fail-safe,java.util.concurrent包下面的所有的类
    都是安全失败的,在遍历过程中,如果已经遍历的数组上的内容变化了,迭代器不会抛出
    ConcurrentModificationException异常。如果未遍历的数组上的内容发生了变化,则有可能反映到迭
    代过程中。这就是ConcurrentHashMap迭代器弱一致的表现。ConcurrentHashMap的弱一致性主要是
    为了提升效率,是一致性与效率之间的一种权衡。要成为强一致性,就得到处使用锁,甚至是全局锁,
    这就与Hashtable和同步的HashMap一样了。)等,都属于运行时异常。
    常见的五种运行时异常:
    ClassCastException (类转换异常)
    IndexOutOfBoundsException (数组越界)
    NullPointerException (空指针异常)
    ArrayStoreException (数据存储异常,操作数组是类型不一致)
    BufferOverflowException 2、被检查异常
    定义:Exception类本身,以及Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异
    常。
    特点 : Java编译器会检查它。 此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获
    处理,否则不能通过编译。例如,CloneNotSupportedException就属于被检查异常。
    当通过clone()接口去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出
    CloneNotSupportedException异常。被检查异常通常都是可以恢复的。 如:
    IOException FileNotFoundException SQLException
    被检查的异常适用于那些不是因程序引起的错误情况,比如:读取文件时文件不存在引发的
    FileNotFoundException 。然而,不被检查的异常通常都是由于糟糕的编程引起的,比如:在对象引
    用时没有确保对象非空而引起的 NullPointerException 。 3、错误
    定义 : Error类及其子类。
    特点 : 和运行时异常一样,编译器也不会对错误进行检查。
    当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修复这
    些错误的。例如,VirtualMachineError就属于错误。出现这种错误会导致程序终止运行。
    OutOfMemoryError、ThreadDeath。
    Java虚拟机规范规定JVM的内存分为了好几块,比如堆,栈,程序计数器,方法区等
  14. try {}里有一个return语句,那么紧跟在这个try后的finally{}里 的code会不会被执行,什么时候被执行,在return前还是后?
    我们知道finally{}中的语句是一定会执行的,那么这个可能正常脱口而出就是return之前,return之后可
    能就出了这个方法了,鬼知道跑哪里去了,但更准确的应该是在return中间执行,请看下面程序代码的
    运行结果:
    public classTest { public static void main(String[]args) { System.out.println(newTest().test());; }static int test() { intx = 1; try {
    执行结果如下:
    运行结果是1,为什么呢?主函数调用子函数并得到结果的过程,好比主函数准备一个空罐子,当子函数
    要返回结果时,先把结果放在罐子里,然后再将程序逻辑返回到主函数。所谓返回,就是子函数说,我
    不运行了,你主函数继续运行吧,这没什么结果可言,结果是在说这话之前放进罐子里的。
  15. 运行时异常与一般异常有何异同?
    异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异
    常,是一种常见运行错误。java编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求
    必须声明抛出未被捕获的运行时异常。
  16. error和exception有什么区别?
    error 表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出。不可能指望程序能处
    理这样的情况。exception表示一种设计或实现问题。也就是说,它表示如果程序运行正常,从不会发生
    的情况。
  17. 简单说说Java中的异常处理机制的简单原理和应用。
    异常是指java程序运行时(非编译)所发生的非正常情况或错误,与现实生活中的事件很相似,现实生
    活中的事件可以包含事件发生的时间、地点、人物、情节等信息,可以用一个对象来表示,Java使用面
    向对象的方式来处理异常,它把程序中发生的每个异常也都分别封装到一个对象来表示的,该对象中包
    含有异常的信息。
    Java对异常进行了分类,不同类型的异常分别用不同的Java类表示,所有异常的根类为
    java.lang.Throwable。
    Throwable下面又派生了两个子类:
    Error和Exception,Error表示应用程序本身无法克服和恢复的一种严重问题,程序只有奔溃了,
    例如,说内存溢出和线程死锁等系统问题。
    Exception表示程序还能够克服和恢复的问题,其中又分为系统异常和普通异常:
    系统异常是软件本身缺陷所导致的问题,也就是软件开发人员考虑不周所导致的问题,软件使用者无法
    克服和恢复这种问题,但在这种问题下还可以让软件系统继续运行或者让软件挂掉,例如,数组脚本越
    界(ArrayIndexOutOfBoundsException),空指针异常(NullPointerException)、类转换异常
    (ClassCastException);
    return x; }finally { ++x; } } }1
    普通异常是运行环境的变化或异常所导致的问题,是用户能够克服的问题,例如,网络断线,硬盘空间
    不够,发生这样的异常后,程序不应该死掉。
    java为系统异常和普通异常提供了不同的解决方案,编译器强制普通异常必须try…catch处理或用throws
    声明继续抛给上层调用方法处理,所以普通异常也称为checked异常,而系统异常可以处理也可以不处
    理,所以,编译器不强制用try…catch处理或用throws声明,所以系统异常也称为unchecked异常。
  18. == 和 equals 的区别是什么?
    ""
    对于基本类型和引用类型 == 的作用效果是不同的,如下所示:
    基本类型:比较的是值是否相同;
    引用类型:比较的是引用是否相同;
    因为 x 和 y 指向的是同一个引用,所以 == 也是 true,而 new String()方法则重写开辟了内存空
    间,所以 == 结果为 false,而 equals 比较的一直是值,所以结果都为 true。
    equals
    equals 本质上就是 ,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较。看下面的
    代码就明白了。
    首先来看默认情况下 equals 比较一个有相同值的对象,代码如下:
    String x = “string”; String y = “string”; String z = new String(“string”); System.out.println(x
    y); // true System.out.println(x
    z); // false System.out.println(x.equals(y)); // true System.out.println(x.equals(z)); // true class Cat { public Cat(String name) { this.name = name; }private String name; public String getName() { return name; }public void setName(String name) { this.name = name; } }Cat c1 = new Cat(“叶痕秋”); Cat c2 = new Cat(“叶痕秋”); System.out.println(c1.equals(c2)); // false
    输出结果出乎我们的意料,竟然是 false?这是怎么回事,看了 equals 源码就知道了,源码如下:
    原来 equals 本质上就是 ==。
    那问题来了,两个相同值的 String 对象,为什么返回的是 true?代码如下:
    同样的,当我们进入 String 的 equals 方法,找到了答案,代码如下:
    原来是 String 重写了 Object 的 equals 方法,把引用比较改成了值比较。
    总结
    == 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;而 equals 默认情况下是引用比
    较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下
    equals 比较的是值是否相等。
  19. Hashcode的作用
    java的集合有两类,一类是List,还有一类是Set。前者有序可重复,后者无序不重复。当我们在set中插
    入的时候怎么判断是否已经存在该元素呢,可以通过equals方法。但是如果元素太多,用这样的方法就
    会比较满。
    public boolean equals(Object obj) { return (this == obj); }String s1 = new String(“叶子”); String s2 = new String(“叶子”); System.out.println(s1.equals(s2)); // true public boolean equals(Object anObject) { if (this == anObject) { return true; }if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; }return true; } }return false; }
    于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每个对
    象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的哈希码就
    可以确定该对象应该存储的那个区域。
    hashCode方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当集合
    要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。
    如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上
    已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地
    址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
  20. 两个对象的 hashCode() 相同, 那么 equals() 也一定为 true
    吗?
    不对,两个对象的 hashCode() 相同,equals() 不一定 true。
    代码示例:
    执行结果:
    代码解读:很显然“keep”和“brother”的 hashCode() 相同,然而 equals() 则为 false,因为在散列表
    中,hashCode() 相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。
  21. 泛型常用特点
    泛型是Java SE 1.5之后的特性, 《Java 核心技术》中对泛型的定义是:
    “泛型” 意味着编写的代码可以被不同类型的对象所重用。
    “泛型”,顾名思义,“泛指的类型”。我们提供了泛指的概念,但具体执行的时候却可以有具体的规则来约
    束,比如我们用的非常多的ArrayList就是个泛型类,ArrayList作为集合可以存放各种元素,如Integer,
    String,自定义的各种类型等,但在我们使用的时候通过具体的规则来约束,如我们可以约束集合中只
    存放Integer类型的元素,如
    使用泛型的好处?
    以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整型集
    合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型数据,而
    这并不是最重要的,因为我们只要把底层存储设置了Object即可,添加的数据全部都可向上转型为
    Object。 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。
    String str1 = “keep”; String str2 = “brother”; System. out. println(String. format(“str1:%d | str2:%d”, str1. hashCode(),str2. hashCode())); System. out. println(str1. equals(str2)); str1:1179395 | str2:1179395 false List iniData = new ArrayList<>()
  22. 面向对象的特征
    面向对象的编程语言有封装、继承 、抽象、多态等4个主要的特征。
  23. 封装: 把描述一个对象的属性和行为的代码封装在一个模块中,也就是一个类中,属性用变量定
    义,行为用方法进行定义,方法可以直接访问同一个对象中的属性。
  24. 抽象: 把现实生活中的对象抽象为类。分为过程抽象和数据抽象
    数据抽象 -->鸟有翅膀,羽毛等(类的属性)
    过程抽象 -->鸟会飞,会叫(类的方法) 1. 继承:子类继承父类的特征和行为。子类可以有父类的方法,属性(非private)。子类也可以对父
    类进行扩展,也可以重写父类的方法。缺点就是提高代码之间的耦合性。
  25. 多态: 多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程
    时并不确定,而是在程序运行期间才确定(比如:向上转型,只有运行才能确定其对象属性)。方法
    覆盖和重载体现了多态性。
  26. Java多态的理解
  27. 多态是继封装、继承之后,面向对象的第三大特性。
  28. 多态现实意义理解:
    现实事物经常会体现出多种形态,如学生,学生是人的一种,则一个具体的同学张三既是学生也是
    人,即出现两种形态。
    Java作为面向对象的语言,同样可以描述一个事物的多种形态。如Student类继承了Person类,一
    个Student的对象便既是Student,又是Person。 1. 多态体现为父类引用变量可以指向子类对象。
  29. 前提条件:必须有子父类关系。
    注意:在使用多态后的父类引用变量调用方法时,会调用子类重写后的方法。
  30. 多态的定义与使用格式
    定义格式:父类类型 变量名=new 子类类型();
  31. 重载和重写的区别
    重写(Override)
    从字面上看,重写就是 重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子类继
    承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列
    表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行
    修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。
    重写 总结:
    1.发生在父类与子类之间
    2.方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同
    3.访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)
    4.重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常
    重载(Overload)
    在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚至是参数顺序不同)
    则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来
    判断重载。 public class Father { public static void main(String[] args) { // TODO Auto-generated method stub Son s = new Son(); s.sayHello(); }public void sayHello() { System.out.println(“Hello”); } }class Son extends Father{ @Override public void sayHello() { // TODO Auto-generated method stub System.out.println("hello by "); } }public class Father { public static void main(String[] args) { // TODO Auto-generated method stub Father s = new Father(); s.sayHello(); s.sayHello(“wintershii”);
    重载 总结:
    1.重载Overload是一个类中多态性的一种表现
    2.重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序)
    3.重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准
  32. Java创建对象有几种方式?
    java中提供了以下四种创建对象的方式:
    new创建新对象
    通过反射机制
    采用clone机制
    通过序列化机制
  33. ConcurrentModificationException异常出现的原因
    执行上段代码是有问题的,会抛出 ConcurrentModificationException 异常。
    原因:调用 list.remove() 方法导致 modCount 和 expectedModCount 的值不一致。
    }public void sayHello() { System.out.println(“Hello”); }public void sayHello(String name) { System.out.println(“Hello” + " " + name); } }public class Test { public static void main(String[] args) { ArrayList list = new ArrayList(); list.add(2); Iterator iterator = list.iterator(); while(iterator.hasNext()){ Integer integer = iterator.next(); if(integer==2) list.remove(integer); } } }final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
    解决办法:在迭代器中如果要删除元素的话,需要调用 Iterator 类的 remove 方法。
  34. HashMap和HashTable、ConcurrentHashMap区别?
    相同点: 1. HashMap和Hashtable都实现了Map接口
  35. 都可以存储key-value数据
    不同点:
  36. HashMap可以把null作为key或value,HashTable不可以
  37. HashMap线程不安全,效率高。HashTable线程安全,效率低。
  38. HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast
    的。
    什么是fail-fast?
    就是最快的时间能把错误抛出而不是让程序执行。
  39. 如何保证线程安全又效率高?
    Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
    ConcurrentHashMap将整个Map分为N个segment(类似HashTable),可以提供相同的线程安全,但是
    效率提升N倍,默认N为16。
  40. 我们能否让HashMap同步?
    HashMap可以通过下面的语句进行同步:
    public class Test { public static void main(String[] args) { ArrayList list = new ArrayList(); list.add(2); Iterator iterator = list.iterator(); while(iterator.hasNext()){ Integer integer = iterator.next(); if(integer==2) iterator.remove(); //注意这个地方 } } }Map m = Collections.synchronizeMap(hashMap);
  41. Java 中 IO 流分为几种?
    按功能来分:输入流(input)、输出流(output)。
    按类型来分:字节流和字符流。
    字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符
    为单位输入输出数据。
  42. BIO、NIO、AIO 有什么区别?
    BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并
    发处理能力低。
    NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通
    讯,实现了多路复用。
    AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于
    事件和回调机制。
  43. Files的常用方法都有哪些?
    Files. exists():检测文件路径是否存在。
    Files. createFile():创建文件。
    Files. createDirectory():创建文件夹。
    Files. delete():删除一个文件或目录。
    Files. copy():复制文件。
    Files. move():移动文件。
    Files. size():查看文件个数。
    Files. read():读取文件。
    Files. write():写入文件。
  44. Java反射的作用于原理
    1、定义:
    反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意个对象,都能
    够调用它的任意一个方法。在java中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。
    这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
    2、哪里会用到反射机制?
    jdbc就是典型的反射
    这就是反射。如hibernate,struts等框架使用反射实现的。
    Class.forName(‘com.mysql.jdbc.Driver.class’);//加载MySQL的驱动类
  45. 反射的实现方式
    第一步:获取Class对象,有4种方法: 1)Class.forName(“类的路径”); 2)类名.class 3)对象
    名.getClass() 4)基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象
  46. 实现Java反射的类:
    1)Class:表示正在运行的Java应用程序中的类和接口 注意: 所有获取对象的信息都需要Class类来实
    现。 2)Field:提供有关类和接口的属性信息,以及对它的动态访问权限。 3)Constructor:提供关于
    类的单个构造方法的信息以及它的访问权限 4)Method:提供类或接口中某个方法的信息
  47. 反射机制的优缺点:
    优点:
    1、能够运行时动态获取类的实例,提高灵活性;
    2、与动态编译结合
    缺点:
    1、使用反射性能较低,需要解析字节码,将内存中的对象进行解析。
    解决方案:
    1、通过setAccessible(true)关闭JDK的安全检查来提升反射速度;
    2、多次创建一个类的实例时,有缓存会快很多
    3、ReflectASM工具类,通过字节码生成的方式加快反射速度
    2、相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性)
  48. Java 中 IO 流分为几种?
    按照流的流向分,可以分为输入流和输出流;
    按照操作单元划分,可以划分为字节流和字符流;
    按照流的角色划分为节点流和处理流。
    Java Io 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的
    联系, Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
    InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
    OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
    按操作方式分类结构图:
    字符串&集合面试题汇总
  49. Java 中操作字符串都有哪些类?它们之间有什么区别?
    操作字符串的类有: String 、 StringBuffer 、 StringBuilder 。
    String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成
    新的 String 对象,然后将指针指向新的 String 对象。
    而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况
    下最好不要使用 String。
    StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是
    非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,
    所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。
  50. String、StringBuffer和StringBuilder区别(类似上一题) 1、数据可变和不可变
  51. String 底层使用一个不可变的字符数组 private final char value[]; 所以它内容不可变。
  52. StringBuffer 和 StringBuilder 都继承了 AbstractStringBuilder 底层使用的是可变字符数
    组: char[] value; 2、线程安全
    StringBuilder 是线程不安全的,效率较高;而 StringBuffer 是线程安全的,效率较低。
    通过他们的 append() 方法来看, StringBuffer 是有同步锁,而 StringBuilder 没有:
    3、 相同点
    @Override public synchronized StringBuffer append(Object obj) { toStringCache = null; super.append(String.valueOf(obj)); return this; }@Override public StringBuilder append(String str) { super.append(str); return this; }
    StringBuilder 与 StringBuffer 有公共父类 AbstractStringBuilder 。
    最后,操作可变字符串速度: StringBuilder > StringBuffer > String ,这个答案就显得不足为奇
    了。
  53. String str="i"与 String str=new String(“i”)一样吗?
    不一样,因为内存的分配方式不一样。String str="i"的方式,Java 虚拟机会将其分配到常量池中;而
    String str=new String(“i”) 则会被分到堆内存中。
    代码示例:
    String x = “叶痕秋” 的方式,Java 虚拟机会将其分配到常量池中,而常量池中没有重复的元素,比如当
    执行“叶痕秋”时,java虚拟机会先在常量池中检索是否已经有“叶痕秋”,如果有那么就将“叶痕秋”的地址
    赋给变量,如果没有就创建一个,然后在赋给变量;
    而 String z = new String(“叶痕秋”) 则会被分到堆内存中,即使内容一样还是会创建新的对象。
  54. String 类的常用方法都有那些?
    indexOf():返回指定字符的索引。
    charAt():返回指定索引处的字符。
    replace():字符串替换。
    trim():去除字符串两端空白。
    split():分割字符串,返回一个分割后的字符串数组。
    getBytes():返回字符串的 byte 类型数组。
    length():返回字符串长度。
    toLowerCase():将字符串转成小写字母。
    toUpperCase():将字符串转成大写字符。
    substring():截取字符串。
    equals():字符串比较。
  55. String s = new String(“xyz”);创建了几个StringObject?是否可
    以继承String类?
    两个或一个都有可能,”xyz”对应一个对象,这个对象放在字符串常量缓冲区,常量”xyz”不管出现多少
    遍,都是缓冲区中的那一个。NewString每写一遍,就创建一个新的对象,它使用常量”xyz”对象的内容
    来创建出一个新String对象。如果以前就用过’xyz’,那么这里就不会创建”xyz”了,直接从缓冲区拿,这
    时创建了一个StringObject;但如果以前没有用过"xyz",那么此时就会创建一个对象并放入缓冲区,这
    种情况它创建两个对象。至于String类是否继承,答案是否定的,因为String默认final修饰,是不可继承
    的。
    String x = “叶痕秋”; String y = “叶痕秋”; String z = new String(“叶痕秋”); System.out.println(x == y); // true System.out.println(x == z); // false
  56. 下面这条语句一共创建了多少个对象:String
    s=“a”+“b”+“c”+“d”;
    对于如下代码:
    第一条语句打印的结果为false,第二条语句打印的结果为true,这说明javac编译可以对字符串常量直接
    相加的表达式进行优化,不必要等到运行期再去进行加法运算处理,而是在编译时去掉其中的加号,直
    接将其编译成一个这些常量相连的结果。
    题目中的第一行代码被编译器在编译时优化后,相当于直接定义了一个”abcd”的字符串,所以,上面的
    代码应该只创建了一个String对象。写如下两行代码,
    最终打印的结果应该为true。
  57. 简述Java中的集合
  58. Collection下:List系(有序、元素允许重复)和Set系(无序、元素不重复)
    set根据equals和hashcode判断,一个对象要存储在Set中,必须重写equals和hashCode方 法 2. Map下:HashMap线程不同步;TreeMap线程同步
  59. Collection系列和Map系列:Map是对Collection的补充,两个没什么关系
  60. List、Map、Set三个接口,存取元素时,各有什么特点?
    首先,List与Set具有相似性,它们都是单列元素的集合,所以,它们有一个共同的父接口,叫
    Collection。 1、Set里面不允许有重复的元素
    即不能有两个相等(注意,不是仅仅是相同)的对象,即假设Set集合中有了一个A对象,现在我要向Set
    集合再存入一个B对象,但B对象与A对象equals相等,则B对象存储不进去,所以,Set集合的add方法
    有一个boolean的返回值,当集合中没有某个元素,此时add方法可成功加入该元素时,则返回true,
    当集合含有与某个元素equals相等的元素时,此时add方法无法加入该元素,返回结果为false。Set取
    元素时,不能细说要取第几个,只能以Iterator接口取得所有的元素,再逐一遍历各个元素。
    2、List表示有先后顺序的集合
    String s1 = “a”; String s2 = s1 + “b”; String s3 = “a” + “b”; System.out.println(s2 == “ab”); System.out.println(s3 == “ab”); String s =“a” + “b” +“c” + “d”; System.out.println(s== “abcd”);
    注意,不是那种按年龄、按大小、按价格之类的排序。当我们多次调用add(Obje)方法时,每次加入的对
    象就像火车站买票有排队顺序一样,按先来后到的顺序排序。有时候,也可以插队,即调用
    add(intindex,Obj e)方法,就可以指定当前对象在集合中的存放位置。一个对象可以被反复存储进List
    中,每调用一次add方法,这个对象就被插入进集合中一次,其实,并不是把这个对象本身存储进了集
    合中,而是在集合中用一个索引变量指向这个对象,当这个对象被add多次时,即相当于集合中有多个
    索引指向了这个对象。List除了可以用Iterator接口取得所有的元素,再逐一遍历各个元素之外,还可以
    调用get(index i)来明确说明取第几个。
    3、Map与List和Set不同
    它是双列的集合,其中有put方法,定义如下:put(obj key,obj value),每次存储时,要存储一对
    key/value,不能存储重复的key,这个重复的规则也是按equals比较相等。取则可以根据key获得相应
    的value,即get(Object key)返回值为key所对应的value。另外,也可以获得所有的key的结合,还可以
    获得所有的value的结合,还可以获得key和value组合成的Map.Entry对象的集合。
    总结
    List以特定次序来持有元素,可有重复元素。Set无法拥有重复元素,内部排序。Map保存key-value值,
    value可多值。
  61. Set里的元素是不能重复的,那么用什么方法来区分重复与否呢?
    是用==还是equals()?它们有何区别?
    Set里的元素是不能重复的,元素重复与否是使用equals()方法进行判断的。
    和equal区别也是考烂了的题,这里再重复说一下:
    操作符专门用来比较两个变量的值是否相等,也就是用于比较变量所对应的内存中所存储的数值是否
    相同,要比较两个基本类型的数据或两个引用变量是否相等,只能用
    操作符。
    equals方法是用于比较两个独立对象的内容是否相同,就好比去比较两个人的长相是否相同,它比较的
    两个对象是独立的。
    比如:两条new语句创建了两个对象,然后用a/b这两个变量分别指向了其中一个对象,这是两个不同的
    对象,它们的首地址是不同的,即a和b中存储的数值是不相同的,所以,表达式a
    b将返回false,而
    这两个对象中的内容是相同的,所以,表达式a.equals(b)将返回true。
  62. ArrayList和LinkedList区别?
  63. ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
  64. 对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
  65. 对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
  66. ArrayList和Vector的区别
    两个类都实现了List接口(List接口继承了 Collection 接口),他们都是有序集合,即存储在这两个集
    合中的元素的位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引号取出某个元素,
    并且其中的数据是允许重复的,这是与 HashSet 之类的集合的最大不同处, HashSet 之类的集合不可以
    按索引号去检索其中的元素,也不允许有重复的元素。
    ArrayList与Vector的区别主要包括两个方面:.
    同步性:
    Vector 是线程安全的,也就是说是它的方法之间是线程同步的,而 ArrayList 是线程序不安全的,它
    的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用 ArrayList ,因为它不考
    虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用 Vector ,因为不需要我们自
    己再去考虑和编写线程安全的代码。
    数据增长:
    ArrayList 与 Vector 都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需
    要增加 ArrayList 与 Vector 的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增
    加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。
    Vector 默认增长为原来两倍,而 ArrayList 的增长策略在文档中没有明确规定(从源代码看到的是增
    长为原来的1.5倍)。 ArrayList 与 Vector 都可以设置初始的空间大小, Vector 还可以设置增长的空
    间大小,而 ArrayList 没有提供设置增长空间的方法。
    总结:即Vector增长原来的一倍, ArrayList 增加原来的0.5倍。
  67. ArrayList,Vector,LinkedList的存储性能和特性
    ArrayList 和 Vector 都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入
    元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据
    快而插入数据慢,
    Vector 由于使用了 synchronized 方法(线程安全),通常性能上较 ArrayList 差。而 LinkedList
    使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,索引就变慢了,但是插入数据时只
    需要记录本项的前后项即可,所以插入速度较快。
    LinkedList 也是线程不安全的, LinkedList 提供了一些方法,使得 LinkedList 可以被当作堆栈和
    队列来使用。
  68. HashMap和Hashtable的区别
    HashMap 是 Hashtable 的轻量级实现(非线程安全的实现),他们都完成了Map接口,
    主要区别在于 HashMap 允许空(null)键值(key),由于非线程安全,在只有一个线程访问的情况下,
    效率要高于 Hashtable 。 HashMap 允许将null作为一个entry的key或者value,而 Hashtable 不允许。
    HashMap 把 Hashtable 的 contains 方法去掉了,改成 containsvalue 和 containsKey 。因为
    contains方法容易让人引起误解。
    Hashtable 继承自 Dictionary 类,而 HashMap 是Java1.2引进的Map interface的一个实现。
    最大的不同是, Hashtable 的方法是 Synchronize 的,而 HashMap 不是,在多个线程访问 Hashtable
    时,不需要自己为它的方法实现同步,而 HashMap 就必须为之提供同步。
    就 HashMap 与 HashTable 主要从三方面来说。
    历史原因: Hashtable 是基于陈旧的 Dictionary 类的, HashMap 是Java 1.2引进的Map接口的一
    个实现
    同步性: Hashtable 是线程安全的,也就是说是同步的,而 HashMap 是线程序不安全的,不是同步

    值:只有 HashMap 可以让你将空值作为一个表的条目的key或value
  69. Java中的同步集合与并发集合有什么区别?
    同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。
    在Java1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。
    Java5介绍了并发集合 ConcurrentHashMap ,不仅提供线程安全还用锁分离和内部分区等现代技术提高
    了可扩展性。
    不管是同步集合还是并发集合他们都支持线程安全,他们之间主要的区别体现在性能和可扩展性,还有
    他们如何实现的线程安全上。
    同步 HashMap , Hashtable , HashSet , Vector , ArrayList 相比他们并发的实现
    ( ConcurrentHashMap , CopyOnWriteArrayList , CopyOnWriteHashSet )会慢得多。造成如此慢的
    主要原因是锁, 同步集合会把整个Map或List锁起来,而并发集合不会。并发集合实现线程安全是通过
    使用先进的和成熟的技术像锁剥离。
    比如 ConcurrentHashMap 会把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程
    访问其他未上锁的片段。
    同样的, CopyOnWriteArrayList 允许多个线程以非同步的方式读,当有线程写的时候它会将整个List
    复制一个副本给它。
    如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性。
  70. Java中的集合及其继承关系
    关于集合的体系是每个人都应该烂熟于心的,尤其是对我们经常使用的List,Map的原理更该如此.这里我们
    看这张图即可:
  71. poll()方法和remove()方法区别?
    poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是
    remove() 失败的时候会抛出异常。
  72. LinkedHashMap和PriorityQueue的区别
    PriorityQueue 是一个优先级队列,保证最高或者最低优先级的的元素总是在队列头部,但是
    LinkedHashMap 维持的顺序是元素插入的顺序。当遍历一个 PriorityQueue 时,没有任何顺序保证,
    但是 LinkedHashMap 课保证遍历顺序是元素插入的顺序。
  73. WeakHashMap与HashMap的区别是什么?
    WeakHashMap 的工作与正常的 HashMap 类似,但是使用弱引用作为 key,意思就是当 key 对象没有
    任何引用时,key/value 将会被回收。
  74. ArrayList和LinkedList的区别?
    最明显的区别是 ArrrayList底层的数据结构是数组,支持随机访问,而 LinkedList 的底层数据结构是双
    向循环链表,不支持随机访问。使用下标访问一个元素,ArrayList 的时间复杂度是 O(1),而 LinkedList
    是 O(n)。
  75. ArrayList和Array有什么区别?
    Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
    ArrayList 是Java集合框架类的一员,可以称它为一个动态数组. array 是静态的,所以一个数据一旦创建就
    无法更改他的大小
  76. ArrayList和HashMap默认大小? 在 java 7 中,ArrayList 的默认大小是 10 个元素,HashMap 的默认大小是16个元素(必须是2的
    幂)。这就是 Java 7 中 ArrayList 和 HashMap 类的代码片段
  77. Comparator和Comparable的区别?
    相同点
    都是用于比较两个对象“顺序”的接口
    都可以使用Collections.sort()方法来对对象集合进行排序
    不同点
    Comparable位于java.lang包下,而Comparator则位于java.util包下
    Comparable 是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序
    总结
    使用Comparable接口来实现对象之间的比较时,可以使这个类型(设为A)实现Comparable接口,并
    可以使用Collections.sort()方法来对A类型的List进行排序,之后可以通过a1.comparaTo(a2)来比较两个
    对象;
    当使用Comparator接口来实现对象之间的比较时,只需要创建一个实现Comparator接口的比较器(设
    为AComparator),并将其传给Collections.sort()方法即可对A类型的List进行排序,之后也可以通过调
    用比较器AComparator.compare(a1, a2)来比较两个对象。
    可以说一个是自己完成比较,一个是外部程序实现比较的差别而已。
    用 Comparator 是策略模式(strategy design pattern),就是不改变对象自身,而用一个策略对象
    (strategy object)来改变它的行为。
    private static final int DEFAULT_CAPACITY = 10; //from HashMap.java JDK 7 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    比如:你想对整数采用绝对值大小来排序,Integer 是不符合要求的,你不需要去修改 Integer 类(实际
    上你也不能这么做)去改变它的排序行为,这时候只要(也只有)使用一个实现了 Comparator 接口的
    对象来实现控制它的排序就行了。
    两种方式,各有各的特点:使用Comparable方式比较时,我们将比较的规则写入了比较的类型中,其
    特点是高内聚。但如果哪天这个规则需要修改,那么我们必须修改这个类型的源代码。如果使用
    Comparator方式比较,那么我们不需要修改比较的类,其特点是易维护,但需要自定义一个比较器,
    后续比较规则的修改,仅仅是改这个比较器中的代码即可。
  78. 如何实现集合排序?
    你可以使用有序集合,如 TreeSet 或 TreeMap,你也可以使用有顺序的的集合,如 list,然后通过
    Collections.sort() 来排序。
  79. 如何打印数组内容
    你可以使用 Arrays.toString() 和 Arrays.deepToString() 方法来打印数组。由于数组没有实现 toString()
    方法,所以如果将数组传递给 System.out.println() 方法,将无法打印出数组的内容,但是
    Arrays.toString() 可以打印每个元素。
  80. LinkedList的是单向链表还是双向?
    双向循环列表,具体实现自行查阅源码.
  81. TreeMap是实现原理
    TreeMap是一个通过红黑树实现有序的key-value集合。
    TreeMap继承AbstractMap,也即实现了Map,它是一个Map集合
    TreeMap实现了NavigableMap接口,它支持一系列的导航方法,
    TreeMap实现了Cloneable接口,它可以被克隆
    TreeMap本质是Red-Black Tree,它包含几个重要的成员变量:root、size、comparator。其中root是
    红黑树的根节点。它是Entry类型,Entry是红黑树的节点,它包含了红黑树的6个基本组成:key、
    value、left、right、parent和color。Entry节点根据根据Key排序,包含的内容是value。Entry中key比
    较大小是根据比较器comparator来进行判断的。size是红黑树的节点个数。
  82. 遍历ArrayList时如何正确移除一个元素
    错误写法示例一:
    public static void remove(ArrayList list) { for (int i = 0; i < list.size(); i++) { String s = list.get(i); if (s.equals(“bb”)) { list.remove(s); } } }
    错误写法示例二:
    public static void remove(ArrayList list) { for (String s : list) { if (s.equals(“bb”)) { list.remove(s); } } }
    要分析产生上述错误现象的原因唯有翻一翻jdk的ArrayList源码,先看下ArrayList中的remove方法(注
    意ArrayList中的remove有两个同名方法,只是入参不同,这里看的是入参为Object的remove方法)是
    怎么实现的:
    public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } }return false; }
    按一般执行路径会走到else路径下最终调用faseRemove方法:
    private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[–size] = null; // Let gc do its work }
    可以看到会执行System.arraycopy方法,导致删除元素时涉及到数组元素的移动。针对错误写法一,在
    遍历第二个元素字符串bb时因为符合删除条件,所以将该元素从数组中删除,并且将后一个元素移动
    (也是字符串bb)至当前位置,导致下一次循环遍历时后一个字符串bb并没有遍历到,所以无法删除。
    针对这种情况可以倒序删除的方式来避免:
    public static void remove(ArrayList list) { for (int i = list.size() - 1; i >= 0; i–) { String s = list.get(i); if (s.equals(“bb”)) { list.remove(s); } } }
    因为数组倒序遍历时即使发生元素删除也不影响后序元素遍历。
    而错误二产生的原因却是foreach写法是对实际的Iterable、hasNext、next方法的简写,问题同样处在
    上文的fastRemove方法中,可以看到第一行把modCount变量的值加一,但在ArrayList返回的迭代器
    (该代码在其父类AbstractList中):
    public Iterator iterator() { return new Itr(); }
    这里返回的是AbstractList类内部的迭代器实现private class Itr implements Iterator,看这个类的next
    方法:
    public E next() { checkForComodification(); try {E next = get(cursor); lastRet = cursor++; return next; } catch (IndexOutOfBoundsException e) { checkForComodification(); throw new NoSuchElementException(); } }
    第一行checkForComodification方法:
    final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
    这里会做迭代器内部修改次数检查,因为上面的remove(Object)方法把修改了modCount的值,所以才
    会报出并发修改异常。要避免这种情况的出现则在使用迭代器迭代时(显示或foreach的隐式)不要使用
    ArrayList的remove,改为用Iterator的remove即可。
  83. HashMap的实现原理
    HashMap是基于哈希表实现的map,哈希表(也叫关联数组)一种通用的数据结构,是Java开发者常用
    的类,常用来存储和获取数据,功能强大使用起来也很方便,是居家旅行…不对,是Java开发需要掌握
    的基本技能,也是面试必考的知识点,所以,了解HashMap是很有必要的。
    原理
    简单讲解下HashMap的原理:HashMap基于Hash算法,我们通过put(key,value)存储,get(key)来获
    取。当传入key时,HashMap会根据key.hashCode()计算出hash值,根据hash值将value保存在bucket
    里。当计算出的hash值相同时怎么办呢,我们称之为Hash冲突,HashMap的做法是用链表和红黑树存
    储相同hash值的value。当Hash冲突的个数比较少时,使用链表,否则使用红黑树。
    内部存储结构
    HashMap类实现了Map< K, V>接口,主要包含以下几个方法:
    V put(K key, V value)
    V get(Object key)
    V remove(Object key)
    Boolean containsKey(Object key)
    HashMap使用了一个内部类Node< K, V>来存储数据
    我阅读的是Java 8的源码,在Java 8之前存储数据的内部类是Entry< K, V>,代码大体都是一样的
    Node代码:
    可以看见Node类中除了键值对(key-value)以外,还有额外的两个数据:
    hash : 这个是通过计算得到的散列值
    next:指向另一个Node,这样HashMap可以像链表一样存储数据
    因此可以知道,HashMap的结构大致如下:
    我们可以将每个横向看成一个个的桶,每个桶中存放着具有相同Hash值的Node,通过一个list来存放每个
    桶。
    内部变量
    public static void remove(ArrayList list) { Iterator it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.equals(“bb”)) { it.remove(); } } }static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; … }
    // 默认容量大小 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 装载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 转换为二叉树的阀值 static final int TREEIFY_THRESHOLD = 8; // 转换为二叉树的最低阀值 static final int UNTREEIFY_THRESHOLD = 6; // 二叉树最小容量 static final int MIN_TREEIFY_CAPACITY = 64; // 哈希表 transient Node<K,V>[] table; // 键值对的数量 transient int size; // 记录HashMap结构改变次数,与HashMap的快速失败相关 transient int modCount; // 扩容的阈值 int threshold; // 装载因子 final float loadFactor;
    常用方法
    put操作
    put函数大致的思路为:
  84. 对key的hashCode()做hash,然后再计算index;
  85. 如果没碰撞直接放到bucket里;
  86. 如果碰撞了,以链表的形式存在buckets后;
  87. 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
  88. 如果节点已经存在就替换old value(保证key的唯一性) 6. 如果bucket满了(超过load factor*current capacity),就要resize。 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // resize()是调整table数组大小的,如果table数组 为空或长度为0,重新调整大小 if ((p = tab[i = (n - 1) & hash]) == null) // i = (n - 1) & hash | 这里计算出来 的i值就是存放数组的位置,如果当前位置为空,则直接放入其中 tab[i] = newNode(hash, key, value, null); else { // hash冲突 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 如果hash相 同,并且key值也相同,则找到存放位置 e = p;
    else if (p instanceof TreeNode) // 如果当前p是二叉树,则放入二叉树中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 存放到链表中 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { // 遍历链表并将值放到链表最后 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); // 如果链表中的值大于 TREEIFY_THRESHOLD - 1,则将链表转换成二叉树 break; }if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } }if (e != null) { // 表示对于当前key早已经存在 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) // 如果onlyIfAbsent为false或则 oldValue为空,替换原来的值 e.value = value; afterNodeAccess(e); return oldValue; // 返回原来的值 } }++modCount; // HashMap结构修改次数,主要用于判断迭代器中fail-fast if (++size > threshold) // 如果++size后的值比阀值大,则重新调整大小 resize(); afterNodeInsertion(evict); return null; }
    代码也比较容易看懂,值得注意的就是
    else if (p instanceof TreeNode) // 如果当前p是二叉树,则放入二叉树中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 与if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); // 如果链表中的值大于TREEIFY_THRESHOLD - 1,则将链表转换成二 叉树
    这是Java 8相对于以前版本一个比较大的改变。
    在Java 8以前,每次产生hash冲突,就将记录追加到链表后面,然后通过遍历链表来查找。如果某个链
    表中记录过大,每次遍历的数据就越多,效率也就很低,复杂度为O(n); 在Java 8中,加入了一个常量TREEIFY_THRESHOLD=8,如果某个链表中的记录大于这个常量的话,
    HashMap会动态的使用一个专门的treemap实现来替换掉它。这样复杂度是O(logn),比链表的O(n)会
    好很多。
    对于前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能通过遍历来进
    行查找。但是超过这个阈值后HashMap开始将列表升级成一个二叉树,使用哈希值作为树的分支变量,
    如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,
    HashMap希望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。
    get操作
    在理解了put之后,get就很简单了。大致思路如下:
  89. bucket里的第一个节点,直接命中;
  90. 如果有冲突,则通过key.equals(k)去查找对应的entry
  91. 若为树,则在树中通过key.equals(k)查找,O(logn); 4. 若为链表,则在链表中通过key.equals(k)查找,O(n)。
  92. HashMap自动扩容
    如果在初始化HashMap中没有指定初始容量,那么默认容量为16,但是如果后来HashMap中存放的数
    量超过了16,那么便会有大量的hash冲突;在HashMap中有自动扩容机制,如果当前存放的数量大于
    某个界限,HashMap便会调用resize()方法,扩大HashMap的容量。
    当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为
    0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就
    把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常
    消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高
    hashmap的性能。
    public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) // 如果 hash相同并且key值一样则返回当前node return first; if ((e = first.next) != null) { if (first instanceof TreeNode) // 如果当前node为二叉树,则在二叉树中查找 return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { // 遍历链表 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } }return null; }
    HashMap的capacity必须满足是2的N次方,如果在构造函数内指定的容量n不满足,HashMap会通过下面
    的算法将其转换为大于n的最小的2的N次方数。
  93. HashMap线程安全吗?
    HashMap是非线程安全的,如果在多线程环境下,可以使用HashTable,HashTable中所有CRUD操作
    都是线程同步的,同样的,线程同步的代价就是效率变低了。
    再Java 5以后,有了一个线程安全的HashMap——ConcurrentHashMap,ConcurrentHashMap相对
    于HashTable来说,ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常
    用操作只锁当前需要用到的桶。试想,原来只能一个线程进入,现在却能同时16个写线程进入(写线程
    才需要锁定,而读线程几乎不受限制,并发性的提升是显而易见。
    快速失败(fast-fail)
    “快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的
    操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、
    线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面
    的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出
    ConcurrentModificationException 异常,从而产生fail-fast机制。
    在HashMap的forEach方法中有以下代码:
    // 减1→移位→按位或运算→加1返回 static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }@Override public void forEach(BiConsumer<? super K, ? super V> action) { Node<K,V>[] tab; if (action == null) throw new NullPointerException(); if (size > 0 && (tab = table) != null) { int mc = modCount; for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) action.accept(e.key, e.value);
    在上面我们说到,modCount是记录每次HashMap结构修改。 forEach方法会在在进入for循环之前,将
    modCount赋值给mc,如果在for循环之后,HashMap的结构变化了,那么导致的结果就是modCount
    != mc,则抛出ConcurrentModificationException()异常。
  94. HashMap总结
    1、什么时候会使用HashMap?他有什么特点? 是基于Map接口的实现,存储键值对时,它可以接收
    null的键值,是非同步的,HashMap存储着Entry(hash, key, value, next)对象。
    2、你知道HashMap的工作原理吗? 通过hash的方法,通过put和get存储和获取对象。存储对象时,我
    们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会
    根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将
    K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。
    如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个
    bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。
    3、你知道get和put的原理吗?equals()和hashCode()的都有什么作用? 通过对key的hashCode()进行
    hashing,并计算下标( n-1 & hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方
    法去链表或树中去查找对应的节点
    4、你知道hash的实现吗?为什么要这样实现? 在Java 1.8的实现中,是通过hashCode()的高16位异或
    低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在
    bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。
    5、如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办? 如果超过了负载因子(默认
    0.75),则会重新resize一个原来长度两倍的HashMap,并且重新调用hash方法。
    前段时间因为找工作的缘故背了一些关于HashMap的面试题,死记硬背,也不是很懂,最近看了
    源码,很多知识才变的清晰,而且看源码挺有趣的。再接再厉。
  95. Java集合框架是什么?说出一些集合框架的优点?
    每种编程语言中都有集合。集合框架的部分优点如下:
    1、使用核心集合类降低开发成本,而非实现我们自己的集合类。
    2、随着使用经过严格测试的集合框架类,代码质量会得到提高。
    3、通过使用JDK附带的集合类,可以降低代码维护成本。
    4、复用性和可操作性。
    }if (modCount != mc) throw new ConcurrentModificationException(); } }
  96. 集合框架中的泛型有什么优点?
    Java1.5引入了泛型,所有的集合接口和实现都大量地使用它。
    泛型允许我们为集合提供一个可以容纳的对象类型,因此,如果你添加其它类型的任何元素,它会在编
    译时报错。这避免了在运行时出现ClassCastException,因为你将会在编译时得到报错信息。泛型也使
    得代码整洁,我们不需要使用显式转换和instanceOf操作符。它也给运行时带来好处,因为不会产生类
    型检查的字节码指令。
  97. Java集合框架的基础接口有哪些?
    Collection为集合层级的根接口。一个集合代表一组对象,这些对象即为它的元素。Java平台不提供这个
    接口任何直接的实现。
    Set是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合,就如一副
    牌。
    List是一个有序集合,可以包含重复元素。你可以通过它的索引来访问任何元素。List更像长度动态变换
    的数组。
    Map是一个将key映射到value的对象.一个Map不能包含重复的key:每个key最多只能映射一个value。
    一些其它的接口有Queue、Dequeue、SortedSet、SortedMap和ListIterator。
  98. 为何Collection不从Cloneable和Serializable接口继承?
    克隆(cloning)或者是序列化(serialization)的语义和含义是跟具体的实现相关的。因此,应该由集合类的
    具体实现来决定如何被克隆或者是序列化。
  99. 为何Map接口不继承Collection接口?
    尽管Map接口和它的实现也是集合框架的一部分,但Map不是集合,集合也不是Map。因此,Map继承
    Collection毫无意义,反之亦然。
    如果Map继承Collection接口,那么元素去哪儿?Map包含key-value对,它提供抽取key或value列表集
    合的方法,但是它不适合“一组对象”规范。
  100. Iterator是什么?
    Iterator接口提供遍历任何Collection的接口。我们可以从一个Collection中使用迭代器方法来获取迭代
    器实例。迭代器取代了Java集合框架中的Enumeration。迭代器允许调用者在迭代过程中移除元素。
  101. Iterator和ListIterator的区别是什么?
    下面列出了他们的区别: Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。
    Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。 ListIterator实现了Iterator接口,
    并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。
  102. Enumeration和Iterator接口的区别?
    Enumeration速度是Iterator的2倍,同时占用更少的内存。但是,Iterator远远比Enumeration安全,
    因为其他线程不能够修改正在被iterator遍历的集合里面的对象。同时,Iterator允许调用者删除底层集
    合里面的元素,这对Enumeration来说是不可能的。
  103. 为何没有像Iterator.add()这样的方法,向集合中添加元素?
    语义不明,已知的是,Iterator的协议不能确保迭代的次序。然而要注意,ListIterator没有提供一个add
    操作,它要确保迭代的顺序。
  104. 为何迭代器没有一个方法可以直接获取下一个元素,而不需要移
    动游标?
    它可以在当前Iterator的顶层实现,但是它用得很少,如果将它加到接口中,每个继承都要去实现它,这
    没有意义。
  105. Iterater和ListIterator之间有什么区别?
    1、我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List。 2、Iterator只可以向前遍历,而LIstIterator可以双向遍历。
    3、ListIterator从Iterator接口继承,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、
    获取前面或后面元素的索引位置。
  106. 遍历一个List有哪些不同的方式?
    使用迭代器更加线程安全,因为它可以确保,在当前遍历的集合元素被更改的时候,它会抛出
    ConcurrentModificationException。 List strList = new ArrayList<>(); //使用for-each循环 for(String obj : strList){ System.out.println(obj); }//using iterator Iterator it = strList.iterator(); while(it.hasNext()){ String obj = it.next(); System.out.println(obj); }
  107. 通过迭代器fail-fast属性,你明白了什么?
    每次我们尝试获取下一个元素的时候,Iterator fail-fast属性检查当前集合结构里的任何改动。如果发现
    任何改动,它抛出ConcurrentModificationException。Collection中所有Iterator的实现都是按fail-fast
    来设计的(ConcurrentHashMap和CopyOnWriteArrayList这类并发集合类除外)。
  108. fail-fast与fail-safe有什么区别?
    Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所
    有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭
    代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。
  109. 在迭代一个集合的时候,如何避免
    ConcurrentModificationException?
    在遍历一个集合的时候,我们可以使用并发集合类来避免ConcurrentModificationException,比如使
    用CopyOnWriteArrayList,而不是ArrayList。
  110. 为何Iterator接口没有具体的实现?
    Iterator接口定义了遍历集合的方法,但它的实现则是集合实现类的责任。每个能够返回用于遍历的
    Iterator的集合类都有它自己的Iterator实现内部类。
    这就允许集合类去选择迭代器是fail-fast还是fail-safe的。比如,ArrayList迭代器是fail-fast的,而
    CopyOnWriteArrayList迭代器是fail-safe的。
  111. UnsupportedOperationException是什么?
    UnsupportedOperationException是用于表明操作不支持的异常。在JDK类中已被大量运用,在集合框
    架java.util.Collections.UnmodifiableCollection将会在所有add和remove操作中抛出这个异常。
  112. 在Java中,HashMap是如何工作的?
    HashMap在Map.Entry静态内部类实现中存储key-value对。HashMap使用哈希算法,在put和get方法
    中,它使用hashCode()和equals()方法。当我们通过传递key-value对调用put方法的时候,HashMap使 用Key hashCode()和哈希算法来找出存储key-value对的索引。Entry存储在LinkedList中,所以如果存
    在entry,它使用equals()方法来检查传递的key是否已经存在,如果存在,它会覆盖value,如果不存
    在,它会创建一个新的entry然后保存。当我们通过传递key调用get方法时,它再次使用hashCode()来
    找到数组中的索引,然后使用equals()方法找出正确的Entry,然后返回它的值。下面的图片解释了详细
    内容。
    其它关于HashMap比较重要的问题是容量、负荷系数和阀值调整。HashMap默认的初始容量是32,负
    荷系数是0.75。阀值是为负荷系数乘以容量,无论何时我们尝试添加一个entry,如果map的大小比阀值
    大的时候,HashMap会对map的内容进行重新哈希,且使用更大的容量。容量总是2的幂,所以如果你
    知道你需要存储大量的key-value对,比如缓存从数据库里面拉取的数据,使用正确的容量和负荷系数对
    HashMap进行初始化是个不错的做法。
  113. hashCode()和equals()方法有何重要性?
    HashMap使用Key对象的hashCode()和equals()方法去决定key-value对的索引。当我们试着从
    HashMap中获取值的时候,这些方法也会被用到。如果这些方法没有被正确地实现,在这种情况下,两
    个不同Key也许会产生相同的hashCode()和equals()输出,HashMap将会认为它们是相同的,然后覆盖
    它们,而非把它们存储到不同的地方。同样的,所有不允许存储重复数据的集合类都使用hashCode()和
    equals()去查找重复,所以正确实现它们非常重要。equals()和hashCode()的实现应该遵循以下规则:
    (1)如果o1.equals(o2),那么o1.hashCode() == o2.hashCode()总是为true的。
    (2)如果o1.hashCode() == o2.hashCode(),并不意味着o1.equals(o2)会为true。
  114. 我们能否使用任何类作为Map的key?
    我们可以使用任何类作为Map的key,然而在使用它们之前,需要考虑以下几点:
    (1)如果类重写了equals()方法,它也应该重写hashCode()方法。
    (2)类的所有实例需要遵循与equals()和hashCode()相关的规则。请参考之前提到的这些规则。
    (3)如果一个类没有使用equals(),你不应该在hashCode()中使用它。
    (4)用户自定义key类的最佳实践是使之为不可变的,这样,hashCode()值可以被缓存起来,拥有更好
    的性能。不可变的类也可以确保hashCode()和equals()在未来不会改变,这样就会解决与可变相关的问
    题了。
    比如,我有一个类MyKey,在HashMap中使用它。
    //传递给MyKey的name参数被用于equals()和hashCode()中 MyKey key = new MyKey(‘Pankaj’);
    //assume hashCode=1234 myHashMap.put(key, ‘Value’); // 以下的代码会改变key的hashCode()和
    equals()值 key.setName(‘Amit’); //assume new hashCode=7890 //下面会返回null,因为HashMap会
    尝试查找存储同样索引的key,而key已被改变了,匹配失败,返回null myHashMap.get(new
    MyKey(‘Pankaj’)); 那就是为何String和Integer被作为HashMap的key大量使用。
  115. Map接口提供了哪些不同的集合视图?
    Map接口提供三个集合视图:
    1、Set keyset():返回map中包含的所有key的一个Set视图。集合是受map支持的,map的变化会在
    集合中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,若map被修改了(除迭代器自身的移
    除操作以外),迭代器的结果会变为未定义。集合支持通过Iterator的Remove、Set.remove、
    removeAll、retainAll和clear操作进行元素移除,从map中移除对应的映射。它不支持add和addAll操
    作。
    2、Collection values():返回一个map中包含的所有value的一个Collection视图。这个collection受
    map支持的,map的变化会在collection中反映出来,反之亦然。当一个迭代器正在遍历一个collection
    时,若map被修改了(除迭代器自身的移除操作以外),迭代器的结果会变为未定义。集合支持通过
    Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进行元素移除,从map中移除对应
    的映射。它不支持add和addAll操作。
    3、Set<Map.Entry<K,V>> entrySet():返回一个map钟包含的所有映射的一个集合视图。这个集合受
    map支持的,map的变化会在collection中反映出来,反之亦然。当一个迭代器正在遍历一个集合时,
    若map被修改了(除迭代器自身的移除操作,以及对迭代器返回的entry进行setValue外),迭代器的结
    果会变为未定义。集合支持通过Iterator的Remove、Set.remove、removeAll、retainAll和clear操作进
    行元素移除,从map中移除对应的映射。它不支持add和addAll操作。
  116. HashMap和HashTable有何不同?
    (1)HashMap允许key和value为null,而HashTable不允许。
    (2)HashTable是同步的,而HashMap不是。所以HashMap适合单线程环境,HashTable适合多线程
    环境。
    (3)在Java1.4中引入了LinkedHashMap,HashMap的一个子类,假如你想要遍历顺序,你很容易从
    HashMap转向LinkedHashMap,但是HashTable不是这样的,它的顺序是不可预知的。
    (4)HashMap提供对key的Set进行遍历,因此它是fail-fast的,但HashTable提供对key的
    Enumeration进行遍历,它不支持fail-fast。 (5)HashTable被认为是个遗留的类,如果你寻求在迭代的时候修改Map,你应该使用
    CocurrentHashMap。
  117. 如何决定选用HashMap还是TreeMap?
    对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序
    的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元
    素会更快,将map换为TreeMap进行有序key的遍历。
  118. ArrayList和Vector有何异同点?
    ArrayList和Vector在很多时候都很类似。
    1、两者都是基于索引的,内部由一个数组支持。
    2、两者维护插入的顺序,我们可以根据插入顺序来获取元素。
    3、ArrayList和Vector的迭代器实现都是fail-fast的。
    4、ArrayList和Vector两者允许null值,也可以使用索引值对元素进行随机访问。
    以下是ArrayList和Vector的不同点。
    1、Vector是同步的,而ArrayList不是。然而,如果你寻求在迭代的时候对列表进行改变,你应该使用
    CopyOnWriteArrayList。 2、ArrayList比Vector快,它因为有同步,不会过载。
    3、ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表。
  119. Array和ArrayList有何区别?什么时候更适合用Array?
    Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
    Array是指定大小的,而ArrayList大小是固定的。
    Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等。尽管ArrayList明显是更好
    的选择,但也有些时候Array比较好用。
    1、如果列表的大小已经指定,大部分情况下是存储和遍历它们。
    2、对于遍历基本数据类型,尽管Collections使用自动装箱来减轻编码任务,在指定大小的基本类型的
    列表上工作也会变得很慢。
    3、如果你要使用多维数组,使用[][]比List<List<>>更容易。
  120. ArrayList和LinkedList有何区别?
    ArrayList和LinkedList两者都实现了List接口,但是它们之间有些不同。
    1、ArrayList是由Array所支持的基于一个索引的数据结构,所以它提供对元素的随机访问,复杂度为
    O(1),但LinkedList存储一系列的节点数据,每个节点都与前一个和下一个节点相连接。所以,尽管有
    使用索引获取元素的方法,内部实现是从起始点开始遍历,遍历到索引的节点然后返回元素,时间复杂
    度为O(n),比ArrayList要慢。
    2、与ArrayList相比,在LinkedList中插入、添加和删除一个元素会更快,因为在一个元素被插入到中间
    的时候,不会涉及改变数组的大小,或更新索引。
    3、LinkedList比ArrayList消耗更多的内存,因为LinkedList中的每个节点存储了前后节点的引用。
  121. 哪些集合类提供对元素的随机访问?
    ArrayList、HashMap、TreeMap和HashTable类提供对元素的随机访问。
  122. EnumSet是什么?
    java.util.EnumSet是使用枚举类型的集合实现。当集合创建时,枚举集合中的所有元素必须来自单个指
    定的枚举类型,可以是显示的或隐示的。EnumSet是不同步的,不允许值为null的元素。它也提供了一
    些有用的方法,比如copyOf(Collection c)、of(E first,E…rest)和complementOf(EnumSet s)。
  123. 哪些集合类是线程安全的?
    Vector、HashTable、Properties和Stack是同步类,所以它们是线程安全的,可以在多线程环境下使
    用。Java1.5并发API包括一些集合类,允许迭代时修改,因为它们都工作在集合的克隆上,所以它们在
    多线程环境中是安全的。
  124. 并发集合类是什么?
    Java1.5并发包(java.util.concurrent)包含线程安全集合类,允许在迭代时修改集合。迭代器被设计为
    fail-fast的,会抛出ConcurrentModificationException。一部分类为:CopyOnWriteArrayList、
    ConcurrentHashMap、CopyOnWriteArraySet。
  125. BlockingQueue是什么?
    Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变
    为非空;当在添加一个元素时,它会等待队列中的可用空间。
    BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待
    生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。
    Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、
    PriorityBlockingQueue,、SynchronousQueue等。
  126. 队列和栈是什么,列出它们的区别?
    栈和队列两者都被用来预存储数据。 java.util.Queue是一个接口,它的实现类在Java并发包中。队列允
    许先进先出(FIFO)检索元素,但并非总是这样。Deque接口允许从两端检索元素。
    栈与队列很相似,但它允许对元素进行后进先出(LIFO)进行检索。
    Stack是一个扩展自Vector的类,而Queue是一个接口。
  127. Collections类是什么?
    Java.util.Collections是一个工具类仅包含静态方法,它们操作或返回集合。它包含操作集合的多态算
    法,返回一个由指定集合支持的新集合和其它一些内容。这个类包含集合框架算法的方法,比如折半搜
    索、排序、混编和逆序等。
  128. Comparable和Comparator接口是什么?
    如果我们想使用Array或Collection的排序方法时,需要在自定义类里实现Java提供Comparable接口。
    Comparable接口有compareTo(T OBJ)方法,它被排序方法所使用。我们应该重写这个方法,如果“this”
    对象比传递的对象参数更小、相等或更大时,它返回一个负整数、0或正整数。
    但是,在大多数实际情况下,我们想根据不同参数进行排序。
    比如,作为一个CEO,我想对雇员基于薪资进行排序,一个HR想基于年龄对他们进行排序。这就是我们
    需要使用Comparator接口的情景,因为Comparable.compareTo(Object o)方法实现只能基于一个字段
    进行排序,我们不能根据对象排序的需要选择字段。
    Comparator接口的compare(Object o1, Object o2)方法的实现需要传递两个对象参数,若第一个参数
    比第二个小,返回负整数;若第一个等于第二个,返回0;若第一个比第二个大,返回正整数。
  129. Comparable和Comparator接口有何区别?
    Comparable和Comparator接口被用来对对象集合或者数组进行排序。
    Comparable接口被用来提供对象的自然排序,我们可以使用它来提供基于单个逻辑的排序。
    Comparator接口被用来提供不同的排序算法,我们可以选择需要使用的Comparator来对给定的对象集
    合进行排序。
  130. 我们如何对一组对象进行排序?
    如果我们需要对一个对象数组进行排序,我们可以使用Arrays.sort()方法。如果我们需要排序一个对象
    列表,我们可以使用Collection.sort()方法。两个类都有用于自然排序(使用Comparable)或基于标准
    的排序(使用Comparator)的重载方法sort()。Collections内部使用数组排序方法,所有它们两者都有
    相同的性能,只是Collections需要花时间将列表转换为数组。
  131. 当一个集合被作为参数传递给一个函数时,如何才可以确保函数
    不能修改它?
    在作为参数传递之前,我们可以使用Collections.unmodifiableCollection(Collection c)方法创建一个只
    读集合,这将确保改变集合的任何操作都会抛出UnsupportedOperationException。
  132. 我们如何从给定集合那里创建一个synchronized的集合?
    我们可以使用Collections.synchronizedCollection(Collection c)根据指定集合来获取一个
    synchronized(线程安全的)集合。
  133. 集合框架里实现的通用算法有哪些?
    Java集合框架提供常用的算法实现,比如排序和搜索。Collections类包含这些方法实现。大部分算法是
    操作List的,但一部分对所有类型的集合都是可用的。部分算法有排序、搜索、混编、最大最小值。
  134. 大写的O是什么?举几个例子?
    大写的O描述的是,就数据结构中的一系列元素而言,一个算法的性能。Collection类就是实际的数据结
    构,我们通常基于时间、内存和性能,使用大写的O来选择集合实现。
    比如: 例子1:ArrayList的get(index i)是一个常量时间操作,它不依赖list中元素的数量。所以它的性能
    是O(1)。
    例子2:一个对于数组或列表的线性搜索的性能是O(n),因为我们需要遍历所有的元素来查找需要的元
    素。
  135. 与Java集合框架相关的有哪些最好的实践?
    1、根据需要选择正确的集合类型。比如,如果指定了大小,我们会选用Array而非ArrayList。如果我们
    想根据插入顺序遍历一个Map,我们需要使用TreeMap。如果我们不想重复,我们应该使用Set。 2、一些集合类允许指定初始容量,所以如果我们能够估计到存储元素的数量,我们可以使用它,就避免
    了重新哈希或大小调整。
    3、基于接口编程,而非基于实现编程,它允许我们后来轻易地改变实现。
    4、总是使用类型安全的泛型,避免在运行时出现ClassCastException。 5、使用JDK提供的不可变类作为Map的key,可以避免自己实现hashCode()和equals()。 6、尽可能使用Collections工具类,或者获取只读、同步或空的集合,而非编写自己的实现。它将会提
    供代码重用性,它有着更好的稳定性和可维护性。
  136. TreeMap和TreeSet在排序时如何比较元素?Collections工具类
    中的sort()方法如何比较元素?
    TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方
    法,当插入元素时会回调该方法比较元素的大小。TreeMap要求存放的键值对映射的键必须实现
    Comparable接口从而根据键对元素进行排序。Collections工具类的sort方法有两种重载的形式,第一
    种要求传入的待排序容器中存放的对象比较实现Comparable接口以实现元素的比较;第二种不强制性
    的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要
    重写compare方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元
    素大小的算法,也是对回调模式的应用(Java中对函数式编程的支持)。
    Java并发编程
  137. 多线程有什么用?
    一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这个回
    答更扯淡。所谓"知其然知其所以然",“会用"只是"知其然”,“为什么用"才是"知其所以然”,只有达到"知
    其然知其所以然"的程度才可以说是把一个知识点运用自如。OK,下面说说我对这个问题的看法:
    1、发挥多核CPU的优势
    随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核
    的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单
    核CPU上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得
    比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻
    辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的
    2、防止阻塞
    从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多
    线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU我们还是要应用多线程,就是为了
    防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,
    对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可
    以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的
    执行。
    3、便于建模
    这是另外一个没有这么明显的优点了。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立
    整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别
    建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
  138. 多线程和单线程的区别和联系?
    1、在单核 CPU 中,将 CPU 分为很小的时间片,在每一时刻只能有一个线程在执行,是一种微观上轮流
    占用 CPU 的机制。
    2、多线程会存在线程上下文切换,会导致程序执行速度变慢,即采用一个拥有两个线程的进程执行所需
    要的时间比一个线程的进程执行两次所需要的时间要多一些。
    结论:即采用多线程不会提高程序的执行速度,反而会降低速度,但是对于用户来说,可以减少用户的
    响应时间。
  139. 简述线程、程序、进程的基本概念。以及他们之间关系是什么?
    线程
    与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与
    进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在
    各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
    程序
    是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
    进程
    是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个
    进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令
    接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,文件,输入输出
    设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进程划分成的更
    小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程
    中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可
    以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
  140. 线程的创建方式
    方法一:继承Thread类,作为线程对象存在(继承Thread对象)
    常规方法,不多做介绍了,interrupted方法,是来判断该线程是否被中断。(终止线程不允许用stop方
    法,该方法不会施放占用的资源。所以我们在设计程序的时候,要按照中断线程的思维去设计,就像上
    面的代码一样)。
    让线程等待的方法
    Thread.sleep(200); //线程休息2ms
    Object.wait(); //让线程进入等待,直到调用Object的notify或者notifyAll时,线程停止休眠
    方法二:实现runnable接口,作为线程任务存在
    public class CreatThreadDemo1 extends Thread{ /*** 构造方法: 继承父类方法的Thread(String name);方法 * @param name / public CreatThreadDemo1(String name){ super(name); }@Override public void run() { while (!interrupted()){ System.out.println(getName()+“线程执行了…”); try {Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } }public static void main(String[] args) { CreatThreadDemo1 d1 = new CreatThreadDemo1(“first”); CreatThreadDemo1 d2 = new CreatThreadDemo1(“second”); d1.start(); d2.start(); d1.interrupt(); //中断第一个线程 } }public class CreatThreadDemo2 implements Runnable { @Override public void run() { while (true){ System.out.println(“线程执行了…”); } }
    public static void main(String[] args) { //将线程任务传给线程对象 Thread thread = new Thread(new CreatThreadDemo2()); //启动线程 thread.start(); } }
    Runnable 只是来修饰线程所执行的任务,它不是一个线程对象。想要启动Runnable对象,必须将它放
    到一个线程对象里。
    方法三:匿名内部类创建线程对象
    public class CreatThreadDemo3 extends Thread{ public static void main(String[] args) { //创建无参线程对象 new Thread(){ @Override public void run() { System.out.println(“线程执行了…”); } }.start(); //创建带线程任务的线程对象 new Thread(new Runnable() { @Override public void run() { System.out.println(“线程执行了…”); } }).start(); //创建带线程任务并且重写run方法的线程对象 new Thread(new Runnable() { @Override public void run() { System.out.println(“runnable run 线程执行了…”); } }){ @Override public void run() { System.out.println(“override run 线程执行了…”); } }.start(); } }
    创建带线程任务并且重写run方法的线程对象中,为什么只运行了Thread的run方法。我们看看Thread
    类的源码,
    我们可以看到Thread实现了Runnable接口,而Runnable接口里有一个run方法。
    所以,我们最终调用的重写的方法应该是Thread类的run方法。而不是Runnable接口的run方法。
    方法四:创建带返回值的线程
    public class CreatThreadDemo4 implements Callable { public static void main(String[] args) throws ExecutionException, InterruptedException { CreatThreadDemo4 demo4 = new CreatThreadDemo4(); FutureTask task = new FutureTask(demo4); //FutureTask最 终实现的是runnable接口 Thread thread = new Thread(task); thread.start(); System.out.println(“我可以在这里做点别的业务逻辑…因为FutureTask是提前完成任 务”); //拿出线程执行的返回值 Integer result = task.get(); System.out.println(“线程中运算的结果为:”+result); }//重写Callable接口的call方法 @Override public Object call() throws Exception { int result = 1; System.out.println(“业务逻辑计算中…”); Thread.sleep(3000); return result; } }
    Callable接口介绍:
    public interface Callable { /
    ** Computes a result, or throws an exception if unable to do so. ** @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception; }
    返回指定泛型的call方法。然后调用FutureTask对象的get方法得道call方法的返回值。
    方法五:定时器Timer
    方法六:线程池创建线程
    方法七:利用java8新特性 stream 实现并发
  141. 线程有哪些基本状态?
    Java 线程在运行的生命周期中的指定时刻只可能处于下面6种不同状态的其中一个状态(图源《Java 并
    发编程艺术》
    public class CreatThreadDemo5 { public static void main(String[] args) { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println(“定时器线程执行了…”); } },0,1000); //延迟0,周期1s } }public class CreatThreadDemo6 { public static void main(String[] args) { //创建一个具有10个线程的线程池 ExecutorService threadPool = Executors.newFixedThreadPool(10); long threadpoolUseTime = System.currentTimeMillis(); for (int i = 0;i<10;i++){ threadPool.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+“线程执行 了…”); } }); }long threadpoolUseTime1 = System.currentTimeMillis(); System.out.println(“多线程用时”+(threadpoolUseTime1-threadpoolUseTime)); //销毁线程池 threadPool.shutdown(); threadpoolUseTime = System.currentTimeMillis(); } }
    线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态
    变迁如下图所示(图源《Java 并发编程艺术》4.1.4节):
    操作系统隐藏 Java虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态
    (图源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系统一般将这两个
    状态统称为 RUNNABLE(运行中) 状态 。
    操作系统隐藏 Java虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE
    状态(图源:HowToDoInJava:),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行
    中) 状态 。
    当线程执行 wait() 方法之后,线程进入 WAITING(等待)状态。进入等待状态的线程需要依靠其他
    线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加
    了超时限制,比如通过 sleep(long millis) 方法或 wait(long millis) 方法可以将 Java 线程置
    于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步
    方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable
    的 run() 方法之后将会进入到 TERMINATED(终止) 状态。
  142. 如何停止一个正在运行的线程
    1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
    2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的
    方法。
    3、使用interrupt方法中断线程。
  143. start()方法和run()方法的区别
    只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。
    如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行
    完毕之后,另外一个线程才可以执行其run()方法里面的代码。
    class MyThread extends Thread { volatile boolean stop = false; public void run() { while (!stop) { System.out.println(getName() + " is running"); try {sleep(1000); } catch (InterruptedException e) { System.out.println(“week up from blcok…”); stop = true; // 在异常处理代码中修改共享变量的状态 } }System.out.println(getName() + " is exiting…"); } }class InterruptThreadDemo3 { public static void main(String[] args) throws InterruptedException { MyThread m1 = new MyThread(); System.out.println(“Starting thread…”); m1.start(); Thread.sleep(3000); System.out.println("Interrupt thread…: " + m1.getName()); m1.stop = true; // 设置共享变量为true m1.interrupt(); // 阻塞时退出阻塞状态 Thread.sleep(3000); // 主线程休眠3秒以便观察线程m1的中断情况 System.out.println(“Stopping application…”); } }
  144. 为什么我们调用start()方法时会执行run()方法,为什么我们不能
    直接调用run()方法?
    看看Thread的start方法说明哈~
    JVM执行start方法,会另起一条线程执行thread的run方法,这才起到多线程的效果~ 「为什么我们不能
    直接调用run()方法?」如果直接调用Thread的run()方法,其方法还是运行在主线程中,没有起到多线
    程效果。
  145. Runnable接口和Callable接口的区别
    有点深的问题了,也看出一个Java程序员学习知识的广度。
    1、Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而
    已;
    2、Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取
    异步执行的结果。
    这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满
    着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经
    赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而
    Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据
    的情况下取消该线程的任务,真的是非常有用。
    /** * Causes this thread to begin execution; the Java Virtual Machine * calls the run method of this thread. *

    * The result is that two threads are running concurrently: the * current thread (which returns from the call to the * start method) and the other thread (which executes its * run method). *

    * It is never legal to start a thread more than once. * In particular, a thread may not be restarted once it has completed * execution. ** @exception IllegalThreadStateException if the thread was already * started. * @see #run() * @see #stop() */ public synchronized void start() { … }

  146. 什么是线程安全?
    线程安全就是说多线程访问同一代码,不会产生不确定的结果。
    在多线程环境中,当各线程不共享数据的时候,即都是私有(private)成员,那么一定是线程安全的。
    但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。
    线程安全一般都涉及到synchronized, 就是一段代码同时只能有一个线程来操作 不然中间过程可能会
    产生不可预制的结果。
    如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运
    行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
  147. 线程的状态转换?
    1、新建状态(New):新创建了一个线程对象。
    2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位
    于可运行线程池中,变得可运行,等待获取CPU的使用权。
    3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
    4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进
    入就绪状态,才有机会转到运行状态。
    阻塞的情况分三种:
    (一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
    (二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线
    程放入锁池中。
    (三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻
    塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状
    态。(注意,sleep是不会释放持有的锁)
    5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
  148. 在多线程中,什么是上下文切换(context-switching)?
    单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU
    分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程
    时同时执行的,时间片一般是几十毫秒(ms)。
    操作系统中,CPU时间分片切换到另一个就绪的线程,则需要保存当前线程的运行的位置,同时需要加
    载需要恢复线程的环境信息。
  149. Java中堆和栈有什么不同?
    栈:在函数中定义的基本类型的变量和对象的引用变量都是在函数的栈内存中分配。
    堆:堆内存用于存放由new创建的对象和数组。
    从通俗化的角度来说,堆是用来存放对象的,栈是用来存放执行程序的
  150. 如何确保线程安全?
    对非安全的代码进行加锁控制
    使用线程安全的类
    多线程并发情况下,线程共享的变量改为方法级的局部变量
  151. 什么是竞态条件?你怎样发现和解决竞争?
    当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
    在临界区中使用适当的同步就可以避免竞态条件。
    界区实现方法有两种,一种是用synchronized,一种是用Lock显式锁实现。
  152. 用户线程和守护线程有什么区别?
    守护线程都是为JVM中所有非守护线程的运行提供便利服务: 只要当前JVM实例中尚存在任何一个非守
    护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结
    束工作。
    User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部
    退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。
    因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。
  153. 如何创建守护线程?以及在什么场合来使用它?
    任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on);true则把该线程
    设置为守护线程,反之则为用户线程。Thread.setDaemon()必须在Thread.start()之前调用,否则运行
    时会抛出异常。
    守护线程相当于后台管理者 比如 : 进行内存回收,垃圾清理等工作
  154. 线程安全的级别
    不可变
    不可变的对象一定是线程安全的,并且永远也不需要额外的同步。
    Java类库中大多数基本数值类如Integer、String和BigInteger都是不可变的。
    无条件的线程安全
    由类的规格说明所规定的约束在对象被多个线程访问时仍然有效,不管运行时环境如何排列,线程都不
    需要任何额外的同步。
    如 Random 、ConcurrentHashMap、Concurrent集合、atomic
    有条件的线程安全
    有条件的线程安全类对于单独的操作可以是线程安全的,但是某些操作序列可能需要外部同步。
    有条件线程安全的最常见的例子是遍历由 Hashtable 或者 Vector 或者返回的迭代器
    非线程安全(线程兼容)
    线程兼容类不是线程安全的,但是可以通过正确使用同步而在并发环境中安全地使用。
    如ArrayList HashMap
    线程对立
    线程对立是那些不管是否采用了同步措施,都不能在多线程环境中并发使用的代码。
    如如System.setOut()、System.runFinalizersOnExit()
  155. 你对线程优先级的理解是什么?
    每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度
    的实现,这个实现是和操作系统相关的(OSdependent)。
    可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是
    一个int变量(从1-10),1代表最低优先级,10代表最高优先级。
  156. 什么是线程调度器(Thread Scheduler)和时间分片(Time
    Slicing)?
    线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦创建一个线程并启
    动它,它的执行便依赖于线程调度器的实现。
    时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级
    或者线程等待的时间。
    线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择。
  157. volatile关键字的作用
    一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
    保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他
    线程来说是立即可见的。
    禁止进行指令重排序。
    volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
    synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
    volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
    volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可
    见性和原子性。
    volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
    volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
    从实践角度而言,volatile的一个重要作用就是和CAS结合,保证了原子性,详细的可以参见
    java.util.concurrent.atomic 包下的类,比如 AtomicInteger 。
  158. volatile 变量和 atomic 变量有什么不同?
    首先,volatile 变量和 atomic 变量看起来很像,但功能却不一样。
    Volatile变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用
    volatile修饰count变量那么 count++ 操作就不是原子性的。
    而AtomicInteger类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的
    进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
  159. volatile 是什么?可以保证有序性吗?
    一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
    1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线
    程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
    2)禁止进行指令重排序。
    volatile 不是原子性操作
    什么叫保证部分有序性?当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已
    经对后面的操作可见;在其后面的操作肯定还没有进行;
    由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前
    面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是
    不作任何保证的。
    使用 Volatile 一般用于 状态标记量 和 单例模式的双检锁
  160. 什么是Java内存模型
    Java内存模型定义了一种多线程访问Java内存的规范。Java内存模型要完整讲不是这里几句话能说清楚
    的,我简单总结一下Java内存模型的几部分内容:
    1、Java内存模型将内存分为了主内存和工作内存。类的状态,也就是类之间共享的变量,是存储在主内
    存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自
    己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的
    那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去
    2、定义了几个原子操作,用于操作主内存和工作内存中的变量
    3、定义了volatile变量的使用规则
    4、happens-before,即先行发生原则,定义了操作A必然先行发生于操作B的一些规则,比如在同一个
    线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于
    后面对于同一个锁进行锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段
    代码不符合所有的happens-before规则,则这段代码一定是线程非安全的
  161. sleep方法和wait方法有什么区别
    对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中
    的。
    sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当
    指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。
    当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用
    notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。
    x = 2; //语句1 y = 0; //语句2 flag = true; //语句3 x = 4; //语句4 y = -1; //语句5
  162. 线程的sleep()方法和yield()方法有什么区别?
    ① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;
    yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
    ② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
    ③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
    ④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。
  163. Thread.sleep(0)的作用是什么
    由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让
    某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配
    时间片的操作,这也是平衡CPU控制权的一种操作。
  164. 线程类的构造方法、静态块是被哪个线程调用的
    这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程
    所调用的,而run方法里面的代码才是被线程自身所调用的。
    如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new
    了Thread2,那么:
    1、Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的
    2、Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的
  165. 在线程中你怎么处理不可控制异常?
    在Java中有两种异常。
    非运行时异常(Checked Exception):这种异常必须在方法声明的throws语句指定,或者在方法体内
    捕获。例如:IOException和ClassNotFoundException。
    运行时异常(Unchecked Exception):这种异常不必在方法声明中指定,也不需要在方法体中捕获。
    例如,NumberFormatException。
    因为run()方法不支持throws语句,所以当线程对象的run()方法抛出非运行异常时,我们必须捕获并且
    处理它们。当运行时异常从run()方法中抛出时,默认行为是在控制台输出堆栈记录并且退出程序。
    好在,java提供给我们一种在线程对象里捕获和处理运行时异常的一种机制。实现用来处理运行时异常
    的类,这个类实现UncaughtExceptionHandler接口并且实现这个接口的uncaughtException()方法。示
    例:
    package concurrency; import java.lang.Thread.UncaughtExceptionHandler; public class Main2 { public static void main(String[] args) {

当一个线程抛出了异常并且没有被捕获时(这种情况只可能是运行时异常),JVM检查这个线程是否被
预置了未捕获异常处理器。如果找到,JVM将调用线程对象的这个方法,并将线程对象和异常作为传入
参数。
Thread类还有另一个方法可以处理未捕获到的异常,即静态方法
setDefaultUncaughtExceptionHandler()。这个方法在应用程序中为所有的线程对象创建了一个异常处
理器。
当线程抛出一个未捕获到的异常时,JVM将为异常寻找以下三种可能的处理器。
首先,它查找线程对象的未捕获异常处理器。
如果找不到,JVM继续查找线程对象所在的线程组(ThreadGroup)的未捕获异常处理器。
如果还是找不到,如同本节所讲的,JVM将继续查找默认的未捕获异常处理器。
如果没有一个处理器存在,JVM则将堆栈异常记录打印到控制台,并退出程序。
30. 同步方法和同步块,哪个是更好的选择
同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条
原则:同步的范围越小越好。
借着这一条,我额外提一点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗
化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全
的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味
着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态
和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多
次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁–>解锁的次
数,有效地提升了代码执行的效率。
Task task = new Task(); Thread thread = new Thread(task); thread.setUncaughtExceptionHandler(new ExceptionHandler()); thread.start(); } }class Task implements Runnable{ @Override public void run() { int numero = Integer.parseInt(“TTT”); } }class ExceptionHandler implements UncaughtExceptionHandler{ @Override public void uncaughtException(Thread t, Throwable e) { System.out.printf(“An exception has been captured\n”); System.out.printf(“Thread: %s\n”, t.getId()); System.out.printf(“Exception: %s: %s\n”, e.getClass().getName(),e.getMessage()); System.out.printf(“Stack Trace: \n”); e.printStackTrace(System.out); System.out.printf(“Thread status: %s\n”,t.getState()); } }

  1. 有三个线程T1,T2,T3,如何保证顺序执行?
    在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线
    程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,
    T2调用T1),这样T1就会先完成而T3最后完成。
    实际上先启动三个线程中哪一个都行, 因为在每个线程的run方法中用join方法限定了三个线程的执行
    顺序。
    public class JoinTest2 { // 1.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行 public static void main(String[] args) { final Thread t1 = new Thread(new Runnable() { @Override public void run() { System.out.println(“t1”); } }); final Thread t2 = new Thread(new Runnable() { @Override public void run() { try {// 引用t1线程,等待t1线程执行完 t1.join(); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println(“t2”); } }); Thread t3 = new Thread(new Runnable() { @Override public void run() { try {// 引用t2线程,等待t2线程执行完 t2.join(); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println(“t3”); } }); t3.start();//这里三个线程的启动顺序可以任意,大家可以试下! t2.start(); t1.start(); } }

  2. 什么是CAS
    CAS,全称为Compare and Swap,即比较-替换。
    假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会
    将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才
    能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会
    变的值A,只要某次CAS操作失败,永远都不可能成功。

  3. CAS?CAS 有什么缺陷,如何解决?
    CAS 涉及3个操作数,内存地址值V,预期原值A,新值B;如果内存位置的值V与预期原A值相匹
    配,就更新为新值B,否则不更新
    CAS有什么缺陷?.
    ABA 问题
    并发环境下,假设初始条件是A,去修改数据时,发现是A就会执行修改。但是看到的虽然是A,中间可
    能发生了A变B,B又变回A的情况。此时A已经非彼A,数据即使成功修改,也可能有问题。
    可以通过AtomicStampedReference「解决ABA问题」,它,一个带有标记的原子引用类,通过控制变
    量值的版本来保证CAS的正确性。
    循环时间长开销
    自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。
    很多时候,CAS思想体现,是有个自旋次数的,就是为了避开这个耗时问题~
    只能保证一个变量的原子操作。
    CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原
    子性的。
    可以通过这两个方式解决这个问题:
    使用互斥锁来保证原子性;

将多个变量封装成对象,通过AtomicReference来保证原子性。
34. 什么是AQS
简单说一下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。
如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,
ReentrantLock、CountDownLatch、Semaphore等等都用到了它。
AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个
Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开
始运行。
AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根
据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。
35. 线程池作用
(如果问到了这样的问题,可以展开的说一下线程池如何用、线程池的好处、线程池的启动策略)合理
利用线程池能够带来三个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系
统的稳定性,使用线程池可以进行统一的分配,调优和监控。
36. ThreadLocal是什么
从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该
变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可
以访问自己内部的副本变量。
从字面意思来看非常容易理解,但是从实际使用的角度来看,就没那么容易了,作为一个面试常问的
点,使用场景那也是相当的丰富:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
37. ThreadLocal有什么用
简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的
ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了

  1. ThreadLocal原理,使用注意点,应用场景有哪些?
    回答四个主要点:
    ThreadLocal是什么?
    ThreadLocal原理
    ThreadLocal使用注意点
    ThreadLocal的应用场景
    ThreadLocal是什么?
    ThreadLocal,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都
    会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,
    从而起到线程隔离的作用,避免了线程安全问题。
    ThreadLocal原理
    ThreadLocal内存结构图:
    由结构图是可以看出:
    Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量。
    ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本
    身,value是ThreadLocal的泛型值。
    对照着几段关键源码来看,更容易理解一点哈~ //创建一个ThreadLocal变量 static ThreadLocal localVariable = new ThreadLocal<>();

public class Thread implements Runnable { //ThreadLocal.ThreadLocalMap是Thread的属性 ThreadLocal.ThreadLocalMap threadLocals = null; }
ThreadLocal中的关键方法set()和get()
public void set(T value) { Thread t = Thread.currentThread(); //获取当前线程t ThreadLocalMap map = getMap(t); //根据当前线程获取到ThreadLocalMap if (map != null) map.set(this, value); //K,V设置到ThreadLocalMap中 elsecreateMap(t, value); //创建一个新的ThreadLocalMap }public T get() { Thread t = Thread.currentThread();//获取当前线程t ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap if (map != null) { //由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings(“unchecked”) T result = (T)e.value; return result; } }return setInitialValue(); }
ThreadLocalMap的Entry数组
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } }
所以怎么回答「ThreadLocal的实现原理」?如下,最好是能结合以上结构图一起说明哈~
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有
一个属于自己的ThreadLocalMap。
ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本
身,value是ThreadLocal的泛型值。
每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个
ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

ThreadLocal 内存泄露问题
先看看一下的TreadLocal的引用示意图哈,
ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用,如下
弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存。
弱引用比较容易被回收。因此,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但
是因为ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:
ThreadLocalMap的key没了,value还在,这就会「造成了内存泄漏问题」。
如何「解决内存泄漏问题」?使用完ThreadLocal后,及时调用remove()方法释放内存空间。
ThreadLocal的应用场景
数据库连接池
会话管理中使用
39. notify()和notifyAll()有什么区别?
notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。
void notify(): 唤醒一个正在等待该对象的线程。
void notifyAll(): 唤醒所有正在等待该对象的线程。

notify可能会导致死锁,而notifyAll则不会
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码
使用notifyall,可以唤醒 所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一
个。
wait() 应配合while循环使用,不应使用if,务必在wait()调用前后都检查条件,如果不满足,必须调用
notify()唤醒另外的线程来处理,自己继续wait()直至条件满足再往下执行。
notify() 是对notifyAll()的一个优化,但它有很精确的应用场景,并且要求正确使用。不然可能导致死
锁。正确的场景应该是 WaitSet中等待的是相同的条件,唤醒任一个都能正确处理接下来的事项,如果
唤醒的线程无法正确处理,务必确保继续notify()下一个线程,并且自身需要重新回到WaitSet中.
40. 为什么wait()方法和notify()/notifyAll()方法要在同步块中被调

这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁
41. wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什
么区别
wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:wait()方法立即释放对象监视
器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。
42. wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什
么区别
wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:wait()方法立即释放对象监视
器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。
43. 线程中断是否能直接调用stop,为什么?
Java提供的终止方法只有一个stop,但是不建议使用此方法,因为它有以下三个问题: 1. stop方法是过时的 从Java编码规则来说,已经过时的方式不建议采用. 2. stop方法会导致代码逻辑不完整 stop方法是一种"恶意"的中断,一旦执行stop方法,即终止当前正在
运行的线程,不管线程逻辑是否完整,这是非常危险的.
44. 什么是阻塞(Blocking)和非阻塞(Non-Blocking)?
阻塞和非阻塞通常用来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其他所有需要
这个而资源的线程就必须在这个临界区中进行等待。等待会导致线程挂起,这种情况就是阻塞。此时,
如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作。
非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行。所有的线程都会尝试不断前向执
行。

  1. 什么是自旋
    很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是
    一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。
    既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的
    边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的
    策略。
  2. 自旋锁的优缺点?
    自旋锁不会引起调用者休眠,如果自旋锁已经被别的线程保持,调用者就一直循环在那里看是否该自旋
    锁的保持者释放了锁。由于自旋锁不会引起调用者休眠,所以自旋锁的效率远高于互斥锁。
    虽然自旋锁效率比互斥锁高,但它会存在下面两个问题: 1、自旋锁一直占用CPU,在未获得锁的情况
    下,一直运行,如果不能在很短的时间内获得锁,会导致CPU效率降低。 2、试图递归地获得自旋锁会
    引起死锁。递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋
    锁。
    由此可见,我们要慎重的使用自旋锁,自旋锁适合于锁使用者保持锁时间比较短并且锁竞争不激烈的情
    况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的
    效率远高于互斥锁。
  3. 什么是线程池? 为什么要使用它?
    创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创
    建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程
    池,里面的线程叫工作线程。
    从JDK1.5开始,Java API提供了Executor框架让你可以创建不同的线程池。比如单线程池,每次处理一
    个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)
    线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如
    已完成任务的数量。
    这里借用《Java并发编程的艺术》提到的来说一下使用线程池的好处:
    降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
    提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统
    的稳定性,使用线程池可以进行统一的分配,调优和监控。
  4. 常用的线程池模式以及不同线程池的使用场景?
    以下是Java自带的几种线程池:
    1、newFixedThreadPool 创建一个指定工作线程数量的线程池。 每当提交一个任务就创建一个工作线
    程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
    2、newCachedThreadPool 创建一个可缓存的线程池。 这种类型的线程池特点是:

1).工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE),这样可灵活的
往线程池中添加线程。
2).如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工
作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
3、newSingleThreadExecutor创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任
务,如果这个线程异常结束,会有另一个取代它,保证顺序执行(我觉得这点是它的特色)。
单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动
的。
4、newScheduleThreadPool 创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于
Timer。
49. 在Java中Executor、ExecutorService、Executors的区别?
Executor 和 ExecutorService 这两个接口主要的区别是:
ExecutorService 接口继承了 Executor 接口,是 Executor 的子接口
Executor 和 ExecutorService 第二个区别是:Executor 接口定义了 execute()方法用来接收一个
Runnable接口的对象,而 ExecutorService 接口中的 submit()方法可以接受Runnable和Callable
接口的对象。
Executor 和 ExecutorService 接口第三个区别是 Executor 中的 execute() 方法不返回任何结果,
而 ExecutorService 中的 submit()方法可以通过一个 Future 对象返回运算结果。
Executor 和 ExecutorService 接口第四个区别是除了允许客户端提交一个任务,ExecutorService
还提供用来控制线程池的方法。比如:调用 shutDown() 方法终止线程池。
Executors 类提供工厂方法用来创建不同类型的线程池。
比如: newSingleThreadExecutor() 创建一个只有一个线程的线程池,newFixedThreadPool(int
numOfThreads)来创建固定线程数的线程池,newCachedThreadPool()可以根据需要创建新的线程,
但如果已有线程是空闲的会重用已有线程。
50. 请说出与线程同步以及线程调度相关的方法。
wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理
InterruptedException异常;
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待
状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞
争,只有获得锁的线程才能进入就绪状态;

  1. 举例说明同步和异步。
    如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被
    另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取
    (数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行
    的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往
    更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。

  2. 不使用stop停止线程?
    当run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile 布尔
    变量来退出run()方法的循环或者是取消任务来中断线程。
    使用自定义的标志位决定线程的执行情况
    public class SafeStopThread implements Runnable{ private volatile boolean stop=false;//此变量必须加上volatile int a=0; @Override public void run() { // TODO Auto-generated method stub while(!stop){ synchronized ("") { a++; try {Thread.sleep(100); } catch (Exception e) { // TODO: handle exception }a–; String tn=Thread.currentThread().getName(); System.out.println(tn+":a="+a); } } //线程终止 public void terminate(){ stop=true; } public static void main(String[] args) { SafeStopThread t=new SafeStopThread(); Thread t1=new Thread(t); t1.start(); for(int i=0;i<5;i++){ new Thread(t).start(); } t.terminate(); } }

  3. 如何控制某个方法允许并发访问线程的大小?
    Semaphore两个重要的方法就是semaphore.acquire() 请求一个信号量,这时候的信号量个数-1(一旦
    没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信
    号量)semaphore.release()释放一个信号量,此时信号量个数+1

  4. 如何创建线程池
    《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过
    ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽
    的风险**
    Executors 返回线程池对象的弊端如下:
    FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为
    Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
    CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为
    Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
    方式一:通过构造方法实现
    public class SemaphoreTest { private Semaphore mSemaphore = new Semaphore(5); public void run(){ for(int i=0; i< 100; i++){ new Thread(new Runnable() { @Override public void run() { test(); } }).start(); } }private void test(){ try {mSemaphore.acquire(); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println(Thread.currentThread().getName() + " 进来了"); try {Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println(Thread.currentThread().getName() + " 出去了"); mSemaphore.release(); } }

方式二:通过Executor 框架的工具类Executors来实现 我们可以创建三种类型的
ThreadPoolExecutor:
FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。
当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在
一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程
池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数
量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新
的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复
用。
对应Executors工具类中的方法如图所示:
55. 高并发、任务执行时间短的业务怎样使用线程池?并发不高、任
务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业
务怎样使用线程池?
这是我在并发编程网上看到的一个问题,希望每个人都能看到并且思考一下,因为这个问题非常好、非
常实际、非常专业。关于这个问题,个人看法是:
1、高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
2、并发不高、任务执行时间长的业务要区分开看:
假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要
让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样
吧,线程池中的线程数设置得少一些,减少线程上下文的切换
3、并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些
业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。
最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

  1. 什么是线程安全
    又是一个理论的问题,各式各样的答案有很多,我给出一个个人认为解释地最好的:如果你的代码在多
    线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
    1、不可变
    像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新
    创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
    2、绝对线程安全
    不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,
    Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中
    也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
    3、相对线程安全
    相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操
    作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,
    99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
    4、线程非安全
    这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类
  2. Java中interrupted 和isInterruptedd方法的区别?
    interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。
    Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标
    识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。
    非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何
    抛出InterruptedException异常的方法都会将中断状态清零。
    无论如何,一个线程的中断状态都有可能被其它线程调用中断来改变。
  3. Java线程池中submit() 和 execute()方法有什么区别?
    两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而
    submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了
    Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方
    法。
  4. 说一说自己对于 synchronized 关键字的了解
    synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰
    的方法或者代码块在任意时刻只能有一个线程执行。 另外,在 Java 早期版本中,synchronized属于重
    量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java
    的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完
    成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较
    长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6
    之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很
    不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、
    轻量级锁等技术来减少锁操作的开销。
  5. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
    synchronized关键字最主要的三种使用方式:
    修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
    修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对
    象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果
    一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静
    态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是
    当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
    修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 总结:
    synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。
    synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为
    JVM中,字符串常量池具有缓存功能!
  6. Java中如何获取到线程dump文件
    死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就
    是线程堆栈,获取到线程堆栈有两步:
    1、获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java
    2、打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid
    另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,
    因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈,
  7. 一个线程如果出现了运行时异常会怎么样
    如果这个异常没有被捕获的话,这个线程就停止执行了。
    另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放
  8. 如何在两个线程之间共享数据
    通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和
    等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的
  9. 如何在两个线程间共享数据?
    同一个Runnable,使用全局变量。
    第一种:将共享数据封装到一个对象中,把这个共享数据所在的对象传递给不同的Runnable

第二种:将这些Runnable对象作为某一个类的内部类,共享的数据作为外部类的成员变量,对共享数据
的操作分配给外部类的方法来完成,以此实现对操作共享数据的互斥和通信,作为内部类的Runnable来
操作外部类的方法,实现对数据的操作
65. Java中活锁和死锁有什么区别?
活锁:一个线程通常会有会响应其他线程的活动。如果其他线程也会响应另一个线程的活动,那么就有
可能发生活锁。同死锁一样,发生活锁的线程无法继续执行。然而线程并没有阻塞——他们在忙于响应
对方无法恢复工作。这就相当于两个在走廊相遇的人:甲向他自己的左边靠想让乙过去,而乙向他的右
边靠想让甲过去。可见他们阻塞了对方。甲向他的右边靠,而乙向他的左边靠,他们还是阻塞了对方。
死锁:两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁。死锁通常发生在多个线程同时
但以不同的顺序请求同一组锁的时候,死锁会让你的程序挂起无法完成任务。
class ShareData { private int x = 0; public synchronized void addx(){ x++; System.out.println("x++ : "+x); }public synchronized void subx(){ x–; System.out.println("x-- : "+x); }}public class ThreadsVisitData { public static ShareData share = new ShareData(); public static void main(String[] args) { //final ShareData share = new ShareData(); new Thread(new Runnable() { public void run() { for(int i = 0;i<100;i++){ share.addx(); } } }).start(); new Thread(new Runnable() { public void run() { for(int i = 0;i<100;i++){ share.subx(); } }}).start(); }}

  1. Java中的死锁
    在Java中使用多线程,就会有可能导致死锁问题。死锁会让程序一直卡住,不再程序往下执行。我们只
    能通过中止并重启的方式来让程序重新执行。这是我们非常不愿意看到的一种现象,我们要尽可能避免
    死锁的情况发生!
    死锁的四个必要条件
    1、互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如
    果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。
    2、请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程
    占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
    3、不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
    4、环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{A,B,C,
    ···,Z} 中的A正在等待一个B占用的资源;B正在等待C占用的资源,……,Z正在等待已被A占用的资源。
    死锁实例
    /*** 死锁类示例 */ public class DeadLock implements Runnable { public int flag = 1; //静态对象是类的所有对象共享的 private static Object o1 = new Object(), o2 = new Object(); @Override public void run() { System.out.println(“flag:{}”+flag); if (flag == 1) { //先锁o1,再对o2加锁,环路等待条件 synchronized (o1) { try {Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); }synchronized (o2) { System.out.println(“1”); } } }if (flag == 0) {//先锁o2,在锁01 synchronized (o2) { try {Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); }synchronized (o1) { System.out.println(“0”); } } } }

1、当DeadLock 类的对象flag=1时(td1),先锁定o1,睡眠500毫秒
2、而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
3、td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;
4、td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
5、td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
67. 如何避免死锁和检测
预防死锁
破坏互斥条件:使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁
破坏请求和保持条件:采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要
的全部资源,且直至所要的资源全部得到满足后才开始执行,只要有一个资源得不到分配,也不给
这个进程分配其他的资源。
破坏不剥夺条件:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源,但是只
适用于内存和处理器资源。
破坏循环等待条件:给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依
次进行。
设置加锁顺序
如果两个线程(A和B),当A线程已经锁住了Z,而又去尝试锁住X,而X已经被线程B锁住,线程A和线程
B分别持有对应的锁,而又去争夺其他一个锁(尝试锁住另一个线程已经锁住的锁)的时候,就会发生
死锁,如下图:
两个线程试图以不同的顺序来获得相同的锁,如果按照相同的顺序来请求锁,那么就不会出现循环的加
锁依赖性,因此也就不会产生死锁,每个需要锁Z和锁X的线程都以相同的顺序来获取Z和X,那么就不会
发生死锁了,如下图所示:
public static void main(String[] args) { DeadLock td1 = new DeadLock(); DeadLock td2 = new DeadLock(); td1.flag = 1; td2.flag = 0; //td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。 //td2的run()可能在td1的run()之前运行 new Thread(td1).start(); new Thread(td2).start(); } }

这样死锁就永远不会发生。 针对两个特定的锁,可以尝试按照锁对象的hashCode值大小的顺序,
分别获得两个锁,这样锁总是会以特定的顺序获得锁,我们通过设置锁的顺序,来防止死锁的发
生,在这里我们使用System.identityHashCode方法来定义锁的顺序,这个方法将返回由
Obejct.hashCode 返回的值,这样就可以消除死锁发生的可能性。
public class DeadLockExample3 { // 加时赛锁,在极少数情况下,如果两个hash值相等,使用这个锁进行加锁 private static final Object tieLock = new Object(); public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException { class Helper { public void transfer() throws InsufficientFundsException { if (fromAcct.getBalance().compareTo(amount) < 0) throw new InsufficientFundsException(); else {fromAcct.debit(amount); toAcct.credit(amount); } } }// 得到两个锁的hash值 int fromHash = System.identityHashCode(fromAcct); int toHash = System.identityHashCode(toAcct); // 根据hash值判断锁顺序,决定锁的顺序 if (fromHash < toHash) { synchronized (fromAcct) { synchronized (toAcct) { new Helper().transfer(); } } } else if (fromHash > toHash) { synchronized (toAcct) { synchronized (fromAcct) { new Helper().transfer(); } } } else {// 如果两个对象的hash相等,通过tieLock来决定加锁的顺序,否则又会重新引入死 锁——加时赛锁 synchronized (tieLock) { synchronized (fromAcct) {
在极少数情况下,两个对象可能拥有两个相同的散列值,此时必须通过某种任意的方法来决定锁的
顺序,否则可能又会重新引入死锁。
为了避免这种情况,可以使用 “加时(Tie-Breaking))”锁 ,这获得这两个Account锁之前,从而
消除了死锁发生的可能性
68. 什么是可重入锁(ReentrantLock)?
Java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为Java 类,而不是
作为语言的特性来实现。这就为Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特
性或者锁定语义。
ReentrantLock 类实现了Lock ,它拥有与synchronized 相同的并发性和内存语义,但是添加了类似锁
投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换
句话说,当许多线程都想访问共享资源时,JVM可以花更少的时候来调度线程,把更多时间用在执行线
程上。)
Reentrant 锁意味着什么呢?
简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加
1,然后锁需要被释放两次才能获得真正释放。这模仿了synchronized 的语义;如果线程进入由线程已
经拥有的监控器保护的synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续)
synchronized块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个synchronized 块时,
才释放锁。
69. 讲一下 synchronized 关键字的底层原理
synchronized 关键字底层原理属于 JVM 层面。
① synchronized 同步语句块的情况
通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录
执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行 javap -c -s -v -l SynchronizedDemo.class 。synchronized (toAcct) { new Helper().transfer(); } } } } } }public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println(“synchronized 代码块”); } } }

从上面我们可以看出:
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中
monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当
执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的
对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原
因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行
monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等
待,直到锁被另外一个线程释放为止。
② synchronized 修饰方法的的情况
public class SynchronizedDemo2 { public synchronized void method() { System.out.println(“synchronized 方法”); } }

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是
ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
70. synchronized和ReentrantLock的区别
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既
然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方
法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
1、ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
2、ReentrantLock可以获取各种锁的信息
3、ReentrantLock可以灵活地实现多路通知
另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,
synchronized操作的应该是对象头中mark word,这点我不能确定。
71. ConcurrentHashMap的并发度是什么
ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操
作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,
Hashtable能同时有两条线程获取Hashtable中的数据吗?
72. ReadWriteLock是什么
首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用
ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如
果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降
低了程序的性能。
因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,
ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,
写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

  1. FutureTask是什么
    FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这
    个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask
    也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。
  2. 如果你提交任务时,线程池队列已满,这时会发生什么
    这里区分一下:
    如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队
    列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务
    如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,
    ArrayBlockingQueue满了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处
    理不过来,ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了
    的任务,默认是AbortPolicy
  3. 生产者消费者模型的作用是什么
    这个问题很理论,但是很重要:
    1、通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型
    最重要的作用
    2、解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可
    以独自发展而不需要收到相互的制约
  4. 什么是乐观锁和悲观锁
    1、乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总
    是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,
    如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
    2、悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总
    是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十
    一,直接上了锁就操作资源了。
  5. CyclicBarrier和CountDownLatch的区别
    两个看上去有点像的类,都在 java.util.concurrent 下,都可以用来表示代码运行到某个点上,二者
    的区别在于:
    1、 CyclicBarrier 的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这
    个点,所有线程才重新运行; CountDownLatch 则不是,某线程运行到某个点上之后,只是给某个数
    值-1而已,该线程继续运行
    2、 CyclicBarrier 只能唤起一个任务, CountDownLatch 可以唤起多个任务

3、 CyclicBarrier 可重用, CountDownLatch 不可重用,计数值为0该 CountDownLatch 就不可再用

78. Hashtable的size()方法中明明只有一条语句"return count",
为什么还要做同步?
这是我之前的一个困惑,不知道大家有没有想过这个问题。某个方法中如果有多条语句,并且都在操作
同一个类变量,那么在多线程环境下不加锁,势必会引发线程安全问题,这很好理解,但是size()方法明
明只有一条语句,为什么还要加锁?
关于这个问题,在慢慢地工作、学习中,有了理解,主要原因有两点:
1、同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访
问。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则可以正常调用
size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数
据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。
而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完毕之后才可以调
用,这样就保证了线程安全性
2、CPU执行代码,执行的不是Java代码,这点很关键,一定得记住。Java代码最终是被翻译成机器码执
行的,机器码才是真正可以和硬件电路交互的代码。即使你看到Java代码只有一行,甚至你看到Java代
码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句"return
count"假设被翻译成了三句汇编语句执行,一句汇编语句和其机器码做对应,完全可能执行完第一句,
线程就切换了。

  1. Linux环境下如何查找哪个线程使用CPU最长
    这是一个比较偏实践的问题,这种问题我觉得挺有意义的。可以这么做:
    1、获取项目的pid,jps或者ps -ef | grep java,这个前面有讲过
    2、top -H -p pid,顺序不能改变
    这样就可以打印出当前的项目,每条线程占用CPU时间的百分比。注意这里打出的是LWP,也就是操作
    系统原生线程的线程号,我笔记本山没有部署Linux环境下的Java工程,因此没有办法截图演示,网友朋
    友们如果公司是使用Linux环境部署项目的话,可以尝试一下。
    使用"top -H -p pid"+"jps pid"可以很容易地找到某条占用CPU高的线程的线程堆栈,从而定位占用CPU
    高的原因,一般是因为不当的代码操作导致了死循环。
    最后提一点,"top -H -p pid"打出来的LWP是十进制的,"jps pid"打出来的本地线程号是十六进制的,转
    换一下,就能定位到占用CPU高的线程的当前线程堆栈了。
    JVM面试题
  2. 什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?
    Java虚拟机是一个可以执行Java字节码的虚拟机进程。
    Java源文件被编译成能被Java虚拟机执行的字节码文件。 Java被设计成允许应用程序可以运行在任意的
    平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知
    道底层硬件平台的指令长度和其他特性。
  3. Java内存结构?

方法区和堆是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内
存区域。
1、Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区
域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分
配内存。
2、方法区(Method Area),方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
3、程序计数器(Program Counter Register),程序计数器(Program Counter Register)是一块较小
的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
4、JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程
私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时
候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信
息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
5、本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的
作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法
栈则是为虚拟机使用到的Native方法服务。

  1. Java内存分配
    寄存器:我们无法控制。
    静态域:static 定义的静态成员。
    常量池:编译时被确定并保存在 .class 文件中的(final)常量值和一些文本修饰的符号引用(类和
    接口的全限定名,字段的名称和描述符,方法和名称和描述符)。
    非 RAM 存储:硬盘等永久存储空间。
    堆内存:new 创建的对象和数组,由 Java 虚拟机自动垃圾回收器管理,存取速度慢。
    栈内存:基本类型的变量和对象的引用变量(堆内存空间的访问地址),速度快,可以共享,但是
    大小与生存期必须确定,缺乏灵活性。
  2. Java 堆的结构是什么样子的?什么是堆中的永久代(Perm Gen
    space)?
    JVM 的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在 JVM 启动的时候被创建。对
    象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。堆内存是由存活和死亡的对象组成的。
    存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器
    回收掉的对象。一直到垃圾收集器把这些 对象回收掉之前,他们会一直占据堆内存空间。
  3. Java 中堆和栈有什么区别?
    JVM 中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而对象总是在
    堆上分配。栈通常都比堆小,也不会在多个线程之间共享,而堆被整个 JVM 的所有线程共享。

    在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配,当在一段代码块定
    义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java 会自动释放掉为
    该变量分配的内存空间,该内存空间可以立即被另作它用。

    堆内存用来存放由 new 创建的对象和数组,在堆中分配的内存,由 Java 虚拟机的自动垃圾回收器来管
    理。在堆中产生了一个数组或者对象之后,还可以在栈中定义一个特殊的变量,让栈中的这个变量的取
    值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量,以后就可以在
    程序中使用栈中的引用变量来访问堆中的数组或者对象,引用变量就相当于是为数组或者对象起的一个
    名称。
  4. 解释内存中的栈(stack)、堆(heap)和方法区(method area)的用

    通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用JVM中
    的栈空间;
    而通过new关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃
    圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为Eden、
    Survivor(又可分为From Survivor和To Survivor)、Tenured;

方法区和堆都是各个线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量、JIT编
译器编译后的代码等数据;程序中的字面量(literal)如直接书写的100、”hello”和常量都是放在常量池
中,常量池是方法区的一部分。栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈
和堆的大小都可以通过JVM的启动参数来进行调整,栈空间用光了会引发StackOverflowError,而堆和
常量池空间不足则会引发OutOfMemoryError。
上面的语句中变量str放在栈上,用new创建出来的字符串对象放在堆上,而”hello”这个字面量是放在方
法区的。
补充1:
较新版本的Java(从Java 6的某个更新开始)中,由于JIT编译器的发展和”逃逸分析”技术的逐渐成熟,栈
上分配、标量替换等优化技术使得对象一定分配在堆上这件事情已经变得不那么绝对了。
补充2:
运行时常量池相当于Class文件常量池具有动态性,Java语言并不要求常量一定只有编译期间才能产生,
运行期间也可以将新的常量放入池中,String类的intern()方法就是这样的。 看看下面代码的执行结果是
什么并且比较一下Java 7以前和以后的运行结果是否一致。
7. JVM内存分哪几个区,每个区的作用是什么?
Java虚拟机主要分为以下一个区:
方法区:

  1. 有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主
    要是对方法区里的常量池和对类 型的卸载
  2. 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数
    据。
  3. 该区域是被线程共享的。
  4. 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态
    性,也就是说常量并不一定是编 译时确定,运行时生成的常量也会存在这个常量池中。
    虚拟机栈: 1. 虚拟机栈也就是我们平常所称的栈内存,它为java方法服务,每个方法在执行的时候都会创建一个栈
    帧,用于存储局部变量表、操 作数栈、动态链接和方法出口等信息。
  5. 虚拟机栈是线程私有的,它的生命周期与线程相同。
  6. 局部变量表里存储的是基本数据类型、returnAddress类型(指向一条字节码指令的地址)和对象
    引用,这个对象引用有可能是指 向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对
    String str = new String(“hello”); String s1 = new StringBuilder(“go”).append(“od”).toString(); System.out.println(s1.intern() == s1); String s2 = new StringBuilder(“ja”).append(“va”).toString(); System.out.println(s2.intern() == s2);
    象相关联的位置。局部变量所需的内存空间在编译器间确定
  7. 操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,
    而是压栈和出栈的方式
  8. 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调
    用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。
    本地方法栈
    本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务。

    Java堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此
    该区域经常发生垃圾回收操作 。
    程序计数器
    内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、
    循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个java虚拟
    机规范没有规定任何OOM情况的区域。
  9. 怎么获取 Java 程序使用的内存?堆使用的百分比?
    可以通过 java.lang.Runtime 类中与内存相关方法来获取剩余的内存,总内存及最大堆内存。
    通过这些方法你也可以获取到堆使用的百分比及堆内存的剩余空间。
    Runtime.freeMemory() 方法返回剩余空间的字节数
    Runtime.totalMemory()方法总内存的字节数
    Runtime.maxMemory() 返回最大内存的字节数
  10. JVM有哪些内存区域?(JVM的内存布局是什么?)
    JVM包含堆、元空间、Java虚拟机栈、本地方法栈、程序计数器等内存区域。其中,堆是占用内存最大
    的一块。我们平常的-Xmx、-Xms等参数,就是针对于堆进行设计的。
    堆:JVM堆中的数据,是共享的,是占用内存最大的一块区域
    虚拟机栈:Java虚拟机栈,是基于线程的,用来服务字节码指令的运行
    程序计数器:当前线程所执行的字节码的行号指示器
    元空间:方法区就在这里,非堆本地内存:其他的内存占用空间
  11. 类加载器
    1、启动类加载器:Bootstrap ClassLoader
    负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中
    的,并且能被虚拟机识别的类库
    2、扩展类加载器:Extension ClassLoader
    该加载器由sun.misc.LauncherKaTeX parse error: Undefined control sequence: \jre at position 25: …oader实现,它负责加载DK\̲j̲r̲e̲\lib\ext目录中,或者由…AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指
    定的类,开发者可以直接使用该类加载器
  12. JVM加载class文件的原理机制?
    JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的
    Java运行时系统组件,它负责在运行时查找和装入类文件中的类。
    由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java
    程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加
    载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与
    所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后
    就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符
    号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:
    1、如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
    2、如果类中存在初始化语句,就依次执行这些初始化语句。
    类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、
    系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。
    从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安
    全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的
    加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程
    序提供对Bootstrap的引用。下面是关于几个类加载器的说明:
    1、Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
    2、Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap; 3、System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量
    classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。
  13. Java类加载过程
    Java 类加载需要经历一下 7 个过程:
    1、加载
    加载是类加载的第一个过程,在这个阶段,将完成一下三件事情:
    通过一个类的全限定名获取该类的二进制流。
    将该二进制流中的静态存储结构转化为方法去运行时数据结构。
    在内存中生成该类的 Class 对象,作为该类的数据访问入口。
    2、验证
    验证的目的是为了确保 Class 文件的字节流中的信息不回危害到 虚拟机.在该阶段主要完成以下四钟验
    证:
    文件格式验证:验证字节流是否符合 Class 文件的规范,如 主次版本号是否在当前虚拟机范围内,常量
    池中的常量是否 有不被支持的类型.
    元数据验证:对字节码描述的信息进行语义分析,如这个类是 否有父类,是否集成了不被继承的类等。
    字节码验证:是整个验证过程中最复杂的一个阶段,通过验 证数据流和控制流的分析,确定程序语义是
    否正确,主要针 对方法体的验证。如:方法中的类型转换是否正确,跳转指 令是否正确等。
    符号引用验证:这个动作在后面的解析过程中发生,主要是 为了确保解析动作能正确执行。
    3、准备
    准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些 内存都将在方法区中进行分配。准备
    阶段不分配类中的实例变量的 内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆 中。
    4、解析
    该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一 定在初始化动作完成之前,也有可能
    在初始化之后。
    初始化 初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段 用户应用程序可以通过自定义
    类加载器参与之外,其余动作完全由
    5、初始化
    初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载
    器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java 程
    序代码。
    6、使用
    7、卸载
  14. JVM中对象的创建过程
    1、拿到内存创建指令
    当虚拟机遇到内存创建的指令的时候(new 类名),来到了方法区,找 根据new的参数在常量池中定位
    一个类的符号引用。
    2、 检查符号引用
    检查该符号引用有没有被加载、解析和初始化过,如果没有则执行类加载过程,否则直接准备为新的对
    象分配内存
    3、分配内存
    虚拟机为对象分配内存(堆)分配内存分为指针碰撞和空闲列表两种方式;分配内存还要要保证并发安
    全,有两种方式。
    1)指针碰撞
    所有的存储空间分为两部分,一部分是空闲,一部分是占用,需要分配空间的时候,只需要计算指针
    移动的长度即可。
    2)空闲列表
    虚拟机维护了一个空闲列表,需要分配空间的时候去查该空闲列表进行分配并对空闲列表做更新。可
    以看出,内存分配方式是由java堆是否规整决定的,java堆的规整是由垃圾回收机制来决定的
    public static int value=123;//在准备阶段 value 初始值为 0 。在初 始化阶段才会变为 123 。
    3) 安全性问题的思考
    假如分配内存策略是指针碰撞,如果在高并发情况下,多个对象需要分配内存,如果不做处理,肯定
    会出现线程安全问题,导致一些对象分配不到空间等。
    下面是解决方案:
    也就是每个线程都进行同步,防止出现线程安全。
    本地线程分配缓冲
    也称TLAB(Thread Local Allocation Buffer),在堆中为每一个线程分配一小块独立的内存,这
    样以来就不存并发问题了,Java 层面与之对应的是 ThreadLocal 类的实现
    4、初始化
  15. 分配完内存后要对对象的头(Object Header)进行初始化,这新信息包括:该对象对应类的元数
    据、该对象的GC代、对象的哈希码。
  16. 抽象数据类型默认初始化为null,基本数据类型为0,布尔为false…
    5、 调用对象的初始化方法
    也就是执行构造方法。
  17. Java对象结构
    Java对象由三个部分组成:对象头、实例数据、对齐填充。
    1、对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、
    线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即
    对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。
    2、实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)
    3、对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)
  18. 类的生命周期
    类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程,如下
    图;
    1、加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象
  • 线程同步策略
    2、连接,连接又包含三块内容:验证、准备、初始化。 1)验证,文件格式、元数据、字节码、符号引
    用验证; 2)准备,为类的静态变量分配内存,并将其初始化为默认值; 3)解析,把类中的符号引用
    转换为直接引用
    3、初始化,为类的静态变量赋予正确的初始值
    4、使用,new出对象程序中使用
    5、卸载,执行垃圾回收
  1. 如何判断对象可以被回收?
    在堆里面存放着Java世界中几乎所有的对象实例, 垃圾收集器在对堆进行回收前, 第一件事就是判断哪些
    对象已死(可回收).
    1、引用计数法
    在JDK1.2之前,使用的是引用计数器算法。
    在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效的时
    候,计数器的值就-1,当引用计数器被减为零的时候,标志着这个对象已经没有引用了,可以回收了!
    问题:
    如果在A类中调用B类的方法,B类中调用A类的方法,这样当其他所有的引用都消失了之后,A和B还有
    一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,
    已经是垃圾了。但是该算法并不会计算出该类型的垃圾。
    2、可达性分析法
    在主流商用语言(如Java、C#)的主流实现中, 都是通过可达性分析算法来判定对象是否存活的: 通过一系
    列的称为 GC Roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为引用链/Reference Chain, 当
    一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的, 如下图:虽然
    E和F相互关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象。
    注: 即使在可达性分析算法中不可达的对象, VM也并不是马上对其回收, 因为要真正宣告一个对象死亡, 至
    少要经历两次标记过程: 第一次是在可达性分析后发现没有与GC Roots相连接的引用链, 第二次是GC对 在F-Queue执行队列中的对象进行的小规模标记(对象需要覆盖finalize()方法且没被调用过).
  2. 如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占
    用的内存?
    不会,在下一个垃圾回收周期中,这个对象将是可被回收的。
  3. Java的四种引用,强弱软虚
    1、强引用
    强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用方式:
    2、软引用
    软引用在程序内存不足时,会被回收,使用方式:
    可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。
    3、弱引用
    弱引用就是只要JVM垃圾回收器发现了它,就会将之回收,使用方式:
    String str = new String(“str”); // 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的, // 这里的软引用指的是指向new String(“str”)的引用,也就是SoftReference类中T SoftReference wrf = new SoftReference(new String(“str”));
    可用场景: Java源码中的 java.util.WeakHashMap 中的 key 就是使用弱引用,我的理解就是,一旦我
    不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。
    4、虚引用
    虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入 ReferenceQueue 中。注意哦,其它
    引用是被JVM回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多被用于引用销毁
    前的处理工作。还有就是,虚引用创建的时候,必须带有 ReferenceQueue ,使用例子:
    可用场景: 对象销毁前的一些操作,比如说资源释放等。** Object.finalize() 虽然也可以做这类动
    作,但是这个方式即不安全又低效
    上诉所说的几类引用,都是指对象本身的引用,而不是指 Reference 的四个子类的引用
    ( SoftReference 等)。
  4. 什么情况下会发生栈溢出?
    栈的大小可以通过-Xss参数进行设置,当递归层次太深的时候,就会发生栈溢出。比如循环调用,递归
    等。
  5. GC是什么?为什么要有GC
    GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序
    或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存
    的目的,Java语言没有提供释放已分配内存的显示操作方法。
    Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方
    法之一:
    但JVM可以屏蔽掉显示的垃圾回收调用。 垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内
    存。
    垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者
    长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行
    垃圾回收。
    在Java诞生初期,垃圾回收是Java最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问
    题,然而时过境迁,如今Java的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常觉得iOS的
    系统比Android系统有更好的用户体验,其中一个深层次的原因就在于android系统中垃圾回收的不可预
    知性。
    补充:
    WeakReference wrf = new WeakReference(str); PhantomReference prf = new PhantomReference(new String(“str”), new ReferenceQueue<>()); System.gc() 或Runtime.getRuntime().gc()
    垃圾回收机制有很多种,包括:分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。标准的Java
    进程既有栈又有堆。栈保存了原始型局部变量,堆保存了要创建的对象。Java平台对堆内存回收和再利
    用的基本算法被称为标记和清除,但是Java对其进行了改进,采用“分代式垃圾收集”。这种方法会跟Java
    对象的生命周期将堆内存划分为不同的区域,在垃圾收集过程中,可能会将对象移动到不同区域:
    伊甸园(Eden):这是对象最初诞生的区域,并且对大多数对象来说,这里是它们唯一存在过的
    区域。
    幸存者乐园(Survivor):从伊甸园幸存下来的对象会被挪到这里。
    终身颐养园(Tenured):这是足够老的幸存对象的归宿。年轻代收集(Minor-GC)过程是不会
    触及这个地方的。当年轻代收集不能把对象放进终身颐养园时,就会触发一次完全收集(MajorGC),这里可能还会牵扯到压缩,以便为大对象腾出足够的空间。 与垃圾回收相关的JVM参数:
    -Xms / -Xmx :堆的初始大小 / 堆的最大大小
    -Xmn: 堆中年轻代的大小
    -XX:-DisableExplicitGC :让System.gc()不产生任何作用
    -XX:+PrintGCDetails:打印GC的细节
    -XX:+PrintGCDateStamps : 打印GC操作的时间戳
    -XX:NewSize / XX:MaxNewSize : 设置新生代大小/新生代最大大小
    -XX:NewRatio : 可以设置老生代和新生代的比例
    -XX:PrintTenuringDistribution : 设置每次新生代GC后输出幸存者乐园中对象年龄的分布
    -XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold :设置老年代阀值的初始值和最大值
    -XX:TargetSurvivorRatio : 设置幸存区的目标使用率
  6. 简述 Java 垃圾回收机制。
    在 Java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有
    一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存
    不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收
  7. JVM 的永久代中会发生垃圾回收么?
    垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。
    注:Java 8 中已经移除了永久代,新加了一个叫做元数据区的native 内存区。
  8. 什么是分布式垃圾回收(DGC)?它是如何工作的?
    DGC 叫做分布式垃圾回收。RMI 使用 DGC 来做自动垃圾回收。因为 RMI 包含了跨虚拟机的远程对象的
    引用,垃圾回收是很困难的。DGC 使用引用计数算法来给远程对象提供自动内存管理。
  9. JVM垃圾处理方法
    1、标记-清除算法(老年代)
    该算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象(可达性分析), 在标记完成后统一清
    理掉所有被标记的对象.
    该算法会有两个问题:
  10. 效率问题,标记和清除效率不高。
  11. 空间问题: 标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分
    配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。
    所以它一般用于"垃圾不太多的区域,比如老年代"。 2、复制算法(新生代)
    该算法的核心是将可用内存按容量划分为大小相等的两块, 每次只用其中一块, 当这一块的内存用完, 就将
    还存活的对象(非垃圾)复制到另外一块上面, 然后把已使用过的内存空间一次清理掉。
    优点:不用考虑碎片问题,方法简单高效。
    缺点:内存浪费严重。
    现代商用VM的新生代均采用复制算法,但由于新生代中的98%的对象都是生存周期极短的,因此并不需
    完全按照1∶1的比例划分新生代空间,而是将新生代划分为一块较大的Eden区和两块较小的Survivor区
    (HotSpot默认Eden和Survivor的大小比例为8∶1), 每次只用Eden和其中一块Survivor。
    当发生MinorGC时,将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor上, 最后清
    理掉Eden和刚才用过的Survivor的空间。当Survivor空间不够用(不足以保存尚存活的对象)时,需要依赖
    老年代进行空间分配担保机制,这部分内存直接进入老年代。
    复制算法的空间分配担保:
    在执行Minor GC前, VM会首先检查老年代是否有足够的空间存放新生代尚存活对象, 由于新生代使用复
    制收集算法, 为了提升内存利用率, 只使用了其中一个Survivor作为轮换备份, 因此当出现大量对象在
    Minor GC后仍然存活的情况时, 就需要老年代进行分配担保, 让Survivor无法容纳的对象直接进入老年代,
    但前提是老年代需要有足够的空间容纳这些存活对象.
    但存活对象的大小在实际完成GC前是无法明确知道的, 因此Minor GC前, VM会先首先检查老年代连续空
    间是否大于新生代对象总大小或历次晋升的平均大小, 如果条件成立, 则进行Minor GC, 否则进行Full
    GC(让老年代腾出更多空间).
    然而取历次晋升的对象的平均大小也是有一定风险的, 如果某次Minor GC存活后的对象突增,远远高于平
    均值的话,依然可能导致担保失败(Handle Promotion Failure, 老年代也无法存放这些对象了), 此时就只
    好在失败后重新发起一次Full GC(让老年代腾出更多空间).
    3、标记-整理算法(老年代)
    标记清除算法会产生内存碎片问题, 而复制算法需要有额外的内存担保空间, 于是针对老年代的特点, 又有
    了标记整理算法. 标记整理算法的标记过程与标记清除算法相同, 但后续步骤不再对可回收对象直接清理,
    而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存.
  12. 你能说出来几个垃圾收集器
    1、Serial
    Serial收集器是Hotspot运行在Client模式下的默认新生代收集器, 它在进行垃圾收集时,会暂停所有的工
    作进程,用一个线程去完成GC工作
    特点:简单高效,适合jvm管理内存不大的情况(十兆到百兆)。
    2、Parnew
    ParNew收集器其实是Serial的多线程版本,回收策略完全一样,但是他们又有着不同。
    我们说了Parnew是多线程gc收集,所以它配合多核心的cpu效果更好,如果是一个cpu,他俩效果就差
    不多。(可用-XX:ParallelGCThreads参数控制GC线程数)
    3、Cms
    CMS(Concurrent Mark Sweep)收集器是一款具有划时代意义的收集器, 一款真正意义上的并发收集器,
    虽然现在已经有了理论意义上表现更好的G1收集器, 但现在主流互联网企业线上选用的仍是CMS(如
    Taobao),又称多并发低暂停的收集器。
    由他的英文组成可以看出,它是基于标记-清除算法实现的。整个过程分4个步骤:
  13. 初始标记(CMS initial mark):仅只标记一下GC Roots能直接关联到的对象, 速度很快
  14. 并发标记(CMS concurrent mark: GC Roots Tracing过程) 3. 重新标记(CMS remark):修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对
    象的标记记录
  15. 并发清除(CMS concurrent sweep: 已死对象将会就地释放)
    可以看到,初始标记、重新标记需要STW(stop the world 即:挂起用户线程)操作。因为最耗时的操作
    是并发标记和并发清除。所以总体上我们认为CMS的GC与用户线程是并发运行的。
    优点:并发收集、低停顿
    缺点:
  16. CMS默认启动的回收线程数=(CPU数目+3)*4
    当CPU数>4时, GC线程最多占用不超过25%的CPU资源, 但是当CPU数<=4时, GC线程可能就会过多
    的占用用户CPU资源, 从而导致应用程序变慢, 总吞吐量降低. 2. 无法清除浮动垃圾(GC运行到并发清除阶段时用户线程产生的垃圾),因为用户线程是需要内存
    的,如果浮动垃圾施放不及时,很可能就造成内存溢出,所以CMS不能像别的垃圾收集器那样等老
    年代几乎满了才触发,CMS提供了参数 -XX:CMSInitiatingOccupancyFraction 来设置GC触发百
    分比(1.6后默认92%),当然我们还得设置启用该策略 -XX:+UseCMSInitiatingOccupancyOnly 3. 因为CMS采用标记-清除算法,所以可能会带来很多的碎片,如果碎片太多没有清理,jvm会因为无
    法分配大对象内存而触发GC,因此CMS提供了 -XX:+UseCMSCompactAtFullCollection 参数,它
    会在GC执行完后接着进行碎片整理,但是又会有个问题,碎片整理不能并发,所以必须单线程去
    处理,所以如果每次GC完都整理用户线程stop的时间累积会很长,所以
    XX:CMSFullGCsBeforeCompaction 参数设置隔几次GC进行一次碎片整理(默认为0)。
    4、G1
    同优秀的CMS垃圾回收器一样,G1也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收
    集,官方也推荐使用G1来代替选择CMS。G1最大的特点是引入分区的思路,弱化分代的概念,合理利
    用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。
    因为每个区都有E、S、O代,所以在G1中,不需要对整个Eden等代进行回收,而是寻找可回收对象比较
    多的区,然后进行回收(虽然也需要STW操作,但是花费的时间是很少的),保证高效率。
    新生代收集
    G1的新生代收集跟ParNew类似,如果存活时间超过某个阈值,就会被转移到S/O区。
    年轻代内存由一组不连续的heap区组成, 这种方法使得可以动态调整各代区域的大小
    老年代收集
    分为以下几个阶段:
  17. 初始标记 (Initial Mark: Stop the World Event)
    在G1中, 该操作附着一次年轻代GC, 以标记Survivor中有可能引用到老年代对象的Regions.
  18. 扫描根区域 (Root Region Scanning: 与应用程序并发执行)
    扫描Survivor中能够引用到老年代的references. 但必须在Minor GC触发前执行完
  19. 并发标记 (Concurrent Marking : 与应用程序并发执行)
    在整个堆中查找存活对象, 但该阶段可能会被Minor GC中断
  20. 重新标记 (Remark : Stop the World Event)
    完成堆内存中存活对象的标记. 使用snapshot-at-the-beginning(SATB, 起始快照)算法, 比CMS所用
    算法要快得多(空Region直接被移除并回收, 并计算所有区域的活跃度).
  21. 清理 (Cleanup : Stop the World Event and Concurrent)
    在含有存活对象和完全空闲的区域上进行统计(STW)、擦除Remembered Sets(使用Remembered
    Set来避免扫描全堆,每个区都有对应一个Set用来记录引用信息、读写操作记录)(STW)、重置空
    regions并将他们返还给空闲列表(free list)(Concurrent)
  22. 简单描述一下(分代)垃圾回收的过程
    分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是
    2/3。
    新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占
    比是 8:1:1,它的执行流程如下:
    当年轻代中的Eden区分配满的时候,就会触发年轻代的GC(Minor GC)。具体过程如下:
    1、在Eden区执行了第一次GC之后,存活的对象会被移动到其中一个Survivor分区(以下简称from) 2、Eden区再次GC,这时会采用复制算法,将Eden和from区一起清理。存活的对象会被复制到to
    区。接下来,只需要清空from区就可以了
  23. 你都用过G1垃圾回收器的哪几个重要参数?
    最重要的是MaxGCPauseMillis,可以通过它设定G1的目标停顿时间,它会尽量的去达成这个目标。
    G1HeapRegionSize可以设置小堆区的大小,一般是2的次幂。
    InitiatingHeapOccupancyPercent,启动并发GC时的堆内存占用百分比。G1用它来触发并发GC周期,
    基于整个堆的使用率,而不只是某一代内存的使用比例,默认是45%。
    再多?不是专家,就没必要要求别人也是。
  24. 有什么堆外内存的排查思路?
    进程占用的内存,可以使用top命令,看RES段占用的值。如果这个值大大超出我们设定的最大堆内存,
    则证明堆外内存占用了很大的区域。
    使用gdb可以将物理内存dump下来,通常能看到里面的内容。更加复杂的分析可以使用perf工具,或者
    谷歌开源的gperftools。那些申请内存最多的native函数,很容易就可以找到。
  25. 串行(serial)收集器和吞吐量(throughput)收集器的区别
    吞吐量收集器使用并行版本的新生代垃圾收集器,它用于中等规模和大规模数据的应用程序。 而串行收
    集器对大多数的小应用(在现代处理器上需要大概 100M 左右的内存)就足够了。
  26. GC日志的real、user、sys是什么意思?
    1、real 实际花费的时间,指的是从开始到结束所花费的时间。比如进程在等待I/O完成,这个阻塞时间
    也会被计算在内。
    2、user 指的是进程在用户态(User Mode)所花费的时间,只统计本进程所使用的时间,是指多核。
    3、sys 指的是进程在核心态(Kernel Mode)花费的CPU时间量,指的是内核中的系统调用所花费的时
    间,只统计本进程所使用的时间。
    这个是用来看日志用的,如果你不看日志,那不了解也无妨。不过,这三个参数的意义,在你能看到的
    地方,基本上都是一致的,比如操作系统。
  27. GC日志分析
    摘录GC日志一部分(前部分为年轻代gc回收;后部分为full gc回收):
    通过上面日志分析得出,PSYoungGen、ParOldGen、PSPermGen属于Parallel收集器。其中
    PSYoungGen表示gc回收前后年轻代的内存变化;ParOldGen表示gc回收前后老年代的内存变化;
    PSPermGen表示gc回收前后永久区的内存变化。young gc 主要是针对年轻代进行内存回收比较频繁,
    耗时短;full gc 会对整个堆内存进行回城,耗时长,因此一般尽量减少full gc的次数
  28. MinorGC,MajorGC、FullGC都什么时候发生?
    MinorGC在年轻代空间不足的时候发生,MajorGC指的是老年代的GC,出现MajorGC一般经常伴有
    MinorGC。
    FullGC有三种情况。
    1、当老年代无法再分配内存的时候
    2、元空间不足的时候
    3、显示调用System.gc的时候。另外,像CMS一类的垃圾回收器,在MinorGC出现promotion failure
    的时候也会发生FullGC
  29. 新生代、老年代、持久代都存储哪些东西
    1、新生代:
  30. 方法中new一个对象,就会先进入新生代。
    2、老年代:
  31. 新生代中经历了N次垃圾回收仍然存活的对象就会被放到老年代中。
  32. 大对象一般直接放入老年代。
  33. 当Survivor空间不足。需要老年代担保一些空间,也会将对象放入老年代。
    3、永久代:
    指的就是方法区。
    2016-07-05T10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs] 2016-07-05T10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]
  34. 对象是怎么从年轻代进入老年代的?
    这是老掉牙的题目了。在下面四种情况下,对象会从年轻代进入老年代。
    1、如果对象够老,会通过提升(Promotion)进入老年代,这一般是根据对象的年龄进行判断的。
    2、动态对象年龄判定。有的垃圾回收算法,比如G1,并不要求age必须达到15才能晋升到老年代,它
    会使用一些动态的计算方法。
    3、分配担保。当 Survivor 空间不够的时候,就需要依赖其他内存(指老年代)进行分配担保。这个时
    候,对象也会直接在老年代上分配。
    4、超出某个大小的对象将直接在老年代分配。不过这个值默认为0,意思是全部首选Eden区进行分配。
  35. 可达性算法中,哪些对象可作为GC Roots对象。
    1、虚拟机栈中引用的对象
    2、方法区静态成员引用的对象
    3、方法区常量引用对象
    4、本地方法栈JNI引用的对象
  36. Java中会存在内存泄漏吗,请简单描述。
    先解释什么是内存泄漏:
    所谓内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中。java中有垃圾回收机制,
    它可以保证当对象不再被引用的时候,对象将自动被垃圾回收器从内存中清除掉。
    由于Java使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如有两个对象,相互引
    用,只要它们和根进程不可达,那么GC也是可以回收它们的。
    java中的内存泄露的情况:
    长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需
    要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景,通
    俗地说,就是程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这
    个对象无用但是却无法被垃圾回收器回收的,这就是java中可能出现内存泄露的情况,例如,缓存系
    统,我们加载了一个对象放在缓存中(例如放在一个全局map对象中),然后一直不再使用它,这个对象
    一直被缓存引用,但却不再被使用。
  37. 什么时候进行MinGC和FullGC
    MinGC:
  38. 当Eden区满时,触发Minor GC.
    FullGC:
  39. 调用System.gc时,系统建议执行Full GC,但是不必然执行
  40. 老年代空间不足
  41. 方法区空间不足
  42. 通过Minor GC后进入老年代的平均大小大于老年代的剩余空间
  43. 堆中分配很大的对象,而老年代没有足够的空间
  44. System.gc() 和 Runtime.gc() 会做什么事情?
    这两个方法用来提示 JVM 要进行垃圾回收。但是,立即开始还是延迟进行垃圾回收是取决于 JVM 的。
  45. 垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存
    吗?有什么办法主动通知虚拟机进行垃圾回收?
    对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采
    用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象
    是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。
    程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
  46. 垃圾收集算法
    GC最基础的算法有三种: 标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采
    用分代收集算法。
    1、标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个
    阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
    2、复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使
    用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过
    的内存空间一次清理掉。
    3、标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清
    理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
    4、分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样
    就可以根据各个年代的特点采用最适当的收集算法。
  47. 调优命令有哪些?
    Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo
    1、jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
    2、jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进
    程中的类装载、内存、垃圾收集、JIT编译等运行数据。
    3、jmap,JVM Memory Map命令用于生成heap dump文件
    4、jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一
    个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
    5、jstack,用于生成java虚拟机当前时刻的线程快照。
    6、jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。
  48. 你知道哪些JVM性能调优
    设定堆内存大小
    -Xmx:堆内存最大限制。
    设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代
    -XX:NewSize:新生代大小
    -XX:NewRatio 新生代和老生代占比
    -XX:SurvivorRatio:伊甸园空间和幸存者空间的占比
    设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC
  49. 常用的调优工具有哪些
    常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer
    Tool)、GChisto。 1、jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和
    管理控制台,用于对JVM中内存,线程和类等的监控
    2、jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
    3、MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java
    heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
    4、GChisto,一款专业分析gc日志的工具
  50. 跟JVM内存相关的几个核心参数图解
  51. OOM你遇到过哪些情况,SOF你遇到过哪些情况
    OOM: 1、OutOfMemoryError异常
    除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可
    能。
    Java Heap 溢出:
    一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess
    java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免
    垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。
    出现这种异常,一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转
    存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是
    内存溢出(Memory Overflow)。
    如果是内存泄漏,可进一步通过工具查看泄漏对象到GCRoots的引用链。于是就能找到泄漏对象是通过
    怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。
    如果不存在泄漏,那就应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。
    2、虚拟机栈和本地方法栈溢出
    如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
    如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
    这里需要注意当栈的大小越大可分配的线程数就越少。
    3、运行时常量池溢出
    异常信息:java.lang.OutOfMemoryError:PermGenspace
    如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作
    用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,
    将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区
    内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容
    量。
    4、方法区溢出
    方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。也有可能是
    方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。
    异常信息:java.lang.OutOfMemoryError:PermGenspace
    方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在
    经常动态生成大量Class的应用中,要特别注意这点。
    SOF(堆栈溢出StackOverflow):
    StackOverflowError 的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。
    因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超
    过1m而导致溢出。
    栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、List、map数据过大。
  52. finalize() 方法什么时候被调用?析构函数 (finalization) 的目的
    是什么?
    垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的 finalize() 方法 但是在 Java 中
    很不幸,如果内存总是充足的,那么垃圾回收可能永远不会进行,也就是说 filalize() 可能永远不被执
    行,显然指望它做收尾工作是靠不住的。
    那么finalize() 究竟是做什么的呢?
    它最主要的用途是回收特殊渠道申请的内存。Java 程序有垃圾回收器,所以一般情况下内存问题不用程
    序员操心。但有一种 JNI(Java Native Interface)调用non-Java 程序(C 或 C++), finalize() 的工作
    就是回收这部分的内存。
  53. 你都有哪些手段用来排查内存溢出?
    (这个话题很大,可以从实践环节中随便摘一个进行总结,下面举例一个最普通的)
    你可以来一个中规中矩的回答:
    内存溢出包含很多种情况,我在平常工作中遇到最多的就是堆溢出。有一次线上遇到故障,重新启动
    后,使用jstat命令,发现Old区在一直增长。我使用jmap命令,导出了一份线上堆栈,然后使用MAT进
    行分析。通过对GC Roots的分析,我发现了一个非常大的HashMap对象,这个原本是有位同学做缓存
    用的,但是一个无界缓存,造成了堆内存占用一直上升。后来,将这个缓存改成 guava的Cache,并设
    置了弱引用,故障就消失了。
    这个回答不是十分出彩,但着实是常见问题,让人挑不出毛病。
  54. 生产上如何配置垃圾收集器的?
    首先是内存大小问题,基本上每一个内存区域我都会设置一个上限,来避免溢出问题,比如元空间。通
    常,堆空间我会设置成操作系统的2/3(这是想给其他进程和操作系统预留一些时间),超过8GB的堆优
    先选用G1。
    接下来,我会对JVM进行初步优化。比如根据老年代的对象提升速度,来调整年轻代和老年代之间的比
    例。
    再接下来,就是专项优化,主要判断的依据就是系统容量、访问延迟、吞吐量等。我们的服务是高并发
    的,所以对STW的时间非常敏感。
    我会通过记录详细的GC日志,来找到这个瓶颈点,借用gceasy(重点)这样的日志分析工具,很容易定
    位到问题。之所以选择采用工具,是因为gc日志看起来实在是太麻烦了,gceasy号称是AI学习分析问
    题,可视化做的较好。
  55. 假如生产环境CPU占用过高,请谈谈你的分析思路和定位。
    这个可真是太太太常见了,不过已经烂大街了。如果你还是一个有经验的开发者,不知道的话,需要反
    省一下了。
    首先,使用top -H命令获取占用CPU最高的线程,并将它转化为16进制。
    然后,使用jstack命令获取应用的栈信息,搜索这个16进制。这样能够方便的找到引起CPU占用过高的
    具体原因。
    如果有条件的话,直接使用arthas就行操作就好了,不用再做这些费事费力的操作。
  56. 对于JDK自带的监控和性能分析工具用过哪些?
    jps:用来显示Java进程;
    jstat:用来查看GC;
    jmap:用来dump堆;
    jstack:用来dump栈;
    jhsdb:用来查看执行中的内存信息;
    都是非常常用的工具,要熟练掌握。因为线上环境通常都有很多限制,用不了图形化工具。当出现这些
    情况,上面的命令就是救命的。
  57. 栈帧都有哪些数据?
    JVM的运行是基于栈的,和C语言的栈类似,它的大多数数据都是在堆里面的,只有少部分运行时的数据
    存在于栈上。
    在JVM中,每个线程栈里面的元素,就叫栈帧。
    栈帧包含:局部变量表、操作数栈、动态连接、返回地址等。
  58. JIT是什么?
    为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并
    进行各种层次的优化。完成这个任务的编译器,就称为即时编译器(Just In Time Compiler),简称 JIT
    编译器。
  59. Java的双亲委托机制是什么?
    它的意思是,除了顶层的启动类加载器以外,其余的类加载器,在加载之前,都会委派给它的父加载器
    进行加载。这样一层层向上传递,直到祖先们都无法胜任,它才会真正的加载。
    Java默认是这种行为。当然Java中也有很多打破双亲行为的骚操作,比如SPI(JDBC驱动加载),OSGI
    等。
  60. 有哪些打破了双亲委托机制的案例?
    1、Tomcat可以加载自己目录下的class文件,并不会传递给父类的加载器。
    2、Java的SPI,发起者是BootstrapClassLoader,BootstrapClassLoader已经是最上层的了。它直接获
    取了AppClassLoader进行驱动加载,和双亲委派是相反的。。
  61. invokedynamic指令是干什么的?
    属于比较高级的题目。没看过虚拟机的一般是不知道的。所以如果你不太熟悉,不要气馁,加油!(小
    拳拳锤你胸口)。
    invokedynamic是Java7之后新加入的字节码指令,使用它可以实现一些动态类型语言的功能。我们使用
    的Lambda表达式,在字节码上就是invokedynamic指令实现的。它的功能有点类似反射,但它是使用
    方法句柄实现的,执行效率更高。
  62. safepoint是什么?
    STW并不会只发生在内存回收的时候。现在程序员这么卷,碰到几次safepoint的问题几率也是比较大
    的。
    当发生GC时,用户线程必须全部停下来,才可以进行垃圾回收,这个状态我们可以认为JVM是安全的
    (safe),整个堆的状态是稳定的。
    如果在GC前,有线程迟迟进入不了safepoint,那么整个JVM都在等待这个阻塞的线程,造成了整体GC
    的时间变长。
    数据结构与算法
  63. 什么是数据结构?
    简单地说,数据结构是以某种特定的布局方式存储数据的容器。这种“布局方式”决定了数据结构对于某
    些操作是高效的,而对于其他操作则是低效的。首先我们需要理解各种数据结构,才能在处理实际问题
    时选取最合适的数据结构。
  64. 为什么我们需要数据结构?
    数据是计算机科学当中最关键的实体,而数据结构则可以将数据以某种组织形式存储,因此,数据结构
    的价值不言而喻。
    无论你以何种方式解决何种问题,你都需要处理数据——无论是涉及员工薪水、股票价格、购物清单,
    还是只是简单的电话簿问题。
    数据需要根据不同的场景,按照特定的格式进行存储。有很多数据结构能够满足以不同格式存储数据的
    需求。
  65. 常见的数据结构
    首先列出一些最常见的数据结构,我们将逐一说明:
    数组

    队列
    链表
    树图
    字典树(这是一种高效的树形结构,但值得单独说明)
    散列表(哈希表)
  66. 数组
    数组是最简单、也是使用最广泛的数据结构。栈、队列等其他数据结构均由数组演变而来。下图是一个
    包含元素(1,2,3和4)的简单数组,数组长度为4。
    每个数据元素都关联一个正数值,我们称之为索引,它表明数组中每个元素所在的位置。大部分语言将
    初始索引定义为零。关注Java技术栈微信公众号,回复"面试"获取更多博主精心整理的面试题。
    以下是数组的两种类型:
    一维数组(如上所示)
    多维数组(数组的数组)
    数组的基本操作
    Insert——在指定索引位置插入一个元素
    Get——返回指定索引位置的元素
    Delete——删除指定索引位置的元素
    Size——得到数组所有元素的数量
    面试中关于数组的常见问题
    寻找数组中第二小的元素
    找到数组中第一个不重复出现的整数
    合并两个有序数组
    重新排列数组中的正值和负值

  67. 著名的撤销操作几乎遍布任意一个应用。但你有没有思考过它是如何工作的呢?这个问题的解决思路是
    按照将最后的状态排列在先的顺序,在内存中存储历史工作状态(当然,它会受限于一定的数量)。这
    没办法用数组实现。但有了栈,这就变得非常方便了。
    可以把栈想象成一列垂直堆放的书。为了拿到中间的书,你需要移除放置在这上面的所有书。这就是
    LIFO(后进先出)的工作原理。
    下图是包含三个数据元素(1,2和3)的栈,其中顶部的3将被最先移除:
    栈的基本操作
    Push——在顶部插入一个元素
    Pop——返回并移除栈顶元素
    isEmpty——如果栈为空,则返回true
    Top——返回顶部元素,但并不移除它
    面试中关于栈的常见问题
    使用栈计算后缀表达式
    对栈的元素进行排序
    判断表达式是否括号平衡
  68. 队列
    与栈相似,队列是另一种顺序存储元素的线性数据结构。栈与队列的最大差别在于栈是LIFO(后进先
    出),而队列是FIFO,即先进先出。
    一个完美的队列现实例子:售票亭排队队伍。如果有新人加入,他需要到队尾去排队,而非队首——排
    在前面的人会先拿到票,然后离开队伍。
    下图是包含四个元素(1,2,3和4)的队列,其中在顶部的1将被最先移除:
    移除先入队的元素、插入新元素
    队列的基本操作
    Enqueue() —— 在队列尾部插入元素
    Dequeue() ——移除队列头部的元素
    isEmpty()——如果队列为空,则返回true
    Top() ——返回队列的第一个元素
    面试中关于队列的常见问题
    使用队列表示栈
    对队列的前k个元素倒序
    使用队列生成从1到n的二进制数
  69. 链表
    链表是另一个重要的线性数据结构,乍一看可能有点像数组,但在内存分配、内部结构以及数据插入和
    删除的基本操作方面均有所不同。关注Java技术栈微信公众号,回复"面试"获取更多博主精心整理的面
    试题。
    链表就像一个节点链,其中每个节点包含着数据和指向后续节点的指针。 链表还包含一个头指针,它指
    向链表的第一个元素,但当列表为空时,它指向null或无具体内容。
    链表一般用于实现文件系统、哈希表和邻接表。
    这是链表内部结构的展示:
    链表包括以下类型:
    单链表(单向)
    双向链表(双向)
    链表的基本操作:
    InsertAtEnd - 在链表的末尾插入指定元素
    InsertAtHead - 在链接列表的开头/头部插入指定元素
    Delete - 从链接列表中删除指定元素
    DeleteAtHead - 删除链接列表的第一个元素
    Search - 从链表中返回指定元素
    isEmpty - 如果链表为空,则返回true
    面试中关于链表的常见问题
    反转链表
    检测链表中的循环
    返回链表倒数第N个节点
    删除链表中的重复项

  70. 图是一组以网络形式相互连接的节点。节点也称为顶点。 一对节点(x,y)称为边(edge),表示顶点
    x连接到顶点y。边可以包含权重/成本,显示从顶点x到y所需的成本。
    图的类型
    无向图
    有向图
    在程序语言中,图可以用两种形式表示:
    邻接矩阵
    邻接表
    常见图遍历算法
    广度优先搜索
    深度优先搜索
    面试中关于图的常见问题
    实现广度和深度优先搜索
    检查图是否为树
    计算图的边数
    找到两个顶点之间的最短路径

  71. 树形结构是一种层级式的数据结构,由顶点(节点)和连接它们的边组成。 树类似于图,但区分树和图
    的重要特征是树中不存在环路。
    树形结构被广泛应用于人工智能和复杂算法,它可以提供解决问题的有效存储机制。
    这是一个简单树的示意图,以及树数据结构中使用的基本术语:
    Root - 根节点
    Parent - 父节点
    Child - 子节点
    Leaf - 叶子节点
    Sibling - 兄弟节点
    以下是树形结构的主要类型:
    N元树
    平衡树
    二叉树
    二叉搜索树
    AVL树
    红黑树
    2-3树
    其中,二叉树和二叉搜索树是最常用的树。
    面试中关于树结构的常见问题:
    求二叉树的高度
    在二叉搜索树中查找第k个最大值
    查找与根节点距离k的节点
    在二叉树中查找给定节点的祖先节点
  72. 字典树(Trie)
    字典树,也称为“前缀树”,是一种特殊的树状数据结构,对于解决字符串相关问题非常有效。它能够提
    供快速检索,主要用于搜索字典中的单词,在搜索引擎中自动提供建议,甚至被用于IP的路由。
    以下是在字典树中存储三个单词“top”,“so”和“their”的例子:
    这些单词以顶部到底部的方式存储,其中绿色节点“p”,“s”和“r”分别表示“top”,“thus”和“theirs”的底
    部。
    面试中关于字典树的常见问题
    计算字典树中的总单词数
    打印存储在字典树中的所有单词
    使用字典树对数组的元素进行排序
    使用字典树从字典中形成单词
    构建T9字典(字典树+ DFS )
  73. 哈希表
    哈希法(Hashing)是一个用于唯一标识对象并将每个对象存储在一些预先计算的唯一索引(称为“键 (key)”)中的过程。因此,对象以键值对的形式存储,这些键值对的集合被称为“字典”。可以使用键
    搜索每个对象。基于哈希法有很多不同的数据结构,但最常用的数据结构是哈希表。
    哈希表通常使用数组实现。
    散列数据结构的性能取决于以下三个因素:
    哈希函数
    哈希表的大小
    碰撞处理方法
    下图为如何在数组中映射哈希键值对的说明。该数组的索引是通过哈希函数计算的。
    面试中关于哈希结构的常见问题:
    在数组中查找对称键值对
    追踪遍历的完整路径
    查找数组是否是另一个数组的子集
    检查给定的数组是否不相交
  74. 冒泡排序
    定义一个布尔变量 hasChange ,用来标记每轮是否进行了交换。在每轮遍历开始时,将 hasChange 设
    置为 false。
    若当轮没有发生交换,说明此时数组已经按照升序排列, hashChange 依然是为 false。此时外层循环直
    接退出,排序结束。
    代码示例
    import java.util.Arrays; public class BubbleSort { private static void bubbleSort(int[] nums) { boolean hasChange = true; for (int i = 0, n = nums.length; i < n - 1 && hasChange; ++i) { hasChange = false; for (int j = 0; j < n - i - 1; ++j) { if (nums[j] > nums[j + 1]) { swap(nums, j, j + 1); hasChange = true; } } } }
    算法分析
    空间复杂度 O(1)、时间复杂度 O(n²)。
    分情况讨论:
  75. 给定的数组按照顺序已经排好:只需要进行 n-1 次比较,两两交换次数为 0,时间复杂度为
    O(n),这是最好的情况。
  76. 给定的数组按照逆序排列:需要进行 n*(n-1)/2 次比较,时间复杂度为 O(n²),这是最坏的情况。
  77. 给定的数组杂乱无章。在这种情况下,平均时间复杂度 O(n²)。
    因此,时间复杂度是 O(n²),这是一种稳定的排序算法。
    稳定是指,两个相等的数,在排序过后,相对位置保持不变。
  78. 插入排序
    先来看一个问题。一个有序的数组,我们往里面添加一个新的数据后,如何继续保持数据有序呢?很简
    单,我们只要遍历数组,找到数据应该插入的位置将其插入即可。
    这是一个动态排序的过程,即动态地往有序集合中添加数据,我们可以通过这种方法保持集合中的数据
    一直有序。而对于一组静态数据,我们也可以借鉴上面讲的插入方法,来进行排序,于是就有了插入排
    序算法。
    那么插入排序具体是如何借助上面的思想来实现排序的呢?
    首先,我们将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,
    就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插
    入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法
    结束。
    与冒泡排序对比:
    在冒泡排序中,经过每一轮的排序处理后,数组后端的数是排好序的。
    在插入排序中,经过每一轮的排序处理后,数组前端的数是排好序的。
    代码示例
    private static void swap(int[] nums, int i, int j) { int t = nums[i]; nums[i] = nums[j]; nums[j] = t; }public static void main(String[] args) { int[] nums = {1, 2, 7, 9, 5, 8}; bubbleSort(nums); System.out.println(Arrays.toString(nums)); } }import java.util.Arrays; public class InsertionSort { private static void insertionSort(int[] nums) { for (int i = 1, j, n = nums.length; i < n; ++i) {
    算法分析
    空间复杂度 O(1),时间复杂度 O(n²)。
    分情况讨论:
  79. 给定的数组按照顺序排好序:只需要进行 n-1 次比较,两两交换次数为 0,时间复杂度为 O(n),这
    是最好的情况。
  80. 给定的数组按照逆序排列:需要进行 n*(n-1)/2 次比较,时间复杂度为 O(n²),这是最坏的情况。
  81. 给定的数组杂乱无章:在这种情况下,平均时间复杂度是 O(n²)。
    因此,时间复杂度是 O(n²),这也是一种稳定的排序算法。
  82. 选择排序
    选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未
    排序区间中找到最小的元素,将其放到已排序区间的末尾。
    代码示例
    int num = nums[i]; for (j = i - 1; j >=0 && nums[j] > num; --j) { nums[j + 1] = nums[j]; }nums[j + 1] = num; } }public static void main(String[] args) { int[] nums = {1, 2, 7, 9, 5, 8}; insertionSort(nums); System.out.println(Arrays.toString(nums)); } }import java.util.Arrays; public class SelectionSort { private static void selectionSort(int[] nums) { for (int i = 0, n = nums.length; i < n - 1; ++i) { int minIndex = i; for (int j = i; j < n; ++j) { if (nums[j] < nums[minIndex]) { minIndex = j; } }swap(nums, minIndex, i); } }private static void swap(int[] nums, int i, int j) { int t = nums[i]; nums[i] = nums[j]; nums[j] = t; }
    算法分析
    空间复杂度 O(1),时间复杂度 O(n²)。
    那选择排序是稳定的排序算法吗?
    答案是否定的,选择排序是一种不稳定的排序算法。选择排序每次都要找剩余未排序元素中的最小值,
    并和前面的元素交换位置,这样破坏了稳定性。
    比如 5,8,5,2,9 这样一组数据,使用选择排序算法来排序的话,第一次找到最小元素 2,与第一个
    5 交换位置,那第一个 5 和中间的 5 顺序就变了,所以就不稳定了。正是因此,相对于冒泡排序和插入
    排序,选择排序就稍微逊色了。
  83. 归并排序
    归并排序的核心思想是分治,把一个复杂问题拆分成若干个子问题来求解。
    归并排序的算法思想是:把数组从中间划分为两个子数组,一直递归地把子数组划分成更小的数组,直
    到子数组里面只有一个元素的时候开始排序。排序的方法就是按照大小顺序合并两个元素。接着依次按
    照递归的顺序返回,不断合并排好序的数组,直到把整个数组排好序。
    代码示例
    public static void main(String[] args) { int[] nums = {1, 2, 7, 9, 5, 8}; selectionSort(nums); System.out.println(Arrays.toString(nums)); } }import java.util.Arrays; public class MergeSort { private static void merge(int[] nums, int low, int mid, int high, int[] temp) { int i = low, j = mid + 1, k = low; while (k <= high) { if (i > mid) { temp[k++] = nums[j++]; } else if (j > high) { temp[k++] = nums[i++]; } else if (nums[i] <= nums[j]) { temp[k++] = nums[i++]; } else { temp[k++] = nums[j++]; } }System.arraycopy(tmp, low, nums, low, high - low + 1); }private static void mergeSort(int[] nums, int low, int high, int[] temp) { if (low >= high) { return; }
    算法分析
    空间复杂度 O(n),时间复杂度 O(nlogn)。
    对于规模为 n 的问题,一共要进行 log(n) 次的切分,每一层的合并复杂度都是 O(n),所以整体时间复杂
    度为 O(nlogn)。
    由于合并 n 个元素需要分配一个大小为 n 的额外数组,所以空间复杂度为 O(n)。
    这是一种稳定的排序算法。
  84. 快速排序
    快速排序也采用了分治的思想:把原始的数组筛选成较小和较大的两个子数组,然后递归地排序两个子
    数组。
    代码示例
    int mid = (low + high) >>> 1; mergeSort(nums, low, mid, temp); mergeSort(nums, mid + 1, high, temp); merge(nums, low, mid, high, temp); }private static void mergeSort(int[] nums) { int n = nums.length; int[] temp = new int[n]; mergeSort(nums, 0, n - 1, temp); }public static void main(String[] args) { int[] nums = {1, 2, 7, 4, 5, 3}; mergeSort(nums); System.out.println(Arrays.toString(nums)); } }import java.util.Arrays; public class QuickSort { private static void quickSort(int[] nums) { quickSort(nums, 0, nums.length - 1); }private static void quickSort(int[] nums, int low, int high) { if (low >= high) { return; }int[] p = partition(nums, low, high); quickSort(nums, low, p[0] - 1); quickSort(nums, p[0] + 1, high); }private static int[] partition(int[] nums, int low, int high) { int less = low - 1, more = high; while (low < more) {
    if (nums[low] < nums[high]) { swap(nums, ++less, low++); } else if (nums[low] > nums[high]) { swap(nums, --more, low); } else { ++low; } }swap(nums, more, high); return new int[] {less + 1, more}; }private static void swap(int[] nums, int i, int j) { int t = nums[i]; nums[i] = nums[j]; nums[j] = t; }public static void main(String[] args) { int[] nums = {1, 2, 7, 4, 5, 3}; quickSort(nums); System.out.println(Arrays.toString(nums)); } }
    算法分析
    空间复杂度 O(logn),时间复杂度 O(nlogn)。
    对于规模为 n 的问题,一共要进行 log(n) 次的切分,和基准值进行 n-1 次比较,n-1 次比较的时间复杂
    度是 O(n),所以快速排序的时间复杂度为 O(nlogn)。
    但是,如果每次在选择基准值的时候,都不幸地选择了子数组里的最大或最小值。即每次把把数组分成
    了两个更小长度的数组,其中一个长度为 1,另一个的长度是子数组的长度减 1。这样的算法复杂度变
    成 O(n²)。
    和归并排序不同,快速排序在每次递归的过程中,只需要开辟 O(1) 的存储空间来完成操作来实现对数组
    的修改;而递归次数为 logn,所以它的整体空间复杂度完全取决于压堆栈的次数。
    如何优化快速排序?
    前面讲到,最坏情况下快速排序的时间复杂度是 O(n²),实际上,这种 O(n²) 时间复杂度出现的主要原因
    还是因为我们基准值选得不够合理。最理想的基准点是:被基准点分开的两个子数组中,数据的数量差
    不多。
    如果很粗暴地直接选择第一个或者最后一个数据作为基准值,不考虑数据的特点,肯定会出现之前讲的
    那样,在某些情况下,排序的最坏情况时间复杂度是 O(n²)。
    有两个比较常用的分区算法。
  85. 三数取中法
    我们从区间的首、尾、中间,分别取出一个数,然后对比大小,取这 3 个数的中间值作为分区点。这样
    每间隔某个固定的长度,取数据出来比较,将中间值作为分区点的分区算法,肯定要比单纯取某一个数
    据更好。但是,如果要排序的数组比较大,那“三数取中”可能就不够了,可能要“五数取中”或者“十数取
    中”。2. 随机法
    随机法就是每次从要排序的区间中,随机选择一个元素作为分区点。这种方法并不能保证每次分区点都
    选的比较好,但是从概率的角度来看,也不大可能会出现每次分区点都选的很差的情况,所以平均情况
    下,这样选的分区点是比较好的。时间复杂度退化为最糟糕的 O(n²) 的情况,出现的可能性不大。
  86. 二分查找
    二分查找是一种非常高效的查找算法,高效到什么程度呢?我们来分析一下它的时间复杂度。
    假设数据大小是 n,每次查找后数据都会缩小为原来的一半,也就是会除以 2。最坏情况下,直到查找
    区间被缩小为空,才停止。
    被查找区间的大小变化为:
    可以看出来,这是一个等比数列。其中 n/(2^k)=1 时,k 的值就是总共缩小的次数。而每一次缩小操
    作只涉及两个数据的大小比较,所以,经过了 k 次区间缩小操作,时间复杂度就是 O(k)。通过
    n/(2^k)=1 ,我们可以求得 k=log2n ,所以时间复杂度就是 O(logn)。
    代码示例
    注意容易出错的 3 个地方。
  87. 循环退出条件是 low <= high ,而不是 low < high ; 2. mid 的取值,可以是 mid = (low + high) / 2 ,但是如果 low 和 high 比较大的话, low + high 可能会溢出,所以这里写为 mid = (low + high) >>> 1 ; 3. low 和 high 的更新分别为 low = mid + 1 、 high = mid - 1 。
    非递归实现:
    n, n/2, n/4, n/8, …, n/(2^k) public class BinarySearch { private static int search(int[] nums, int low, int high, int val) { while (low <= high) { int mid = (low + high) >>> 1; if (nums[mid] == val) { return mid; } else if (nums[mid] < val) { low = mid + 1; } else { high = mid - 1; } }return -1; }/*** 二分查找(非递归) ** @param nums 有序数组 * @param val 要查找的值 * @return 要查找的值在数组中的索引位置 / private static int search(int[] nums, int val) {
    return search(nums, 0, nums.length - 1, val); }public static void main(String[] args) { int[] nums = {1, 2, 5, 7, 8, 9}; // 非递归查找 int r1 = search(nums, 7); System.out.println(r1); } }
    递归实现:
    public class BinarySearch { private static int searchRecursive(int[] nums, int low, int high, int val) { while (low <= high) { int mid = (low + high) >>> 1; if (nums[mid] == val) { return mid; } else if (nums[mid] < val) { return searchRecursive(nums, mid + 1, high, val); } else { return searchRecursive(nums, low, mid - 1, val); } }return -1; }/
    ** 二分查找(递归) ** @param nums 有序数组 * @param val 要查找的值 * @return 要查找的值在数组中的索引位置 */ private static int searchRecursive(int[] nums, int val) { return searchRecursive(nums, 0, nums.length - 1, val); }public static void main(String[] args) { int[] nums = {1, 2, 5, 7, 8, 9}; // 递归查找 int r2 = searchRecursive(nums, 7); System.out.println(r2); } }
  88. 二分查找 II
    前面讲的二分查找算法,是最为简单的一种,在不存在重复元素的有序数组中,查找值等于给定值的元
    素。
    接下来,我们来看看二分查找算法四种常见的变形问题,分别是:
  89. 查找第一个值等于给定值的元素
  90. 查找最后一个值等于给定值的元素
  91. 查找第一个大于等于给定值的元素
  92. 查找最后一个小于等于给定值的元素
    1、查找第一个值等于给定值的元素
    2、查找最后一个值等于给定值的元素
    public static int search(int[] nums, int val) { int n = nums.length; int low = 0, high = n - 1; while (low <= high) { int mid = (low + high) >>> 1; if (nums[mid] < val) { low = mid + 1; } else if (nums[mid] > val) { high = mid - 1; } else { // 如果nums[mid]是第一个元素,或者nums[mid-1]不等于val // 说明nums[mid]就是第一个值为给定值的元素 if (mid == 0 || nums[mid - 1] != val) { return mid; }high = mid - 1; } }return -1; }public static int search(int[] nums, int val) { int n = nums.length; int low = 0, high = n - 1; while (low <= high) { int mid = (low + high) >>> 1; if (nums[mid] < val) {
    low = mid + 1; } else if (nums[mid] > val) { high = mid - 1; } else { // 如果nums[mid]是最后一个元素,或者nums[mid+1]不等于val // 说明nums[mid]就是最后一个值为给定值的元素 if (mid == n - 1 || nums[mid + 1] != val) { return mid; }low = mid + 1; } }return -1; } 3、查找第一个大于等于给定值的元素
    public static int search(int[] nums, int val) { int low = 0, high = nums.length - 1; while (low <= high) { int mid = (low + high) >>> 1; if (nums[mid] < val) { low = mid + 1; } else { // 如果nums[mid]是第一个元素,或者nums[mid-1]小于val // 说明nums[mid]就是第一个大于等于给定值的元素 if (mid == 0 || nums[mid - 1] < val) { return mid; }high = mid - 1; } }return -1; } 4、查找最后一个小于等于给定值的元素
    public static int search(int[] nums, int val) { int n = nums.length; int low = 0, high = n - 1; while (low <= high) { int mid = (low + high) >>> 1; if (nums[mid] > val) { high = mid - 1; } else { // 如果nums[mid]是最后一个元素,或者nums[mid+1]大于val // 说明nums[mid]就是最后一个小于等于给定值的元素 if (mid == n - 1 || nums[mid + 1] > val) { return mid; }low = mid + 1; } }
  93. 删除排序数组中的重复项
    题目描述
    给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的
    新长度。
    不要使用额外的数组空间,你必须在 原地修改输入数组 并在使用 O(1) 额外空间的条件下完成。
    示例 1:
    示例 2:
    说明:
    为什么返回数值是整数,但输出的答案是数组呢?
    请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
    return -1; }给定数组 nums = [1,1,2], 函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。 你不需要考虑数组中超出新长度后面的元素。 给定 nums = [0,0,1,1,1,2,2,3,3,4], 函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。 你不需要考虑数组中超出新长度后面的元素。 class Solution { public int removeDuplicates(int[] nums) { int cnt = 0, n = nums.length; for (int i = 1; i < n; ++i) { if (nums[i] == nums[i - 1]) ++cnt; else nums[i - cnt] = nums[i]; }return n - cnt; } }
  94. 删除排序数组中的重复项 II
    给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组
    的新长度。
    不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
    示例 1:
    示例 2:
    说明:
    为什么返回数值是整数,但输出的答案是数组呢?
    请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
    你可以想象内部操作如下:
    解法
    从数组下标 1 开始遍历数组。
    用计数器 cnt 记录当前数字重复出现的次数, cnt 的最小计数为 0;用 cur 记录新数组下个待覆盖的
    元素位置。
    遍历时,若当前元素 nums[i] 与上个元素 nums[i-1] 相同,则计数器 +1,否则计数器重置为 0。如
    果计数器小于 2,说明当前元素 nums[i] 可以添加到新数组中,即: nums[cur] = nums[i] ,同时
    cur++ 。
    遍历结果,返回 cur 值即可。
    给定 nums = [1,1,1,2,2,3], 函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3 。 你不需要考虑数组中超出新长度后面的元素。 给定 nums = [0,0,1,1,1,1,2,3,3], 函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3 。 你不需要考虑数组中超出新长度后面的元素。 // nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝 int len = removeDuplicates(nums); // 在函数里修改输入数组对于调用者是可见的。 // 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。 for (int i = 0; i < len; i++) { print(nums[i]); }
  95. 移除元素
    题目描述
    给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新
    长度。
    不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
    元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
    示例 1:
    示例 2:
    说明:
    为什么返回数值是整数,但输出的答案是数组呢?
    请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
    你可以想象内部操作如下: class Solution { public int removeDuplicates(int[] nums) { int cnt = 0, cur = 1; for (int i = 1; i < nums.length; ++i) { if (nums[i] == nums[i - 1]) ++cnt; else cnt = 0; if (cnt < 2) nums[cur++] = nums[i]; }return cur; } }给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 你不需要考虑数组中超出新长度后面的元素。 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。 注意这五个元素可为任意顺序。 你不需要考虑数组中超出新长度后面的元素。
    解法
  96. 移动零
    题目描述
    给定一个数组 nums ,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
    示例:
    说明: 1. 必须在原数组上操作,不能拷贝额外的数组。
  97. 尽量减少操作次数。
    解法
    // nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝 int len = removeElement(nums, val); // 在函数里修改输入数组对于调用者是可见的。 // 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。 for (int i = 0; i < len; i++) { print(nums[i]); }class Solution { public int removeElement(int[] nums, int val) { int cnt = 0, n = nums.length; for (int i = 0; i < n; ++i) { if (nums[i] == val) { ++cnt; } else { nums[i - cnt] = nums[i]; } }return n - cnt; } }[0,1,0,3,12] class Solution { public void moveZeroes(int[] nums) { int n; if (nums == null || (n = nums.length) < 1) { return; }int zeroCount = 0; for (int i = 0; i < n; ++i) { if (nums[i] == 0) {
  98. 数组中重复的数字
    题目描述
    找出数组中重复的数字。
    在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内。数组中某些数字是重复的,但不知
    道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
    示例 1:
    限制:
    解法
    0 ~ n-1 范围内的数,分别还原到对应的位置上,如:数字 2 交换到下标为 2 的位置。
    若交换过程中发现重复,则直接返回。
    ++zeroCount; } else { nums[i - zeroCount] = nums[i]; } }while (zeroCount > 0) { nums[n - zeroCount–] = 0; } } }输入: [2, 3, 1, 0, 2, 5, 3] 输出:2 或 3 2 <= n <= 100000 class Solution { public int findRepeatNumber(int[] nums) { for (int i = 0, n = nums.length; i < n; ++i) { while (nums[i] != i) { if (nums[i] == nums[nums[i]]) return nums[i]; swap(nums, i, nums[i]); } }return -1; }private void swap(int[] nums, int i, int j) { int t = nums[i]; nums[i] = nums[j]; nums[j] = t; } }
  99. 旋转数组
    题目描述
    给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
    示例 1:
    示例 2:
    说明:
    尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
    要求使用空间复杂度为 O(1) 的 原地 算法。
    解法
    若 k=3 , nums=[1,2,3,4,5,6,7] 。
    先将 nums 整体翻转: [1,2,3,4,5,6,7] -> [7,6,5,4,3,2,1]
    再翻转 0~k-1 范围内的元素: [7,6,5,4,3,2,1] -> [5,6,7,4,3,2,1]
    最后翻转 k~n-1 范围内的元素,即可得到最终结果: [5,6,7,4,3,2,1] -> [5,6,7,1,2,3,4] [1,2,3,4,5,6,7] [-1,-100,3,99] class Solution { public void rotate(int[] nums, int k) { if (nums == null) { return; }int n = nums.length; k %= n; if (n < 2 || k == 0) { return; }rotate(nums, 0, n - 1); rotate(nums, 0, k - 1); rotate(nums, k, n - 1); }private void rotate(int[] nums, int i, int j) { while (i < j) { int t = nums[i]; nums[i] = nums[j]; nums[j] = t; ++i; --j; } } }
  100. 螺旋矩阵
    题目描述
    给定一个包含 m x n 个元素的矩阵(m 行, n 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。
    示例 1:
    示例 2:
    提示:
    m == matrix.length
    n == matrix[i].length
    1 <= m, n <= 10
    -100 <= matrix[i][j] <= 100
    解法
    从外往里一圈一圈遍历并存储矩阵元素即可。
    输入: [[ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8, 9 ] ]输出: [1,2,3,6,9,8,7,4,5] 输入: [ [1, 2, 3, 4], [5, 6, 7, 8], [9,10,11,12] ]输出: [1,2,3,4,8,12,11,10,9,5,6,7] class Solution { private List res; public List spiralOrder(int[][] matrix) { int m = matrix.length, n = matrix[0].length; res = new ArrayList<>(); int i1 = 0, i2 = m - 1; int j1 = 0, j2 = n - 1; while (i1 <= i2 && j1 <= j2) { add(matrix, i1++, j1++, i2–, j2–); }return res; }private void add(int[][] matrix, int i1, int j1, int i2, int j2) { if (i1 == i2) { for (int j = j1; j <= j2; ++j) { res.add(matrix[i1][j]); }return;
  101. 两数之和
    题目描述
    给定一个整数数组 nums 和一个目标值 target ,请你在该数组中找出和为目标值的那 两个 整数,并
    返回他们的数组下标。
    你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
    示例:
    解法
    用哈希表(字典)存放数组值以及对应的下标。
    遍历数组,当发现 target - nums[i] 在哈希表中,说明找到了目标值。
    }if (j1 == j2) { for (int i = i1; i <= i2; ++i) { res.add(matrix[i][j1]); }return; }for (int j = j1; j < j2; ++j) { res.add(matrix[i1][j]); }for (int i = i1; i < i2; ++i) { res.add(matrix[i][j2]); }for (int j = j2; j > j1; --j) { res.add(matrix[i2][j]); }for (int i = i2; i > i1; --i) { res.add(matrix[i][j1]); } } }给定 nums = [2, 7, 11, 15], target = 9 因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1]
  102. 三数之和
    给你一个包含 n 个整数的数组 nums ,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?
    请你找出所有满足条件且不重复的三元组。
    注意:答案中不可以包含重复的三元组。
    示例:
    解法
    “排序 + 双指针”实现。
    class Solution { public int[] twoSum(int[] nums, int target) { Map<Integer, Integer> map = new HashMap<>(); for (int i = 0, n = nums.length; i < n; ++i) { int num = target - nums[i]; if (map.containsKey(num)) { return new int[]{map.get(num), i}; }map.put(nums[i], i); }return null; } }给定数组 nums = [-1, 0, 1, 2, -1, -4], 满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]class Solution { public List<List> threeSum(int[] nums) { int n; if (nums == null || (n = nums.length) < 3) { return Collections.emptyList(); }Arrays.sort(nums); List<List> res = new ArrayList<>(); for (int i = 0; i < n - 2; ++i) { if (i > 0 && nums[i] == nums[i - 1]) { continue; }int p = i + 1, q = n - 1; while (p < q) { if (p > i + 1 && nums[p] == nums[p - 1]) { ++p;
  103. 四数之和
    题目描述
    给定一个包含 n 个整数的数组 nums 和一个目标值 target ,判断 nums 中是否存在四个元素 a,
    **b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。
    注意:
    答案中不可以包含重复的四元组。
    示例:
    解法
    “排序 + 双指针”实现。
    continue; }if (q < n - 1 && nums[q] == nums[q + 1]) { --q; continue; }if (nums[p] + nums[q] + nums[i] < 0) { ++p; } else if (nums[p] + nums[q] + nums[i] > 0) { --q; } else { res.add(Arrays.asList(nums[p], nums[q], nums[i])); ++p; --q; } } }return res; } }给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 满足要求的四元组集合为: [ [-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2] ]class Solution { public List<List> fourSum(int[] nums, int target) { int n; if (nums == null || (n = (nums.length)) < 4) { return Collections.emptyList(); }Arrays.sort(nums); List<List> res = new ArrayList<>(); for (int i = 0; i < n - 3; ++i) {
  104. 较小的三数之和
    题目描述
    给定一个长度为 n 的整数数组和一个目标值 target,寻找能够使条件 nums[i] + nums[j] + nums[k] < target 成立的三元组 i, j, k 个数( 0 <= i < j < k < n )。
    示例:
    进阶:是否能在 O(n2) 的时间复杂度内解决?
    解法
    双指针解决。
    if (i > 0 && nums[i] == nums[i - 1]) { continue; }for (int j = i + 1; j < n - 2; ++j) { if (j > i + 1 && nums[j] == nums[j - 1]) { continue; }int p = j + 1, q = n - 1; while (p < q) { if (p > j + 1 && nums[p] == nums[p - 1]) { ++p; continue; }if (q < n - 1 && nums[q] == nums[q + 1]) { --q; continue; }int t = nums[i] + nums[j] + nums[p] + nums[q]; if (t == target) { res.add(Arrays.asList(nums[i], nums[j], nums[p], nums[q])); ++p; --q; } else if (t < target) { ++p; } else { --q; } } } }return res; } }[-2,0,1,3] class Solution { public int threeSumSmaller(int[] nums, int target) { Arrays.sort(nums);
  105. 最接近的三数之和
    题目描述
    给定一个包括 n 个整数的数组 nums 和 一个目标值 target 。找出 nums 中的三个整数,使得它们的
    和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。
    解法
    双指针解决。
    int n = nums.length; int count = 0; for (int i = 0; i < n - 2; ++i) { count += threeSumSmaller(nums, i + 1, n - 1, target - nums[i]); }return count; }private int threeSumSmaller(int[] nums, int start, int end, int target) { int count = 0; while (start < end) { if (nums[start] + nums[end] < target) { count += (end - start); ++start; } else { --end; } }return count; } }例如,给定数组 nums = [-1,2,1,-4], 和 target = 1. 与 target 最接近的三个数的和为 2. (-1 + 2 + 1 = 2). class Solution { public int threeSumClosest(int[] nums, int target) { Arrays.sort(nums); int res = 0; int n = nums.length; int diff = Integer.MAX_VALUE; for (int i = 0; i < n - 2; ++i) { int t = twoSumClosest(nums, i + 1, n - 1, target - nums[i]); if (Math.abs(nums[i] + t - target) < diff) { res = nums[i] + t; diff = Math.abs(nums[i] + t - target); } }return res; }private int twoSumClosest(int[] nums, int start, int end, int target) { int res = 0;
  106. 合并两个有序数组
    题目描述
    给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 num1 成为一个有序数
    组。
    说明:
    初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。
    你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
    示例:
    解法
    双指针解决。
    int diff = Integer.MAX_VALUE; while (start < end) { int val = nums[start] + nums[end]; if (val == target) { return val; }if (Math.abs(val - target) < diff) { res = val; diff = Math.abs(val - target); }if (val < target) { ++start; } else { --end; } }return res; } }输入: nums1 = [1,2,3,0,0,0], m = 3 nums2 = [2,5,6], n = 3 输出: [1,2,2,3,5,6]
  107. 寻找旋转排序数组中的最小值
    题目描述
    假设按照升序排序的数组在预先未知的某个点上进行了旋转。
    ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
    请找出其中最小的元素。
    你可以假设数组中不存在重复元素。
    示例 1:
    示例 2:
    解法
    二分查找。
    若 nums[m] > nums[r] ,说明最小值在 m 的右边,否则说明最小值在 m 的左边(包括 m)。
    class Solution { public void merge(int[] nums1, int m, int[] nums2, int n) { int i = m - 1, j = n - 1; int k = m + n - 1; while (j >= 0) { if (i >= 0 && nums1[i] >= nums2[j]) { nums1[k–] = nums1[i–]; } else { nums1[k–] = nums2[j–]; } } } }输入: [3,4,5,1,2] 输出: 1 输入: [4,5,6,7,0,1,2] 输出: 0 class Solution { public int findMin(int[] nums) { int l = 0, r = nums.length - 1; while (l < r) { int m = (l + r) >>> 1; if (nums[m] > nums[r]) { l = m + 1; } else { r = m; } }
  108. 寻找旋转排序数组中的最小值 II
    题目描述
    假设按照升序排序的数组在预先未知的某个点上进行了旋转。
    ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
    请找出其中最小的元素。
    注意数组中可能存在重复的元素。
    示例 1:
    示例 2:
    说明:
    允许重复会影响算法的时间复杂度吗?会如何影响,为什么?
    return nums[l]; } }输入: [1,3,5] 输出: 1 输入: [2,2,2,0,1] 输出: 0 class Solution { public int findMin(int[] nums) { int l = 0, r = nums.length - 1; while (l < r) { int m = (l + r) >>> 1; if (nums[m] > nums[r]) { l = m + 1; } else if (nums[m] < nums[r]) { r = m; } else { --r; } }return nums[l]; } }
  109. 除自身以外数组的乘积
    题目描述
    给你一个长度为 n 的整数数组 nums ,其中 n > 1,返回输出数组 output ,其中 output[i] 等于
    nums 中除 nums[i] 之外其余各元素的乘积。
    示例:
    提示:题目数据保证数组之中任意元素的全部前缀元素和后缀(甚至是整个数组)的乘积都在 32 位整
    数范围内。
    说明: 请不要使用除法,且在 O(n) 时间复杂度内完成此题。
    进阶:
    你可以在常数空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组不被视为额外
    空间。)
    解法
  110. 无重复字符的最长子串
    题目描述
    给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
    示例 1:
    示例 2:
    [1,2,3,4] class Solution { public int[] productExceptSelf(int[] nums) { int n = nums.length; int[] output = new int[n]; for (int i = 0, left = 1; i < n; ++i) { output[i] = left; left *= nums[i]; }for (int i = n - 1, right = 1; i >= 0; --i) { output[i] *= right; right *= nums[i]; }return output; } }“abc”,所以其 “b”
    示例 3:
    解法
    定义一个哈希表存放字符及其出现的位置;
    定义 i, j 分别表示不重复子串的开始位置和结束位置;
    j 向后遍历,若遇到与 [i, j] 区间内字符相同的元素,更新 i 的值,此时 [i, j] 区间内不存在
    重复字符,计算 res 的最大值。
  111. 反转字符串中的元音字母
    题目描述
    编写一个函数,以字符串作为输入,反转该字符串中的元音字母。
    示例 1:
    示例 2:
    说明:
    元音字母不包含字母"y"。
    解法
    将字符串转为字符数组(或列表),定义双指针 p、q,分别指向数组(列表)头部和尾部,当 p、q 指
    向的字符均为元音字母时,进行交换。
    “wke” class Solution { public int lengthOfLongestSubstring(String s) { int res = 0; Map<Character, Integer> chars = new HashMap<>(); for (int i = 0, j = 0; j < s.length(); ++j) { char c = s.charAt(j); if (chars.containsKey©) { // chars.get©+1 可能比 i 还小,通过 max 函数来锁住左边界 // e.g. 在"tmmzuxt"这个字符串中,遍历到最后一步时,最后一个字符’t’和第一个 字符’t’是相等的。如果没有 max 函数,i 就会回到第一个’t’的索引0处的下一个位置 i = Math.max(i, chars.get© + 1); }chars.put(c, j); res = Math.max(res, j - i + 1); }return res; } }输入: “hello” 输出: “holle” 输入: “leetcode” 输出: “leotcede”
    依次遍历,当 p >= q 时,遍历结束。将字符数组(列表)转为字符串返回即可。
  112. 字符串转换整数
    题目描述
    请你来实现一个 atoi 函数,使其能将字符串转换成整数。
    首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。
    class Solution { public String reverseVowels(String s) { if (s == null) { return s; }char[] chars = s.toCharArray(); int p = 0, q = chars.length - 1; while (p < q) { if (!isVowel(chars[p])) { ++p; continue; }if (!isVowel(chars[q])) { --q; continue; }swap(chars, p++, q–); }return String.valueOf(chars); }private void swap(char[] chars, int i, int j) { char t = chars[i]; chars[i] = chars[j]; chars[j] = t; }private boolean isVowel(char c) { switch© { case ‘a’: case ‘e’: case ‘i’: case ‘o’: case ‘u’: case ‘A’: case ‘E’: case ‘I’: case ‘O’: case ‘U’: return true; default: return false; } } }
    当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,
    作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成
    整数。
    该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应
    该造成影响。
    注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字
    符时,则你的函数不需要进行转换。
    在任何情况下,若函数不能进行有效的转换时,请返回 0。
    说明:
    假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 − 1]。如果数值超过
    这个范围,请返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。
    示例 1:
    输入: “42” 输出: 42
    示例 2:
    输入: " -42" 输出: -42 解释: 第一个非空白字符为 ‘-’, 它是一个负号。 我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。
    示例 3:
    输入: “4193 with words” 输出: 4193 解释: 转换截止于数字 ‘3’ ,因为它的下一个字符不为数字。
    示例 4:
    输入: “words and 987” 输出: 0 解释: 第一个非空字符是 ‘w’, 但它不是数字或正、负号。 因此无法执行有效的转换。
    示例 5:
    输入: “-91283472332” 输出: -2147483648 解释: 数字 “-91283472332” 超过 32 位有符号整数范围。 因此返回 INT_MIN (−231) 。
    解法
    遍历字符串,注意做溢出处理。
    class Solution { public int myAtoi(String s) {
  113. 赎金信
    题目描述
    给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串ransom能不能由第
    二个字符串magazines里面的字符构成。如果可以构成,返回 true ;否则返回 false。 (题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。)
    注意:
    你可以假设两个字符串均只含有小写字母。
    解法
    用一个数组或字典 chars 存放 magazine 中每个字母出现的次数。遍历 ransomNote 中每个字母,判断
    chars 是否包含即可。
    if (s == null) return 0; int n = s.length(); if (n == 0) return 0; int i = 0; while (s.charAt(i) == ’ ') { // 仅包含空格 if (++i == n) return 0; }int sign = 1; if (s.charAt(i) == ‘-’) sign = -1; if (s.charAt(i) == ‘-’ || s.charAt(i) == ‘+’) ++i; int res = 0, flag = Integer.MAX_VALUE / 10; for (; i < n; ++i) { // 非数字,跳出循环体 if (s.charAt(i) < ‘0’ || s.charAt(i) > ‘9’) break; // 溢出判断 if (res > flag || (res == flag && s.charAt(i) > ‘7’)) return sign > 0 ? Integer.MAX_VALUE : Integer.MIN_VALUE; res = res * 10 + (s.charAt(i) - ‘0’); }return sign * res; } }canConstruct(“a”, “b”) -> false canConstruct(“aa”, “ab”) -> false canConstruct(“aa”, “aab”) -> true class Solution { public boolean canConstruct(String ransomNote, String magazine) { int[] chars = new int[26]; for (int i = 0, n = magazine.length(); i < n; ++i) { int idx = magazine.charAt(i) - ‘a’; ++chars[idx]; }for (int i = 0, n = ransomNote.length(); i < n; ++i) { int idx = ransomNote.charAt(i) - ‘a’; if (chars[idx] == 0) return false;
  114. 两数相加
    题目描述
    给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并
    且它们的每个节点只能存储 一位 数字。
    如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
    您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
    示例:
    –chars[idx]; }return true; } }输入:(2 -> 4 -> 3) + (5 -> 6 -> 4) 输出:7 -> 0 -> 8 原因:342 + 465 = 807 /*** Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode addTwoNumbers(ListNode l1, ListNode l2) { int carry = 0; ListNode dummy = new ListNode(-1); ListNode cur = dummy; while (l1 != null || l2 != null || carry != 0) { int t = (l1 == null ? 0 : l1.val) + (l2 == null ? 0 : l2.val) + carry; carry = t / 10; cur.next = new ListNode(t % 10); cur = cur.next; l1 = l1 == null ? null : l1.next; l2 = l2 == null ? null : l2.next; }return dummy.next; } }
  115. 两数相加 II
    题目描述
    给定两个非空链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储单个数
    字。将这两数相加会返回一个新的链表。
    你可以假设除了数字 0 之外,这两个数字都不会以零开头。
    进阶:
    如果输入链表不能修改该如何处理?换句话说,你不能对列表中的节点进行翻转。
    示例:
    解法
    利用栈将数字逆序。
    输入: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4) 输出: 7 -> 8 -> 0 -> 7 /*** Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode addTwoNumbers(ListNode l1, ListNode l2) { Deque s1 = new ArrayDeque<>(); Deque s2 = new ArrayDeque<>(); for (; l1 != null; l1 = l1.next) { s1.push(l1.val); }for (; l2 != null; l2 = l2.next) { s2.push(l2.val); }int carry = 0; ListNode dummy = new ListNode(-1); while (!s1.isEmpty() || !s2.isEmpty() || carry != 0) { carry += (s1.isEmpty() ? 0 : s1.pop()) + (s2.isEmpty() ? 0 : s2.pop()); // 创建结点,利用头插法将结点插入链表 ListNode node = new ListNode(carry % 10); node.next = dummy.next; dummy.next = node; carry /= 10; }return dummy.next; } }
  116. 从尾到头打印链表
    题目描述
    输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
    示例 1:
    限制:
    0 <= 链表长度 <= 10000
    解法
    栈实现。或者其它方式,见题解。
    栈实现:
    先计算链表长度 n,然后创建一个长度为 n 的结果数组。最后遍历链表,依次将节点值存放在数组
    上(从后往前)。
    输入:head = [1,3,2] 输出:[2,3,1] /*** Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } / class Solution { public int[] reversePrint(ListNode head) { Stack s = new Stack<>(); while (head != null) { s.push(head.val); head = head.next; }int[] res = new int[s.size()]; int i = 0; while (!s.isEmpty()) { res[i++] = s.pop(); }return res; } } /** Definition for singly-linked list. * public class ListNode { * int val;
  117. 删除链表的节点
    给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
    返回删除后的链表的头节点。
    示例 1:
    示例 2:
    说明:
    题目保证链表中节点的值互不相同
    若使用 C 或 C++ 语言,你不需要 free 或 delete 被删除的节点
    解法
    定义一个虚拟头节点 dummy 指向 head , pre 指针初始指向 dummy 。
    循环遍历链表, pre 往后移动。当指针 pre.next 指向的节点的值等于 val 时退出循环,将
    pre.next 指向 pre.next.next ,然后返回 dummy.next 。 * ListNode next; * ListNode(int x) { val = x; } * } / class Solution { public int[] reversePrint(ListNode head) { if (head == null) return new int[]{}; // 计算链表长度n int n = 0; ListNode cur = head; while (cur != null) { ++n; cur = cur.next; }int[] res = new int[n]; cur = head; while (cur != null) { res[–n] = cur.val; cur = cur.next; }return res; } }输入: head = [4,5,1,9], val = 5 输出: [4,1,9] 解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9. 输入: head = [4,5,1,9], val = 1 输出: [4,5,9] 解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9. /*
  118. 删除排序链表中的重复元素
    题目描述
    给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
    示例 1:
    示例 2:
  • Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } / class Solution { public ListNode deleteNode(ListNode head, int val) { ListNode dummy = new ListNode(0); dummy.next = head; ListNode pre = dummy; while (pre.next != null && pre.next.val != val) { pre = pre.next; }pre.next = pre.next == null ? null : pre.next.next; return dummy.next; } }输入: 1->1->2 输出: 1->2 输入: 1->1->2->3->3 输出: 1->2->3 /** Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode deleteDuplicates(ListNode head) { ListNode cur = head; while (cur != null && cur.next != null) { if (cur.val == cur.next.val) { cur.next = cur.next.next;
  1. 删除排序链表中的重复元素 II
    题目描述
    给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。
    示例 1:
    示例 2:
    解法
    } else { cur = cur.next; } }return head; } }输入: 1->2->3->3->4->4->5 输出: 1->2->5 输入: 1->1->1->2->3 输出: 2->3 /*** Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode deleteDuplicates(ListNode head) { ListNode dummy = new ListNode(-1, head); ListNode cur = dummy; while (cur.next != null && cur.next.next != null) { if (cur.next.val == cur.next.next.val) { int val = cur.next.val; while (cur.next != null && cur.next.val == val) { cur.next = cur.next.next; } } else { cur = cur.next; } }return dummy.next; } }
  2. 移除链表元素
    题目描述
    删除链表中等于给定值 val 的所有节点。
    示例:
    解法
  3. 两两交换链表中的节点
    题目描述
    给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
    你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
    示例:
    解法
    输入: 1->2->6->3->4->5->6, val = 6 输出: 1->2->3->4->5 /*** Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode removeElements(ListNode head, int val) { ListNode dummy = new ListNode(-1, head); ListNode pre = dummy; while (pre != null && pre.next != null) { if (pre.next.val != val) pre = pre.next; else pre.next = pre.next.next; }return dummy.next; } }1->2->3->4
  4. 排序链表
    题目描述
    在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
    示例 1:
    示例 2:
    解法
    先用快慢指针找到链表中点,然后分成左右两个链表,递归排序左右链表。最后合并两个排序的链表即
    可。
    /*** Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } / class Solution { public ListNode swapPairs(ListNode head) { ListNode dummy = new ListNode(0, head); ListNode pre = dummy, cur = head; while (cur != null && cur.next != null) { ListNode t = cur.next; cur.next = t.next; t.next = cur; pre.next = t; pre = cur; cur = pre.next; }return dummy.next; } }输入: 4->2->1->3 输出: 1->2->3->4 输入: -1->5->3->4->0 输出: -1->0->3->4->5 /** Definition for singly-linked list. * public class ListNode { * int val; * ListNode next;
  5. 反转链表
    题目描述
    反转一个单链表。
    示例:
    进阶:
    你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
    解法
    定义指针 p 、 q 分别指向头节点和下一个节点, pre 指向头节点的前一个节点。
    遍历链表,改变指针 p 指向的节点的指向,将其指向 pre 指针指向的节点,即 p.next = pre 。然后
    pre 指针指向 p , p 、 q 指针往前走。
  • ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } / class Solution { public ListNode sortList(ListNode head) { if (head == null || head.next == null) { return head; }ListNode slow = head, fast = head.next; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; }ListNode t = slow.next; slow.next = null; ListNode l1 = sortList(head); ListNode l2 = sortList(t); ListNode dummy = new ListNode(0); ListNode cur = dummy; while (l1 != null && l2 != null) { if (l1.val <= l2.val) { cur.next = l1; l1 = l1.next; } else { cur.next = l2; l2 = l2.next; }cur = cur.next; }cur.next = l1 == null ? l2 : l1; return dummy.next; } }输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL
    当遍历结束后,返回 pre 指针即可。
    迭代版本
    /
    ** Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } / class Solution { public ListNode reverseList(ListNode head) { ListNode pre = null, p = head; while (p != null) { ListNode q = p.next; p.next = pre; pre = p; p = q; }return pre; } }
    递归版本
    /
    ** Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode reverseList(ListNode head) { if (head == null || head.next == null) { return head; }ListNode res = reverseList(head.next); head.next.next = head; head.next = null; return res; } }
  1. 二叉树的前序遍历
    题目描述
    给定一个二叉树,返回它的 前序 遍历。
    示例:
    进阶: 递归算法很简单,你可以通过迭代算法完成吗?
    解法
    递归遍历或利用栈实现非递归遍历。
    非递归的思路如下:
  2. 定义一个栈,先将根节点压入栈
  3. 若栈不为空,每次从栈中弹出一个节点
  4. 处理该节点
  5. 先把节点右孩子压入栈,接着把节点左孩子压入栈(如果有孩子节点)
  6. 重复 2-4
  7. 返回结果
    递归
    输入: [1,null,2,3] 1\2/3 输出: [1,2,3] /*** Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } / class Solution { private List res; public List preorderTraversal(TreeNode root) { res = new ArrayList<>(); preorder(root);
    return res; }private void preorder(TreeNode root) { if (root != null) { res.add(root.val); preorder(root.left); preorder(root.right); } } }
    非递归
    /
    ** Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public List preorderTraversal(TreeNode root) { if (root == null) { return Collections.emptyList(); }List res = new ArrayList<>(); Deque s = new ArrayDeque<>(); s.push(root); while (!s.isEmpty()) { TreeNode node = s.pop(); res.add(node.val); if (node.right != null) { s.push(node.right); }if (node.left != null) { s.push(node.left); } }return res; } }
  8. 二叉树的后序遍历
    题目描述
    给定一个二叉树,返回它的 后序 遍历。
    示例:
    进阶: 递归算法很简单,你可以通过迭代算法完成吗?
    解法
    递归遍历或利用栈实现非递归遍历。
    非递归的思路如下:
    先序遍历的顺序是:头、左、右,如果我们改变左右孩子的顺序,就能将顺序变成:头、右、左。
    我们先不打印头节点,而是存放到另一个收集栈 s2 中,最后遍历结束,输出收集栈元素,即是后序遍
    历:左、右、头。
    递归
    输入: [1,null,2,3] 1\2/3 输出: [3,2,1] /*** Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } / class Solution { private List res; public List postorderTraversal(TreeNode root) { res = new ArrayList<>(); postorder(root); return res; }
    private void postorder(TreeNode root) { if (root != null) { postorder(root.left); postorder(root.right); res.add(root.val); } } }
    非递归
    /
    ** Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public List postorderTraversal(TreeNode root) { if (root == null) { return Collections.emptyList(); }Deque s1 = new ArrayDeque<>(); List s2 = new ArrayList<>(); s1.push(root); while (!s1.isEmpty()) { TreeNode node = s1.pop(); s2.add(node.val); if (node.left != null) { s1.push(node.left); }if (node.right != null) { s1.push(node.right); } }Collections.reverse(s2); return s2; } }
  9. 二叉树的中序遍历
    题目描述
    给定一个二叉树,返回它的中序 遍历。
    示例:
    进阶: 递归算法很简单,你可以通过迭代算法完成吗?
    解法
    递归遍历或利用栈实现非递归遍历。
    非递归的思路如下:
  10. 定义一个栈
  11. 将树的左节点依次入栈
  12. 左节点为空时,弹出栈顶元素并处理
  13. 重复 2-3 的操作
    递归
    输入: [1,null,2,3] 1\2/3 输出: [1,3,2] /*** Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } / class Solution { private List res; public List inorderTraversal(TreeNode root) { res = new ArrayList<>(); inorder(root); return res; }
    private void inorder(TreeNode root) { if (root != null) { inorder(root.left); res.add(root.val); inorder(root.right); } } }
    非递归
    /
    ** Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public List inorderTraversal(TreeNode root) { if (root == null) { return Collections.emptyList(); }List res = new ArrayList<>(); Deque s = new ArrayDeque<>(); while (root != null || !s.isEmpty()) { if (root != null) { s.push(root); root = root.left; } else { root = s.pop(); res.add(root.val); root = root.right; } }return res; } }
  14. 最小栈
    题目描述
    设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。
    push(x) – 将元素 x 推入栈中。
    pop() – 删除栈顶的元素。
    top() – 获取栈顶元素。
    getMin() – 检索栈中的最小元素。
    示例:
    解法
    MinStack minStack = new MinStack(); minStack.push(-2); minStack.push(0); minStack.push(-3); minStack.getMin(); --> 返回 -3. minStack.pop(); minStack.top(); --> 返回 0. minStack.getMin(); --> 返回 -2. class MinStack { private Deque s; private Deque helper; /** initialize your data structure here. / public MinStack() { s = new ArrayDeque<>(); helper = new ArrayDeque<>(); }public void push(int x) { s.push(x); int element = helper.isEmpty() || x < helper.peek() ? x : helper.peek(); helper.push(element); }public void pop() { s.pop(); helper.pop(); }public int top() { return s.peek(); }public int getMin() { return helper.peek(); } }/** Your MinStack object will be instantiated and called as such:
  15. 队列的最大值
    题目描述
    请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数 max_value 、 push_back 和 pop_front 的均摊时间复杂度都是 O(1)。
    若队列为空, pop_front 和 max_value 需要返回 -1
    示例 1:
    示例 2:
    限制:
    1 <= push_back,pop_front,max_value的总操作数 <= 10000 1 <= value <= 10^5
    解法
    利用一个辅助队列按单调顺序存储当前队列的最大值。
  • MinStack obj = new MinStack(); * obj.push(x); * obj.pop(); * int param_3 = obj.top(); * int param_4 = obj.getMin(); */ 输入: [“MaxQueue”,“push_back”,“push_back”,“max_value”,“pop_front”,“max_value”] [[],[1],[2],[],[],[]] 输出: [null,null,null,2,1,2] 输入: [“MaxQueue”,“pop_front”,“max_value”] [[],[],[]] 输出: [null,-1,-1] class MaxQueue { private Deque p; private Deque q; public MaxQueue() { p = new ArrayDeque<>(); q = new ArrayDeque<>(); }public int max_value() { return q.isEmpty() ? -1 : q.peekFirst(); }public void push_back(int value) { while (!q.isEmpty() && q.peekLast() < value) { q.pollLast();
  1. 冒泡排序
    冒泡排序是最简单的排序之一了,其大体思想就是通过与相邻元素的比较和交换来把小的数交换到最前
    面。这个过程类似于水泡向上升一样,因此而得名。举个栗子,对5,3,8,6,4这个无序序列进行冒泡排
    序。首先从后向前冒泡,4和6比较,把4交换到前面,序列变成5,3,8,4,6。同理4和8交换,变成
    5,3,4,8,6,3和4无需交换。5和3交换,变成3,5,4,8,6,3.这样一次冒泡就完了,把最小的数3排到最前面
    了。对剩下的序列依次冒泡就会得到一个有序序列。冒泡排序的时间复杂度为O(n^2)。
    实现代码:
    }p.offerLast(value); q.offerLast(value); }public int pop_front() { if (p.isEmpty()) return -1; int res = p.pollFirst(); if (q.peek() == res) q.pollFirst(); return res; } }/*** Your MaxQueue object will be instantiated and called as such: * MaxQueue obj = new MaxQueue(); * int param_1 = obj.max_value(); * obj.push_back(value); * int param_3 = obj.pop_front(); */ public class BubbleSort { public static void bubbleSort(int[] arr) { if(arr == null || arr.length == 0) return ; for(int i=0; i<arr.length-1; i++) { for(int j=arr.length-1; j>i; j–) { if(arr[j] < arr[j-1]) { swap(arr, j-1, j); } } } }public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
  2. 选择排序
    选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面。但是过程不
    同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择。举个栗子,对5,3,8,6,4这个
    无序序列进行简单选择排序,首先要选择5以外的最小数来和5交换,也就是选择3和5交换,一次排序后
    就变成了3,5,8,6,4.对剩下的序列一次进行选择和交换,最终就会得到一个有序序列。其实选择排序可以
    看成冒泡排序的优化,因为其目的相同,只是选择排序只有在确定了最小数的前提下才进行交换,大大
    减少了交换的次数。选择排序的时间复杂度为O(n^2)
    实现代码:
  3. 插入排序
    插入排序不是通过交换位置而是通过比较找到合适的位置插入元素来达到排序的目的的。相信大家都有
    过打扑克牌的经历,特别是牌数较大的。在分牌时可能要整理自己的牌,牌多的时候怎么整理呢?就是
    拿到一张牌,找到一个合适的位置插入。这个原理其实和插入排序是一样的。举个栗子,对5,3,8,6,4这
    个无序序列进行简单插入排序,首先假设第一个数的位置时正确的,想一下在拿到第一张牌的时候,没
    必要整理。然后3要插到5前面,把5后移一位,变成3,5,8,6,4.想一下整理牌的时候应该也是这样吧。然
    后8不用动,6插在8前面,8后移一位,4插在5前面,从5开始都向后移一位。注意在插入一个数的时候
    要保证这个数前面的数已经有序。简单插入排序的时间复杂度也是O(n^2)。
    实现代码:
    public class SelectSort { public static void selectSort(int[] arr) { if(arr == null || arr.length == 0) return ; int minIndex = 0; for(int i=0; i<arr.length-1; i++) { //只需要比较n-1次 minIndex = i; for(int j=i+1; j<arr.length; j++) { //从i+1开始比较,因为minIndex默认为i 了,i就没必要比了。if(arr[j] < arr[minIndex]) { minIndex = j; } }if(minIndex != i) { //如果minIndex不为i,说明找到了更小的值,交换之。 swap(arr, i, minIndex); } } }public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }public class InsertSort {
  4. 快速排序
    快速排序一听名字就觉得很高端,在实际应用当中快速排序确实也是表现最好的排序算法。快速排序虽
    然高端,但其实其思想是来自冒泡排序,冒泡排序是通过相邻元素的比较和交换把最小的冒泡到最顶
    端,而快速排序是比较和交换小数和大数,这样一来不仅把小数冒泡到上面同时也把大数沉到下面。
    举个栗子:对5,3,8,6,4这个无序序列进行快速排序,思路是右指针找比基准数小的,左指针找比基准数
    大的,交换之。
    5,3,8,6,4 用5作为比较的基准,最终会把5小的移动到5的左边,比5大的移动到5的右边。
    5,3,8,6,4 首先设置i,j两个指针分别指向两端,j指针先扫描(思考一下为什么?)4比5小停止。然后i扫
    描,8比5大停止。交换i,j位置。
    5,3,4,6,8 然后j指针再扫描,这时j扫描4时两指针相遇。停止。然后交换4和基准数。
    4,3,5,6,8 一次划分后达到了左边比5小,右边比5大的目的。之后对左右子序列递归排序,最终得到有序
    序列。
    上面留下来了一个问题为什么一定要j指针先动呢?首先这也不是绝对的,这取决于基准数的位置,因为
    在最后两个指针相遇的时候,要交换基准数到相遇的位置。一般选取第一个数作为基准数,那么就是在
    左边,所以最后相遇的数要和基准数交换,那么相遇的数一定要比基准数小。所以j指针先移动才能先找
    到比基准数小的数。
    快速排序是不稳定的,其时间平均时间复杂度是O(nlgn)。
    实现代码:
    public static void insertSort(int[] arr) { if(arr == null || arr.length == 0) return ; for(int i=1; i<arr.length; i++) { //假设第一个数位置时正确的;要往后移,必须要假 设第一个。 int j = i; int target = arr[i]; //待插入的 //后移 while(j > 0 && target < arr[j-1]) { arr[j] = arr[j-1]; j --; }//插入 arr[j] = target; } } }public class QuickSort { //一次划分 public static int partition(int[] arr, int left, int right) {
    int pivotKey = arr[left]; int pivotPointer = left; while(left < right) { while(left < right && arr[right] >= pivotKey) right --; while(left < right && arr[left] <= pivotKey) left ++; swap(arr, left, right); //把大的交换到右边,把小的交换到左边。 }swap(arr, pivotPointer, left); //最后把pivot交换到中间 return left; }public static void quickSort(int[] arr, int left, int right) { if(left >= right) return ; int pivotPos = partition(arr, left, right); quickSort(arr, left, pivotPos-1); quickSort(arr, pivotPos+1, right); }public static void sort(int[] arr) { if(arr == null || arr.length == 0) return ; quickSort(arr, 0, arr.length-1); }public static void swap(int[] arr, int left, int right) { int temp = arr[left]; arr[left] = arr[right]; arr[right] = temp; } }
    其实上面的代码还可以再优化,上面代码中基准数已经在pivotKey中保存了,所以不需要每次交换都设
    置一个temp变量,在交换左右指针的时候只需要先后覆盖就可以了。这样既能减少空间的使用还能降低
    赋值运算的次数。优化代码如下:
    public class QuickSort { /*** 划分 * @param arr * @param left * @param right * @return */ public static int partition(int[] arr, int left, int right) { int pivotKey = arr[left]; while(left < right) { while(left < right && arr[right] >= pivotKey) right --; arr[left] = arr[right]; //把小的移动到左边 while(left < right && arr[left] <= pivotKey)
    总结快速排序的思想:冒泡+二分+递归分治,慢慢体会。。。
  5. 堆排序
    堆排序是借助堆来实现的选择排序,思想同简单的选择排序,以下以大顶堆为例。注意:如果想升序排
    序就使用大顶堆,反之使用小顶堆。原因是堆顶元素需要交换到序列尾部。
    首先,实现堆排序需要解决两个问题:
    如何由一个无序序列键成一个堆?
    如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?
    第一个问题,可以直接使用线性数组来表示一个堆,由初始的无序序列建成一个堆就需要自底向上从第
    一个非叶元素开始挨个调整成一个堆。
    第二个问题,怎么调整成堆?首先是将堆顶元素和最后一个元素交换。然后比较当前堆顶元素的左右孩
    子节点,因为除了当前的堆顶元素,左右孩子堆均满足条件,这时需要选择当前堆顶元素与左右孩子节
    点的较大者(大顶堆)交换,直至叶子节点。我们称这个自堆顶自叶子的调整成为筛选。
    从一个无序序列建堆的过程就是一个反复筛选的过程。若将此序列看成是一个完全二叉树,则最后一个
    非终端节点是n/2取底个元素,由此筛选即可。举个栗子:
    left ++; arr[right] = arr[left]; //把大的移动到右边 }arr[left] = pivotKey; //最后把pivot赋值到中间 return left; }/*** 递归划分子序列 * @param arr * @param left * @param right / public static void quickSort(int[] arr, int left, int right) { if(left >= right) return ; int pivotPos = partition(arr, left, right); quickSort(arr, left, pivotPos-1); quickSort(arr, pivotPos+1, right); }public static void sort(int[] arr) { if(arr == null || arr.length == 0) return ; quickSort(arr, 0, arr.length-1); } }
    49,38,65,97,76,13,27,49序列的堆排序建初始堆和调整的过程如下
    实现代码:
    public class HeapSort { /
    ** 堆筛选,除了start之外,start~end均满足大顶堆的定义。 * 调整之后start~end称为一个大顶堆。 * @param arr 待调整数组 * @param start 起始指针 * @param end 结束指针
  6. 希尔排序
    希尔排序是插入排序的一种高效率的实现,也叫缩小增量排序。简单的插入排序中,如果待排序列是正
    序时,时间复杂度是O(n),如果序列是基本有序的,使用直接插入排序效率就非常高。希尔排序就利用
    了这个特点。基本思想是:先将整个待排记录序列分割成为若干子序列分别进行直接插入排序,待整个
    序列中的记录基本有序时再对全体记录进行一次直接插入排序。
    举个栗子:
    / public static void heapAdjust(int[] arr, int start, int end) { int temp = arr[start]; for(int i=2start+1; i<=end; i*=2) { //左右孩子的节点分别为2i+1,2i+2 //选择出左右孩子较小的下标 if(i < end && arr[i] < arr[i+1]) { i ++; }if(temp >= arr[i]) { break; //已经为大顶堆,=保持稳定性。 }arr[start] = arr[i]; //将子节点上移 start = i; //下一轮筛选 }arr[start] = temp; //插入正确的位置 }public static void heapSort(int[] arr) { if(arr == null || arr.length == 0) return ; //建立大顶堆 for(int i=arr.length/2; i>=0; i–) { heapAdjust(arr, i, arr.length-1); }for(int i=arr.length-1; i>=0; i–) { swap(arr, 0, i); heapAdjust(arr, 0, i-1); } }public static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
    从上述排序过程可见,希尔排序的特点是,子序列的构成不是简单的逐段分割,而是将某个相隔某个增
    量的记录组成一个子序列。如上面的例子,第一堂排序时的增量为5,第二趟排序的增量为3。由于前两
    趟的插入排序中记录的关键字是和同一子序列中的前一个记录的关键字进行比较,因此关键字较小的记
    录就不是一步一步地向前挪动,而是跳跃式地往前移,从而使得进行最后一趟排序时,整个序列已经做
    到基本有序,只要作记录的少量比较和移动即可。因此希尔排序的效率要比直接插入排序高。
    希尔排序的分析是复杂的,时间复杂度是所取增量的函数,这涉及一些数学上的难题。但是在大量实验
    的基础上推出当n在某个范围内时,时间复杂度可以达到O(n^1.3)。
    实现代码:
    public class ShellSort { /*** 希尔排序的一趟插入 * @param arr 待排数组 * @param d 增量 */ public static void shellInsert(int[] arr, int d) { for(int i=d; i<arr.length; i++) { int j = i - d; int temp = arr[i]; //记录要插入的数据 while (j>=0 && arr[j]>temp) { //从后向前,找到比其小的数的位置 arr[j+d] = arr[j]; //向后挪动 j -= d; }if (j != i - d) //存在比其小的数 arr[j+d] = temp; } }public static void shellSort(int[] arr) { if(arr == null || arr.length == 0) return ; int d = arr.length / 2; while(d >= 1) { shellInsert(arr, d);
  7. 归并排序
    归并排序是另一种不同的排序方法,因为归并排序使用了递归分治的思想,所以理解起来比较容易。其
    基本思想是,先递归划分子问题,然后合并结果。把待排序列看成由两个有序的子序列,然后合并两个
    子序列,然后把子序列看成由两个有序序列。。。。。倒着来看,其实就是先两两合并,然后四四合
    并。。。最终形成有序序列。空间复杂度为O(n),时间复杂度为O(nlogn)。
    举个栗子:
    实现代码:
    d /= 2; } } }public class MergeSort { public static void mergeSort(int[] arr) { mSort(arr, 0, arr.length-1); }/*** 递归分治 * @param arr 待排数组 * @param left 左指针 * @param right 右指针 / public static void mSort(int[] arr, int left, int right) { if(left >= right) return ; int mid = (left + right) / 2; mSort(arr, left, mid); //递归排序左边 mSort(arr, mid+1, right); //递归排序右边 merge(arr, left, mid, right); //合并 }/*
  8. 计数排序
    如果在面试中有面试官要求你写一个O(n)时间复杂度的排序算法,你千万不要立刻说:这不可能!虽然
    前面基于比较的排序的下限是O(nlogn)。但是确实也有线性时间复杂度的排序,只不过有前提条件,就
    是待排序的数要满足一定的范围的整数,而且计数排序需要比较多的辅助空间。其基本思想是,用待排
    序的数作为计数数组的下标,统计每个数字的个数。然后依次输出即可得到有序序列。
    实现代码:
  • 合并两个有序数组 * @param arr 待合并数组 * @param left 左指针 * @param mid 中间指针 * @param right 右指针 */ public static void merge(int[] arr, int left, int mid, int right) { //[left, mid] [mid+1, right] int[] temp = new int[right - left + 1]; //中间数组 int i = left; int j = mid + 1; int k = 0; while(i <= mid && j <= right) { if(arr[i] <= arr[j]) { temp[k++] = arr[i++]; }else {temp[k++] = arr[j++]; } }while(i <= mid) { temp[k++] = arr[i++]; }while(j <= right) { temp[k++] = arr[j++]; }for(int p=0; p<temp.length; p++) { arr[left + p] = temp[p]; } } }public class CountSort { public static void countSort(int[] arr) { if(arr == null || arr.length == 0) return ; int max = max(arr); int[] count = new int[max+1];
  1. 桶排序
    桶排序算是计数排序的一种改进和推广,但是网上有许多资料把计数排序和桶排序混为一谈。其实桶排
    序要比计数排序复杂许多。
    对桶排序的分析和解释借鉴这位兄弟的文章(有改动):http://hxraid.iteye.com/blog/647759
    桶排序的基本思想:
    假设有一组长度为N的待排关键字序列K[1…n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某
    种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中
    的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用
    快排)。然后依次枚举输出B[0]…B[M]中的全部内容即是一个有序序列。bindex=f(key)其中,bindex 为
    桶数组B的下标(即第bindex个桶), k为待排序列的关键字。桶排序之所以能够高效,其关键在于这个映射
    函数,它必须做到:如果关键字k1<k2,那么f(k1)<=f(k2)。
    也就是说B(i)中的最小数据都要大于B(i-1)中最大数据。很显然,映射函数的确定与数据本身的特点有很
    大的关系。
    Arrays.fill(count, 0); for(int i=0; i<arr.length; i++) { count[arr[i]] ++; }int k = 0; for(int i=0; i<=max; i++) { for(int j=0; j<count[i]; j++) { arr[k++] = i; } } }public static int max(int[] arr) { int max = Integer.MIN_VALUE; for(int ele : arr) { if(ele > max) max = ele; }return max; } }
    举个栗子:
    假如待排序列K={49、38、35、97、76、73、27、49}。这些数据全部在1—100之间。因此我们定制10
    个桶,然后确定映射函数f(k)=k/10。则第一个关键字49将定位到第4个桶中(49/10=4)。依次将所有关键
    字全部堆入桶中,并在每个非空的桶中进行快速排序后得到如图所示。只要顺序输出每个B[i]中的数据就
    可以得到有序序列了。
    桶排序分析:
    桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的f(k)值的计算,其作用就相
    当于快排中划分,希尔排序中的子序列,归并排序中的子问题,已经把大量数据分割成了基本有序的数
    据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。
    对N个关键字进行桶排序的时间复杂度分为两个部分:
    (1) 循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)。
    (2) 利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为∑ O(NilogNi) 。其中Ni
    为第i个桶的数据量。
    很显然,第(2)部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为
    基于比较排序的最好平均时间复杂度只能达到O(N
    logN)了)。因此,我们需要尽量做到下面两点:
    (1) 映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。
    (2) 尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排
    序操作。当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪
    费严重。这就是一个时间代价和空间代价的权衡问题了。
    对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:
    O(N)+O(M(N/M)log(N/M))=O(N+N(logN-logM))=O(N+NlogN-NlogM) 当N=M时,即极限情况下每个桶
    只有一个数据时。桶排序的最好效率能够达到O(N)。
    总结: 桶排序的平均时间复杂度为线性的O(N+C),其中C=N
    (logN-logM)。如果相对于同样的N,桶数
    量M越大,其效率越高,最好的时间复杂度达到O(N)。 当然桶排序的空间复杂度为O(N+M),如果输入
    数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。
    实现代码:
    public class BucketSort { public static void bucketSort(int[] arr) { if(arr == null && arr.length == 0) return ; int bucketNums = 10; //这里默认为10,规定待排数[0,100) List<List> buckets = new ArrayList<List>(); //桶的索引
  2. 基数排序
    基数排序又是一种和前面排序方式不同的排序方式,基数排序不需要进行记录关键字之间的比较。基数
    排序是一种借助多关键字排序思想对单逻辑关键字进行排序的方法。所谓的多关键字排序就是有多个优
    先级不同的关键字。比如说成绩的排序,如果两个人总分相同,则语文高的排在前面,语文成绩也相同
    则数学高的排在前面。。。如果对数字进行排序,那么个位、十位、百位就是不同优先级的关键字,如
    果要进行升序排序,那么个位、十位、百位优先级一次增加。基数排序是通过多次的收分配和收集来实
    现的,关键字优先级低的先进行分配和收集。
    for(int i=0; i<10; i++) { buckets.add(new LinkedList()); //用链表比较合适 }//划分桶 for(int i=0; i<arr.length; i++) { buckets.get(f(arr[i])).add(arr[i]); }//对每个桶进行排序 for(int i=0; i<buckets.size(); i++) { if(!buckets.get(i).isEmpty()) { Collections.sort(buckets.get(i)); //对每个桶进行快排 } }//还原排好序的数组 int k = 0; for(List bucket : buckets) { for(int ele : bucket) { arr[k++] = ele; } } }/*** 映射函数 * @param x * @return / public static int f(int x) { return x / 10; } }
    举个栗子:
    实现代码:
    public class RadixSort { public static void radixSort(int[] arr) { if(arr == null && arr.length == 0) return ; int maxBit = getMaxBit(arr);
    for(int i=1; i<=maxBit; i++) { List<List> buf = distribute(arr, i); //分配 collecte(arr, buf); //收集 } }/
    ** 分配 * @param arr 待分配数组 * @param iBit 要分配第几位 * @return / public static List<List> distribute(int[] arr, int iBit) { List<List> buf = new ArrayList<List>(); for(int j=0; j<10; j++) { buf.add(new LinkedList()); }for(int i=0; i<arr.length; i++) { buf.get(getNBit(arr[i], iBit)).add(arr[i]); }return buf; }/** 收集 * @param arr 把分配的数据收集到arr中 * @param buf / public static void collecte(int[] arr, List<List> buf) { int k = 0; for(List bucket : buf) { for(int ele : bucket) { arr[k++] = ele; } } }/** 获取最大位数 * @param x * @return / public static int getMaxBit(int[] arr) { int max = Integer.MIN_VALUE; for(int ele : arr) { int len = (ele+"").length(); if(len > max) max = len; }return max; }/*

  1. 排序算法的各自的使用场景和适用场合。
  2. 从平均时间来看,快速排序是效率最高的,但快速排序在最坏情况下的时间性能不如堆排序和归并
    排序。而后者相比较的结果是,在n较大时归并排序使用时间较少,但使用辅助空间较多。
  3. 上面说的简单排序包括除希尔排序之外的所有冒泡排序、插入排序、简单选择排序。其中直接插入
    排序最简单,但序列基本有序或者n较小时,直接插入排序是好的方法,因此常将它和其他的排序
    方法,如快速排序、归并排序等结合在一起使用。
  4. 基数排序的时间复杂度也可以写成O(d*n)。因此它最使用于n值很大而关键字较小的的序列。若关
    键字也很大,而序列中大多数记录的最高关键字均不同,则亦可先按最高关键字不同,将序列分成
    若干小的子序列,而后进行直接插入排序。
  5. 从方法的稳定性来比较,基数排序是稳定的内排方法,所有时间复杂度为O(n^2)的简单排序也是稳
    定的。但是快速排序、堆排序、希尔排序等时间性能较好的排序方法都是不稳定的。稳定性需要根
    据具体需求选择。
  6. 上面的算法实现大多数是使用线性存储结构,像插入排序这种算法用链表实现更好,省去了移动元
    素的时间。具体的存储结构在具体的实现版本中也是不同的。
    附:基于比较排序算法时间下限为O(nlogn)的证明:
    基于比较排序下限的证明是通过决策树证明的,决策树的高度Ω(nlgn),这样就得出了比较排序的下
    限。
  • 获取x的第n位,如果没有则为0. * @param x * @param n * @return */ public static int getNBit(int x, int n) { String sx = x + “”; if(sx.length() < n) return 0; elsereturn sx.charAt(sx.length()-n) - ‘0’; } }
    首先要引入决策树。 首先决策树是一颗二叉树,每个节点表示元素之间一组可能的排序,它予以京进行
    的比较相一致,比较的结果是树的边。 先来说明一些二叉树的性质,令T是深度为d的二叉树,则T最多
    有2^片树叶。 具有L片树叶的二叉树的深度至少是logL。 所以,对n个元素排序的决策树必然有n!片树叶
    (因为n个数有n!种不同的大小关系),所以决策树的深度至少是log(n!),即至少需要log(n!)次比较。 而
    log(n!)=logn+log(n-1)+log(n-2)+…+log2+log1 >=logn+log(n-1)+log(n-2)+…+log(n/2) >=(n/2)log(n/2)

=(n/2)logn-n/2 =O(nlogn)所以只用到比较的排序算法最低时间复杂度是O(nlogn)。
网络协议面试题

  1. 什么是网络编程
    网络编程的本质是多台计算机之间的数据交换。数据传递本身没有多大的难度,不就是把一个设备
    中的数据发送给其他设备,然后接受另外一个设备反馈的数据。现在的网络编程基本上都是基于请
    求/响应方式的,也就是一个设备发送请求数据给另外一个,然后接收另一个设备的反馈。在网络
    编程中,发起连接程序,也就是发送第一次请求的程序,被称作客户端(Client),等待其他程序连接
    的程序被称作服务器(Server)。客户端程序可以在需要的时候启动,而服务器为了能够时刻相应连
    接,则需要一直启动。
    例如以打电话为例,首先拨号的人类似于客户端,接听电话的人必须保持电话畅通类似于服务器。
    连接一旦建立以后,就客户端和服务器端就可以进行数据传递了,而且两者的身份是等价的。在一
    些程序中,程序既有客户端功能也有服务器端功能,最常见的软件就是QQ、微信这类软件了。
  2. 网络编程中两个主要的问题
    1、一个是如何准确的定位网络上一台或多台主机,
    2、另一个就是找到主机后如何可靠高效的进行数据传输。
    在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的路由,由IP地址可以唯一地确定
    Internet上的一台主机。
    而TCP层则提供面向应用的可靠(TCP)的或非可靠(UDP)的数据传输机制,这是网络编程的主
    要对象,一般不需要关心IP层是如何处理数据的。
    目前较为流行的网络编程模型是客户机/服务器(C/S)结构。即通信双方一方作为服务器等待客户
    提出请求并予以响应。客户则在需要服务时向服务器提 出申请。服务器一般作为守护进程始终运
    行,监听网络端口,一旦有客户请求,就会启动一个服务进程来响应该客户,同时自己继续监听服
    务端口,使后来的客户也 能及时得到服务。
  3. 网络协议是什么
    在计算机网络要做到井井有条的交换数据,就必须遵守一些事先约定好的规则,比如交换数据的格式、
    是否需要发送一个应答信息。这些规则被称为网络协议。
  4. 为什么要对网络协议分层
    1、简化问题难度和复杂度。由于各层之间独立,我们可以分割大问题为小问题。
    2、灵活性好。当其中一层的技术变化时,只要层间接口关系保持不变,其他层不受影响。
    3、易于实现和维护。
    4、促进标准化工作。分开后,每层功能可以相对简单地被描述
  5. 计算机网络体系结构
    .
    OSI参考模型
    OSI(Open System Interconnect),即开放式系统互联。一般都叫OSI参考模型,是ISO(国际标
    准化组织)组织在1985年研究的网络互连模型。ISO为了更好的使网络应用更为普及,推出了OSI
    参考模型,这样所有的公司都按照统一的标准来指定自己的网络,就可以互通互联了。
    OSI定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用
    层)。
    TCP/IP参考模型
    TCP/IP四层协议(数据链路层、网络层、传输层、应用层)
    1、应用层 应用层最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服
    务。我们常见应用层的网络服务协议有:HTTP,HTTPS,FTP,TELNET等。
    2、传输层 建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输
    服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到
    的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说
    的,TCP UDP就是在这一层。端口号既是这里的“端”。 3、网络层 本层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由
    和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说
    的IP协议层。IP协议是Internet的基础。
    4、数据链路层 通过一些规程或协议来控制这些数据的传输,以保证被传输数据的正确性。实现这些规
    程或协议的 硬件 和软件加到物理线路,这样就构成了数据链路,
  6. 什么是TCP/IP和UDP
    1、TCP/IP即传输控制/网络协议,是面向连接的协议,发送数据前要先建立连接(发送方和接收方的成对
    的两个之间必须建 立连接),TCP提供可靠的服务,也就是说,通过TCP连接传输的数据不会丢失,没有
    重复,并且按顺序到达
    2、UDP它是属于TCP/IP协议族中的一种。是无连接的协议,发送数据前不需要建立连接,是没有可靠
    性的协议。因为不需要建立连接所以可以在在网络上以任何可能的路径传输,因此能否到达目的地,到
    达目的地的时间以及内容的正确性都是不能被保证的。
  7. TCP与UDP区别:
    1、TCP是面向连接的协议,发送数据前要先建立连接,TCP提供可靠的服务,也就是说,通过TCP连接
    传输的数据不会丢失,没有重复,并且按顺序到达;
    2、UDP是无连接的协议,发送数据前不需要建立连接,是没有可靠性;
    3、TCP通信类似于于要打个电话,接通了,确认身份后,才开始进行通行;
    4、UDP通信类似于学校广播,靠着广播播报直接进行通信。
    5、TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多;
    6、TCP是面向字节流的,UDP是面向报文的; 面向字节流是指发送数据时以字节为单位,一个数据包
    可以拆分成若干组进行发送,而UDP一个报文只能一次发完。
    7、TCP首部开销(20字节)比UDP首部开销(8字节)要大
    8、UDP 的主机不需要维持复杂的连接状态表
  8. TCP和UDP的应用场景:
    对某些实时性要求比较高的情况使用UDP,比如游戏,媒体通信,实时直播,即使出现传输错误也可以
    容忍;其它大部分情况下,HTTP都是用TCP,因为要求传输的内容可靠,不出现丢失的情况
  9. 形容一下TCP和UDP
    TCP通信可看作打电话:
    李三(拨了个号码):喂,是王五吗? 王五:哎,您谁啊? 李三:我是李三,我想给你说点事儿,你现在
    方便吗? 王五:哦,我现在方便,你说吧。 甲:那我说了啊? 乙:你说吧。 (连接建立了,接下来就是
    说正事了…)
    UDP通信可看为学校里的广播:
    播音室:喂喂喂!全体操场集合
  10. 运行在TCP 或UDP的应用层协议分析。
    运行在TCP协议上的协议:
    HTTP(Hypertext Transfer Protocol,超文本传输协议),主要用于普通浏览。
    HTTPS(HTTP over SSL,安全超文本传输协议),HTTP协议的安全版本。
    FTP(File Transfer Protocol,文件传输协议),用于文件传输。
    POP3(Post Office Protocol, version 3,邮局协议),收邮件用。
    SMTP(Simple Mail Transfer Protocol,简单邮件传输协议),用来发送电子邮件。
    TELNET(Teletype over the Network,网络电传),通过一个终端(terminal)登陆到网络。
    SSH(Secure Shell,用于替代安全性差的TELNET),用于加密安全登陆用。
    运行在UDP协议上的协议:
    BOOTP(Boot Protocol,启动协议),应用于无盘设备。
    NTP(Network Time Protocol,网络时间协议),用于网络同步。
    DHCP(Dynamic Host Configuration Protocol,动态主机配置协议),动态配置IP地址。
    运行在TCP和UDP协议上:
    DNS(Domain Name Service,域名服务),用于完成地址查找,邮件转发等工作。
    ECHO(Echo Protocol,回绕协议),用于查错及测量应答时间(运行在TCP和UDP协议上)。
    SNMP(Simple Network Management Protocol,简单网络管理协议),用于网络信息的收集和
    网络管理。
    DHCP(Dynamic Host Configuration Protocol,动态主机配置协议),动态配置IP地址。
    ARP(Address Resolution Protocol,地址解析协议),用于动态解析以太网硬件的地址。
  11. 什么是Http协议?
    Http协议是对客户端和服务器端之间数据之间实现可靠性的传输文字、图片、音频、视频等超文本
    数据的规范,格式简称为“超文本传输协议”
    Http协议属于应用层,及用户访问的第一层就是http
  12. Http和Https的区别?
    Http协议运行在TCP之上,明文传输,客户端与服务器端都无法验证对方的身份;Https是身披
    SSL(Secure Socket Layer)外壳的Http,运行于SSL上,SSL运行于TCP之上,是添加了加密和认证机制的
    HTTP。二者之间存在如下不同:
    端口不同:Http与Http使用不同的连接方式,用的端口也不一样,前者是80,后者是443;
    资源消耗:和HTTP通信相比,Https通信会由于加减密处理消耗更多的CPU和内存资源;
    开销:Https通信需要证书,而证书一般需要向认证机构购买;
    Https的加密机制是一种共享密钥加密和公开密钥加密并用的混合加密机制。
  13. 什么是http的请求体?
    1、HTTP请求体是我们请求数据时先发送给服务器的数据,毕竟我向服务器那数据,先要表明我要什么
    吧2、HTTP请求体由:请求行 、请求头、请求数据组成的,
    3、注意:GIT请求是没有请求体的
    POST请求
    GET请求是没有请求体的
  14. HTTP的响应报文有哪些?
    1、http的响应报是服务器返回给我们的数据,必须先有请求体再有响应报文
    2、响应报文包含三部分 状态行、响应首部字段、响应内容实体实现
  15. HTTPS工作原理
    1、首先HTTP请求服务端生成证书,客户端对证书的有效期、合法性、域名是否与请求的域名一致、证
    书的公钥(RSA加密)等进行校验;
    2、客户端如果校验通过后,就根据证书的公钥的有效, 生成随机数,随机数使用公钥进行加密(RSA
    加密);
    3、消息体产生的后,对它的摘要进行MD5(或者SHA1)算法加密,此时就得到了RSA签名;
    4、发送给服务端,此时只有服务端(RSA私钥)能解密。
    5、解密得到的随机数,再用AES加密,作为密钥(此时的密钥只有客户端和服务端知道)。
  16. 三次握手与四次挥手
    (1). 三次握手(我要和你建立链接,你真的要和我建立链接么,我真的要和你建立链接,成功)
    第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,
    Client进入SYN_SENT状态,等待Server确认。
    第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN
    和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,
    Server进入SYN_RCVD状态。
    第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为
    1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则
    连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间
    可以开始传输数据了。
    (2). 四次挥手(我要和你断开链接;好的,断吧。我也要和你断开链接;好的,断吧):第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1
    状态。
    第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一
    个FIN占用一个序号),Server进入CLOSE_WAIT状态。此时TCP链接处于半关闭状态,即客户端已
    经没有要发送的数据了,但服务端若发送数据,则客户端仍要接收。
    第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状
    态。
    第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序
    号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
  17. 为什么 TCP 链接需要三次握手,两次不可以么?
    “三次握手” 的目的是为了防止已失效的链接请求报文突然又传送到了服务端,因而产生错误。
    正常的情况:A 发出连接请求,但因连接请求报文丢失而未收到确认,于是 A 再重传一次连接请
    求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接。A 共发送了两个连接请求报
    文段,其中第一个丢失,第二个到达了 B。没有 “已失效的连接请求报文段”。
    现假定出现了一种异常情况:即 A 发出的第一个连接请求报文段并没有丢失,而是在某个网络结点
    长时间的滞留了,以致延误到连接释放以后的某个时间才到达 B。本来这是一个早已失效的报文
    段。但 B 收到此失效的连接请求报文段后,就误认为是 A 再次发出的一个新的连接请求。于是就
    向 A 发出确认报文段,同意建立连接。
    假设不采用“三次握手”,那么只要 B 发出确认,新的连接就建立了。由于现在 A 并没有发出建立连接的
    请求,因此不会理睬 B 的确认,也不会向 B 发送数据。但 B 却以为新的运输连接已经建立,并一直等待
    A 发来数据。这样,B 的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。
  18. 用现实理解三次握手的具体细节
    三次握手的目的是建立可靠的通信信道,主要的目的就是双方确认自己与对方的发送与接收机能正常。
    1、第一次握手:客户什么都不能确认;服务器确认了对方发送正常
    2、第二次握手:客户确认了:自己发送、接收正常,对方发送、接收正常;服务器确认 了:自己接收
    正常,对方发送正常
    3、第三次握手:客户确认了:自己发送、接收正常,对方发送、接收正常;服务器确认 了:自己发
    送、接收正常,对方发送接收正常 所以三次握手就能确认双发收发功能都正常,缺一不可。
  19. 建立连接可以两次握手吗?为什么?
    不可以。
    因为可能会出现已失效的连接请求报文段又传到了服务器端。 > client 发出的第一个连接请求报文段并
    没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达 server。
    本来这是一个早已失效的报文段。但 server 收到此失效的连接请求报文段后,就误认为是 client 再次发
    出的一个新的连接请求。于是就向 client 发出确认报文段,同意建立连接。假设不采用 “三次握手”,那
    么只要 server 发出确认,新的连接就建立了。由于现在 client 并没有发出建立连接的请求,因此不会理
    睬 server 的确认,也不会向 server 发送数据。但 server 却以为新的运输连接已经建立,并一直等待
    client 发来数据。这样,server 的很多资源就白白浪费掉了。采用 “三次握手” 的办法可以防止上述现象
    发生。例如刚才那种情况,client 不会向 server 的确认发出确认。server 由于收不到确认,就知道
    client 并没有要求建立连接。
    而且,两次握手无法保证Client正确接收第二次握手的报文(Server无法确认Client是否收到),也无法
    保证Client和Server之间成功互换初始序列号。
  20. 为什么要四次挥手?
    TCP 协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP 是全双工模式,这就意味
    着,当 A 向 B 发出 FIN 报文段时,只是表示 A 已经没有数据要发送了,而此时 A 还是能够接受到来自 B
    发出的数据;B 向 A 发出 ACK 报文段也只是告诉 A ,它自己知道 A 没有数据要发了,但 B 还是能够向
    A 发送数据。
    所以想要愉快的结束这次对话就需要四次挥手。
  21. TCP 协议如何来保证传输的可靠性
    TCP 提供一种面向连接的、可靠的字节流服务。其中,面向连接意味着两个使用 TCP 的应用(通常是一
    个客户和一个服务器)在彼此交换数据之前必须先建立一个 TCP 连接。在一个 TCP 连接中,仅有两方进
    行彼此通信;而字节流服务意味着两个应用程序通过 TCP 链接交换 8 bit 字节构成的字节流,TCP 不在
    字节流中插入记录标识符。
    对于可靠性,TCP通过以下方式进行保证:
    数据包校验:目的是检测数据在传输过程中的任何变化,若校验出包有错,则丢弃报文段并且不给
    出响应,这时TCP发送数据端超时后会重发数据;
    对失序数据包重排序:既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此
    TCP报文段的到达也可能会失序。TCP将对失序数据进行重新排序,然后才交给应用层;
    丢弃重复数据:对于重复数据,能够丢弃重复数据;
    应答机制:当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,
    通常将推迟几分之一秒;
    超时重发:当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能
    及时收到一个确认,将重发这个报文段;
    流量控制:TCP连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送接收端缓
    冲区所能接纳的数据,这可以防止较快主机致使较慢主机的缓冲区溢出,这就是流量控制。TCP使
    用的流量控制协议是可变大小的滑动窗口协议。
  22. 客户端不断进行请求链接会怎样?DDos(Distributed Denial of
    Service)攻击?
    服务器端会为每个请求创建一个链接,并向其发送确认报文,然后等待客户端进行确认
    (1). DDos 攻击:
    客户端向服务端发送请求链接数据包
    服务端向客户端发送确认数据包
    客户端不向服务端发送确认数据包,服务器一直等待来自客户端的确认
    (2). DDos 预防:(没有彻底根治的办法,除非不使用TCP)
    限制同时打开SYN半链接的数目
    缩短SYN半链接的Time out 时间
    关闭不必要的服务
  23. GET 与 POST 的区别?
    GET与POST是我们常用的两种HTTP Method,二者之间的区别主要包括如下五个方面:
    1、 从功能上讲,GET一般用来从服务器上获取资源,POST一般用来更新服务器上的资源;
    2、从REST服务角度上说,GET是幂等的,即读取同一个资源,总是得到相同的数据,而POST不是幂等
    的,因为每次请求对资源的改变并不是相同的;进一步地,GET不会改变服务器上的资源,而POST会对
    服务器资源进行改变;
    3、从请求参数形式上看,GET请求的数据会附在URL之后,即将请求数据放置在HTTP报文的 请求头
    中,以?分割URL和传输数据,参数之间以&相连。特别地,如果数据是英文字母/数字,原样发送;否
    则,会将其编码为 application/x-www-form-urlencoded MIME 字符串(如果是空格,转换为+,如果是
    中文/其他字符,则直接把字符串用BASE64加密,得出如:%E4%BD%A0%E5%A5%BD,其中%XX中的
    XX为该符号以16进制表示的ASCII);而POST请求会把提交的数据则放置在是HTTP请求报文的 请求体
    中。
    4、就安全性而言,POST的安全性要比GET的安全性高,因为GET请求提交的数据将明文出现在URL上,
    而且POST请求参数则被包装到请求体中,相对更安全。
    5、从请求的大小看,GET请求的长度受限于浏览器或服务器对URL长度的限制,允许发送的数据量比较
    小,而POST请求则是没有大小限制的。
  24. 为什么在GET请求中会对URL进行编码?
    我们知道,在GET请求中会对URL中非西文字符进行编码,这样做的目的就是为了 避免歧义。看下面的
    例子,
    针对 “name1=value1&name2=value2” 的例子,我们来谈一下数据从客户端到服务端的解析过程。首
    先,上述字符串在计算机中用ASCII吗表示为:
    服务端在接收到该数据后就可以遍历该字节流,一个字节一个字节的吃,当吃到3D这字节后,服务端就
    知道前面吃得字节表示一个key,再往后吃,如果遇到26,说明从刚才吃的3D到26子节之间的是上一个
    key的value,以此类推就可以解析出客户端传过来的参数。
    现在考虑这样一个问题,如果我们的参数值中就包含=或&这种特殊字符的时候该怎么办?比如,
    “name1=value1”,其中value1的值是“va&lu=e1”字符串,那么实际在传输过程中就会变成这样
    “name1=va&lu=e1”。这样,我们的本意是只有一个键值对,但是服务端却会解析成两个键值对,这样
    就产生了歧义。
    那么,如何解决上述问题带来的歧义呢?解决的办法就是对参数进行URL编码:例如,我们对上述会产
    生歧义的字符进行URL编码后结果:“name1=va%26lu%3D”,这样服务端会把紧跟在“%”后的字节当成
    普通的字节,就是不会把它当成各个参数或键值对的分隔符。
    6E616D6531 3D 76616C756531 26 6E616D6532 3D 76616C756532 6E616D6531:name1 3D:= 76616C756531:value1 26:& 6E616D6532:name2 3D:= 76616C756532: value2复制代码
  25. TCP与UDP的区别
    TCP (Transmission Control Protocol)和UDP(User Datagram Protocol)协议属于传输层协议,它们之
    间的区别包括:
    TCP是面向连接的,UDP是无连接的;
    TCP是可靠的,UDP是不可靠的;
    TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多的通信模式;
    TCP是面向字节流的,UDP是面向报文的;
    TCP有拥塞控制机制;UDP没有拥塞控制,适合媒体通信;
    TCP首部开销(20个字节)比UDP的首部开销(8个字节)要大;
  26. TCP和UDP分别对应的常见应用层协议
    1、 TCP 对应的应用层协议:
    FTP:定义了文件传输协议,使用21端口。常说某某计算机开了FTP服务便是启动了文件传输服
    务。下载文件,上传主页,都要用到FTP服务。
    Telnet:它是一种用于远程登陆的端口,用户可以以自己的身份远程连接到计算机上,通过这种端
    口可以提供一种基于DOS模式下的通信服务。如以前的BBS是-纯字符界面的,支持BBS的服务器将
    23端口打开,对外提供服务。
    SMTP:定义了简单邮件传送协议,现在很多邮件服务器都用的是这个协议,用于发送邮件。如常
    见的免费邮件服务中用的就是这个邮件服务端口,所以在电子邮件设置-中常看到有这么SMTP端口
    设置这个栏,服务器开放的是25号端口。
    POP3:它是和SMTP对应,POP3用于接收邮件。通常情况下,POP3协议所用的是110端口。也是
    说,只要你有相应的使用POP3协议的程序(例如Fo-xmail或Outlook),就可以不以Web方式登
    陆进邮箱界面,直接用邮件程序就可以收到邮件(如是163邮箱就没有必要先进入网易网站,再进
    入自己的邮-箱来收信)。
    HTTP:从Web服务器传输超文本到本地浏览器的传送协议。
    2、 UDP 对应的应用层协议:
    DNS:用于域名解析服务,将域名地址转换为IP地址。DNS用的是53号端口。
    SNMP:简单网络管理协议,使用161号端口,是用来管理网络设备的。由于网络设备很多,无连
    接的服务就体现出其优势。
    TFTP(Trival File Transfer Protocal):简单文件传输协议,该协议在熟知端口69上使用UDP服务
  27. TCP 的拥塞避免机制
    拥塞:对资源的需求超过了可用的资源。若网络中许多资源同时供应不足,网络的性能就要明显变坏,
    整个网络的吞吐量随之负荷的增大而下降。
    拥塞控制:防止过多的数据注入到网络中,使得网络中的路由器或链路不致过载。
    拥塞控制的方法:. 1、 慢启动 + 拥塞避免:
    慢启动:不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞
    窗口的大小;
    拥塞避免:拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd
    加1,而不是加倍,这样拥塞窗口按线性规律缓慢增长。
    2、快重传 + 快恢复:
    快重传:快重传要求接收方在收到一个 失序的报文段 后就立即发出 重复确认(为的是使发送方及早知
    道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收
    到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。
    快恢复:快重传配合使用的还有快恢复算法,当发送方连续收到三个重复确认时,就执行“乘法减小”算
    法,把ssthresh门限减半,但是接下去并不执行慢开始算法:因为如果网络出现拥塞的话就不会收到好
    几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将
    cwnd设置为ssthresh的大小,然后执行拥塞避免算法。
  28. 什么是Socket
    1、网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个
    Socket。Socket通常用来实现客户方和服务方的连接。Socket是TCP/IP协议的一个十分流行的编程界
    面,一个Socket由一个IP地址和一个端口号唯一确定。
    2、但是,Socket所支持的协议种类也不光TCP/IP、UDP,因此两者之间是没有必然联系的。在Java环境
    下,Socket编程主要是指基于TCP/IP协议的网络编程。
    3、socket连接就是所谓的长连接,客户端和服务器需要互相连接,理论上客户端和服务器端一旦建立起
    连接将不会主动断掉的,但是有时候网络波动还是有可能的
    4、Socket偏向于底层。一般很少直接使用Socket来编程,框架底层使用Socket比较多,
  29. socket属于网络的那个层面
    Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就
    是一个外观模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是
    全部,让Socket去组织数据,以符合指定的协议。
  30. Socket通讯的过程
    基于TCP:
    服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客
    户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时
    客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应
    数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
    基于UDP:
    UDP 协议是用户数据报协议的简称,也用于网络数据的传输。虽然 UDP 协议是一种不太可靠的协议,
    但有时在需要较快地接收数据并且可以忍受较小错误的情况下,UDP 就会表现出更大的优势。我客户端
    只需要发送,服务端能不能接收的到我不管
  31. Socket和http的区别和应用场景
    1、Socket连接就是所谓的长连接,理论上客户端和服务器端一旦建立起连接将不会主动断掉;
    2、Socket适用场景:网络游戏,银行持续交互,直播,在线视屏等。
    3、http连接就是所谓的短连接,即客户端向服务器端发送一次请求,服务器端响应后连接即会断开等待
    下次连接
    4、http适用场景:公司OA服务,互联网服务,电商,办公,网站等等等等
  32. 一次完整的HTTP请求所经历几个步骤?
    HTTP通信机制是在一次完整的HTTP通信过程中,Web浏览器与Web服务器之间将完成下列7个步骤:
    1、建立TCP连接
    怎么建立连接的,看上面的三次握手
    2、Web浏览器向Web服务器发送请求行
    一旦建立了TCP连接,Web浏览器就会向Web服务器发送请求命令。例如:GET /sample/hello.jsp
    HTTP/1.1。 3、Web浏览器发送请求头
    浏览器发送其请求命令之后,还要以头信息的形式向Web服务器发送一些别的信息,之后浏览器发送了
    一空白行来通知服务器,它已经结束了该头信息的发送。
    4、Web服务器应答
    客户机向服务器发出请求后,服务器会客户机回送应答, HTTP/1.1 200 OK ,应答的第一部分是协议的
    版本号和应答状态码。
    5、Web服务器发送应答头
    正如客户端会随同请求发送关于自身的信息一样,服务器也会随同应答向用户发送关于它自己的数据及
    被请求的文档。
    6、Web服务器向浏览器发送数据
    Web服务器向浏览器发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着,它就
    以Content-Type应答头信息所描述的格式发送用户所请求的实际数据。
    7、Web服务器关闭TCP连接
  33. 浏览器中输入:“ www.xxx.com ” 之后都发生了什么?请详细阐
    述。
    解析:经典的网络协议问题。
    1、由域名→IP地址 寻找IP地址的过程依次经过了浏览器缓存、系统缓存、hosts文件、路由器缓存、 递
    归搜索根域名服务器。
    2、建立TCP/IP连接(三次握手具体过程)
    3、由浏览器发送一个HTTP请求
    4、经过路由器的转发,通过服务器的防火墙,该HTTP请求到达了服务器
    5、服务器处理该HTTP请求,返回一个HTML文件
    6、浏览器解析该HTML文件,并且显示在浏览器端
    7、这里需要注意:
    HTTP协议是一种基于TCP/IP的应用层协议,进行HTTP数据请求必须先建立TCP/IP连接
    可以这样理解:HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网
    络通信的能力。
    两个计算机之间的交流无非是两个端口之间的数据通信,具体的数据会以什么样的形式展现是以不同
    的应用层协议来定义的。
  34. 什么是 HTTP 协议无状态协议?怎么解决Http协议无状态协议?
    HTTP 是一个无状态的协议,也就是没有记忆力,这意味着每一次的请求都是独立的,缺少状态意味着
    如果后续处理需要前面的信息,则它必须要重传,这样可能导致每次连接传送的数据量增大。另一方
    面,在服务器不需要先前信息时它的应答就很快。
    HTTP 的这种特性有优点也有缺点:
    优点:解放了服务器,每一次的请求“点到为止”,不会造成不必要的连接占用
    缺点:每次请求会传输大量重复的内容信息,并且,在请求之间无法实现数据的共享
    解决方案:
  35. 使用参数传递机制:
    将参数拼接在请求的 URL 后面,实现数据的传递(GET方式),例如: /param/list? username=wmyskxz
    问题:可以解决数据共享的问题,但是这种方式一不安全,二数据允许传输量只有1kb
  36. 使用 Cookie 技术
  37. 使用 Session 技术
  38. Session、Cookie 与 Application
    Cookie和Session都是客户端与服务器之间保持状态的解决方案,具体来说,cookie机制采用的是在客
    户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。
    1、Cookie 及其相关 API :
    Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用
    response向客户端浏览器颁发一个Cookie,而客户端浏览器会把Cookie保存起来。当浏览器再请求该
    网站时,浏览器把请求的网址连同该Cookie一同提交给服务器,服务器检查该Cookie,以此来辨认用户
    状态。服务器还可以根据需要修改Cookie的内容。
    2、Session 及其相关 API:
    同样地,会话状态也可以保存在服务器端。客户端请求服务器,如果服务器记录该用户状态,就获取
    Session来保存状态,这时,如果服务器已经为此客户端创建过session,服务器就按照sessionid把这个
    session检索出来使用;如果客户端请求不包含sessionid,则为此客户端创建一个session并且生成一个
    与此session相关联的sessionid,并将这个sessionid在本次响应中返回给客户端保存。保存这个
    sessionid的方式可以采用 cookie机制 ,这样在交互过程中浏览器可以自动的按照规则把这个标识发挥
    给服务器;若浏览器禁用Cookie的话,可以通过 URL重写机制将sessionid传回服务器。
    3、 Session 与 Cookie 的对比:
    实现机制:Session的实现常常依赖于Cookie机制,通过Cookie机制回传SessionID;
    大小限制:Cookie有大小限制并且浏览器对每个站点也有cookie的个数限制,Session没有大小限
    制,理论上只与服务器的内存大小有关;
    安全性:Cookie存在安全隐患,通过拦截或本地文件找得到cookie后可以进行攻击,而Session由
    于保存在服务器端,相对更加安全;
    服务器资源消耗:Session是保存在服务器端上会存在一段时间才会消失,如果session过多会增加
    服务器的压力。
    4、Application:
    Application(ServletContext):与一个Web应用程序相对应,为应用程序提供了一个全局的状态,所
    有客户都可以使用该状态。
  39. 滑动窗口机制
    由发送方和接收方在三次握手阶段,互相将自己的最大可接收的数据量告诉对方。
    也就是自己的数据接收缓冲池的大小。这样对方可以根据已发送的数据量来计算是否可以接着发送。在
    处理过程中,当接收缓冲池的大小发生变化时,要给对方发送更新窗口大小的通知。这就实现了流量的
    控制。
  40. 常用的HTTP方法有哪些?
    GET:用于请求访问已经被URI(统一资源标识符)识别的资源,可以通过URL传参给服务器
    POST:用于传输信息给服务器,主要功能与GET方法类似,但一般推荐使用POST方式。
    PUT:传输文件,报文主体中包含文件内容,保存到对应URI位置。
    HEAD:获得报文首部,与GET方法类似,只是不返回报文主体,一般用于验证URI是否有效。
    DELETE:删除文件,与PUT方法相反,删除对应URI位置的文件。
    OPTIONS:查询相应URI支持的HTTP方法。
  41. 常见HTTP状态码
    1、1xx(临时响应)
    2、2xx(成功)
    3、3xx(重定向):表示要完成请求需要进一步操作
    4、4xx(错误):表示请求可能出错,妨碍了服务器的处理
    5、5xx(服务器错误):表示服务器在尝试处理请求时发生内部错误
    常见状态码:
    200(成功)
    304(未修改):自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内

    401(未授权):请求要求身份验证
    403(禁止):服务器拒绝请求
    404(未找到):服务器找不到请求的网页
  42. SQL 注入
    SQL注入就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗
    服务器执行恶意的SQL命令。
    1、SQL注入攻击的总体思路:
  43. 寻找到SQL注入的位置
  44. 判断服务器类型和后台数据库类型
  45. 针对不通的服务器和数据库特点进行SQL注入攻击
    2、SQL注入攻击实例:
    比如,在一个登录界面,要求输入用户名和密码,可以这样输入实现免帐号登录:
    用户一旦点击登录,如若没有做特殊处理,那么这个非法用户就很得意的登陆进去了。这是为什么呢?下
    面我们分析一下:从理论上说,后台认证程序中会有如下的SQL语句:
    用户名: ‘or 1 = 1 --密 码:复制代码 String sql = “select * from user_table where username=’ “+userName+” ’ and password=’ “+password+” ‘”;
    因此,当输入了上面的用户名和密码,上面的SQL语句变成:
    分析上述SQL语句我们知道,username=‘ or 1=1 这个语句一定会成功;然后后面加两个-,这意味着注
    释,它将后面的语句注释,让他们不起作用。这样,上述语句永远都能正确执行,用户轻易骗过系统,
    获取合法身份。
    3、应对方法:
    1.参数绑定:
    使用预编译手段,绑定参数是最好的防SQL注入的方法。目前许多的ORM框架及JDBC等都实现了SQL预
    编译和参数绑定功能,攻击者的恶意SQL会被当做SQL的参数而不是SQL命令被执行。在mybatis的
    mapper文件中,对于传递的参数我们一般是使用#和
    时,变量就是直接追加在sql中,一般会有sql注入问题。
    2.使用正则表达式过滤传入的参数
  46. XSS 攻击
    XSS是一种经常出现在web应用中的计算机安全漏洞,与SQL注入一起成为web中最主流的攻击方式。
    XSS是指恶意攻击者利用网站没有对用户提交数据进行转义处理或者过滤不足的缺点,进而添加一些脚
    本代码嵌入到web页面中去,使别的用户访问都会执行相应的嵌入代码,从而盗取用户资料、利用用户
    身份进行某种动作或者对访问者进行病毒侵害的一种攻击方式。
    1、XSS攻击的危害:
    盗取各类用户帐号,如机器登录帐号、用户网银帐号、各类管理员帐号
    控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力
    盗窃企业重要的具有商业价值的资料
    非法转账
    强制发送电子邮件
    网站挂马
    控制受害者机器向其它网站发起攻击
    2、原因解析:
    主要原因:过于信任客户端提交的数据!
    解决办法:不信任任何客户端提交的数据,只要是客户端提交的数据就应该先进行相应的过滤处理
    然后方可进行下一步的操作。
    进一步分析细节:客户端提交的数据本来就是应用所需要的,但是恶意攻击者利用网站对客户端提
    交数据的信任,在数据中插入一些符号以及javascript代码,那么这些数据将会成为应用代码中的
    SELECT * FROM user_table WHERE username=’’or 1 = 1 – and password=’’ 不能识别此Latex公式:来获取参数值。当使用#时,变量是占位符,就是一般我们使用javajdbc的 PrepareStatement时的占位符,所有可以防止sql注入;当使用复制代码
    一部分了,那么攻击者就可以肆无忌惮地展开攻击啦,因此我们绝不可以信任任何客户端提交的数
    据!!!
    3、XSS 攻击分类:
  47. 反射性 XSS 攻击(非持久性 XSS 攻击):
    漏洞产生的原因是攻击者注入的数据反映在响应中。一个典型的非持久性XSS攻击包含一个带XSS攻击向
    量的链接(即每次攻击需要用户的点击),例如,正常发送消息:
    http://www.test.com/message.php?send=Hello,World!复制代码
    接收者将会接收信息并显示Hello,World;但是,非正常发送消息:
    http://www.test.com/message.php?send= 复制代码
    并将数据提交、存储到数据库中;当其他用户取出数据显示的时候,将会执行这些攻击性代码。
    4、修复漏洞方针:
    漏洞产生的根本原因是 太相信用户提交的数据,对用户所提交的数据过滤不足所导致的,因此解决方案
    也应该从这个方面入手,具体方案包括:
    将重要的cookie标记为http only, 这样的话Javascript 中的document.cookie语句就不能获取到
    cookie了(如果在cookie中设置了HttpOnly属性,那么通过js脚本将无法读取到cookie信息,这样
    能有效的防止XSS攻击);
    表单数据规定值的类型,例如:年龄应为只能为int、name只能为字母数字组合。。。。
    对数据进行Html Encode 处理
    过滤或移除特殊的Html标签,例如:
  • 11011111 11111111 11111111 11111111)。C类IP地址的子网掩码为255.255.255.0,每个网络支
    持的最大主机数为256-2=254台。
    4、D类地址:多播地址,用于1对多通信,最高位必须是“1110”
    D类IP地址在历史上被叫做多播地址(multicast address),即组播地址。在以太网中,多播地址命名
    了一组应该在这个网络中应用接收到一个分组的站点。多播地址的最高位必须是“1110”,范围从
    224.0.0.0到239.255.255.255。 5、E类地址:为保留地址,最高位必须是“1111”
  1. IP地址与物理地址
    物理地址是数据链路层和物理层使用的地址,IP地址是网络层和以上各层使用的地址,是一种逻辑地
    址,其中ARP协议用于IP地址与物理地址的对应。
  2. 影响网络传输的因素有哪些?
    将一份数据从一个地方正确地传输到另一个地方所需要的时间我们称之为响应时间。影响这个响应时间
    的因素有很多。
    1、网络带宽:
    所谓带宽就是一条物理链路在 1s 内能够传输的最大比特数,注意这里是比特(bit)而不是字节数,也
    就是 b/s 。网络带宽肯定是影响数据传输的一个关键环节,因为在当前的网络环境中,平均网络带宽只
    有 1.7 MB/s 左右。
    2、传输距离:
    也就是数据在光纤中要走的距离,虽然光的传播速度很快,但也是有时间的,由于数据在光纤中的移动
    并不是走直线的,会有一个折射率,所以大概是光的 2/3,这个时间也就是我们通常所说的传输延时。
    传输延时是一个无法避免的问题,例如,你要给在杭州和青岛的两个机房的一个数据库进行同步数据操
    作,那么必定会存在约 30ms 的一个延时。
    3、TCP 拥塞控制:
    我们知道 TCP 传输是一个 “停-等-停-等” 的协议,传输方和接受方的步调要一致,要达到步调一致就要通
    过拥塞控制来调节。TCP 在传输时会设定一个 “窗口”,这个窗口的大小是由带宽和 RTT(Round-Trip
    Time,数据在两端的来回时间,也就是响应时间)决定的。计算的公式是带宽(b/s)xRTT(s)。通过
    这个值就可以得出理论上最优的 TCP 缓冲区的大小。Linux 2.4 已经可以自动地调整发送端的缓冲区的
    大小,而到 Linux 2.6.7 时接收端也可以自动调整了。
  3. 什么是对称加密与非对称加密
    对称密钥加密是指加密和解密使用同一个密钥的方式,这种方式存在的最大问题就是密钥发送问题,即
    如何安全地将密钥发给对方;
    而非对称加密是指使用一对非对称密钥,即公钥和私钥,公钥可以随意发布,但私钥只有自己知道。发
    送密文的一方使用对方的公钥进行加密处理,对方接收到加密信息后,使用自己的私钥进行解密。 由于
    非对称加密的方式不需要发送用来解密的私钥,所以可以保证安全性;但是和对称加密比起来,非常的
  4. 什么是Cookie
    cookie是由Web服务器保存在用户浏览器上的文件(key-value格式),可以包含用户相关的信息。客户
    端向服务器发起请求,就提取浏览器中的用户信息由http发送给服务器
  5. 什么是Session
    session 是浏览器和服务器会话过程中,服务器会分配的一块储存空间给session。
    服务器默认为客户浏览器的cookie中设置 sessionid,这个sessionid就和cookie对应,浏览器在向服务
    器请求过程中传输的cookie 包含 sessionid ,服务器根据传输cookie 中的 sessionid 获取出会话中存储
    的信息,然后确定会话的身份信息。
  6. Cookie和Session对于HTTP有什么用?
    HTTP协议本身是无法判断用户身份。所以需要cookie或者session
  7. Cookie与Session区别
    1、Cookie数据存放在客户端上,安全性较差,Session数据放在服务器上,安全性相对更高
    2、单个cookie保存的数据不能超过4K,session无此限制
    3、session一定时间内保存在服务器上,当访问增多,占用服务器性能,考虑到服务器性能方面,应当
    使用cookie。
    数据库
    MySQL面试题
  8. MySQL中的varchar和char有什么区别.
    char是一个定长字段,假如申请了 char(10) 的空间,那么无论实际存储多少内容.该字段都占用10个字符, 而varchar是变长的,也就是说申请的只是最大长度,占用的空间为实际字符长度+1,最后一个字符存储使用
    了多长的空间.
    在检索效率上来讲,char > varchar,因此在使用中,如果确定某个字段的值的长度,可以使用char,否则应该
    尽量使用varchar.例如存储用户MD5加密后的密码,则应该使用char.
  9. varchar(10)和int(10)代表什么含义?
    varchar的10代表了申请的空间长度,也是可以存储的数据的最大长度,而int的10只是代表了展示的长度,
    不足10位以0填充.也就是说,int(1)和int(10)所能存储的数字大小以及占用的空间都是相同的,只是在展示
    时按照长度展示.
  10. MySQL中varchar与char的区别以及varchar(50)中的50代表的
    涵义
    1、varchar与char的区别char是一种固定长度的类型,varchar则是一种可变长度的类型
    2、varchar(50)中50的涵义最多存放50个字符,varchar(50)和(200)存储hello所占空间一样,但后者在
    排序时会消耗更多内存,因为order by col采用fixed_length计算col长度(memory引擎也一样) 3、int(20)中20的涵义是指显示字符的长度但要加参数的,最大为255,比如它是记录行数的id,插入
    10笔资料,它就显示00000000001 ~~~00000000010,当字符的位数超过11,它也只显示11位,如果你
    没有加那个让它未满11位就前面加0的参数,它不会在前面加020表示最大显示宽度为20,但仍占4字节
    存储,存储范围不变;
    4、mysql为什么这么设计对大多数应用没有意义,只是规定一些工具用来显示字符的个数;int(1)和
    int(20)存储和计算均一样;
  11. innodb的事务与日志的实现方式
    1、有多少种日志;错误日志:记录出错信息,也记录一些警告信息或者正确的信息。查询日志:记录所
    有对数据库请求的信息,不论这些请求是否得到了正确的执行。慢查询日志:设置一个阈值,将运行时
    间超过该值的所有SQL语句都记录到慢查询的日志文件中。二进制日志:记录对数据库执行更改的所有
    操作。中继日志:事务日志:
    2、事物的4种隔离级别隔离级别读未提交(RU)读已提交(RC)可重复读(RR)串行
    3、事务是如何通过日志来实现的,说得越深入越好。事务日志是通过redo和innodb的存储引擎日志缓
    冲(Innodb log buffer)来实现的,当开始一个事务的时候,会记录该事务的lsn(log sequence
    number)号; 当事务执行时,会往InnoDB存储引擎的日志的日志缓存里面插入事务日志;当事务提交
    时,必须将存储引擎的日志缓冲写入磁盘(通过innodb_flush_log_at_trx_commit来控制),也就是写
    数据前,需要先写日志。这种方式称为“预写日志方式”
  12. MySQL的binlog有有几种录入格式?分别有什么区别?**
    有三种格式,statement,row和mixed.
    statement模式下,记录单元为语句.即每一个sql造成的影响会记录.由于sql的执行是有上下文的,因
    此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制.
    row级别下,记录单元为每一行的改动,基本是可以全部记下来但是由于很多操作,会导致大量行的改
    动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大.
    mixed. 一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row.
    此外,新版的MySQL中对row级别也做了一些优化,当表结构发生变化的时候,会记录语句而不是逐行记录.
  13. 超大分页怎么处理?**
    超大的分页一般从两个方向上来解决.
    数据库层面,这也是我们主要集中关注的(虽然收效没那么大),类似于 select * from table where age > 20 limit 1000000,10 这种查询其实也是有可以优化的余地的. 这条语句需要
    load1000000数据然后基本上全部丢弃,只取10条当然比较慢. 当时我们可以修改为 select * from table where id in (select id from table where age > 20 limit 1000000,10) .这样虽
    然也load了一百万的数据,但是由于索引覆盖,要查询的所有字段都在索引中,所以速度会很快. 同时如
    果ID连续的好,我们还可以 select * from table where id > 1000000 limit 10 ,效率也是不
    错的,优化的可能性有许多种,但是核心思想都一样,就是减少load的数据.
    从需求的角度减少这种请求….主要是不做类似的需求(直接跳转到几百万页之后的具体某一页.只允
    许逐页查看或者按照给定的路线走,这样可预测,可缓存)以及防止ID泄漏且连续被人恶意攻击.
    解决超大分页,其实主要是靠缓存,可预测性的提前查到内容,缓存至redis等k-V数据库中,直接返回即可.
    在阿里巴巴《Java开发手册》中,对超大分页的解决办法是类似于上面提到的第一种.
  14. 关心过业务系统里面的sql耗时吗?统计过慢查询吗?对慢查询都怎
    么优化过?**
    在业务系统中,除了使用主键进行的查询,其他的我都会在测试库上测试其耗时,慢查询的统计主要由运维
    在做,会定期将业务中的慢查询反馈给我们.
    慢查询的优化首先要搞明白慢的原因是什么? 是查询条件没有命中索引?是load了不需要的数据列?还是数
    据量太大?
    所以优化也是针对这三个方向来的,
    首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多
    结果中并不需要的列,对语句进行分析以及重写.
    分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能
    的命中索引.
    如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵
    向的分表.
  15. 上面提到横向分表和纵向分表,可以分别举一个适合他们的例子吗?
    横向分表是按行分表.假设我们有一张用户表,主键是自增ID且同时是用户的ID.数据量较大,有1亿多条,那
    么此时放在一张表里的查询效果就不太理想.我们可以根据主键ID进行分表,无论是按尾号分,或者按ID的
    区间分都是可以的. 假设按照尾号0-99分为100个表,那么每张表中的数据就仅有100w.这时的查询效率无
    疑是可以满足要求的.
    纵向分表是按列分表.假设我们现在有一张文章表.包含字段 id-摘要-内容 .而系统中的展示形式是刷新出一
    个列表,列表中仅包含标题和摘要,当用户点击某篇文章进入详情时才需要正文内容.此时,如果数据量大,将
    内容这个很大且不经常使用的列放在一起会拖慢原表的查询速度.我们可以将上面的表分为两张. id-摘 要 , id-内容 .当用户点击详情,那主键再来取一次内容即可.而增加的存储量只是很小的主键字段.代价很小.
    当然,分表其实和业务的关联度很高,在分表之前一定要做好调研以及benchmark.不要按照自己的猜想盲
    目操作.
  16. 什么是存储过程?有哪些优缺点?
    存储过程是一些预编译的SQL语句。1、更加直白的理解:存储过程可以说是一个记录集,它是由一些TSQL语句组成的代码块,这些T-SQL语句代码像一个方法一样实现一些功能(对单表或多表的增删改
    查),然后再给这个代码块取一个名字,在用到这个功能的时候调用他就行了。2、存储过程是一个预编
    译的代码块,执行效率比较高,一个存储过程替代大量T_SQL语句 ,可以降低网络通信量,提高通信速率,
    可以一定程度上确保数据安全
    但是,在互联网项目中,其实是不太推荐存储过程的,比较出名的就是阿里的《Java开发手册》中禁止使用存
    储过程,我个人的理解是,在互联网项目中,迭代太快,项目的生命周期也比较短,人员流动相比于传统的项目
    也更加频繁,在这样的情况下,存储过程的管理确实是没有那么方便,同时,复用性也没有写在服务层那么好.
  17. 说一说三个范式
    第一范式: 每个列都不可以再拆分. 第二范式: 非主键列完全依赖于主键,而不能是依赖于主键的一部分. 第
    三范式: 非主键列只依赖于主键,不依赖于其他非主键.
    在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由.比如性能. 事实上我们经常会
    为了性能而妥协数据库的设计.
  18. MySQL的复制原理以及流程
    基本原理流程,3个线程以及之间的关联;
    1、主:binlog线程——记录下所有改变了数据库数据的语句,放进master上的binlog中;
    2、从:io线程——在使用start slave 之后,负责从master上拉取 binlog 内容,放进 自己的relay log
    中;
    3、从:sql执行线程——执行relay log中的语句;
  19. MySQL由哪些部分组成, 分别用来做什么
    1、Server
    连接器: 管理连接, 权限验证.
    分析器: 词法分析, 语法分析.
    优化器: 执行计划生成, 索引的选择.
    执行器: 操作存储引擎, 返回执行结果. 2、存储引擎: 存储数据, 提供读写接口.
  20. 如果一个表有一列定义为TIMESTAMP,将发生什么?
    每当行被更改时, 时间戳字段将获取当前时间戳。列设置为 AUTO INCREMENT 时, 如果在表中达到最
    大值, 会发生什么情况?它会停止递增, 任何进一步的插入都将产生错误, 因为密钥已被使用。
    怎样才能找出最后一次插入时分配了哪个自动增量?LAST_INSERT_ID 将返回由 Auto_increment 分配
    的最后一个值, 并且不需要指定表名称。
  21. MySQL 里记录货币用什么字段类型好
    NUMERIC 和 DECIMAL 类型被 MySQL 实现为同样的类型, 这在 SQL92 标准允许。他们被用于保存
    值, 该值的准确精度是极其重要的值, 例如与金钱有关的数据。当声明一个类是这些类型之一时, 精
    度和规模的能被(并且通常是)指定。
    例如:
    在这个例子中, 9(precision)代表将被用于存储值的总的小数位数,而 2(scale)代表将被用于存储小数点
    后的位数。因此, 在这种情况下, 能被存储在 salary 列中的值的范围是从-9999999.99 到
    9999999.99。
  22. MySQL 数据库作发布系统的存储,一天五万条以上的增量, 预
    计运维三年,怎么优化?
    1、设计良好的数据库结构, 允许部分数据冗余, 尽量避免 join 查询, 提高效率。
    2、选择合适的表字段数据类型和存储引擎, 适当的添加索引。
    3、MySQL 库主从读写分离。
    4、找规律分表, 减少单表中的数据量提高查询速度。5、添加缓存机制, 比如 memcached, apc
    等。
    5、不经常改动的页面, 生成静态页面。
    6、书写高效率的 SQL。比如 SELECT * FROM TABEL 改为 SELECT field_1, field_2, field_3 FROM
    TABLE.
    salary DECIMAL(9,2)
  23. 优化数据库的方法
    1、选取最适用的字段属性,尽可能减少定义字段宽度,尽量把字段设置 NOTNULL, 例如’ 省份’、’ 性 别’ 最好适用 ENUM
    2、使用连接(JOIN)来代替子查询
    3、适用联合(UNION)来代替手动创建的临时表
    4、事务处理
    5、锁定表、优化事务处理
    6、适用外键, 优化锁定表
    7、建立索引
    8、优化查询语句
  24. 简单描述 MySQL 中,索引,主键,唯一索引,联合索引的区
    别,对数据库的性能有什么影响(从读写两方面)
    索引是一种特殊的文件(InnoDB 数据表上的索引是表空间的一个组成部分), 它们包含着对数据表里所有
    记录的引用指针。
    普通索引(由关键字 KEY 或 INDEX 定义的索引)的唯一任务是加快对数据的访问速度。
    普通索引允许被索引的数据列包含重复的值。如果能确定某个数据列将只包含彼此各不相同的值, 在为
    这个数据列创建索引的时候就应该用关键字 UNIQUE 把它定义为一个唯一索引。也就是说, 唯一索引可
    以保证数据记录的唯一性。
    主键, 是一种特殊的唯一索引, 在一张表中只能定义一个主键索引, 主键用于唯一标识一条记录, 使
    用关键字 PRIMARY KEY 来创建。
    索引可以覆盖多个数据列,如像 INDEX(columnA, columnB)索引,这就是联合索引。
    索引可以极大的提高数据的查询速度, 但是会降低插入、删除、更新表的速度, 因为在执行这些写操作
    时, 还要操作索引文件。
  25. SQL 注入漏洞产生的原因?如何防止?
    SQL 注入产生的原因: 程序开发过程中不注意规范书写 sql 语句和对特殊字符进行过滤,导致客户端可
    以通过全局变量 POST 和 GET 提交一些 sql 语句正常执行。防止 SQL 注入的方式:
    开启配置文件中的 magic_quotes_gpc 和 magic_quotes_runtime 设置
    执行 sql 语句时使用 addslashes 进行 sql 语句转换Sql 语句书写尽量不要省略双引号和单引号。
    过滤掉 sql 语句中的一些关键词: update、insert、delete、select、 * 。
    提高数据库表和字段的命名技巧, 对一些重要的字段根据程序的特点命名, 取不易被猜到的。
  26. 存储时期
    Datatime: 以 YYYY-MM-DD HH:MM:SS 格式存储时期时间, 精确到秒, 占用 8 个字节得存储空间, datatime 类
    型与时区无关
    Timestamp:
    以时间戳格式存储,占用 4 个字节,范围小 1970-1-1 到 2038-1-19, 显示依赖于所指定得时区, 默认
    在第一个列行的数据修改时可以自动得修改timestamp 列得值
    Date( 生日):
    占用得字节数比使用字符串.datatime.int 储存要少, 使用 date 只需要 3 个字节, 存储日期月份, 还
    可以利用日期时间函数进行日期间得计算Time:存储时间部分得数据
    注意:不要使用字符串类型来存储日期时间数据( 通常比字符串占用得储存空间小, 在进行查找过滤可
    以利用日期得函数)使用 int 存储日期时间不如使用 timestamp 类型
  27. 解释 MySQL 外连接、内连接与自连接的区别
    先说什么是交叉连接:
    交叉连接又叫笛卡尔积,它是指不使用任何条件,直接将一个表的所有记录和另一个表中的所有记录一
    一匹配。
    内连接 则是只有条件的交叉连接,根据某个条件筛选出符合条件的记录,不符合条件的记录不会出现在
    结果集中, 即内连接只连接匹配的行。
    外连接 其结果集中不仅包含符合连接条件的行,而且还会包括左表、右表或两个表中的所有数据行,
    这三种情况依次称之为左外连接, 右外连接, 和全外连接。左外连接, 也称左连接, 左表为主表, 左
    表中的所有记录都会出现在结果集中, 对于那些在右表中并没有匹配的记录, 仍然要显示, 右边对应
    的那些字段值以NULL 来填充。右外连接,也称右连接,右表为主表,右表中的所有记录都会出现在结
    果集中。左连接和右连接可以互换, MySQL 目前还不支持全外连接。
  28. 存储引擎常用命令
    查看MySQL提供的所有存储引擎
    mysql> show engines;
    从上图我们可以查看出 MySQL 当前默认的存储引擎是InnoDB,并且在5.7版本所有的存储引擎中只有
    InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务。
    查看MySQL当前默认的存储引擎
    我们也可以通过下面的命令查看默认的存储引擎。
    查看表的存储引擎
  29. MySQL支持哪些存储引擎?
    MySQL支持多种存储引擎,比如InnoDB,MyISAM,Memory,Archive等等.在大多数的情况下,直接选择使用
    InnoDB引擎都是最合适的,InnoDB也是MySQL的默认存储引擎.
  30. InnoDB和MyISAM有什么区别?
    InnoDB支持事物,而MyISAM不支持事物
    InnoDB支持行级锁,而MyISAM支持表级锁
    InnoDB支持MVCC, 而MyISAM不支持
    InnoDB支持外键,而MyISAM不支持
    InnoDB不支持全文索引,而MyISAM支持。
    mysql> show variables like ‘%storage_engine%’; show table status like “table_name” ;
    MyISAM Innodb
    文件格式 数据和索引是分别存储的,
    数据 .MYD ,索引 .MYI
    数据和索引是集中存储
    的, .ibd
    文件能否移动 能,一张表就对应 .frm 、 MYD 、 MYI 3个文件
    否,因为关联的还有
    data 下的其它文件
    记录存储顺序 按记录插入顺序保存 按主键大小有序插入
    空间碎片(删除记录并 flush table 表名 之后,表文件大小不变)
    产生。定时整理:使用命令
    optimize table 表名 实现 不产生
    事务 不支持 支持
    外键 不支持 支持
    锁支持(锁是避免资源争用的一个机
    制,MySQL锁对用户几乎是透明的) 表级锁定 行级锁定、表级锁定,
    锁定力度小并发能力高
  31. myisamchk 是用来做什么的?
    它用来压缩 MyISAM 表, 这减少了磁盘或内存使用。
    MyISAM Static 和 MyISAM Dynamic 有什么区别?
    在 MyISAM Static 上的所有字段有固定宽度。动态 MyISAM 表将具有像 TEXT, BLOB 等字段, 以适应
    不同长度的数据类型。
    MyISAM Static 在受损情况下更容易恢复。
  32. 为什么要尽量设定一个主键?**
    主键是数据库确保数据行在整张表唯一性的保障,即使业务上本张表没有主键,也建议添加一个自增长的ID
    列作为主键.设定了主键之后,在后续的删改查的时候可能更加快速以及确保操作数据范围安全.
  33. 主键使用自增ID还是UUID?
    推荐使用自增ID,不要使用UUID.
    因为在InnoDB存储引擎中,主键索引是作为聚簇索引存在的,也就是说,主键索引的B+树叶子节点上存储了
    主键索引以及全部的数据(按照顺序),如果主键索引是自增ID,那么只需要不断向后排列即可,如果是UUID,
    由于到来的ID与原来的大小不确定,会造成非常多的数据插入,数据移动,然后导致产生很多的内存碎片,进
    而造成插入性能的下降.
    总之,在数据量大一些的情况下,用自增主键性能会好一些. 图片来源于《高性能MySQL》: 其中默认后缀为使用自增ID,_uuid为使用UUID为主键的测试,测试了插入
    100w行和300w行的性能.
    关于主键是聚簇索引,如果没有主键,InnoDB会选择一个唯一键来作为聚簇索引,如果没有唯一键,会生成一
    个隐式的主键.
    If you define a PRIMARY KEY on your table, InnoDB uses it as the clustered index.
    If you do not define a PRIMARY KEY for your table, MySQL picks the first UNIQUE index that
    has only NOT NULL columns as the primary key and InnoDB uses it as the clustered index.
  34. 字段为什么要求定义为not null?
    MySQL官网这样介绍:
    NULL columns require additional space in the rowto record whether their values are NULL.
    For MyISAM tables, each NULL columntakes one bit extra, rounded up to the nearest byte.
    null值会占用更多的字节,且会在程序中造成很多与预期不符的情况.
  35. 如果要存储用户的密码散列,应该使用什么字段进行存储?
    密码散列,盐,用户身份证号等固定长度的字符串应该使用char而不是varchar来存储,这样可以节省空间且
    提高检索效率.
  36. 什么是索引?
    索引是一种数据结构,可以帮助我们快速的进行数据的查找.
  37. 索引是个什么样的数据结构呢?
    索引的数据结构和具体存储引擎的实现有关, 在MySQL中使用较多的索引有Hash索引,B+树索引等,而我
    们经常使用的InnoDB存储引擎的默认索引实现为:B+树索引.
  38. 唯一索引比普通索引快吗, 为什么
    唯一索引不一定比普通索引快, 还可能慢. 1、查询时, 在未使用 limit 1 的情况下, 在匹配到一条数据后, 唯一索引即返回, 普通索引会继续匹配下
    一条数据, 发现不匹配后返回. 如此看来唯一索引少了一次匹配, 但实际上这个消耗微乎其微.
    2、更新时, 这个情况就比较复杂了. 普通索引将记录放到 change buffer 中语句就执行完毕了. 而对唯
    一索引而言, 它必须要校验唯一性, 因此, 必须将数据页读入内存确定没有冲突, 然后才能继续操作. 对于写
    多读少的情况, 普通索引利用 change buffer 有效减少了对磁盘的访问次数, 因此普通索引性能要高于唯
    一索引.
  39. 索引的优缺点
    优点
    提高数据检索的效率,降低数据库的 IO 成本。
    通过索引列对数据进行排序,降低数据排序的成本,降低了 CPU 的消耗。
    缺点
    虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行 INSERT、UPDATE 和
    DELETE。因为更新表时,MySQL 不仅要保存数据,还要保存一下索引文件每次更新添加了索引列
    的字段,都会调整因为 更新所带来的键值变化后的索引信息。
    实际上索引也是一张表,该表保存了主键与索引字段,并指向实体表的记录,所以索引列也是要占
    用空间 的。
  40. 做过哪些MySQL索引相关优化
    尽量使用主键查询: 聚簇索引上存储了全部数据, 相比普通索引查询, 减少了回表的消耗.
    MySQL5.6之后引入了索引下推优化, 通过适当的使用联合索引, 减少回表判断的消耗.
    若频繁查询某一列数据, 可以考虑利用覆盖索引避免回表.
    联合索引将高频字段放在最左边.
  41. 怎么看到为表格定义的所有索引?
    索引是通过以下方式为表格定义的:
    SHOW INDEX FROM ;
  42. 索引分类
    单值索引:即一个索引只包含单个列,一个表可以有多个单列索引
    建表时,加上 key(列名) 指定
    单独创建, create index 索引名 on 表名(列名)
    单独创建, alter table 表名 add index 索引名(列名)
    唯一索引:索引列的值必须唯一,但允许有 null 且 null 可以出现多次
    建表时,加上 unique(列名) 指定
    单独创建, create unique index idx_表名_列名 on 表名(列名)
    单独创建, alter table 表名 add unique 索引名(列名)
    主键索引:设定为主键后数据库会自动建立索引,innodb 为聚簇索引,值必须唯一且不能为 null
    建表时,加上 primary key(列名) 指定
    复合索引:即一个索引包含多个列
    建表时,加上 key(列名列表) 指定
    单独创建, create index 索引名 on 表名(列名列表)
    单独创建, alter table 表名 add index 索引名(列名列表)
  43. 什么情况下设置了索引但无法使用
    1、以“%” 开头的 LIKE 语句, 模糊匹配
    2、OR 语句前后没有同时使用索引
    3、数据类型出现隐式转化( 如 varchar 不加单引号的话可能会自动转换为 int 型)
  44. B-Tree 和 B+Tree
    区别
  45. B-Tree 的关键字和记录是放在一起的,叶子节点可以看作外部节点,不包含任何信息;B+Tree 的
    非叶子节点中只有关键字和指向下一个节点的索引,记录只放在叶子节点中。
  46. 在 B-Tree 中,越靠近根节点的记录查找时间越快,只要找到关键字即可确定记录的存在;而
    B+Tree 中每个记录的查找时间基本是一样的,都需要从根节点走到叶子节点,而且在叶子节点中
    还要再比较关键字。从这个角度看 B-Tree 的性能好像要比 B+Tree 好,而在实际应用中却是
    B+Tree 的性能要好些。因为 B+Tree 的非叶子节点不存放实际的数据,这样每个节点可容纳的元
    素个数比 B-Tree 多,树高比 B-Tree 小,这样带来的好处是减少磁盘访问次数。尽管 B+Tree 找到
    一个记录所需的比较次数要比 B-Tree 多,但是一次磁盘访问的时间相当于成百上千次内存比较的
    时间,因此实际中 B+Tree 的性能可能还会好些,而且 B+Tree 的叶子节点使用指针连接在一起,
    方便顺序遍历(例如查看一个目录下的所有文件,一个表中的所有记录等),这也是很多数据库和
    文件系统使用 B+Tree 的缘故。
    为什么 B+Tree 比 B-Tree 更适合实际应用中操作系统的文件索引和数据库索引?
  47. B+Tree 的磁盘读写代价更低
    B+Tree 的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对 B-Tree 更小。如果
    把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性
    读入内存中的需要查找的关键字也就越多。相对来说 IO 读写次数也就降低了。
  48. B+Tree 的查询效率更加稳定
    由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字
    的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的
    查询效率相当。
  49. Hash索引和B+树所有有什么区别或者说优劣呢?**
    首先要知道Hash索引和B+树索引的底层实现原理:
    hash索引底层就是hash表,进行查找时,调用一次hash函数就可以获取到相应的键值,之后进行回表查询获
    得实际数据.B+树底层实现是多路平衡查找树.对于每一次的查询都是从根节点出发,查找到叶子节点方可
    以获得所查键值,然后根据查询判断是否需要回表查询数据.
    那么可以看出他们有以下的不同:
    hash索引进行等值查询更快(一般情况下),但是却无法进行范围查询.
    因为在hash索引中经过hash函数建立索引之后,索引的顺序与原顺序无法保持一致,不能支持范围查询.而
    B+树的的所有节点皆遵循(左节点小于父节点,右节点大于父节点,多叉树也类似),天然支持范围.
    hash索引不支持使用索引进行排序,原理同上.
    hash索引不支持模糊查询以及多列索引的最左前缀匹配.原理也是因为hash函数的不可预测.AAAA
    和AAAAB的索引没有相关性.
    hash索引任何时候都避免不了回表查询数据,而B+树在符合某些条件(聚簇索引,覆盖索引等)的时候
    可以只通过索引完成查询.
    hash索引虽然在等值查询上较快,但是不稳定.性能不可预测,当某个键值存在大量重复的时候,发生
    hash碰撞,此时效率可能极差.而B+树的查询效率比较稳定,对于所有的查询都是从根节点到叶子节
    点,且树的高度较低.
    因此,在大多数情况下,直接选择B+树索引可以获得稳定且较好的查询速度.而不需要使用hash索引.
  50. 为什么用 B+ 树做索引而不用哈希表做索引? 1、哈希表是把索引字段映射成对应的哈希码然后再存放在对应的位置,这样的话,如果我们要进行模糊
    查找的话,显然哈希表这种结构是不支持的,只能遍历这个表。而B+树则可以通过最左前缀原则快速找
    到对应的数据。
    2、如果我们要进行范围查找,例如查找ID为100 ~ 400的人,哈希表同样不支持,只能遍历全表。
    3、索引字段通过哈希映射成哈希码,如果很多字段都刚好映射到相同值的哈希码的话,那么形成的索引
    结构将会是一条很长的链表,这样的话,查找的时间就会大大增加。
  51. 上面提到了B+树在满足聚簇索引和覆盖索引的时候不需要回表查
    询数据,什么是聚簇索引? 在B+树的索引中,叶子节点可能存储了当前的key值,也可能存储了当前的key值以及整行的数据,这就是聚
    簇索引和非聚簇索引. 在InnoDB中,只有主键索引是聚簇索引,如果没有主键,则挑选一个唯一键建立聚簇
    索引.如果没有唯一键,则隐式的生成一个键来建立聚簇索引.
    当查询使用聚簇索引时,在对应的叶子节点,可以获取到整行数据,因此不用再次进行回表查询.
  52. 非聚簇索引一定会回表查询吗?**
    不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回
    表查询.
    举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行 select age from employee where age < 20 的查询时,在索引的叶子节点上,已经包含了age信息,不会再次进行回表查询.
  53. 在建立索引的时候,都有哪些需要考虑的因素呢?**
    建立索引的时候一般要考虑到字段的使用频率,经常作为条件进行查询的字段比较适合.如果需要建立联合
    索引的话,还需要考虑联合索引中的顺序.此外也要考虑其他方面,比如防止过多的所有对表造成太大的压
    力.这些都和实际的表结构以及查询方式有关.
  54. 联合索引是什么?为什么需要注意联合索引中的顺序?**
    MySQL可以使用多个字段同时建立一个索引,叫做联合索引.在联合索引中,如果想要命中索引,需要按照建
    立索引时的字段顺序挨个使用,否则无法命中索引.
    具体原因为:
    MySQL使用索引时需要索引有序,假设现在建立了"name,age,school"的联合索引,那么索引的排序为: 先
    按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序.
    当进行查询时,此时索引仅仅按照name严格有序,因此必须首先使用name字段进行等值查询,之后对于匹
    配到的列而言,其按照age字段严格有序,此时可以使用age字段用做索引查找,以此类推.因此在建立联合
    索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面.此外可
    以根据特例的查询或者表结构进行单独的调整.
  55. 创建的索引有没有被使用到?或者说怎么才可以知道这条语句运行
    很慢的原因?
    MySQL提供了explain命令来查看语句的执行计划,MySQL在执行某个语句之前,会将该语句过一遍查询优
    化器,之后会拿到对语句的分析,也就是执行计划,其中包含了许多信息. 可以通过其中和索引有关的信息来
    分析是否命中了索引,例如possilbe_key,key,key_len等字段,分别说明了此语句可能会使用的索引,实际使
    用的索引以及使用的索引长度.
  56. 那么在哪些情况下会发生针对该列创建了索引但是在查询的时候
    并没有使用呢?
    使用不等于查询,
    列参与了数学运算或者函数
    在字符串like时左边是通配符.类似于’%aaa’.
    当mysql分析全表扫描比使用索引快的时候不使用索引.
    当使用联合索引,前面一个条件为范围查询,后面的即使符合最左前缀原则,也无法使用索引.
    以上情况,MySQL无法使用索引.
  57. 什么是事务?**
    事务是逻辑上的一组操作,要么都执行,要么都不执行。
    理解什么是事务最经典的就是转账的栗子,相信大家也都了解,这里就不再说一边了.
    事务是一系列的操作,他们要符合ACID特性.最常见的理解就是:事务中的操作要么全部成功,要么全部失败.
    但是只是这样还不够的.
  58. ACID是什么?可以详细说一下吗?
    A=Atomicity
    原子性:就是上面说的,要么全部成功,要么全部失败.不可能只执行一部分操作.
    C=Consistency
    一致性:系统(数据库)总是从一个一致性的状态转移到另一个一致性的状态,不会存在中间状态.
    I=Isolation
    隔离性: 通常来说:一个事务在完全提交之前,对其他事务是不可见的.注意前面的通常来说加了红色,意味着
    有例外情况.
    D=Durability
    持久性:一旦事务提交,那么就永远是这样子了,哪怕系统崩溃也不会影响到这个事务的结果.
  59. 同时有多个事务在进行会怎么样呢?**
    事务( transaction) 是作为一个单元的一组有序的数据库操作。如果组中的所有操作都成功, 则认为
    事务成功, 即使只有一个操作失败, 事务也不成功。如果所有操作完成, 事务则提交, 其修改将作用
    于所有其他数据库进程。如果一个操作失败, 则事务将回滚, 该事务所有操作的影响都将取消。
    事务特性:
    1、原子性。 即不可分割性, 事务要么全部被执行, 要么就全部不被执行。
    2、一致性或可串性。事务的执行使得数据库从一种正确状态转换成另一种正确状 态 3、隔离性。在事务正确提交之前,不允许把该事务对数据的任何改变提供给任何 其他事务,
    4、持久性。事务正确提交后, 其结果将永久保存在数据库中, 即使在事务提交后有了其他故障, 事务
    的处理结果也会得到保存。或者这样理解:事务就是被绑定在一起作为一个逻辑工作单元的 SQL 语句分
    组, 如果任何一个语句操作失败那么整个操作就被失败, 以后操作就会回滚到操作前状态, 或者是上
    有个节点。为了确保要么执行, 要么不执行, 就可以使用事务。要将有组语句作为事务考虑, 就需要
    通过 ACID 测试, 即原子性, 一致性, 隔离性和持久性。
  60. Myql 中的事务回滚机制概述
    事务是用户定义的一个数据库操作序列, 这些操作要么全做要么全不做, 是一个不可分割的工作单位。
    事务回滚是指将该事务已经完成的对数据库的更新操作撤销。要同时修改数据库中两个不同表时, 如果
    它们不是一个事务的话, 当第一个表修改完, 可能第二个表修改过程中出现了异常而没能修改, 此时
    就只有第二个表依旧是未修改之前的状态, 而第一个表已经被修改完毕。而当你把它们设定为一个事务
    的时候, 当第一个表修改完, 第二表修改出现异常而没能修改, 第一个表和第二个表都要回到未修改
    的状态, 这就是所谓的事务回滚
  61. 并发事务带来哪些问题?
    在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一
    数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。
    脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到
    数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提
    交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确
    的。
    丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那
    么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结
    果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务
    1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
    不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束
    时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改
    导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样
    的情况,因此称为不可重复读。
    幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接
    着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一
    些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
    不可重复读和幻读区别:
    不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者
    删除比如多次读取一条记录发现记录增多或减少了。
  62. 怎么解决这些问题呢?MySQL的事务隔离级别了解吗?
    MySQL的四种隔离级别如下:
    未提交读(READ UNCOMMITTED)
    这个隔离级别下,其他事务可以看到本事务没有提交的部分修改.因此会造成脏读的问题(读取到了其他事
    务未提交的部分,而之后该事务进行了回滚).
    这个级别的性能没有足够大的优势,但是又有很多的问题,因此很少使用.
    已提交读(READ COMMITTED)
    其他事务只能读取到本事务已经提交的部分.这个隔离级别有 不可重复读的问题,在同一个事务内的两次
    读取,拿到的结果竟然不一样,因为另外一个事务对数据进行了修改.
    REPEATABLE READ(可重复读)
    可重复读隔离级别解决了上面不可重复读的问题(看名字也知道),但是仍然有一个新问题,就是 幻读,当你读
    取id> 10 的数据行时,对涉及到的所有行加上了读锁,此时例外一个事务新插入了一条id=11的数据,因为是
    新插入的,所以不会触发上面的锁的排斥,那么进行本事务进行下一次的查询时会发现有一条id=11的数据,
    而上次的查询操作并没有获取到,再进行插入就会有主键冲突的问题.
    SERIALIZABLE(可串行化)
    这是最高的隔离级别,可以解决上面提到的所有问题,因为他强制将所以的操作串行执行,这会导致并发性
    能极速下降,因此也不是很常用.
  63. Innodb使用的是哪种隔离级别呢?
    InnoDB默认使用的是可重复读隔离级别.
  64. MySQL 中有哪几种锁?
    1、表级锁: 开销小, 加锁快; 不会出现死锁; 锁定粒度大, 发生锁冲突的概率最高, 并发度最低。
    2、行级锁: 开销大, 加锁慢; 会出现死锁; 锁定粒度最小, 发生锁冲突的概率最低, 并发度也最
    高。
    3、页面锁: 开销和加锁时间界于表锁和行锁之间; 会出现死锁; 锁定粒度界于表锁和行锁之间, 并发
    度一般。
  65. 对MySQL的锁了解吗?
    当数据库有并发事务的时候,可能会产生数据的不一致,这时候需要一些机制来保证访问的次序,锁机制就
    是这样的一个机制.
    就像酒店的房间,如果大家随意进出,就会出现多人抢夺同一个房间的情况,而在房间上装上锁,申请到钥匙
    的人才可以入住并且将房间锁起来,其他人只有等他使用完毕才可以再次使用.
  66. 锁机制与InnoDB锁算法
    MyISAM和InnoDB存储引擎使用的锁:
    MyISAM采用表级锁(table-level locking)。
    InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁
    表级锁和行级锁对比:
    表级锁: MySQL中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比
    较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM
    和 InnoDB引擎都支持表级锁。
    行级锁: MySQL中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数
    据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。
    InnoDB存储引擎的锁的算法有三种:
    Record lock:单个行记录上的锁
    Gap lock:间隙锁,锁定一个范围,不包括记录本身
    Next-key lock:record+gap 锁定一个范围,包含记录本身
  67. MySQL都有哪些锁呢?像上面那样子进行锁定岂不是有点阻碍并
    发效率了?
    从锁的类别上来讲,有共享锁和排他锁.
    共享锁: 又叫做读锁. 当用户要进行数据的读取时,对数据加上共享锁.共享锁可以同时加上多个.
    排他锁: 又叫做写锁. 当用户要进行数据的写入时,对数据加上排他锁.排他锁只可以加一个,他和其他的排
    他锁,共享锁都相斥.
    用上面的例子来说就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的. 一种是真正的
    入住一晚,在这期间,无论是想入住的还是想看房的都不可以.
    锁的粒度取决于具体的存储引擎,InnoDB实现了行级锁,页级锁,表级锁.
    他们的加锁开销从大大小,并发能力也是从大到小.
  68. 锁的优化策略
    1、读写分离
    2、分段加锁
    3、减少锁持有的时间
    多个线程尽量以相同的顺序去获取资源
    不能将锁的粒度过于细化, 不然可能会出现线程的加锁和释放次数过多, 反而效率不如一次加一把大
    锁。
  69. Explain 性能分析
    是什么
    查看执行计划:使用 EXPLAIN 关键字可以模拟优化器执行 SQL 查询语句,从而知道 MySQL 是如何处理
    SQL 语句的。分析查询语句或是表结构的性能瓶颈。
    能干嘛
    表的读取顺序
    数据读取操作的操作类型
    哪些索引可以使用
    哪些索引被实际使用
    表之间的引用
    每张表有多少行被优化器查询
    怎么玩
    Explain + SQL 语句。
    Explain 执行后返回的信息:
    各字段解释
  70. id:select 查询的序列号,包含一组数字,表示查询中执行 select 子句或操作表的顺序。
    id 相同,执行顺序由上至下
    id 不同,如果是子查询,id 的序号会递增,id 值越大优先级越高,越先被执行
    id 有相同也有不同:id 如果相同,可以认为是一组,从上往下顺序执行;在所有组中,id 值
    越大,优先级越高,越先执行
    id 号每个号码,表示一趟独立的查询。一个 sql 的查询趟数越少越好。
  71. select_type:代表查询的类型,主要是用于区别普通查询、联合查询、子查询等的复杂查询,取值
    范围如下:
    simple:简单的 select 查询,查询中不包含子查询或者 UNION
    primary:查询中若包含任何复杂的子部分,最外层查询则被标记为 primary
    derived:在 FROM 列表中包含的子查询被标记为 DERIVED (衍生),MySQL 会递归执行这些
    子查询, 把结果放在临时表里。
    subquery:在 SELECT 或 WHERE 列表中包含了子查询
    depedent subquery:在 SELECT 或 WHERE 列表中包含了子查询,子查询基于外层
    uncacheable subquery:无法使用缓存的子查询
    union:若第二个 SELECT 出现在 UNION 之后,则被标记为 UNION;若 UNION 包含在
    FROM 子句的子查询中,外层 SELECT 将被标记为:DERIVED
    union result:从 UNION 表获取结果的 SELECT
  72. table:这个数据是基于哪张表的。
  73. type:是查询的访问类型。是较为重要的一个指标,结果值从最好到最坏依次是:system > const

eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery >
range > index > ALL,一般来说,得保证查询至少达到 range 级别,最好能达到 ref。
只需要记住:system > const > eq_ref > ref > range > index > ALL 就行了,其他的不常
见。
system:表只有一行记录(等于系统表),这是 const 类型的特列,平时不会出现,这个也
可以忽略不计。
const:表示通过索引一次就找到了,const 用于比较 primary key 或者 unique 索引。因为
只匹配一行数据,所以很快。如将主键置于 where 列表中,MySQL 就能将该查询转换为一个
常量。
eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一
索引扫描。
ref:非唯一性索引扫描,返回匹配某个单独值的所有行。本质上也是一种索引访问,它返回
所有匹配某个单独值的行,然而,它可能会找到多个符合条件的行,所以他应该属于查找和扫
描的混合体。
range:只检索给定范围的行,使用一个索引来选择行。key 列显示使用了哪个索引一般就是
在 where 语句中出现了 between、<、>、in 等的查询这种范围扫描索引扫描比全表扫描要
好,因为它只需要开始于索引的某一点,而结束语另一点,不用扫描全部索引。
index:出现 index 是 sql 使用了索引但是没用索引进行过滤,一般是使用了覆盖索引或者是
利用索引进行了排序分组。
all:将遍历全表以找到匹配的行。
其他 type 如下:
index_merge:在查询过程中需要多个索引组合使用,通常出现在有 or 关键字的 sql 中。
ref_or_null:对于某个字段既需要过滤条件,也需要 null 值的情况下。查询优化器会选择用
ref_or_null 连接查询。
index_subquery:利用索引来关联子查询,不再全表扫描。
unique_subquery:该联接类型类似于 index_subquery。子查询中的唯一索引。

  1. possible_keys:显示可能应用在这张表中的索引,一个或多个。查询涉及到的字段上若存在索
    引,则该索引将被列出,但不一定被查询实际使用。
  2. key:实际使用的索引。如果为 NULL,则没有使用索引。
  3. key_len:表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度。key_len 显示的值
    为索引字段的最大可能长度,并非实际使用长度。如何计算 key_len?
    先看索引上字段的类型 + 长度,比如:int=4; varchar(20)=20; char(20)=20
    如果是 varchar 或者 char 这种字符串字段,视字符集要乘不同的值,比如 utf-8 要乘 3,
    GBK 要乘 2
    varchar 这种动态字符串要加 2 个字节
    允许为空的字段要加 1 个字节
  4. ref:显示索引的哪一列被使用了,如果可能的话,是一个常数。哪些列或常量被用于查找索引列上
    的值。
  5. rows:显示 MySQL 认为它执行查询时必须检查的行数。越少越好!
  6. Extra:其他的额外重要的信息。
    Using filesort:说明 mysql 会对数据使用一个外部的索引排序,而不是按照表内的索引顺序
    进行读取。MySQL 中无法利用索引完成的排序操作称为“文件排序”。排序字段若通过索引去
    访问将大大提高排序速度。
    Using temporary:使用临时表保存中间结果,MySQL 在对查询结果排序时使用临时表。常
    见于排序 order by 和分组查询 group by。
    Using index:表示相应的 select 操作中使用了覆盖索引 (Covering Index),避免访问了表的
    数据行,效率不错!如果同时出现 using where,表明索引被用来执行索引键值的查找;如果
    没有同时出现 using where,表明索引只是用来读取数据而非利用索引执行查找。
    Using where:表明使用了 where 过滤。
    Using join buffer:使用了连接缓存。
    impossible where:where 子句的值总是 false,不能用来获取任何数据。
    select tables optimized away:在没有 group by 子句的情况下,基于索引优化 MIN/MAX 操
    作或者对于 MyISAM 存储引擎优化 COUNT(*) 操作,不必等到执行阶段再进行计算,查询执
    行计划生成的阶段即完成优化。
    distinct:优化 distinct 操作,在找到第一匹配的元祖后即停止找同样值的动作。
  7. 如何优化SQL
    1、SQL语句中IN包含的值不应过多
    MySQL对于IN做了相应的优化,即将IN中的常量全部存储在一个数组里面,而且这个数组是排好序的。
    但是如果数值较多,产生的消耗也是比较大的。再例如: select id from table_name where num in(1,2,3) 对于连续的数值,能用 between 就不要用 in 了;再或者使用连接来替换。
    2、SELECT语句务必指明字段名称
    SELECT 增加很多不必要的消耗(cpu、io、内存、网络带宽);增加了使用覆盖索引的可能性;当表
    结构发生改变时,前断也需要更新。所以要求直接在select后面接上字段名。
    3、当只需要一条数据的时候,使用limit 1
    这是为了使EXPLAIN中type列达到const类型
    4、如果排序字段没有用到索引,就尽量少排序
    5、如果限制条件中其他字段没有索引,尽量少用or
    or两边的字段中,如果有一个不是索引字段,而其他条件也不是索引字段,会造成该查询不走索引的情
    况。很多时候使用 union all 或者是union(必要的时候)的方式来代替“or”会得到更好的效果
    6、尽量用union all代替union
    union和union all的差异主要是前者需要将结果集合并后再进行唯一性过滤操作,这就会涉及到排序,
    增加大量的CPU运算,加大资源消耗及延迟。当然,union all的前提条件是两个结果集没有重复数据。
    7、不使用ORDER BY RAND()
    select id from table_name order by rand() limit 1000;
    上面的sql语句,可优化为
    select id from table_name t1 join (select rand() * (select max(id) from table_name) as nid) t2 ont1.id > t2.nid limit 1000; 8、区分in和exists, not in和not exists
    select * from 表A where id in (select id from 表B)
    上面sql语句相当于
    select * from 表A where exists(select * from 表B where 表B.id=表A.id)
    区分in和exists主要是造成了驱动顺序的改变(这是性能变化的关键),如果是exists,那么以外层表为
    驱动表,先被访问,如果是IN,那么先执行子查询。所以IN适合于外表大而内表小的情况;EXISTS适合
    于外表小而内表大的情况。
    关于not in和not exists,推荐使用not exists,不仅仅是效率问题,not in可能存在逻辑问题。如何高效
    的写出一个替代not exists的sql语句?
    原sql语句
    select colname … from A表 where a.id not in (select b.id from B表)
    高效的sql语句
    取出的结果集如下图表示,A表不在B表中的数据
    9、使用合理的分页方式以提高分页的效率
    使用上述sql语句做分页的时候,可能有人会发现,随着表数据量的增加,直接使用limit分页查询会越来
    越慢。
    优化的方法如下:可以取前一页的最大行数的id,然后根据这个最大的id来限制下一页的起点。比如此
    列中,上一页最大的id是866612。sql可以采用如下的写法:
    10、分段查询
    在一些用户选择页面中,可能一些用户选择的时间范围过大,造成查询缓慢。主要的原因是扫描行数过
    多。这个时候可以通过程序,分段进行查询,循环遍历,将结果合并处理进行展示。
    如下图这个sql语句,扫描的行数成百万级以上的时候就可以使用分段查询
    11、避免在 where 子句中对字段进行 null 值判断
    对于null的判断会导致引擎放弃使用索引而进行全表扫描。
    12、不建议使用%前缀模糊查询
    例如LIKE “%name”或者LIKE “%name%”,这种查询会导致索引失效而进行全表扫描。但是可以使用LIKE
    “name%”。
    那如何查询%name%? select colname … from A表 Left join B表 on where a.id = b.id where b.id is null select id,name from table_name limit 866613, 20 select id,name from table_name where id> 866612 limit 20
    如下图所示,虽然给secret字段添加了索引,但在explain结果果并没有使用
    那么如何解决这个问题呢,答案:使用全文索引
    在我们查询中经常会用到select id,fnum,fdst from table_name where user_name like ‘%zhangsan%’;
    。这样的语句,普通索引是无法满足查询需求的。庆幸的是在MySQL中,有全文索引来帮助我们。
    创建全文索引的sql语法是:
    ALTER TABLE table_name ADD FULLTEXT INDEX idx_user_name (user_name);
    使用全文索引的sql语句是:
    select id,fnum,fdst from table_name where match(user_name) against(‘zhangsan’ in boolean mode);
    注意:在需要创建全文索引之前,请联系DBA确定能否创建。同时需要注意的是查询语句的写法与普通
    索引的区别
    13、避免在where子句中对字段进行表达式操作
    比如
    select user_id,user_project from table_name where age
    2=36;
    中对字段就行了算术运算,这会造成引擎放弃使用索引,建议改成
    select user_id,user_project from table_name where age=36/2;
    14、避免隐式类型转换
    where 子句中出现 column 字段的类型和传入的参数类型不一致的时候发生的类型转换,建议先确定
    where中的参数类型
    15、对于联合索引来说,要遵守最左前缀法则
    举列来说索引含有字段id,name,school,可以直接用id字段,也可以id,name这样的顺序,但是
    name;school都无法使用这个索引。所以在创建联合索引的时候一定要注意索引字段顺序,常用的查询
    字段放在最前面
    16、必要时可以使用force index来强制查询走某个索引
    有的时候MySQL优化器采取它认为合适的索引来检索sql语句,但是可能它所采用的索引并不是我们想要
    的。这时就可以采用force index来强制优化器使用我们制定的索引。
    17、注意范围查询语句
    对于联合索引来说,如果存在范围查询,比如between,>,<等条件时,会造成后面的索引字段失效。
    18、关于JOIN优化
    LEFT JOIN A表为驱动表
    INNER JOIN MySQL会自动找出那个数据少的表作用驱动表
    RIGHT JOIN B表为驱动表
    注意:MySQL中没有full join,可以用以下方式来解决
    select * from A left join B on B.name = A.name where B.name is null union all select * from B;
    尽量使用inner join,避免left join
    参与联合查询的表至少为2张表,一般都存在大小之分。如果连接方式是inner join,在没有其他过滤条
    件的情况下MySQL会自动选择小表作为驱动表,但是left join在驱动表的选择上遵循的是左边驱动右边
    的原则,即left join左边的表名为驱动表。
    合理利用索引
    被驱动表的索引字段作为on的限制字段。
    利用小表去驱动大表
    从原理图能够直观的看出如果能够减少驱动表的话,减少嵌套循环中的循环次数,以减少 IO总量及CPU
    运算的次数。
    巧用STRAIGHT_JOIN
    inner join是由mysql选择驱动表,但是有些特殊情况需要选择另个表作为驱动表,比如有group by、
    order by等「Using filesort」、「Using temporary」时。STRAIGHT_JOIN来强制连接顺序,在
    STRAIGHT_JOIN左边的表名就是驱动表,右边则是被驱动表。在使用STRAIGHT_JOIN有个前提条件是
    该查询是内连接,也就是inner join。其他链接不推荐使用STRAIGHT_JOIN,否则可能造成查询结果不
    准确。
    这个方式有时可能减少3倍的时间。
    52条 SQL性能优化策略
    1、对查询进行优化,应尽量避免全表扫描,首先应考虑在where及order by涉及的列上建立索引。
    2、应尽量避免在where子句中对字段进行null值判断,创建表时NULL是默认值,但大多数时候应该使
    用NOT NULL,或者使用一个特殊的值,如0,-1作为默认值。
    3、应尽量避免在where子句中使用!=或<>操作符,MySQL只有对以下操作符才使用索引:<,<=,=, >,>=,BETWEEN,IN,以及某些时候的LIKE。 4、应尽量避免在where子句中使用or来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,可以
    使用UNION合并查询:select id from t where num=10 union all select id from t where num=20。
    5、in和not in也要慎用,否则会导致全表扫描,对于连续的数值,能用between就不要用in了:Select
    id from t where num between 1 and 3。 6、下面的查询也将导致全表扫描:select id from t where name like‘%abc%’或者select id from t
    where name like‘%abc’若要提高效率,可以考虑全文检索。而select id from t where name
    like‘abc%’才用到索引。
    7、如果在where子句中使用参数,也会导致全表扫描。
    8、应尽量避免在where子句中对字段进行表达式操作,应尽量避免在where子句中对字段进行函数操
    作。
    9、很多时候用exists代替in是一个好的选择:
    select num from a where num in(select num from b)
    用下面的语句替换:
    select num from a where exists(select 1 from b where num=a.num)
    10、索引固然可以提高相应的select的效率,但同时也降低了insert及update的效率,因为insert或
    update时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不
    要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。
    11、应尽可能的避免更新clustered索引数据列, 因为clustered索引数据列的顺序就是表记录的物理存
    储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁
    更新clustered索引数据列,那么需要考虑是否应将该索引建为clustered索引。
    12、尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性
    能,并会增加存储开销。
    13、尽可能的使用varchar/nvarchar代替char/nchar,因为首先变长字段存储空间小,可以节省存储空
    间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
    14、最好不要使用”“返回所有:select from t ,用具体的字段列表代替“”,不要返回用不到的任何字
    段。
    15、尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。
    16、使用表的别名(Alias):当在SQL语句中连接多个表时,请使用表的别名并把别名前缀于每个Column
    上。这样一来,就可以减少解析的时间并减少那些由Column歧义引起的语法错误。
    17、使用“临时表”暂存中间结果 :
    简化SQL语句的重要方法就是采用临时表暂存中间结果,但是临时表的好处远远不止这些,将临时结果
    暂存在临时表,后面的查询就在tempdb中了,这可以避免程序中多次扫描主表,也大大减少了程序执
    行中“共享锁”阻塞“更新锁”,减少了阻塞,提高了并发性能。
    18、一些SQL查询语句应加上nolock,读、写是会相互阻塞的,为了提高并发性能,对于一些查询,可
    以加上nolock,这样读的时候可以允许写,但缺点是可能读到未提交的脏数据。
    使用nolock有3条原则:
    查询的结果用于“插、删、改”的不能加nolock;
    查询的表属于频繁发生页分裂的,慎用nolock ;
    使用临时表一样可以保存“数据前影”,起到类似Oracle的undo表空间的功能,能采用临时表提高并
    发性能的,不要用nolock。
    19、常见的简化规则如下:
    不要有超过5个以上的表连接(JOIN),考虑使用临时表或表变量存放中间结果。少用子查询,视图嵌
    套不要过深,一般视图嵌套不要超过2个为宜。
    20、将需要查询的结果预先计算好放在表中,查询的时候再Select。这在SQL7.0以前是最重要的手段,
    例如医院的住院费计算。
    21、用OR的字句可以分解成多个查询,并且通过UNION 连接多个查询。他们的速度只同是否使用索引
    有关,如果查询需要用到联合索引,用UNION all执行的效率更高。多个OR的字句没有用到索引,改写
    成UNION的形式再试图与索引匹配。一个关键的问题是否用到索引。
    22、在IN后面值的列表中,将出现最频繁的值放在最前面,出现得最少的放在最后面,减少判断的次
    数。
    23、尽量将数据的处理工作放在服务器上,减少网络的开销,如使用存储过程。
    存储过程是编译好、优化过、并且被组织到一个执行规划里、且存储在数据库中的SQL语句,是控制流
    语言的集合,速度当然快。反复执行的动态SQL,可以使用临时存储过程,该过程(临时表)被放在
    Tempdb中。
    24、当服务器的内存够多时,配制线程数量 = 最大连接数+5,这样能发挥最大的效率;否则使用 配制
    线程数量<最大连接数启用SQL SERVER的线程池来解决,如果还是数量 = 最大连接数+5,严重的损害服
    务器的性能。
    25、查询的关联同写的顺序 :
    26、尽量使用exists代替select count(1)来判断是否存在记录,count函数只有在统计表中所有行数时使
    用,而且count(1)比count(
    )更有效率。
    27、尽量使用“>=”,不要使用“>”。
    28、索引的使用规范:
    索引的创建要与应用结合考虑,建议大的OLTP表不要超过6个索引;
    尽可能的使用索引字段作为查询条件,尤其是聚簇索引,必要时可以通过index index_name来强
    制指定索引;
    避免对大表查询时进行table scan,必要时考虑新建索引;
    在使用索引字段作为条件时,如果该索引是联合索引,那么必须使用到该索引中的第一个字段作为
    条件时才能保证系统使用该索引,否则该索引将不会被使用;
    要注意索引的维护,周期性重建索引,重新编译存储过程。
    29、下列SQL条件语句中的列都建有恰当的索引,但执行速度却非常慢:
    分析:
    WHERE子句中对列的任何操作结果都是在SQL运行时逐列计算得到的,因此它不得不进行表搜索,而没
    有使用该列上面的索引。
    select a.personMemberID, * from chineseresume a,personmember b where personMemberID = b.referenceid and a.personMemberID = ‘JCNPRH39681’ (A = B ,B = ‘号码’) select a.personMemberID, * from chineseresume a,personmember b where a.personMemberID = b.referenceid and a.personMemberID = ‘JCNPRH39681’ and b.referenceid = ‘JCNPRH39681’ (A = B ,B = ‘号码’, A = ‘号码’) select a.personMemberID, * from chineseresume a,personmember b where b.referenceid = ‘JCNPRH39681’ and a.personMemberID = ‘JCNPRH39681’ (B = ‘号码’, A = ‘号码’) SELECT * FROM record WHERE substrINg(card_no,1,4)=’5378’ (13秒) SELECT * FROM record WHERE amount/30< 1000 (11秒) SELECT * FROM record WHERE convert(char(10),date,112)=’19991201’ (10秒)
    如果这些结果在查询编译时就能得到,那么就可以被SQL优化器优化,使用索引,避免表搜索,因此将
    SQL重写成下面这样:
    SELECT * FROM record WHERE card_no like ‘5378%’ (< 1秒) SELECT * FROM record WHERE amount< 100030 (< 1秒) SELECT * FROM record WHERE date= ‘1999/12/01’ (< 1秒)
    30、当有一批处理的插入或更新时,用批量插入或批量更新,绝不会一条条记录的去更新。
    31、在所有的存储过程中,能够用SQL语句的,我绝不会用循环去实现。
    例如:列出上个月的每一天,我会用connect by去递归查询一下,绝不会去用循环从上个月第一天到最
    后一天。
    32、选择最有效率的表名顺序(只在基于规则的优化器中有效):
    Oracle的解析器按照从右到左的顺序处理FROM子句中的表名,FROM子句中写在最后的表(基础表
    driving table)将被最先处理,在FROM子句中包含多个表的情况下,你必须选择记录条数最少的表作为
    基础表。
    如果有3个以上的表连接查询,那就需要选择交叉表(intersection table)作为基础表,交叉表是指那
    个被其他表所引用的表。
    33、提高GROUP BY语句的效率,可以通过将不需要的记录在GROUP BY之前过滤掉。下面两个查询返
    回相同结果,但第二个明显就快了许多。
    低效:
    SELECT JOB , AVG(SAL) FROM EMP GROUP BY JOB HAVING JOB =’PRESIDENT’ OR JOB =’MANAGER’
    高效:
    SELECT JOB , AVG(SAL) FROM EMP WHERE JOB =’PRESIDENT’ OR JOB =’MANAGER’ GROUP BY JOB
    34、SQL语句用大写,因为Oracle总是先解析SQL语句,把小写的字母转换成大写的再执行。
    35、别名的使用,别名是大型数据库的应用技巧,就是表名、列名在查询中以一个字母为别名,查询速
    度要比建连接表快1.5倍。
    36、避免死锁,在你的存储过程和触发器中访问同一个表时总是以相同的顺序;事务应经可能地缩短,
    在一个事务中应尽可能减少涉及到的数据量;永远不要在事务中等待用户输入。
    37、避免使用临时表,除非却有需要,否则应尽量避免使用临时表,相反,可以使用表变量代替;大多
    数时候(99%),表变量驻扎在内存中,因此速度比临时表更快,临时表驻扎在TempDb数据库中,因此临
    时表上的操作需要跨数据库通信,速度自然慢。
    38、最好不要使用触发器:
    触发一个触发器,执行一个触发器事件本身就是一个耗费资源的过程;
    如果能够使用约束实现的,尽量不要使用触发器;
    不要为不同的触发事件(Insert,Update和Delete)使用相同的触发器;
    不要在触发器中使用事务型代码。
    39、索引创建规则:
    表的主键、外键必须有索引;
    数据量超过300的表应该有索引;
    经常与其他表进行连接的表,在连接字段上应该建立索引;
    经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;
    索引应该建在选择性高的字段上;
    索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
    复合索引的建立需要进行仔细分析,尽量考虑用单字段索引代替;
    正确选择复合索引中的主列字段,一般是选择性较好的字段;
    复合索引的几个字段是否经常同时以AND方式出现在Where子句中?单字段查询是否极少甚至没
    有?如果是,则可以建立复合索引;否则考虑单字段索引;
    如果复合索引中包含的字段经常单独出现在Where子句中,则分解为多个单字段索引;
    如果复合索引所包含的字段超过3个,那么仔细考虑其必要性,考虑减少复合的字段;
    如果既有单字段索引,又有这几个字段上的复合索引,一般可以删除复合索引;
    频繁进行数据操作的表,不要建立太多的索引;
    删除无用的索引,避免对执行计划造成负面影响;
    表上建立的每个索引都会增加存储开销,索引对于插入、删除、更新操作也会增加处理上的开销。
    另外,过多的复合索引,在有单字段索引的情况下,一般都是没有存在价值的;相反,还会降低数
    据增加删除时的性能,特别是对频繁更新的表来说,负面影响更大。
    尽量不要对数据库中某个含有大量重复的值的字段建立索引。
    40、MySQL查询优化总结:
    使用慢查询日志去发现慢查询,使用执行计划去判断查询是否正常运行,总是去测试你的查询看看是否
    他们运行在最佳状态下。
    久而久之性能总会变化,避免在整个表上使用count(
    ),它可能锁住整张表,使查询保持一致以便后续
    相似的查询可以使用查询缓存,在适当的情形下使用GROUP BY而不是DISTINCT,在WHERE、GROUP
    BY和ORDER BY子句中使用有索引的列,保持索引简单,不在多个索引中包含同一个列。
    有时候MySQL会使用错误的索引,对于这种情况使用USE INDEX,检查使用SQL_MODE=STRICT的问
    题,对于记录数小于5的索引字段,在UNION的时候使用LIMIT不是是用OR。
    为了避免在更新前SELECT,使用INSERT ON DUPLICATE KEY或者INSERT IGNORE,不要用UPDATE去
    实现,不要使用MAX,使用索引字段和ORDER BY子句,LIMIT M,N实际上可以减缓查询在某些情况
    下,有节制地使用,在WHERE子句中使用UNION代替子查询,在重新启动的MySQL,记得来温暖你的
    数据库,以确保数据在内存和查询速度快,考虑持久连接,而不是多个连接,以减少开销。
    基准查询,包括使用服务器上的负载,有时一个简单的查询可以影响其他查询,当负载增加在服务器
    上,使用SHOW PROCESSLIST查看慢的和有问题的查询,在开发环境中产生的镜像数据中测试的所有可
    疑的查询。
    41、MySQL备份过程:
    从二级复制服务器上进行备份;
    在进行备份期间停止复制,以避免在数据依赖和外键约束上出现不一致;
    彻底停止MySQL,从数据库文件进行备份;
    如果使用MySQL dump进行备份,请同时备份二进制日志文件 – 确保复制没有中断;
    不要信任LVM快照,这很可能产生数据不一致,将来会给你带来麻烦;
    为了更容易进行单表恢复,以表为单位导出数据——如果数据是与其他表隔离的。
    当使用mysqldump时请使用–opt;
    在备份之前检查和优化表;
    为了更快的进行导入,在导入时临时禁用外键约束。;
    为了更快的进行导入,在导入时临时禁用唯一性检测;
    在每一次备份后计算数据库,表以及索引的尺寸,以便更够监控数据尺寸的增长;
    通过自动调度脚本监控复制实例的错误和延迟;
    定期执行备份。
    42、查询缓冲并不自动处理空格,因此,在写SQL语句时,应尽量减少空格的使用,尤其是在SQL首和
    尾的空格(因为查询缓冲并不自动截取首尾空格)。
    43、member用mid做标准进行分表方便查询么?一般的业务需求中基本上都是以username为查询依
    据,正常应当是username做hash取模来分表。
    而分表的话MySQL的partition功能就是干这个的,对代码是透明的;在代码层面去实现貌似是不合理
    的。
    44、我们应该为数据库里的每张表都设置一个ID做为其主键,而且最好的是一个INT型的(推荐使用
    UNSIGNED),并设置上自动增加的AUTO_INCREMENT标志。
    45、在所有的存储过程和触发器的开始处设置SET NOCOUNT ON,在结束时设置SET NOCOUNT
    OFF。无需在执行存储过程和触发器的每个语句后向客户端发送DONE_IN_PROC消息。
    46、MySQL查询可以启用高速查询缓存。这是提高数据库性能的有效MySQL优化方法之一。当同一个
    查询被执行多次时,从缓存中提取数据和直接从数据库中返回数据快很多。
    47、EXPLAIN SELECT查询用来跟踪查看效果:
    使用EXPLAIN关键字可以让你知道MySQL是如何处理你的SQL语句的。这可以帮你分析你的查询语句或
    是表结构的性能瓶颈。EXPLAIN的查询结果还会告诉你你的索引主键被如何利用的,你的数据表是如何
    被搜索和排序的。
    48、当只要一行数据时使用LIMIT 1 :
    当你查询表的有些时候,你已经知道结果只会有一条结果,但因为你可能需要去fetch游标,或是你也许
    会去检查返回的记录数。
    在这种情况下,加上LIMIT 1可以增加性能。这样一来,MySQL数据库引擎会在找到一条数据后停止搜
    索,而不是继续往后查少下一条符合记录的数据。
    49、选择表合适存储引擎:
    myisam:应用时以读和插入操作为主,只有少量的更新和删除,并且对事务的完整性,并发性要
    求不是很高的。
    InnoDB:事务处理,以及并发条件下要求数据的一致性。除了插入和查询外,包括很多的更新和
    删除。(InnoDB有效地降低删除和更新导致的锁定)。
    对于支持事务的InnoDB类型的表来说,影响速度的主要原因是AUTOCOMMIT默认设置是打开的,
    而且程序没有显式调用BEGIN 开始事务,导致每插入一条都自动提交,严重影响了速度。可以在执
    行SQL前调用begin,多条SQL形成一个事物(即使autocommit打开也可以),将大大提高性能。
    50、优化表的数据类型,选择合适的数据类型:
    原则:更小通常更好,简单就好,所有字段都得有默认值,尽量避免null。
    例如:数据库表设计时候更小的占磁盘空间尽可能使用更小的整数类型。(mediumint就比int更合适)
    比如时间字段:datetime和timestamp,datetime占用8个字节,而timestamp占用4个字节,只用了一
    半,而timestamp表示的范围是1970—2037适合做更新时间
    MySQL可以很好的支持大数据量的存取,但是一般说来,数据库中的表越小,在它上面执行的查询也就
    会越快。
    因此,在创建表的时候,为了获得更好的性能,我们可以将表中字段的宽度设得尽可能小。
    例如:在定义邮政编码这个字段时,如果将其设置为CHAR(255),显然给数据库增加了不必要的空间。
    甚至使用VARCHAR这种类型也是多余的,因为CHAR(6)就可以很好的完成任务了。
    同样的,如果可以的话,我们应该使用MEDIUMINT而不是BIGIN来定义整型字段,应该尽量把字段设置
    为NOT NULL,这样在将来执行查询的时候,数据库不用去比较NULL值。
    对于某些文本字段,例如“省份”或者“性别”,我们可以将它们定义为ENUM类型。因为在MySQL中,
    ENUM类型被当作数值型数据来处理,而数值型数据被处理起来的速度要比文本类型快得多。这样,我
    们又可以提高数据库的性能。
    51、字符串数据类型:char,varchar,text选择区别。
    52、任何对列的操作都将导致表扫描,它包括数据库函数、计算表达式等等,查询时要尽可能将操作移
    至等号右边。
    一千行SQL命令
    基本操作
    /* Windows服务 / – 启动MySQL net start mysql – 创建Windows服务 sc create mysql binPath= mysqld_bin_path(注意:等号与值之间有空格) / 连接与断开服务器 / mysql -h 地址 -P 端口 -u 用户名 -p 密码 SHOW PROCESSLIST – 显示哪些线程正在运行 SHOW VARIABLES – 显示系统变量信息
    数据库操作
    表的操作
    /
    数据库操作 / ------------------ – 查看当前数据库 SELECT DATABASE(); – 显示当前时间、用户名、数据库版本 SELECT now(), user(), version(); – 创建库 CREATE DATABASE[ IF NOT EXISTS] 数据库名 数据库选项 数据库选项: CHARACTER SET charset_name COLLATE collation_name – 查看已有库 SHOW DATABASES[ LIKE ‘PATTERN’] – 查看当前库信息 SHOW CREATE DATABASE 数据库名 – 修改库的选项信息 ALTER DATABASE 库名 选项信息 – 删除库 DROP DATABASE[ IF EXISTS] 数据库名 同时删除该数据库相关的目录及其目录内容 – 创建表 CREATE [TEMPORARY] TABLE[ IF NOT EXISTS] [库名.]表名 ( 表的结构定义 )[ 表选项] 每个字段必须有数据类型 最后一个字段后不能有逗号 TEMPORARY 临时表,会话结束时表自动消失 对于字段的定义: 字段名 数据类型 [NOT NULL | NULL] [DEFAULT default_value] [AUTO_INCREMENT] [UNIQUE [KEY] | [PRIMARY] KEY] [COMMENT ‘string’] – 表选项 – 字符集 CHARSET = charset_name 如果表没有设定,则使用数据库字符集 – 存储引擎 ENGINE = engine_name 表在管理数据时采用的不同的数据结构,结构不同会导致处理方式、提供的特性操作等不同 常见的引擎:InnoDB MyISAM Memory/Heap BDB Merge Example CSV MaxDB Archive 不同的引擎在保存表的结构和数据时采用不同的方式 MyISAM表文件含义:.frm表定义,.MYD表数据,.MYI表索引 InnoDB表文件含义:.frm表定义,表空间数据和日志文件 SHOW ENGINES – 显示存储引擎的状态信息 SHOW ENGINE 引擎名 {LOGS|STATUS} – 显示存储引擎的日志或状态信息 – 自增起始数 AUTO_INCREMENT = 行数 – 数据文件目录 DATA DIRECTORY = ‘目录’ – 索引文件目录 INDEX DIRECTORY = ‘目录’ – 表注释 COMMENT = ‘string’
    – 分区选项 PARTITION BY … (详细见手册) – 查看所有表 SHOW TABLES[ LIKE ‘pattern’] SHOW TABLES FROM 库名 – 查看表结构 SHOW CREATE TABLE 表名 (信息更详细) DESC 表名 / DESCRIBE 表名 / EXPLAIN 表名 / SHOW COLUMNS FROM 表名 [LIKE ‘PATTERN’] SHOW TABLE STATUS [FROM db_name] [LIKE ‘pattern’] – 修改表 – 修改表本身的选项 ALTER TABLE 表名 表的选项 eg: ALTER TABLE 表名 ENGINE=MYISAM; – 对表进行重命名 RENAME TABLE 原表名 TO 新表名 RENAME TABLE 原表名 TO 库名.表名 (可将表移动到另一个数据库) – RENAME可以交换两个表名 – 修改表的字段机构(13.1.2. ALTER TABLE语法) ALTER TABLE 表名 操作名 – 操作名 ADD[ COLUMN] 字段定义 – 增加字段 AFTER 字段名 – 表示增加在该字段名后面 FIRST – 表示增加在第一个 ADD PRIMARY KEY(字段名) – 创建主键 ADD UNIQUE [索引名] (字段名)-- 创建唯一索引 ADD INDEX [索引名] (字段名) – 创建普通索引 DROP[ COLUMN] 字段名 – 删除字段 MODIFY[ COLUMN] 字段名 字段属性 – 支持对字段属性进行修改,不能修改字段名 (所有原有属性也需写上) CHANGE[ COLUMN] 原字段名 新字段名 字段属性 – 支持对字段名修改 DROP PRIMARY KEY – 删除主键(删除主键前需删除其AUTO_INCREMENT属性) DROP INDEX 索引名 – 删除索引 DROP FOREIGN KEY 外键 – 删除外键 – 删除表 DROP TABLE[ IF EXISTS] 表名 … – 清空表数据 TRUNCATE [TABLE] 表名 – 复制表结构 CREATE TABLE 表名 LIKE 要复制的表名 – 复制表结构和数据 CREATE TABLE 表名 [AS] SELECT * FROM 要复制的表名 – 检查表是否有错误 CHECK TABLE tbl_name [, tbl_name] … [option] … – 优化表 OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] … – 修复表 REPAIR [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] … [QUICK] [EXTENDED] [USE_FRM] – 分析表 ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] …
    数据操作
    字符集编码
    数据类型(列类型)
    /
    数据操作 / ------------------ – 增INSERT [INTO] 表名 [(字段列表)] VALUES (值列表)[, (值列表), …] – 如果要插入的值列表包含所有字段并且顺序一致,则可以省略字段列表。 – 可同时插入多条数据记录! REPLACE 与 INSERT 完全一样,可互换。 INSERT [INTO] 表名 SET 字段名=值[, 字段名=值, …] – 查SELECT 字段列表 FROM 表名[ 其他子句] – 可来自多个表的多个字段 – 其他子句可以不使用 – 字段列表可以用代替,表示所有字段 – 删DELETE FROM 表名[ 删除条件子句] 没有条件子句,则会删除全部 – 改UPDATE 表名 SET 字段名=新值[, 字段名=新值] [更新条件] /* 字符集编码 / ------------------ – MySQL、数据库、表、字段均可设置编码 – 数据编码与客户端编码不需一致 SHOW VARIABLES LIKE ‘character_set_%’ – 查看所有字符集编码项 character_set_client 客户端向服务器发送数据时使用的编码 character_set_results 服务器端将结果返回给客户端所使用的编码 character_set_connection 连接层编码 SET 变量名 = 变量值 SET character_set_client = gbk; SET character_set_results = gbk; SET character_set_connection = gbk; SET NAMES GBK; – 相当于完成以上三个设置 – 校对集 校对集用以排序 SHOW CHARACTER SET [LIKE ‘pattern’]/SHOW CHARSET [LIKE ‘pattern’] 查看所有字 符集 SHOW COLLATION [LIKE ‘pattern’] 查看所有校对集 CHARSET 字符集编码 设置字符集编码 COLLATE 校对集编码 设置校对集编码 / 数据类型(列类型) */ ------------------ 1. 数值类型 – a. 整型 ---------- 类型 字节 范围(有符号位) tinyint 1字节 -128 ~ 127 无符号位:0 ~ 255 smallint 2字节 -32768 ~ 32767

mediumint 3字节 -8388608 ~ 8388607 int 4字节 bigint 8字节 int(M) M表示总位数 - 默认存在符号位,unsigned 属性修改 - 显示宽度,如果某个数不够定义字段时设置的位数,则前面以0补填,zerofill 属性修改 例:int(5) 插入一个数’123’,补填后为’00123’ - 在满足要求的情况下,越小越好。 - 1表示bool值真,0表示bool值假。MySQL没有布尔类型,通过整型0和1表示。常用tinyint(1)表 示布尔型。 – b. 浮点型 ---------- 类型 字节 范围 float(单精度) 4字节 double(双精度) 8字节 浮点型既支持符号位 unsigned 属性,也支持显示宽度 zerofill 属性。 不同于整型,前后均会补填0. 定义浮点型时,需指定总位数和小数位数。 float(M, D) double(M, D) M表示总位数,D表示小数位数。 M和D的大小会决定浮点数的范围。不同于整型的固定范围。 M既表示总位数(不包括小数点和正负号),也表示显示宽度(所有显示符号均包括)。 支持科学计数法表示。 浮点数表示近似值。 – c. 定点数 ---------- decimal – 可变长度 decimal(M, D) M也表示总位数,D表示小数位数。 保存一个精确的数值,不会发生数据的改变,不同于浮点数的四舍五入。 将浮点数转换为字符串来保存,每9位数字保存为4个字节。 2. 字符串类型 – a. char, varchar ---------- char 定长字符串,速度快,但浪费空间 varchar 变长字符串,速度慢,但节省空间 M表示能存储的最大长度,此长度是字符数,非字节数。 不同的编码,所占用的空间不同。 char,最多255个字符,与编码无关。 varchar,最多65535字符,与编码有关。 一条有效记录最大不能超过65535个字节。 utf8 最大为21844个字符,gbk 最大为32766个字符,latin1 最大为65532个字符 varchar 是变长的,需要利用存储空间保存 varchar 的长度,如果数据小于255个字节,则采用一个 字节来保存长度,反之需要两个字节来保存。 varchar 的最大有效长度由最大行大小和使用的字符集确定。 最大有效长度是65532字节,因为在varchar存字符串时,第一个字节是空的,不存在任何数据,然后还 需两个字节来存放字符串的长度,所以有效长度是65535-1-2=65532字节。 例:若一个表定义为 CREATE TABLE tb(c1 int, c2 char(30), c3 varchar(N)) charset=utf8; 问N的最大值是多少? 答:(65535-1-2-4-303)/3 – b. blob, text ---------- blob 二进制字符串(字节字符串) tinyblob, blob, mediumblob, longblob text 非二进制字符串(字符字符串) tinytext, text, mediumtext, longtext text 在定义时,不需要定义长度,也不会计算总长度。 text 类型在定义时,不可给default值 – c. binary, varbinary ---------- 类似于char和varchar,用于保存二进制字符串,也就是保存字节字符串而非字符字符串。 char, varchar, text 对应 binary, varbinary, blob. 3. 日期时间类型 一般用整型保存时间戳,因为PHP可以很方便的将时间戳进行格式化。 datetime 8字节 日期及时间 1000-01-01 00:00:00 到 9999-12-31 23:59:59
列属性(列约束)
date 3字节 日期 1000-01-01 到 9999-12-31 timestamp 4字节 时间戳 19700101000000 到 2038-01-19 03:14:07 time 3字节 时间 -838:59:59 到 838:59:59 year 1字节 年份 1901 - 2155 datetime YYYY-MM-DD hh:mm:ss timestamp YY-MM-DD hh:mm:ss YYYYMMDDhhmmss YYMMDDhhmmss YYYYMMDDhhmmss YYMMDDhhmmss date YYYY-MM-DD YY-MM-DD YYYYMMDD YYMMDD YYYYMMDD YYMMDD time hh:mm:ss hhmmss hhmmss year YYYY YYYYYY YY 4. 枚举和集合 – 枚举(enum) ---------- enum(val1, val2, val3…) 在已知的值中进行单选。最大数量为65535. 枚举值在保存时,以2个字节的整型(smallint)保存。每个枚举值,按保存的位置顺序,从1开始逐一递 增。 表现为字符串类型,存储却是整型。 NULL值的索引是NULL。 空字符串错误值的索引值是0。 – 集合(set) ---------- set(val1, val2, val3…) create table tab ( gender set(‘男’, ‘女’, ‘无’) ); insert into tab values (‘男, 女’); 最多可以有64个不同的成员。以bigint存储,共8个字节。采取位运算的形式。 当创建表时,SET成员值的尾部空格将自动被删除。 /
列属性(列约束) / ------------------ 1. PRIMARY 主键 - 能唯一标识记录的字段,可以作为主键。 - 一个表只能有一个主键。 - 主键具有唯一性。 - 声明字段时,用 primary key 标识。 也可以在字段列表之后声明 例:create table tab ( id int, stu varchar(10), primary key (id)); - 主键字段的值不能为null。 - 主键可以由多个字段共同组成。此时需要在字段列表后声明的方法。 例:create table tab ( id int, stu varchar(10), age int, primary key (stu, age)); 2. UNIQUE 唯一索引(唯一约束)
建表规范
使得某字段的值也不能重复。 3. NULL 约束 null不是数据类型,是列的一个属性。 表示当前列是否可以为null,表示什么都没有。 null, 允许为空。默认。 not null, 不允许为空。 insert into tab values (null, ‘val’); – 此时表示将第一个字段的值设为null, 取决于该字段是否允许为null 4. DEFAULT 默认值属性 当前字段的默认值。 insert into tab values (default, ‘val’); – 此时表示强制使用默认值。 create table tab ( add_time timestamp default current_timestamp ); – 表示将当前时间的时间戳设为默认值。 current_date, current_time 5. AUTO_INCREMENT 自动增长约束 自动增长必须为索引(主键或unique) 只能存在一个字段为自动增长。 默认为1开始自动增长。可以通过表属性 auto_increment = x进行设置,或 alter table tbl auto_increment = x; 6. COMMENT 注释 例:create table tab ( id int ) comment ‘注释内容’; 7. FOREIGN KEY 外键约束 用于限制主表与从表数据完整性。 alter table t1 add constraint t1_t2_fk foreign key (t1_id) references t2(id); – 将表t1的t1_id外键关联到表t2的id字段。 – 每个外键都有一个名字,可以通过 constraint 指定 存在外键的表,称之为从表(子表),外键指向的表,称之为主表(父表)。 作用:保持数据一致性,完整性,主要目的是控制存储在外键表(从表)中的数据。 MySQL中,可以对InnoDB引擎使用外键约束: 语法: foreign key (外键字段) references 主表名 (关联字段) [主表记录删除时的动作] [主表记录 更新时的动作] 此时需要检测一个从表的外键需要约束为主表的已存在的值。外键在没有关联的情况下,可以设置为 null.前提是该外键列,没有not null。 可以不指定主表记录更改或更新时的动作,那么此时主表的操作被拒绝。 如果指定了 on update 或 on delete:在删除或更新时,有如下几个操作可以选择: 1. cascade,级联操作。主表数据被更新(主键值更新),从表也被更新(外键值更新)。主表记录被 删除,从表相关记录也被删除。 2. set null,设置为null。主表数据被更新(主键值更新),从表的外键被设置为null。主表记录 被删除,从表相关记录外键被设置成null。但注意,要求该外键列,没有not null属性约束。 3. restrict,拒绝父表删除和更新。 注意,外键只被InnoDB存储引擎所支持。其他引擎是不支持的。 /
建表规范 / ------------------ – Normal Format, NF - 每个表保存一个实体信息 - 每个具有一个ID字段作为主键 - ID主键 + 原子表 – 1NF, 第一范式 字段不能再分,就满足第一范式。 – 2NF, 第二范式
SELECT
满足第一范式的前提下,不能出现部分依赖。 消除复合主键就可以避免部分依赖。增加单列关键字。 – 3NF, 第三范式 满足第二范式的前提下,不能出现传递依赖。 某个字段依赖于主键,而有其他字段依赖于该字段。这就是传递依赖。 将一个实体信息的数据放在一个表内实现。 /
SELECT / ------------------ SELECT [ALL|DISTINCT] select_expr FROM -> WHERE -> GROUP BY [合计函数] -> HAVING - > ORDER BY -> LIMIT a. select_expr – 可以用 * 表示所有字段。 select * from tb; – 可以使用表达式(计算公式、函数调用、字段也是个表达式) select stu, 29+25, now() from tb; – 可以为每个列使用别名。适用于简化列标识,避免多个列标识符重复。 - 使用 as 关键字,也可省略 as. select stu+10 as add10 from tb; b. FROM 子句 用于标识查询来源。 – 可以为表起别名。使用as关键字。 SELECT * FROM tb1 AS tt, tb2 AS bb; – from子句后,可以同时出现多个表。 – 多个表会横向叠加到一起,而数据会形成一个笛卡尔积。 SELECT * FROM tb1, tb2; – 向优化符提示如何选择索引 USE INDEX、IGNORE INDEX、FORCE INDEX SELECT * FROM table1 USE INDEX (key1,key2) WHERE key1=1 AND key2=2 AND key3=3; SELECT * FROM table1 IGNORE INDEX (key3) WHERE key1=1 AND key2=2 AND key3=3; c. WHERE 子句 – 从from获得的数据源中进行筛选。 – 整型1表示真,0表示假。 – 表达式由运算符和运算数组成。 – 运算数:变量(字段)、值、函数返回值 – 运算符: =, <=>, <>, !=, <=, <, >=, >, !, &&, ||, in (not) null, (not) like, (not) in, (not) between and, is (not), and, or, not, xor is/is not 加上ture/false/unknown,检验某个值的真假 <=>与<>功能相同,<=>可用于null比较 d. GROUP BY 子句, 分组子句 GROUP BY 字段/别名 [排序方式] 分组后会进行排序。升序:ASC,降序:DESC 以下[合计函数]需配合 GROUP BY 使用: count 返回不同的非NULL值数目 count()、count(字段) sum 求和 max 求最大值 min 求最小值 avg 求平均值 group_concat 返回带有来自一个组的连接的非NULL值的字符串结果。组内字符串连接。
UNION
子查询
e. HAVING 子句,条件子句 与 where 功能、用法相同,执行时机不同。 where 在开始时执行检测数据,对原数据进行过滤。 having 对筛选出的结果再次进行过滤。 having 字段必须是查询出来的,where 字段必须是数据表存在的。 where 不可以使用字段的别名,having 可以。因为执行WHERE代码时,可能尚未确定列值。 where 不可以使用合计函数。一般需用合计函数才会用 having SQL标准要求HAVING必须引用GROUP BY子句中的列或用于合计函数中的列。 f. ORDER BY 子句,排序子句 order by 排序字段/别名 排序方式 [,排序字段/别名 排序方式]… 升序:ASC,降序:DESC 支持多个字段的排序。 g. LIMIT 子句,限制结果数量子句 仅对处理好的结果进行数量限制。将处理好的结果的看作是一个集合,按照记录出现的顺序,索引从0开 始。 limit 起始位置, 获取条数 省略第一个参数,表示从索引0开始。limit 获取条数 h. DISTINCT, ALL 选项 distinct 去除重复记录 默认为 all, 全部记录 /* UNION / ------------------ 将多个select查询的结果组合成一个结果集合。 SELECT … UNION [ALL|DISTINCT] SELECT … 默认 DISTINCT 方式,即所有返回的行都是唯一的 建议,对每个SELECT查询加上小括号包裹。 ORDER BY 排序时,需加上 LIMIT 进行结合。 需要各select查询的字段数量一样。 每个select查询的字段列表(数量、类型)应一致,因为结果中的字段名以第一条select语句为准。 / 子查询 / ------------------ - 子查询需用括号包裹。 – from型 from后要求是一个表,必须给子查询结果取个别名。 - 简化每个查询内的条件。 - from型需将结果生成一个临时表格,可用以原表的锁定的释放。 - 子查询返回一个表,表型子查询。 select * from (select * from tb where id>0) as subfrom where id>1; – where型 - 子查询返回一个值,标量子查询。 - 不需要给子查询取别名。 - where子查询内的表,不能直接用以更新。 select * from tb where money = (select max(money) from tb); – 列子查询 如果子查询结果返回的是一列。 使用 in 或 not in 完成查询
连接查询(join)
TRUNCATE
exists 和 not exists 条件 如果子查询返回数据,则返回1或0。常用于判断条件。 select column1 from t1 where exists (select * from t2); – 行子查询 查询条件是一个行。 select * from t1 where (id, gender) in (select id, gender from t2); 行构造符:(col1, col2, …) 或 ROW(col1, col2, …) 行构造符通常用于与对能返回两个或两个以上列的子查询进行比较。 – 特殊运算符 != all() 相当于 not in = some() 相当于 in。any 是 some 的别名 != some() 不等同于 not in,不等于其中某一个。 all, some 可以配合其他运算符一起使用。 /
连接查询(join) / ------------------ 将多个表的字段进行连接,可以指定连接条件。 – 内连接(inner join) - 默认就是内连接,可省略inner。 - 只有数据存在时才能发送连接。即连接结果不能出现空行。 on 表示连接条件。其条件表达式与where类似。也可以省略条件(表示条件永远为真) 也可用where表示连接条件。 还有 using, 但需字段名相同。 using(字段名) – 交叉连接 cross join 即,没有条件的内连接。 select * from tb1 cross join tb2; – 外连接(outer join) - 如果数据不存在,也会出现在连接结果中。 – 左外连接 left join 如果数据不存在,左表记录会出现,而右表为null填充 – 右外连接 right join 如果数据不存在,右表记录会出现,而左表为null填充 – 自然连接(natural join) 自动判断连接条件完成连接。 相当于省略了using,会自动查找相同字段名。 natural join natural left join natural right join select info.id, info.name, info.stu_num, extra_info.hobby, extra_info.sex from info, extra_info where info.stu_num = extra_info.stu_id;
备份与还原
视图
/
TRUNCATE / ------------------ TRUNCATE [TABLE] tbl_name 清空数据 删除重建表 区别: 1,truncate 是删除表再创建,delete 是逐条删除 2,truncate 重置auto_increment的值。而delete不会 3,truncate 不知道删除了几条,而delete知道。 4,当被用于带分区的表时,truncate 会保留分区 / 备份与还原 */ ------------------ 备份,将数据的结构与表内数据保存起来。 利用 mysqldump 指令完成。 – 导出 mysqldump [options] db_name [tables] mysqldump [options] —database DB1 [DB2 DB3…] mysqldump [options] --all–database 1. 导出一张表 mysqldump -u用户名 -p密码 库名 表名 > 文件名(D:/a.sql) 2. 导出多张表 mysqldump -u用户名 -p密码 库名 表1 表2 表3 > 文件名(D:/a.sql) 3. 导出所有表 mysqldump -u用户名 -p密码 库名 > 文件名(D:/a.sql) 4. 导出一个库 mysqldump -u用户名 -p密码 --lock-all-tables --database 库名 > 文件名(D:/a.sql) 可以-w携带WHERE条件 – 导入 1. 在登录mysql的情况下: source 备份文件 2. 在不登录的情况下 mysql -u用户名 -p密码 库名 < 备份文件 什么是视图: 视图是一个虚拟表,其内容由查询定义。同真实的表一样,视图包含一系列带有名称的列和行数据。但 是,视图并不在数据库中以存储的数据值集形式存在。行和列数据来自由定义视图的查询所引用的表,并且在 引用视图时动态生成。 视图具有表结构文件,但不存在数据文件。 对其中所引用的基础表来说,视图的作用类似于筛选。定义视图的筛选可以来自当前或其它数据库的一个 或多个表,或者其它视图。通过视图进行查询没有任何限制,通过它们进行数据修改时的限制也很少。 视图是存储在数据库中的查询的sql语句,它主要出于两种原因:安全原因,视图可以隐藏一些数据, 如:社会保险基金表,可以用视图只显示姓名,地址,而不显示社会保险号和工资数等,另一原因是可使复杂 的查询易于理解和使用。 – 创建视图 CREATE [OR REPLACE] [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] VIEW view_name [(column_list)] AS select_statement
事务

  • 视图名必须唯一,同时不能与表重名。 - 视图可以使用select语句查询到的列名,也可以自己指定相应的列名。 - 可以指定视图执行的算法,通过ALGORITHM指定。 - column_list如果存在,则数目必须等于SELECT语句检索的列数 – 查看结构 SHOW CREATE VIEW view_name – 删除视图 - 删除视图后,数据依然存在。 - 可同时删除多个视图。 DROP VIEW [IF EXISTS] view_name … – 修改视图结构 - 一般不修改视图,因为不是所有的更新视图都会映射到表上。 ALTER VIEW view_name [(column_list)] AS select_statement – 视图作用 1. 简化业务逻辑 2. 对客户端隐藏真实的表结构 – 视图算法(ALGORITHM) MERGE 合并 将视图的查询语句,与外部查询需要先合并再执行! TEMPTABLE 临时表 将视图执行完毕后,形成临时表,再做外层查询! UNDEFINED 未定义(默认),指的是MySQL自主去选择相应的算法。 事务是指逻辑上的一组操作,组成这组操作的各个单元,要不全成功要不全失败。 - 支持连续SQL的集体成功或集体撤销。 - 事务是数据库在数据完整性方面的一个功能。 - 需要利用 InnoDB 或 BDB 存储引擎,对自动提交的特性支持完成。 - InnoDB被称为事务安全型引擎。 – 事务开启 START TRANSACTION; 或者 BEGIN; 开启事务后,所有被执行的SQL语句均被认作当前事务内的SQL语句。 – 事务提交 COMMIT; – 事务回滚 ROLLBACK; 如果部分操作发生问题,映射到事务开启前。 – 事务的特性 1. 原子性(Atomicity) 事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。 2. 一致性(Consistency) 事务前后数据的完整性必须保持一致。 - 事务开始和结束时,外部数据一致 - 在整个事务过程中,操作是连续的 3. 隔离性(Isolation) 多个用户并发访问数据库时,一个用户的事务不能被其它用户的事物所干扰,多个并发事务之间的 数据要相互隔离。 4. 持久性(Durability) 一个事务一旦被提交,它对数据库中的数据改变就是永久性的。 – 事务的实现 1. 要求是事务支持的表类型
    锁表
    触发器
  1. 执行一组相关的操作前开启事务 3. 整组操作完成后,都成功,则提交;如果存在失败,选择回滚,则会回到事务开始的备份点。 – 事务的原理 利用InnoDB的自动提交(autocommit)特性完成。 普通的MySQL执行语句后,当前的数据提交操作均可被其他客户端可见。 而事务是暂时关闭“自动提交”机制,需要commit提交持久化数据操作。 – 注意1. 数据定义语言(DDL)语句不能被回滚,比如创建或取消数据库的语句,和创建、取消或更改表或存 储的子程序的语句。 2. 事务不能被嵌套 – 保存点 SAVEPOINT 保存点名称 – 设置一个事务保存点 ROLLBACK TO SAVEPOINT 保存点名称 – 回滚到保存点 RELEASE SAVEPOINT 保存点名称 – 删除保存点 – InnoDB自动提交特性设置 SET autocommit = 0|1; 0表示关闭自动提交,1表示开启自动提交。 - 如果关闭了,那普通操作的结果对其他客户端也不可见,需要commit提交后才能持久化数据操作。 - 也可以关闭自动提交来开启事务。但与START TRANSACTION不同的是, SET autocommit是永久改变服务器的设置,直到下次再次修改该设置。(针对当前连接) 而START TRANSACTION记录开启前的状态,而一旦事务提交或回滚后就需要再次开启事务。(针对 当前事务) /* 锁表 / 表锁定只用于防止其它客户端进行不正当地读取和写入 MyISAM 支持表锁,InnoDB 支持行锁 – 锁定LOCK TABLES tbl_name [AS alias] – 解锁UNLOCK TABLES / 触发器 / ------------------ 触发程序是与表有关的命名数据库对象,当该表出现特定事件时,将激活该对象 监听:记录的增加、修改、删除。 – 创建触发器 CREATE TRIGGER trigger_name trigger_time trigger_event ON tbl_name FOR EACH ROW trigger_stmt 参数: trigger_time是触发程序的动作时间。它可以是 before 或 after,以指明触发程序是在激活它的 语句之前或之后触发。 trigger_event指明了激活触发程序的语句的类型 INSERT:将新行插入表时激活触发程序 UPDATE:更改某一行时激活触发程序 DELETE:从表中删除某一行时激活触发程序 tbl_name:监听的表,必须是永久性的表,不能将触发程序与TEMPORARY表或视图关联起来。 trigger_stmt:当触发程序激活时执行的语句。执行多个语句,可使用BEGIN…END复合语句结构 – 删除
    SQL编程
    DROP TRIGGER [schema_name.]trigger_name 可以使用old和new代替旧的和新的数据 更新操作,更新前是old,更新后是new. 删除操作,只有old. 增加操作,只有new. – 注意1. 对于具有相同触发程序动作时间和事件的给定表,不能有两个触发程序。 – 字符连接函数 concat(str1,str2,…]) concat_ws(separator,str1,str2,…) – 分支语句 if 条件 then 执行语句 elseif 条件 then 执行语句 else执行语句 end if; – 修改最外层语句结束符 delimiter 自定义结束符号 SQL语句 自定义结束符号 delimiter ; – 修改回原来的分号 – 语句块包裹 begin语句块 end – 特殊的执行 1. 只要添加记录,就会触发程序。 2. Insert into on duplicate key update 语法会触发: 如果没有重复记录,会触发 before insert, after insert; 如果有重复记录并更新,会触发 before insert, before update, after update; 如果有重复记录但是没有发生更新,则触发 before insert, before update 3. Replace 语法 如果有记录,则执行 before insert, before delete, after delete, after insert /
    SQL编程 */ ------------------ --// 局部变量 ---------- – 变量声明 declare var_name[,…] type [default value] 这个语句被用来声明局部变量。要给变量提供一个默认值,请包含一个default子句。值可以被指定为一 个表达式,不需要为一个常数。如果没有default子句,初始值为null。 – 赋值使用 set 和 select into 语句为变量赋值。 - 注意:在函数内是可以使用全局变量(用户自定义的变量) --// 全局变量 ---------- – 定义、赋值 set 语句可以定义并为变量赋值。 set @var = value; 也可以使用select into语句为变量初始化并赋值。这样要求select语句只能返回一行,但是可以是多个字 段,就意味着同时为多个变量进行赋值,变量的数量需要与查询的列数一致。
    还可以把赋值语句看作一个表达式,通过select执行完成。此时为了避免=被当作关系运算符看待,使用:=代 替。(set语句可以使用= 和 :=)。 select @var:=20; select @v1:=id, @v2=name from t1 limit 1; select * from tbl_name where @var:=30; select into 可以将表中查询获得的数据赋给变量。 -| select max(height) into @max_height from tb; – 自定义变量名 为了避免select语句中,用户自定义的变量与系统标识符(通常是字段名)冲突,用户自定义变量在变量名前 使用@作为开始符号。 @var=10; - 变量被定义后,在整个会话周期都有效(登录到退出) --// 控制结构 ---------- – if语句 if search_condition then statement_list [elseif search_condition then statement_list] … [elsestatement_list] end if; – case语句 CASE value WHEN [compare-value] THEN result [WHEN [compare-value] THEN result …] [ELSE result] END – while循环 [begin_label:] while search_condition do statement_list end while [end_label]; - 如果需要在循环内提前终止 while循环,则需要使用标签;标签需要成对出现。 – 退出循环 退出整个循环 leave 退出当前循环 iterate 通过退出的标签决定退出哪个循环 --// 内置函数 ---------- – 数值函数 abs(x) – 绝对值 abs(-10.9) = 10 format(x, d) – 格式化千分位数值 format(1234567.456, 2) = 1,234,567.46 ceil(x) – 向上取整 ceil(10.1) = 11 floor(x) – 向下取整
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/618925
推荐阅读
相关标签
  

闽ICP备14008679号