精华内容
下载资源
问答
  • 最新Java面试题,常见面试题及答案汇总

    万次阅读 多人点赞 2019-07-12 08:56:55
    Java最新面试题面试题答案汇总

    Java最新常见面试题 + 答案汇总

    原文地址:https://blog.csdn.net/sufu1065/article/details/88051083

    1、面试题模块汇总

    面试题包括以下十九个模块: Java 基础、容器、多线程、反射、对象拷贝、Java Web 模块、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、Mybatis、RabbitMQ、Kafka、Zookeeper、MySql、Redis、JVM 。如下图所示:

    可能对于初学者不需要后面的框架和 JVM 模块的知识,读者朋友们可根据自己的情况,选择对应的模块进行阅读。

    适宜阅读人群

    • 需要面试的初/中/高级 java 程序员
    • 想要查漏补缺的人
    • 想要不断完善和扩充自己 java 技术栈的人
    • java 面试官

    具体面试题

    下面一起来看 208 道面试题,具体的内容。

    一、Java 基础

    1.JDK 和 JRE 有什么区别?

    2.== 和 equals 的区别是什么?

    3.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?

    4.final 在 java 中有什么作用?

    5.java 中的 Math.round(-1.5) 等于多少?

    6.String 属于基础的数据类型吗?

    7.java 中操作字符串都有哪些类?它们之间有什么区别?

    8.String str="i"与 String str=new String(“i”)一样吗?

    9.如何将字符串反转?

    10.String 类的常用方法都有那些?

    11.抽象类必须要有抽象方法吗?

    12.普通类和抽象类有哪些区别?

    13.抽象类能使用 final 修饰吗?

    14.接口和抽象类有什么区别?

    15.java 中 IO 流分为几种?

    16.BIO、NIO、AIO 有什么区别?

    17.Files的常用方法都有哪些?

    二、容器

    18.java 容器都有哪些?

    19.Collection 和 Collections 有什么区别?

    20.List、Set、Map 之间的区别是什么?

    21.HashMap 和 Hashtable 有什么区别?

    22.如何决定使用 HashMap 还是 TreeMap?

    23.说一下 HashMap 的实现原理?

    24.说一下 HashSet 的实现原理?

    25.ArrayList 和 LinkedList 的区别是什么?

    26.如何实现数组和 List 之间的转换?

    27.ArrayList 和 Vector 的区别是什么?

    28.Array 和 ArrayList 有何区别?

    29.在 Queue 中 poll()和 remove()有什么区别?

    30.哪些集合类是线程安全的?

    31.迭代器 Iterator 是什么?

    32.Iterator 怎么使用?有什么特点?

    33.Iterator 和 ListIterator 有什么区别?

    34.怎么确保一个集合不能被修改?

    三、多线程

    35.并行和并发有什么区别?

    36.线程和进程的区别?

    37.守护线程是什么?

    38.创建线程有哪几种方式?

    39.说一下 runnable 和 callable 有什么区别?

    40.线程有哪些状态?

    41.sleep() 和 wait() 有什么区别?

    42.notify()和 notifyAll()有什么区别?

    43.线程的 run()和 start()有什么区别?

    44.创建线程池有哪几种方式?

    45.线程池都有哪些状态?

    46.线程池中 submit()和 execute()方法有什么区别?

    47.在 java 程序中怎么保证多线程的运行安全?

    48.多线程锁的升级原理是什么?

    49.什么是死锁?

    50.怎么防止死锁?

    51.ThreadLocal 是什么?有哪些使用场景?

    52.说一下 synchronized 底层实现原理?

    53.synchronized 和 volatile 的区别是什么?

    54.synchronized 和 Lock 有什么区别?

    55.synchronized 和 ReentrantLock 区别是什么?

    56.说一下 atomic 的原理?

    四、反射

    57.什么是反射?

    58.什么是 java 序列化?什么情况下需要序列化?

    59.动态代理是什么?有哪些应用?

    60.怎么实现动态代理?

    五、对象拷贝

    61.为什么要使用克隆?

    62.如何实现对象克隆?

    63.深拷贝和浅拷贝区别是什么?

    六、Java Web

    64.jsp 和 servlet 有什么区别?

    65.jsp 有哪些内置对象?作用分别是什么?

    66.说一下 jsp 的 4 种作用域?

    67.session 和 cookie 有什么区别?

    68.说一下 session 的工作原理?

    69.如果客户端禁止 cookie 能实现 session 还能用吗?

    70.spring mvc 和 struts 的区别是什么?

    71.如何避免 sql 注入?

    72.什么是 XSS 攻击,如何避免?

    73.什么是 CSRF 攻击,如何避免?

    七、异常

    74.throw 和 throws 的区别?

    75.final、finally、finalize 有什么区别?

    76.try-catch-finally 中哪个部分可以省略?

    77.try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?

    78.常见的异常类有哪些?

    八、网络

    79.http 响应码 301 和 302 代表的是什么?有什么区别?

    80.forward 和 redirect 的区别?

    81.简述 tcp 和 udp的区别?

    82.tcp 为什么要三次握手,两次不行吗?为什么?

    83.说一下 tcp 粘包是怎么产生的?

    84.OSI 的七层模型都有哪些?

    85.get 和 post 请求有哪些区别?

    86.如何实现跨域?

    87.说一下 JSONP 实现原理?

    九、设计模式

    88.说一下你熟悉的设计模式?

    89.简单工厂和抽象工厂有什么区别?

    十、Spring/Spring MVC

    90.为什么要使用 spring?

    91.解释一下什么是 aop?

    92.解释一下什么是 ioc?

    93.spring 有哪些主要模块?

    94.spring 常用的注入方式有哪些?

    95.spring 中的 bean 是线程安全的吗?

    96.spring 支持几种 bean 的作用域?

    97.spring 自动装配 bean 有哪些方式?

    98.spring 事务实现方式有哪些?

    99.说一下 spring 的事务隔离?

    100.说一下 spring mvc 运行流程?

    101.spring mvc 有哪些组件?

    102.@RequestMapping 的作用是什么?

    103.@Autowired 的作用是什么?

    十一、Spring Boot/Spring Cloud

    104.什么是 spring boot?

    105.为什么要用 spring boot?

    106.spring boot 核心配置文件是什么?

    107.spring boot 配置文件有哪几种类型?它们有什么区别?

    108.spring boot 有哪些方式可以实现热部署?

    109.jpa 和 hibernate 有什么区别?

    110.什么是 spring cloud?

    111.spring cloud 断路器的作用是什么?

    112.spring cloud 的核心组件有哪些?

    十二、Hibernate

    113.为什么要使用 hibernate?

    114.什么是 ORM 框架?

    115.hibernate 中如何在控制台查看打印的 sql 语句?

    116.hibernate 有几种查询方式?

    117.hibernate 实体类可以被定义为 final 吗?

    118.在 hibernate 中使用 Integer 和 int 做映射有什么区别?

    119.hibernate 是如何工作的?

    120.get()和 load()的区别?

    121.说一下 hibernate 的缓存机制?

    122.hibernate 对象有哪些状态?

    123.在 hibernate 中 getCurrentSession 和 openSession 的区别是什么?

    124.hibernate 实体类必须要有无参构造函数吗?为什么?

    十三、Mybatis

    125.mybatis 中 #{}和 ${}的区别是什么?

    126.mybatis 有几种分页方式?

    127.RowBounds 是一次性查询全部结果吗?为什么?

    128.mybatis 逻辑分页和物理分页的区别是什么?

    129.mybatis 是否支持延迟加载?延迟加载的原理是什么?

    130.说一下 mybatis 的一级缓存和二级缓存?

    131.mybatis 和 hibernate 的区别有哪些?

    132.mybatis 有哪些执行器(Executor)?

    133.mybatis 分页插件的实现原理是什么?

    134.mybatis 如何编写一个自定义插件?

    十四、RabbitMQ

    135.rabbitmq 的使用场景有哪些?

    136.rabbitmq 有哪些重要的角色?

    137.rabbitmq 有哪些重要的组件?

    138.rabbitmq 中 vhost 的作用是什么?

    139.rabbitmq 的消息是怎么发送的?

    140.rabbitmq 怎么保证消息的稳定性?

    141.rabbitmq 怎么避免消息丢失?

    142.要保证消息持久化成功的条件有哪些?

    143.rabbitmq 持久化有什么缺点?

    144.rabbitmq 有几种广播类型?

    145.rabbitmq 怎么实现延迟消息队列?

    146.rabbitmq 集群有什么用?

    147.rabbitmq 节点的类型有哪些?

    148.rabbitmq 集群搭建需要注意哪些问题?

    149.rabbitmq 每个节点是其他节点的完整拷贝吗?为什么?

    150.rabbitmq 集群中唯一一个磁盘节点崩溃了会发生什么情况?

    151.rabbitmq 对集群节点停止顺序有要求吗?

    十五、Kafka

    152.kafka 可以脱离 zookeeper 单独使用吗?为什么?

    153.kafka 有几种数据保留的策略?

    154.kafka 同时设置了 7 天和 10G 清除数据,到第五天的时候消息达到了 10G,这个时候 kafka 将如何处理?

    155.什么情况会导致 kafka 运行变慢?

    156.使用 kafka 集群需要注意什么?

    十六、Zookeeper

    157.zookeeper 是什么?

    158.zookeeper 都有哪些功能?

    159.zookeeper 有几种部署模式?

    160.zookeeper 怎么保证主从节点的状态同步?

    161.集群中为什么要有主节点?

    162.集群中有 3 台服务器,其中一个节点宕机,这个时候 zookeeper 还可以使用吗?

    163.说一下 zookeeper 的通知机制?

    十七、MySql

    164.数据库的三范式是什么?

    165.一张自增表里面总共有 7 条数据,删除了最后 2 条数据,重启 mysql 数据库,又插入了一条数据,此时 id 是几?

    166.如何获取当前数据库版本?

    167.说一下 ACID 是什么?

    168.char 和 varchar 的区别是什么?

    169.float 和 double 的区别是什么?

    170.mysql 的内连接、左连接、右连接有什么区别?

    171.mysql 索引是怎么实现的?

    172.怎么验证 mysql 的索引是否满足需求?

    173.说一下数据库的事务隔离?

    174.说一下 mysql 常用的引擎?

    175.说一下 mysql 的行锁和表锁?

    176.说一下乐观锁和悲观锁?

    177.mysql 问题排查都有哪些手段?

    178.如何做 mysql 的性能优化?

    十八、Redis

    179.redis 是什么?都有哪些使用场景?

    180.redis 有哪些功能?

    181.redis 和 memecache 有什么区别?

    182.redis 为什么是单线程的?

    183.什么是缓存穿透?怎么解决?

    184.redis 支持的数据类型有哪些?

    185.redis 支持的 java 客户端都有哪些?

    186.jedis 和 redisson 有哪些区别?

    187.怎么保证缓存和数据库数据的一致性?

    188.redis 持久化有几种方式?

    189.redis 怎么实现分布式锁?

    190.redis 分布式锁有什么缺陷?

    191.redis 如何做内存优化?

    192.redis 淘汰策略有哪些?

    193.redis 常见的性能问题有哪些?该如何解决?

    十九、JVM

    194.说一下 jvm 的主要组成部分?及其作用?

    195.说一下 jvm 运行时数据区?

    196.说一下堆栈的区别?

    197.队列和栈是什么?有什么区别?

    198.什么是双亲委派模型?

    199.说一下类加载的执行过程?

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

    201.java 中都有哪些引用类型?

    202.说一下 jvm 有哪些垃圾回收算法?

    203.说一下 jvm 有哪些垃圾回收器?

    204.详细介绍一下 CMS 垃圾回收器?

    205.新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?

    206.简述分代垃圾回收器是怎么工作的?

    207.说一下 jvm 调优的工具?

    208.常用的 jvm 调优的参数都有哪些?

    2、面试题答案汇总

    (一)基础模块

    (二)容器

    (三)多线程

    (四)反射

    (五)对象拷贝

    (六)JavaWeb

    (七)异常

    (八)网络

    (九)设计模式

    (十)Spring/SpringMVC

    (十一)Spring Boot / Spring Cloud

    (十二)Hibernate

    (十三)Mybatis

    (十四)RabbitMQ

    (十五)Kafka

    (十六)Zookeeper

    (十七)MySql

    (十八)Redis

    (十九)JVM

    展开全文
  • Java面试题全集(上)

    万次阅读 多人点赞 2015-04-08 17:14:00
    2013年年底的时候,我看到了网上流传的一个叫做《Java面试题大全》的东西,认真的阅读了以后发现里面的很多题目是重复且没有价值的题目,还有不少的参考答案也是错误的,于是我花了半个月时间对这个所谓的《Java面试...

    2013年年底的时候,我看到了网上流传的一个叫做《Java面试题大全》的东西,认真的阅读了以后发现里面的很多题目是重复且没有价值的题目,还有不少的参考答案也是错误的,于是我花了半个月时间对这个所谓的《Java面试大全》进行了全面的修订并重新发布在我的CSDN博客。在修订的过程中,参照了当时JDK最新版本(Java 7)给出了题目的答案和相关代码,去掉了EJB 2.x、JSF等无用内容或过时内容,补充了数据结构和算法、大型网站技术架构、设计模式、UML、Spring MVC等内容并对很多知识点进行了深入的剖析,例如hashCode方法的设计、垃圾收集、并发编程、数据库事务等。当时我甚至希望把面试中经常出现的操作系统、数据库、软件测试等内容也补充进去,但是由于各种原因,最终只整理出了150道面试题。让我欣慰的是,这150道题还是帮助到了很多人,而且在我CSDN博客上的总访问量超过了5万次,最终还被很多网站和个人以原创的方式转载了。最近一年内,用百度搜索"Java面试"我写的这些东西基本上都排在搜索结果的前5名,这让我觉得"亚历山大",因为我写的这些东西一旦不准确就可能误导很多人。2014年的时候我又整理了30道题,希望把之前遗漏的面试题和知识点补充上去,但是仍然感觉挂一漏万,而且Java 8问世后很多新的东西又需要去总结和整理。为此,我不止一次的修改了之前的180题,修改到自己已经感觉有些疲惫或者厌烦了。2014年至今,自己带的学生又有很多走上了Java程序员、Java工程师的工作岗位,他们的面试经验也还没来得及跟大家分享,冥冥之中似乎有一股力量在刺激我要重新写一篇《Java面试题全集》,于是这篇文章就诞生了。请不要责备我把那些出现过的内容又写了一次,因为每次写东西就算是重复的内容,我也需要对编程语言和相关技术进行重新思考,不仅字斟句酌更是力求至臻完美,所以请相信我分享的一定是更新的、更好的、更有益的东西,这些内容也诉说着一个职业程序员和培训师的思想、精神和情感。

    提示:我一般不推荐看视频学习,因为进度太缓慢了,但是如果需要免费视频、思维导图、文档资料,可以通过下面的方式进行获取。百度云盘链接: https://pan.baidu.com/s/1rrCfOGXWNXpWcN0GUnuhfg,密码: 0ocq。

    1、面向对象的特征有哪些方面?
    答:面向对象的特征主要有以下几个方面:

    • 抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
    • 继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段(如果不能理解请阅读阎宏博士的《Java与模式》或《设计模式精解》中关于桥梁模式的部分)。
    • 封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口(可以想想普通洗衣机和全自动洗衣机的差别,明显全自动洗衣机封装更好因此操作起来更简单;我们现在使用的智能手机也是封装得足够好的,因为几个按键就搞定了所有的事情)。
    • 多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:当A系统访问B系统提供的服务时,B系统有多种提供服务的方式,但一切对A系统来说都是透明的(就像电动剃须刀是A系统,它的供电系统是B系统,B系统可以使用电池供电或者用交流电,甚至还有可能是太阳能,A系统只会通过B类对象调用供电的方法,但并不知道供电系统的底层实现是什么,究竟通过何种方式获得了动力)。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要做两件事:1). 方法重写(子类继承父类并重写父类中已有的或抽象的方法);2). 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。

    2、访问修饰符public,private,protected,以及不写(默认)时的区别?
    答:

    修饰符当前类同 包子 类其他包
    public
    protected×
    default××
    private×××

    类的成员不写访问修饰时默认为default。默认对于同一个包中的其他类相当于公开(public),对于不是同一个包中的其他类相当于私有(private)。受保护(protected)对子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。Java中,外部类的修饰符只能是public或默认,类的成员(包括内部类)的修饰符可以是以上四种。

    3、String 是最基本的数据类型吗?
    答:不是。Java中的基本数据类型只有8个:byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(reference type),Java 5以后引入的枚举类型也算是一种比较特殊的引用类型。

    4、float f=3.4;是否正确?
    答:不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F;。

    5、short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗?
    答:对于short s1 = 1; s1 = s1 + 1;由于1是int类型,因此s1+1运算结果也是int 型,需要强制转换类型才能赋值给short型。而short s1 = 1; s1 += 1;可以正确编译,因为s1+= 1;相当于s1 = (short)(s1 + 1);其中有隐含的强制类型转换。

    6、Java有没有goto?
    答:goto 是Java中的保留字,在目前版本的Java中没有使用。(根据James Gosling(Java之父)编写的《The Java Programming Language》一书的附录中给出了一个Java关键字列表,其中有goto和const,但是这两个是目前无法使用的关键字,因此有些地方将其称之为保留字,其实保留字这个词应该有更广泛的意义,因为熟悉C语言的程序员都知道,在系统类库中使用过的有特殊意义的单词或单词的组合都被视为保留字)

    7、int和Integer有什么区别?
    答:Java是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java为每一个基本数据类型都引入了对应的包装类型(wrapper class),int的包装类就是Integer,从Java 5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
    Java 为每个原始类型提供了包装类型:

    • 原始类型: boolean,char,byte,short,int,long,float,double
    • 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
    class AutoUnboxingTest {
    
    	public static void main(String[] args) {
    		Integer a = new Integer(3);
    		Integer b = 3;				    // 将3自动装箱成Integer类型
    		int c = 3;
    		System.out.println(a == b);		// false 两个引用没有引用同一对象
    		System.out.println(a == c);		// true a自动拆箱成int类型再和c比较
    	}
    }
    

    最近还遇到一个面试题,也是和自动装箱和拆箱有点关系的,代码如下所示:

    public class Test03 {
    
    	public static void main(String[] args) {
    		Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;
    		
    		System.out.println(f1 == f2);
    		System.out.println(f3 == f4);
    	}
    }
    

    如果不明就里很容易认为两个输出要么都是true要么都是false。首先需要注意的是f1、f2、f3、f4四个变量都是Integer对象引用,所以下面的==运算比较的不是值而是引用。装箱的本质是什么呢?当我们给一个Integer对象赋一个int值的时候,会调用Integer类的静态方法valueOf,如果看看valueOf的源代码就知道发生了什么。

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

    IntegerCache是Integer的内部类,其代码如下所示:

    /**
         * Cache to support the object identity semantics of autoboxing for values between
         * -128 and 127 (inclusive) as required by JLS.
         *
         * The cache is initialized on first usage.  The size of the cache
         * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
         * During VM initialization, java.lang.Integer.IntegerCache.high property
         * may be set and saved in the private system properties in the
         * sun.misc.VM class.
         */
    
        private static class IntegerCache {
            static final int low = -128;
            static final int high;
            static final Integer cache[];
    
            static {
                // high value may be configured by property
                int h = 127;
                String integerCacheHighPropValue =
                    sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
                if (integerCacheHighPropValue != null) {
                    try {
                        int i = parseInt(integerCacheHighPropValue);
                        i = Math.max(i, 127);
                        // Maximum array size is Integer.MAX_VALUE
                        h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                    } catch( NumberFormatException nfe) {
                        // If the property cannot be parsed into an int, ignore it.
                    }
                }
                high = h;
    
                cache = new Integer[(high - low) + 1];
                int j = low;
                for(int k = 0; k < cache.length; k++)
                    cache[k] = new Integer(j++);
    
                // range [-128, 127] must be interned (JLS7 5.1.7)
                assert IntegerCache.high >= 127;
            }
    
            private IntegerCache() {}
        }
    

    简单的说,如果整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象,所以上面的面试题中f1f2的结果是true,而f3f4的结果是false。

    **提醒:**越是貌似简单的面试题其中的玄机就越多,需要面试者有相当深厚的功力。

    8、&和&&的区别?
    答:&运算符有两种用法:(1)按位与;(2)逻辑与。&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true整个表达式的值才是true。&&之所以称为短路运算是因为,如果&&左边的表达式的值是false,右边的表达式会被直接短路掉,不会进行运算。很多时候我们可能都需要用&&而不是&,例如在验证用户登录时判定用户名不是null而且不是空字符串,应当写为:username != null &&!username.equals(""),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的equals比较,否则会产生NullPointerException异常。注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。

    **补充:**如果你熟悉JavaScript,那你可能更能感受到短路运算的强大,想成为JavaScript的高手就先从玩转短路运算开始吧。

    9、解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法。
    答:通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用JVM中的栈空间;而通过new关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为Eden、Survivor(又可分为From Survivor和To Survivor)、Tenured;方法区和堆都是各个线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据;程序中的字面量(literal)如直接书写的100、"hello"和常量都是放在常量池中,常量池是方法区的一部分,。栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过JVM的启动参数来进行调整,栈空间用光了会引发StackOverflowError,而堆和常量池空间不足则会引发OutOfMemoryError。

    String str = new String("hello");
    

    上面的语句中变量str放在栈上,用new创建出来的字符串对象放在堆上,而"hello"这个字面量是放在方法区的。

    **补充1:**较新版本的Java(从Java 6的某个更新开始)中,由于JIT编译器的发展和"逃逸分析"技术的逐渐成熟,栈上分配、标量替换等优化技术使得对象一定分配在堆上这件事情已经变得不那么绝对了。

    补充2:运行时常量池相当于Class文件常量池具有动态性,Java语言并不要求常量一定只有编译期间才能产生,运行期间也可以将新的常量放入池中,String类的intern()方法就是这样的。

    看看下面代码的执行结果是什么并且比较一下Java 7以前和以后的运行结果是否一致。

    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);
    

    10、Math.round(11.5) 等于多少?Math.round(-11.5)等于多少?
    答:Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加0.5然后进行下取整。

    11、switch 是否能作用在byte 上,是否能作用在long 上,是否能作用在String上?
    答:在Java 5以前,switch(expr)中,expr只能是byte、short、char、int。从Java 5开始,Java中引入了枚举类型,expr也可以是enum类型,从Java 7开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。

    12、用最有效率的方法计算2乘以8?
    答: 2 << 3(左移3位相当于乘以2的3次方,右移3位相当于除以2的3次方)。

    **补充:**我们为编写的类重写hashCode方法时,可能会看到如下所示的代码,其实我们不太理解为什么要使用这样的乘法运算来产生哈希码(散列码),而且为什么这个数是个素数,为什么通常选择31这个数?前两个问题的答案你可以自己百度一下,选择31是因为可以用移位和减法运算来代替乘法,从而得到更好的性能。说到这里你可能已经想到了:31 * num 等价于(num << 5) - num,左移5位相当于乘以2的5次方再减去自身就相当于乘以31,现在的VM都能自动完成这个优化。

    public class PhoneNumber {
    	private int areaCode;
    	private String prefix;
    	private String lineNumber;
    
    	@Override
    	public int hashCode() {
    		final int prime = 31;
    		int result = 1;
    		result = prime * result + areaCode;
    		result = prime * result
    				+ ((lineNumber == null) ? 0 : lineNumber.hashCode());
    		result = prime * result + ((prefix == null) ? 0 : prefix.hashCode());
    		return result;
    	}
    
    	@Override
    	public boolean equals(Object obj) {
    		if (this == obj)
    			return true;
    		if (obj == null)
    			return false;
    		if (getClass() != obj.getClass())
    			return false;
    		PhoneNumber other = (PhoneNumber) obj;
    		if (areaCode != other.areaCode)
    			return false;
    		if (lineNumber == null) {
    			if (other.lineNumber != null)
    				return false;
    		} else if (!lineNumber.equals(other.lineNumber))
    			return false;
    		if (prefix == null) {
    			if (other.prefix != null)
    				return false;
    		} else if (!prefix.equals(other.prefix))
    			return false;
    		return true;
    	}
    
    }
    

    13、数组有没有length()方法?String有没有length()方法?
    答:数组没有length()方法,有length 的属性。String 有length()方法。JavaScript中,获得字符串的长度是通过length属性得到的,这一点容易和Java混淆。

    14、在Java中,如何跳出当前的多重嵌套循环?
    答:在最外层循环前加一个标记如A,然后用break A;可以跳出多重循环。(Java中支持带标签的break和continue语句,作用有点类似于C和C++中的goto语句,但是就像要避免使用goto一样,应该避免使用带标签的break和continue,因为它不会让你的程序变得更优雅,很多时候甚至有相反的作用,所以这种语法其实不知道更好)

    15、构造器(constructor)是否可被重写(override)?
    答:构造器不能被继承,因此不能被重写,但可以被重载。

    16、两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?
    答:不对,如果两个对象x和y满足x.equals(y) == true,它们的哈希码(hash code)应当相同。Java对于eqauls方法和hashCode方法是这样规定的:(1)如果两个对象相同(equals方法返回true),那么它们的hashCode值一定要相同;(2)如果两个对象的hashCode相同,它们并不一定相同。当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现在Set集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)。

    **补充:**关于equals和hashCode方法,很多Java程序都知道,但很多人也就是仅仅知道而已,在Joshua Bloch的大作《Effective Java》(很多软件公司,《Effective Java》、《Java编程思想》以及《重构:改善既有代码质量》是Java程序员必看书籍,如果你还没看过,那就赶紧去亚马逊买一本吧)中是这样介绍equals方法的:首先equals方法必须满足自反性(x.equals(x)必须返回true)、对称性(x.equals(y)返回true时,y.equals(x)也必须返回true)、传递性(x.equals(y)和y.equals(z)都返回true时,x.equals(z)也必须返回true)和一致性(当x和y引用的对象信息没有被修改时,多次调用x.equals(y)应该得到同样的返回值),而且对于任何非null值的引用x,x.equals(null)必须返回false。实现高质量的equals方法的诀窍包括:1. 使用==操作符检查"参数是否为这个对象的引用";2. 使用instanceof操作符检查"参数是否为正确的类型";3. 对于类中的关键属性,检查参数传入对象的属性是否与之相匹配;4. 编写完equals方法后,问自己它是否满足对称性、传递性、一致性;5. 重写equals时总是要重写hashCode;6. 不要将equals方法参数中的Object对象替换为其他的类型,在重写时不要忘掉@Override注解。

    17、是否可以继承String类?
    答:String 类是final类,不可以被继承。

    **补充:**继承String本身就是一个错误的行为,对String类型最好的重用方式是关联关系(Has-A)和依赖关系(Use-A)而不是继承关系(Is-A)。

    18、当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
    答:是值传递。Java语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。C++和C#中可以通过传引用或传输出参数来改变传入的参数的值。在C#中可以编写如下所示的代码,但是在Java中却做不到。

    using System;
    
    namespace CS01 {
    	
    	class Program {
    		public static void swap(ref int x, ref int y) {
    			int temp = x;
    			x = y;
    			y = temp;
    		}
    
    		public static void Main (string[] args) {
    			int a = 5, b = 10;
    			swap (ref a, ref b);
    			// a = 10, b = 5;
    			Console.WriteLine ("a = {0}, b = {1}", a, b);
    		}
    	}
    }
    

    说明:Java中没有传引用实在是非常的不方便,这一点在Java 8中仍然没有得到改进,正是如此在Java编写的代码中才会出现大量的Wrapper类(将需要通过方法调用修改的引用置于一个Wrapper类中,再将Wrapper对象传入方法),这样的做法只会让代码变得臃肿,尤其是让从C和C++转型为Java程序员的开发者无法容忍。

    19、String和StringBuilder、StringBuffer的区别?
    答:Java平台提供了两种类型的字符串:String和StringBuffer/StringBuilder,它们可以储存和操作字符串。其中String是只读字符串,也就意味着String引用的字符串内容是不能被改变的。而StringBuffer/StringBuilder类表示的字符串对象可以直接进行修改。StringBuilder是Java 5中引入的,它和StringBuffer的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方面都没有被synchronized修饰,因此它的效率也比StringBuffer要高。

    面试题1 - 什么情况下用+运算符进行字符串连接比调用StringBuffer/StringBuilder对象的append方法连接字符串性能更好?

    面试题2 - 请说出下面程序的输出。

    class StringEqualTest {
    
    	public static void main(String[] args) {
    		String s1 = "Programming";
    		String s2 = new String("Programming");
    		String s3 = "Program";
    		String s4 = "ming";
    		String s5 = "Program" + "ming";
    		String s6 = s3 + s4;
    		System.out.println(s1 == s2);
    		System.out.println(s1 == s5);
    		System.out.println(s1 == s6);
    		System.out.println(s1 == s6.intern());
    		System.out.println(s2 == s2.intern());
    	}
    }
    

    补充:解答上面的面试题需要清除两点:1. String对象的intern方法会得到字符串对象在常量池中对应的版本的引用(如果常量池中有一个字符串与String对象的equals结果是true),如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用;2. 字符串的+操作其本质是创建了StringBuilder对象进行append操作,然后将拼接后的StringBuilder对象用toString方法处理成String对象,这一点可以用javap -c StringEqualTest.class命令获得class文件对应的JVM字节码指令就可以看出来。

    20、重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
    答:方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求。

    **面试题:**华为的面试题中曾经问过这样一个问题 - "为什么不能根据返回类型来区分重载",快说出你的答案吧!

    21、描述一下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的引用。下面是关于几个类加载器的说明:

    • Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
    • Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
    • System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。

    22、char 型变量中能不能存贮一个中文汉字,为什么?
    答:char类型可以存储一个中文汉字,因为Java中使用的编码是Unicode(不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一个char类型占2个字节(16比特),所以放一个中文是没问题的。

    **补充:**使用Unicode意味着字符在JVM内部和外部有不同的表现形式,在JVM内部都是Unicode,当这个字符被从JVM内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以Java中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如InputStreamReader和OutputStreamReader,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务;对于C程序员来说,要完成这样的编码转换恐怕要依赖于union(联合体/共用体)共享内存的特征来实现了。

    23、抽象类(abstract class)和接口(interface)有什么异同?
    答:抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部都是抽象方法。抽象类中的成员可以是private、默认、protected、public的,而接口中的成员全都是public的。抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法。

    24、静态嵌套类(Static Nested Class)和内部类(Inner Class)的不同?
    答:Static Nested Class是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化。而通常的内部类需要在外部类实例化后才能实例化,其语法看起来挺诡异的,如下所示。

    /**
     * 扑克类(一副扑克)
     * @author 骆昊
     *
     */
    public class Poker {
    	private static String[] suites = {"黑桃", "红桃", "草花", "方块"};
    	private static int[] faces = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
    	
    	private Card[] cards;
    	
    	/**
    	 * 构造器
    	 * 
    	 */
    	public Poker() {
    		cards = new Card[52];
    		for(int i = 0; i < suites.length; i++) {
    			for(int j = 0; j < faces.length; j++) {
    				cards[i * 13 + j] = new Card(suites[i], faces[j]);
    			}
    		}
    	}
    	
    	/**
    	 * 洗牌 (随机乱序)
    	 * 
    	 */
    	public void shuffle() {
    		for(int i = 0, len = cards.length; i < len; i++) {
    			int index = (int) (Math.random() * len);
    			Card temp = cards[index];
    			cards[index] = cards[i];
    			cards[i] = temp;
    		}
    	}
    	
    	/**
    	 * 发牌
    	 * @param index 发牌的位置
    	 * 
    	 */
    	public Card deal(int index) {
    		return cards[index];
    	}
    	
    	/**
    	 * 卡片类(一张扑克)
    	 * [内部类]
    	 * @author 骆昊
    	 *
    	 */
    	public class Card {
    		private String suite;	// 花色
    		private int face;		// 点数
    
    		public Card(String suite, int face) {
    			this.suite = suite;
    			this.face = face;
    		}
    		
    		@Override
    		public String toString() {
    			String faceStr = "";
    			switch(face) {
    			case 1: faceStr = "A"; break;
    			case 11: faceStr = "J"; break;
    			case 12: faceStr = "Q"; break;
    			case 13: faceStr = "K"; break;
    			default: faceStr = String.valueOf(face);
    			}
    			return suite + faceStr;
    		}
    	}
    }
    

    测试代码:

    class PokerTest {
    
    	public static void main(String[] args) {
    		Poker poker = new Poker();
    		poker.shuffle();				// 洗牌
    		Poker.Card c1 = poker.deal(0);	// 发第一张牌
    		// 对于非静态内部类Card
    		// 只有通过其外部类Poker对象才能创建Card对象
    		Poker.Card c2 = poker.new Card("红心", 1);	// 自己创建一张牌
    		
    		System.out.println(c1);		// 洗牌后的第一张
    		System.out.println(c2);		// 打印: 红心A
    	}
    }
    

    面试题 - 下面的代码哪些地方会产生编译错误?

    class Outer {
    	
    	class Inner {}
    	
    	public static void foo() { new Inner(); }
    	
    	public void bar() { new Inner(); }
    	
    	public static void main(String[] args) {
    		new Inner();
    	}
    }
    

    注意:Java中非静态内部类对象的创建要依赖其外部类对象,上面的面试题中foo和main方法都是静态方法,静态方法中没有this,也就是说没有所谓的外部类对象,因此无法创建内部类对象,如果要在静态方法中创建内部类对象,可以这样做:

    	new Outer().new Inner();
    

    25、Java 中会存在内存泄漏吗,请简单描述。
    答:理论上Java因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是Java被广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被GC回收,因此也会导致内存泄露的发生。例如Hibernate的Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露。下面例子中的代码也会导致内存泄露。

    import java.util.Arrays;
    import java.util.EmptyStackException;
    
    public class MyStack<T> {
    	private T[] elements;
    	private int size = 0;
    	
    	private static final int INIT_CAPACITY = 16;
    	
    	public MyStack() {
    		elements = (T[]) new Object[INIT_CAPACITY];
    	}
    	
    	public void push(T elem) {
    		ensureCapacity();
    		elements[size++] = elem;
    	}
    	
    	public T pop() {
    		if(size == 0) 
    			throw new EmptyStackException();
    		return elements[--size];
    	}
    	
    	private void ensureCapacity() {
    		if(elements.length == size) {
    			elements = Arrays.copyOf(elements, 2 * size + 1);
    		}
    	}
    }
    

    上面的代码实现了一个栈(先进后出(FILO))结构,乍看之下似乎没有什么明显的问题,它甚至可以通过你编写的各种单元测试。然而其中的pop方法却存在内存泄露的问题,当我们用pop方法弹出栈中的对象时,该对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,因为栈内部维护着对这些对象的过期引用(obsolete reference)。在支持垃圾回收的语言中,内存泄露是很隐蔽的,这种内存泄露其实就是无意识的对象保持。如果一个对象引用被无意识的保留起来了,那么垃圾回收器不会处理这个对象,也不会处理该对象引用的其他对象,即使这样的对象只有少数几个,也可能会导致很多的对象被排除在垃圾回收之外,从而对性能造成重大影响,极端情况下会引发Disk Paging(物理内存与硬盘的虚拟内存交换数据),甚至造成OutOfMemoryError。

    26、抽象的(abstract)方法是否可同时是静态的(static),是否可同时是本地方法(native),是否可同时被synchronized修饰?
    答:都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。本地方法是由本地代码(如C代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。synchronized和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。

    27、阐述静态变量和实例变量的区别。
    答:静态变量是被static修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝;实例变量必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。

    **补充:**在Java开发中,上下文类和工具类中通常会有大量的静态成员。

    28、是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用?
    答:不可以,静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,在调用静态方法时可能对象并没有被初始化。

    29、如何实现对象克隆?
    答:有两种方式:
      1). 实现Cloneable接口并重写Object类中的clone()方法;
      2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆,代码如下。

    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.io.Serializable;
    
    public class MyUtil {
    
    	private MyUtil() {
    		throw new AssertionError();
    	}
    
    	@SuppressWarnings("unchecked")
    	public static <T extends Serializable> T clone(T obj) throws Exception {
    		ByteArrayOutputStream bout = new ByteArrayOutputStream();
    		ObjectOutputStream oos = new ObjectOutputStream(bout);
    		oos.writeObject(obj);
    
    		ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
    		ObjectInputStream ois = new ObjectInputStream(bin);
    		return (T) ois.readObject();
    		
    		// 说明:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义
    		// 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放
    	}
    }
    

    下面是测试代码:

    import java.io.Serializable;
    
    /**
     * 人类
     * @author 骆昊
     *
     */
    class Person implements Serializable {
    	private static final long serialVersionUID = -9102017020286042305L;
    
    	private String name;	// 姓名
    	private int age;		// 年龄
    	private Car car;		// 座驾
    
    	public Person(String name, int age, Car car) {
    		this.name = name;
    		this.age = age;
    		this.car = car;
    	}
    
    	public String getName() {
    		return name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
    
    	public int getAge() {
    		return age;
    	}
    
    	public void setAge(int age) {
    		this.age = age;
    	}
    
    	public Car getCar() {
    		return car;
    	}
    
    	public void setCar(Car car) {
    		this.car = car;
    	}
    
    	@Override
    	public String toString() {
    		return "Person [name=" + name + ", age=" + age + ", car=" + car + "]";
    	}
    
    }
    
    /**
     * 小汽车类
     * @author 骆昊
     *
     */
    class Car implements Serializable {
    	private static final long serialVersionUID = -5713945027627603702L;
    
    	private String brand;		// 品牌
    	private int maxSpeed;		// 最高时速
    
    	public Car(String brand, int maxSpeed) {
    		this.brand = brand;
    		this.maxSpeed = maxSpeed;
    	}
    
    	public String getBrand() {
    		return brand;
    	}
    
    	public void setBrand(String brand) {
    		this.brand = brand;
    	}
    
    	public int getMaxSpeed() {
    		return maxSpeed;
    	}
    
    	public void setMaxSpeed(int maxSpeed) {
    		this.maxSpeed = maxSpeed;
    	}
    
    	@Override
    	public String toString() {
    		return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed + "]";
    	}
    
    }
    
    class CloneTest {
    
    	public static void main(String[] args) {
    		try {
    			Person p1 = new Person("Hao LUO", 33, new Car("Benz", 300));
    			Person p2 = MyUtil.clone(p1);	// 深度克隆
    			p2.getCar().setBrand("BYD");
    			// 修改克隆的Person对象p2关联的汽车对象的品牌属性
    			// 原来的Person对象p1关联的汽车不会受到任何影响
    			// 因为在克隆Person对象时其关联的汽车对象也被克隆了
    			System.out.println(p1);
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    

    **注意:**基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object类的clone方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时。

    30、GC是什么?为什么要有GC?
    答:GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉显示的垃圾回收调用。
    垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。在Java诞生初期,垃圾回收是Java最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今Java的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常觉得iOS的系统比Android系统有更好的用户体验,其中一个深层次的原因就在于Android系统中垃圾回收的不可预知性。

    **补充:**垃圾回收机制有很多种,包括:分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。标准的Java进程既有栈又有堆。栈保存了原始型局部变量,堆保存了要创建的对象。Java平台对堆内存回收和再利用的基本算法被称为标记和清除,但是Java对其进行了改进,采用“分代式垃圾收集”。这种方法会跟Java对象的生命周期将堆内存划分为不同的区域,在垃圾收集过程中,可能会将对象移动到不同区域:

    • 伊甸园(Eden):这是对象最初诞生的区域,并且对大多数对象来说,这里是它们唯一存在过的区域。
    • 幸存者乐园(Survivor):从伊甸园幸存下来的对象会被挪到这里。
    • 终身颐养园(Tenured):这是足够老的幸存对象的归宿。年轻代收集(Minor-GC)过程是不会触及这个地方的。当年轻代收集不能把对象放进终身颐养园时,就会触发一次完全收集(Major-GC),这里可能还会牵扯到压缩,以便为大对象腾出足够的空间。

    与垃圾回收相关的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:设置幸存区的目标使用率

    31、String s = new String("xyz");创建了几个字符串对象?
    答:两个对象,一个是静态区的"xyz",一个是用new创建在堆上的对象。

    32、接口是否可继承(extends)接口?抽象类是否可实现(implements)接口?抽象类是否可继承具体类(concrete class)?
    答:接口可以继承接口,而且支持多重继承。抽象类可以实现(implements)接口,抽象类可继承具体类也可以继承抽象类。

    33、一个".java"源文件中是否可以包含多个类(不是内部类)?有什么限制?
    答:可以,但一个源文件中最多只能有一个公开类(public class)而且文件名必须和公开类的类名完全保持一致。

    34、Anonymous Inner Class(匿名内部类)是否可以继承其它类?是否可以实现接口?
    答:可以继承其他类或实现其他接口,在Swing编程和Android开发中常用此方式来实现事件监听和回调。

    35、内部类可以引用它的包含类(外部类)的成员吗?有没有什么限制?
    答:一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员。

    36、Java 中的final关键字有哪些用法?
    答:(1)修饰类:表示该类不能被继承;(2)修饰方法:表示方法不能被重写;(3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。

    37、指出下面程序的运行结果。

    class A {
    
        static {
            System.out.print("1");
        }
    
        public A() {
            System.out.print("2");
        }
    }
    
    class B extends A{
    
        static {
            System.out.print("a");
        }
    
        public B() {
            System.out.print("b");
        }
    }
    
    public class Hello {
    
        public static void main(String[] args) {
            A ab = new B();
            ab = new B();
        }
    
    }
    

    答:执行结果:1a2b2b。创建对象时构造器的调用顺序是:先初始化静态成员,然后调用父类构造器,再初始化非静态成员,最后调用自身构造器。

    **提示:**如果不能给出此题的正确答案,说明之前第21题Java类加载机制还没有完全理解,赶紧再看看吧。

    38、数据类型之间的转换:
    - 如何将字符串转换为基本数据类型?
    - 如何将基本数据类型转换为字符串?
    答:

    • 调用基本数据类型对应的包装类中的方法parseXXX(String)或valueOf(String)即可返回相应基本类型;
    • 一种方法是将基本数据类型与空字符串("")连接(+)即可获得其所对应的字符串;另一种方法是调用String 类中的valueOf()方法返回相应字符串

    39、如何实现字符串的反转及替换?
    答:方法很多,可以自己写实现也可以使用String或StringBuffer/StringBuilder中的方法。有一道很常见的面试题是用递归实现字符串反转,代码如下所示:

    	public static String reverse(String originStr) {
    		if(originStr == null || originStr.length() <= 1) 
    			return originStr;
    		return reverse(originStr.substring(1)) + originStr.charAt(0);
    	}
    

    40、怎样将GB2312编码的字符串转换为ISO-8859-1编码的字符串?
    答:代码如下所示:

    String s1 = "你好";
    String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1");
    

    41、日期和时间:
    - 如何取得年月日、小时分钟秒?
    - 如何取得从1970年1月1日0时0分0秒到现在的毫秒数?
    - 如何取得某月的最后一天?
    - 如何格式化日期?
    答:
    问题1:创建java.util.Calendar 实例,调用其get()方法传入不同的参数即可获得参数所对应的值。Java 8中可以使用java.time.LocalDateTimel来获取,代码如下所示。

    public class DateTimeTest {
    	public static void main(String[] args) {
    		Calendar cal = Calendar.getInstance();
    		System.out.println(cal.get(Calendar.YEAR));
    		System.out.println(cal.get(Calendar.MONTH));	// 0 - 11
    		System.out.println(cal.get(Calendar.DATE));
    		System.out.println(cal.get(Calendar.HOUR_OF_DAY));
    		System.out.println(cal.get(Calendar.MINUTE));
    		System.out.println(cal.get(Calendar.SECOND));
    		
    		// Java 8
    		LocalDateTime dt = LocalDateTime.now();
    		System.out.println(dt.getYear());
    		System.out.println(dt.getMonthValue());		// 1 - 12
    		System.out.println(dt.getDayOfMonth());
    		System.out.println(dt.getHour());
    		System.out.println(dt.getMinute());
    		System.out.println(dt.getSecond());
    	}
    }
    

    问题2:以下方法均可获得该毫秒数。

    Calendar.getInstance().getTimeInMillis();
    System.currentTimeMillis();
    Clock.systemDefaultZone().millis();	// Java 8
    

    问题3:代码如下所示。

    Calendar time = Calendar.getInstance();
    time.getActualMaximum(Calendar.DAY_OF_MONTH);
    

    问题4:利用java.text.DataFormat 的子类(如SimpleDateFormat类)中的format(Date)方法可将日期格式化。Java 8中可以用java.time.format.DateTimeFormatter来格式化时间日期,代码如下所示。

    import java.text.SimpleDateFormat;
    import java.time.LocalDate;
    import java.time.format.DateTimeFormatter;
    import java.util.Date;
    
    class DateFormatTest {
    
    	public static void main(String[] args) {
    		SimpleDateFormat oldFormatter = new SimpleDateFormat("yyyy/MM/dd");
    		Date date1 = new Date();
    		System.out.println(oldFormatter.format(date1));
    		
    		// Java 8
    		DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
    		LocalDate date2 = LocalDate.now();
    		System.out.println(date2.format(newFormatter));
    	}
    }
    

    补充:Java的时间日期API一直以来都是被诟病的东西,为了解决这一问题,Java 8中引入了新的时间日期API,其中包括LocalDate、LocalTime、LocalDateTime、Clock、Instant等类,这些的类的设计都使用了不变模式,因此是线程安全的设计。如果不理解这些内容,可以参考我的另一篇文章《关于Java并发编程的总结和思考》

    42、打印昨天的当前时刻。
    答:

    import java.util.Calendar;
    
    class YesterdayCurrent {
        public static void main(String[] args){
            Calendar cal = Calendar.getInstance();
            cal.add(Calendar.DATE, -1);
            System.out.println(cal.getTime());
        }
    }
    

    在Java 8中,可以用下面的代码实现相同的功能。

    import java.time.LocalDateTime;
    
    class YesterdayCurrent {
    
    	public static void main(String[] args) {
    		LocalDateTime today = LocalDateTime.now();
    		LocalDateTime yesterday = today.minusDays(1);
    		
    		System.out.println(yesterday);
    	}
    }
    

    43、比较一下Java和JavaSciprt。
    答:JavaScript 与Java是两个公司开发的不同的两个产品。Java 是原Sun Microsystems公司推出的面向对象的程序设计语言,特别适合于互联网应用程序开发;而JavaScript是Netscape公司的产品,为了扩展Netscape浏览器的功能而开发的一种可以嵌入Web页面中运行的基于对象和事件驱动的解释性语言。JavaScript的前身是LiveScript;而Java的前身是Oak语言。
    下面对两种语言间的异同作如下比较:

    • 基于对象和面向对象:Java是一种真正的面向对象的语言,即使是开发简单的程序,必须设计对象;JavaScript是种脚本语言,它可以用来制作与网络无关的,与用户交互作用的复杂软件。它是一种基于对象(Object-Based)和事件驱动(Event-Driven)的编程语言,因而它本身提供了非常丰富的内部对象供设计人员使用。
    • 解释和编译:Java的源代码在执行之前,必须经过编译。JavaScript是一种解释性编程语言,其源代码不需经过编译,由浏览器解释执行。(目前的浏览器几乎都使用了JIT(即时编译)技术来提升JavaScript的运行效率)
    • 强类型变量和类型弱变量:Java采用强类型变量检查,即所有变量在编译之前必须作声明;JavaScript中变量是弱类型的,甚至在使用变量前可以不作声明,JavaScript的解释器在运行时检查推断其数据类型。
    • 代码格式不一样。

    **补充:**上面列出的四点是网上流传的所谓的标准答案。其实Java和JavaScript最重要的区别是一个是静态语言,一个是动态语言。目前的编程语言的发展趋势是函数式语言和动态语言。在Java中类(class)是一等公民,而JavaScript中函数(function)是一等公民,因此JavaScript支持函数式编程,可以使用Lambda函数和闭包(closure),当然Java 8也开始支持函数式编程,提供了对Lambda表达式以及函数式接口的支持。对于这类问题,在面试的时候最好还是用自己的语言回答会更加靠谱,不要背网上所谓的标准答案。

    44、什么时候用断言(assert)?
    答:断言在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。一般来说,断言用于保证程序最基本、关键的正确性。断言检查通常在开发和测试时开启。为了保证程序的执行效率,在软件发布后断言检查通常是关闭的。断言是一个包含布尔表达式的语句,在执行这个语句时假定该表达式为true;如果表达式的值为false,那么系统会报告一个AssertionError。断言的使用如下面的代码所示:

    assert(a > 0); // throws an AssertionError if a <= 0
    

    断言可以有两种形式:
    assert Expression1;
    assert Expression1 : Expression2 ;
    Expression1 应该总是产生一个布尔值。
    Expression2 可以是得出一个值的任意表达式;这个值用于生成显示更多调试信息的字符串消息。

    要在运行时启用断言,可以在启动JVM时使用-enableassertions或者-ea标记。要在运行时选择禁用断言,可以在启动JVM时使用-da或者-disableassertions标记。要在系统类中启用或禁用断言,可使用-esa或-dsa标记。还可以在包的基础上启用或者禁用断言。

    **注意:**断言不应该以任何方式改变程序的状态。简单的说,如果希望在不满足某些条件时阻止代码的执行,就可以考虑用断言来阻止它。

    45、Error和Exception有什么区别?
    答:Error表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题;比如内存溢出,不可能指望程序能处理这样的情况;Exception表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,从不会发生的情况。

    面试题:2005年摩托罗拉的面试中曾经问过这么一个问题“If a process reports a stack overflow run-time error, what’s the most possible cause?”,给了四个选项a. lack of memory; b. write on an invalid memory space; c. recursive function calling; d. array index out of boundary. Java程序在运行时也可能会遭遇StackOverflowError,这是一个无法恢复的错误,只能重新修改代码了,这个面试题的答案是c。如果写了不能迅速收敛的递归,则很有可能引发栈溢出的错误,如下所示:

    class StackOverflowErrorTest {
    
    	public static void main(String[] args) {
    		main(null);
    	}
    }
    

    **提示:**用递归编写程序时一定要牢记两点:1. 递归公式;2. 收敛条件(什么时候就不再继续递归)。

    46、try{}里有一个return语句,那么紧跟在这个try后的finally{}里的代码会不会被执行,什么时候被执行,在return前还是后?
    答:会执行,在方法返回调用者前执行。

    **注意:**在finally中改变返回值的做法是不好的,因为如果存在finally代码块,try中的return语句不会立马返回调用者,而是记录下返回值待finally代码块执行完毕之后再向调用者返回其值,然后如果在finally中修改了返回值,就会返回修改后的值。显然,在finally中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java中也可以通过提升编译器的语法检查级别来产生警告或错误,Eclipse中可以在如图所示的地方进行设置,强烈建议将此项设置为编译错误。

    这里写图片描述

    47、Java语言如何进行异常处理,关键字:throws、throw、try、catch、finally分别如何使用?
    答:Java通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的接口。在Java中,每个异常都是一个对象,它是Throwable类或其子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。Java的异常处理是通过5个关键词来实现的:try、catch、throw、throws和finally。一般情况下是用try来执行一段程序,如果系统会抛出(throw)一个异常对象,可以通过它的类型来捕获(catch)它,或通过总是执行代码块(finally)来处理;try用来指定一块预防所有异常的程序;catch子句紧跟在try块后面,用来指定你想要捕获的异常的类型;throw语句用来明确地抛出一个异常;throws用来声明一个方法可能抛出的各种异常(当然声明异常时允许无病呻吟);finally为确保一段代码不管发生什么异常状况都要被执行;try语句可以嵌套,每当遇到一个try语句,异常的结构就会被放入异常栈中,直到所有的try语句都完成。如果下一级的try语句没有对某种异常进行处理,异常栈就会执行出栈操作,直到遇到有处理这种异常的try语句或者最终将异常抛给JVM。

    48、运行时异常与受检异常有何异同?
    答:异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发。Java编译器要求方法必须声明抛出可能发生的受检异常,但是并不要求必须声明抛出未被捕获的运行时异常。异常和继承一样,是面向对象程序设计中经常被滥用的东西,在Effective Java中对异常的使用给出了以下指导原则:

    • 不要将异常处理用于正常的控制流(设计良好的API不应该强迫它的调用者为了正常的控制流而使用异常)
    • 对可以恢复的情况使用受检异常,对编程错误使用运行时异常
    • 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生)
    • 优先使用标准的异常
    • 每个方法抛出的异常都要有文档
    • 保持异常的原子性
    • 不要在catch中忽略掉捕获到的异常

    49、列出一些你常见的运行时异常?
    答:

    • ArithmeticException(算术异常)
    • ClassCastException (类转换异常)
    • IllegalArgumentException (非法参数异常)
    • IndexOutOfBoundsException (下标越界异常)
    • NullPointerException (空指针异常)
    • SecurityException (安全异常)

    50、阐述final、finally、finalize的区别。
    答:

    • final:修饰符(关键字)有三种用法:如果一个类被声明为final,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。将变量声明为final,可以保证它们在使用中不被改变,被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为final的方法也同样只能使用,不能在子类中被重写。
    • finally:通常放在try…catch…的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。
    • finalize:Object类中定义的方法,Java中允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize()方法可以整理系统资源或者执行其他清理工作。

    51、类ExampleA继承Exception,类ExampleB继承ExampleA。
    有如下代码片断:

    try {
        throw new ExampleB("b")
    } catch(ExampleA e){
        System.out.println("ExampleA");
    } catch(Exception e){
        System.out.println("Exception");
    }
    

    请问执行此段代码的输出是什么?
    答:输出:ExampleA。(根据里氏代换原则[能使用父类型的地方一定能使用子类型],抓取ExampleA类型异常的catch块能够抓住try块中抛出的ExampleB类型的异常)

    面试题 - 说出下面代码的运行结果。(此题的出处是《Java编程思想》一书)

    class Annoyance extends Exception {}
    class Sneeze extends Annoyance {}
    
    class Human {
    
    	public static void main(String[] args) 
    		throws Exception {
    		try {
    			try {
    				throw new Sneeze();
    			} 
    			catch ( Annoyance a ) {
    				System.out.println("Caught Annoyance");
    				throw a;
    			}
    		} 
    		catch ( Sneeze s ) {
    			System.out.println("Caught Sneeze");
    			return ;
    		}
    		finally {
    			System.out.println("Hello World!");
    		}
    	}
    }
    

    52、List、Set、Map是否继承自Collection接口?
    答:List、Set 是,Map 不是。Map是键值对映射容器,与List和Set有明显的区别,而Set存储的零散的元素且不允许有重复元素(数学中的集合也是如此),List是线性结构的容器,适用于按数值索引访问元素的情形。

    53、阐述ArrayList、Vector、LinkedList的存储性能和特性。
    答:ArrayList 和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector中的方法由于添加了synchronized修饰,因此Vector是线程安全的容器,但性能上较ArrayList差,因此已经是Java中的遗留容器。LinkedList使用双向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更高),按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。Vector属于遗留容器(Java早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties都是遗留容器),已经不推荐使用,但是由于ArrayList和LinkedListed都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类Collections中的synchronizedList方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强实现)。

    **补充:**遗留容器中的Properties类和Stack类在设计上有严重的问题,Properties是一个键和值都是字符串的特殊的键值对映射,在设计上应该是关联一个Hashtable并将其两个泛型参数设置为String类型,但是Java API中的Properties直接继承了Hashtable,这很明显是对继承的滥用。这里复用代码的方式应该是Has-A关系而不是Is-A关系,另一方面容器都属于工具类,继承工具类本身就是一个错误的做法,使用工具类最好的方式是Has-A关系(关联)或Use-A关系(依赖)。同理,Stack类继承Vector也是不正确的。Sun公司的工程师们也会犯这种低级错误,让人唏嘘不已。

    54、Collection和Collections的区别?
    答:Collection是一个接口,它是Set、List等容器的父接口;Collections是个一个工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、排序、线程安全化等等。

    55、List、Map、Set三个接口存取元素时,各有什么特点?
    答:List以特定索引来存取元素,可以有重复元素。Set不能存放重复元素(用对象的equals()方法来区分元素是否重复)。Map保存键值对(key-value pair)映射,映射关系可以是一对一或多对一。Set和Map容器都有基于哈希存储和排序树的两种实现版本,基于哈希存储的版本理论存取时间复杂度为O(1),而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去重的效果。

    56、TreeMap和TreeSet在排序时如何比较元素?Collections工具类中的sort()方法如何比较元素?
    答:TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序。Collections工具类的sort方法有两种重载的形式,第一种要求传入的待排序容器中存放的对象比较实现Comparable接口以实现元素的比较;第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要重写compare方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java中对函数式编程的支持)。
    例子1:

    
    public class Student implements Comparable<Student> {
    	private String name;		// 姓名
    	private int age;			// 年龄
    
    	public Student(String name, int age) {
    		this.name = name;
    		this.age = age;
    	}
    
    	@Override
    	public String toString() {
    		return "Student [name=" + name + ", age=" + age + "]";
    	}
    
    	@Override
    	public int compareTo(Student o) {
    		return this.age - o.age; // 比较年龄(年龄的升序)
    	}
    
    }
    
    import java.util.Set;
    import java.util.TreeSet;
    
    class Test01 {
    
    	public static void main(String[] args) {
    		Set<Student> set = new TreeSet<>();		// Java 7的钻石语法(构造器后面的尖括号中不需要写类型)
    		set.add(new Student("Hao LUO", 33));
    		set.add(new Student("XJ WANG", 32));
    		set.add(new Student("Bruce LEE", 60));
    		set.add(new Student("Bob YANG", 22));
    		
    		for(Student stu : set) {
    			System.out.println(stu);
    		}
    //		输出结果: 
    //		Student [name=Bob YANG, age=22]
    //		Student [name=XJ WANG, age=32]
    //		Student [name=Hao LUO, age=33]
    //		Student [name=Bruce LEE, age=60]
    	}
    }
    

    例子2:

    public class Student {
    	private String name; 	// 姓名
    	private int age; 		// 年龄
    
    	public Student(String name, int age) {
    		this.name = name;
    		this.age = age;
    	}
    
    	/**
    	 * 获取学生姓名
    	 */
    	public String getName() {
    		return name;
    	}
    
    	/**
    	 * 获取学生年龄
    	 */
    	public int getAge() {
    		return age;
    	}
    
    	@Override
    	public String toString() {
    		return "Student [name=" + name + ", age=" + age + "]";
    	}
    
    }
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.List;
    
    class Test02 {
    
    	public static void main(String[] args) {
    		List<Student> list = new ArrayList<>();		// Java 7的钻石语法(构造器后面的尖括号中不需要写类型)
    		list.add(new Student("Hao LUO", 33));
    		list.add(new Student("XJ WANG", 32));
    		list.add(new Student("Bruce LEE", 60));
    		list.add(new Student("Bob YANG", 22));
    		
    		// 通过sort方法的第二个参数传入一个Comparator接口对象
    		// 相当于是传入一个比较对象大小的算法到sort方法中
    		// 由于Java中没有函数指针、仿函数、委托这样的概念
    		// 因此要将一个算法传入一个方法中唯一的选择就是通过接口回调
    		Collections.sort(list, new Comparator<Student> () {
    
    			@Override
    			public int compare(Student o1, Student o2) {
    				return o1.getName().compareTo(o2.getName());	// 比较学生姓名
    			}
    		});
    		
    		for(Student stu : list) {
    			System.out.println(stu);
    		}
    //		输出结果: 
    //		Student [name=Bob YANG, age=22]
    //		Student [name=Bruce LEE, age=60]
    //		Student [name=Hao LUO, age=33]
    //		Student [name=XJ WANG, age=32]
    	}
    }
    

    57、Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别?
    答:sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态,请参考第66题中的线程状态转换图)。wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。

    **补充:**可能不少人对什么是进程,什么是线程还比较模糊,对于为什么需要多线程编程也不是特别理解。简单的说:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位;线程是进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的能独立运行的基本单位。线程的划分尺度小于进程,这使得多线程程序的并发性高;进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存。使用多线程的编程通常能够带来更好的性能和用户体验,但是多线程的程序对于其他程序是不友好的,因为它可能占用了更多的CPU资源。当然,也不是线程越多,程序的性能就越好,因为线程之间的调度和切换也会浪费CPU时间。时下很时髦的Node.js就采用了单线程异步I/O的工作模式。

    58、线程的sleep()方法和yield()方法有什么区别?
    答:
    ① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
    ② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
    ③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
    ④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。

    59、当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?
    答:不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A方法说明对象锁已经被取走,那么试图进入B方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。

    60、请说出与线程同步以及线程调度相关的方法。
    答:

    • wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
    • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
    • notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
    • notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

    **提示:**关于Java多线程和并发编程的问题,建议大家看我的另一篇文章《关于Java并发编程的总结和思考》

    补充:Java 5通过Lock接口提供了显式的锁机制(explicit lock),增强了灵活性以及对线程的协调。Lock接口中定义了加锁(lock())和解锁(unlock())的方法,同时还提供了newCondition()方法来产生用于线程之间通信的Condition对象;此外,Java 5还提供了信号量机制(semaphore),信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信号量的许可(调用Semaphore对象的acquire()方法);在完成对资源的访问后,线程必须向信号量归还许可(调用Semaphore对象的release()方法)。

    下面的例子演示了100个线程同时向一个银行账户中存入1元钱,在没有使用同步机制和使用同步机制情况下的执行情况。

    • 银行账户类:
    /**
     * 银行账户
     * @author 骆昊
     *
     */
    public class Account {
    	private double balance;		// 账户余额
    	
    	/**
    	 * 存款
    	 * @param money 存入金额
    	 */
    	public void deposit(double money) {
    		double newBalance = balance + money;
    		try {
    			Thread.sleep(10);	// 模拟此业务需要一段处理时间
    		}
    		catch(InterruptedException ex) {
    			ex.printStackTrace();
    		}
    		balance = newBalance;
    	}
    	
    	/**
    	 * 获得账户余额
    	 */
    	public double getBalance() {
    		return balance;
    	}
    }
    
    • 存钱线程类:
    /**
     * 存钱线程
     * @author 骆昊
     *
     */
    public class AddMoneyThread implements Runnable {
    	private Account account;	// 存入账户
    	private double money;		// 存入金额
    
    	public AddMoneyThread(Account account, double money) {
    		this.account = account;
    		this.money = money;
    	}
    
    	@Override
    	public void run() {
    		account.deposit(money);
    	}
    
    }
    
    • 测试类:
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Test01 {
    
    	public static void main(String[] args) {
    		Account account = new Account();
    		ExecutorService service = Executors.newFixedThreadPool(100);
    		
    		for(int i = 1; i <= 100; i++) {
    			service.execute(new AddMoneyThread(account, 1));
    		}
    		
    		service.shutdown();
    		
    		while(!service.isTerminated()) {}
    		
    		System.out.println("账户余额: " + account.getBalance());
    	}
    }
    

    在没有同步的情况下,执行结果通常是显示账户余额在10元以下,出现这种状况的原因是,当一个线程A试图存入1元的时候,另外一个线程B也能够进入存款的方法中,线程B读取到的账户余额仍然是线程A存入1元钱之前的账户余额,因此也是在原来的余额0上面做了加1元的操作,同理线程C也会做类似的事情,所以最后100个线程执行结束时,本来期望账户余额为100元,但实际得到的通常在10元以下(很可能是1元哦)。解决这个问题的办法就是同步,当一个线程对银行账户存钱时,需要将此账户锁定,待其操作完成后才允许其他的线程进行操作,代码有如下几种调整方案:

    • 在银行账户的存款(deposit)方法上同步(synchronized)关键字
    /**
     * 银行账户
     * @author 骆昊
     *
     */
    public class Account {
    	private double balance;		// 账户余额
    	
    	/**
    	 * 存款
    	 * @param money 存入金额
    	 */
    	public synchronized void deposit(double money) {
    		double newBalance = balance + money;
    		try {
    			Thread.sleep(10);	// 模拟此业务需要一段处理时间
    		}
    		catch(InterruptedException ex) {
    			ex.printStackTrace();
    		}
    		balance = newBalance;
    	}
    	
    	/**
    	 * 获得账户余额
    	 */
    	public double getBalance() {
    		return balance;
    	}
    }
    
    • 在线程调用存款方法时对银行账户进行同步
    /**
     * 存钱线程
     * @author 骆昊
     *
     */
    public class AddMoneyThread implements Runnable {
    	private Account account;	// 存入账户
    	private double money;		// 存入金额
    
    	public AddMoneyThread(Account account, double money) {
    		this.account = account;
    		this.money = money;
    	}
    
    	@Override
    	public void run() {
    		synchronized (account) {
    			account.deposit(money);	
    		}
    	}
    
    }
    
    • 通过Java 5显示的锁机制,为每个银行账户创建一个锁对象,在存款操作进行加锁和解锁的操作
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * 银行账户
     * 
     * @author 骆昊
     *
     */
    public class Account {
    	private Lock accountLock = new ReentrantLock();
    	private double balance; // 账户余额
    
    	/**
    	 * 存款
    	 * 
    	 * @param money
    	 *            存入金额
    	 */
    	public void deposit(double money) {
    		accountLock.lock();
    		try {
    			double newBalance = balance + money;
    			try {
    				Thread.sleep(10); // 模拟此业务需要一段处理时间
    			}
    			catch (InterruptedException ex) {
    				ex.printStackTrace();
    			}
    			balance = newBalance;
    		}
    		finally {
    			accountLock.unlock();
    		}
    	}
    
    	/**
    	 * 获得账户余额
    	 */
    	public double getBalance() {
    		return balance;
    	}
    }
    

    按照上述三种方式对代码进行修改后,重写执行测试代码Test01,将看到最终的账户余额为100元。当然也可以使用Semaphore或CountdownLatch来实现同步。

    61、编写多线程程序有几种实现方式?
    答:Java 5以前实现多线程有两种实现方法:一种是继承Thread类;另一种是实现Runnable接口。两种方式都要通过重写run()方法来定义线程的行为,推荐使用后者,因为Java中的继承是单继承,一个类有一个父类,如果继承了Thread类就无法再继承其他类了,显然使用Runnable接口更为灵活。

    补充:Java 5以后创建线程还有第三种方式:实现Callable接口,该接口中的call方法可以在线程执行结束时产生一个返回值,代码如下所示:

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future;
    
    
    class MyTask implements Callable<Integer> {
    	private int upperBounds;
    	
    	public MyTask(int upperBounds) {
    		this.upperBounds = upperBounds;
    	}
    	
    	@Override
    	public Integer call() throws Exception {
    		int sum = 0; 
    		for(int i = 1; i <= upperBounds; i++) {
    			sum += i;
    		}
    		return sum;
    	}
    	
    }
    
    class Test {
    
    	public static void main(String[] args) throws Exception {
    		List<Future<Integer>> list = new ArrayList<>();
    		ExecutorService service = Executors.newFixedThreadPool(10);
    		for(int i = 0; i < 10; i++) {
    			list.add(service.submit(new MyTask((int) (Math.random() * 100))));
    		}
    		
    		int sum = 0;
    		for(Future<Integer> future : list) {
    			// while(!future.isDone()) ;
    			sum += future.get();
    		}
    		
    		System.out.println(sum);
    	}
    }
    

    62、synchronized关键字的用法?
    答:synchronized关键字可以将对象或者方法标记为同步,以实现对对象和方法的互斥访问,可以用synchronized(对象) { … }定义同步代码块,或者在声明方法时将synchronized作为方法的修饰符。在第60题的例子中已经展示了synchronized关键字的用法。

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

    64、启动一个线程是调用run()还是start()方法?
    答:启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调(callback)的方法。

    65、什么是线程池(thread pool)?
    答:在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是"池化资源"技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
    Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:

    • newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
    • newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
    • newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
    • newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
    • newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

    第60题的例子中演示了通过Executors工具类创建线程池并使用线程池执行线程的代码。如果希望在服务器上使用线程池,强烈建议使用newFixedThreadPool方法来创建线程池,这样能获得更好的性能。

    66、线程的基本状态以及状态之间的关系?
    答:
    这里写图片描述

    **说明:**其中Running表示运行状态,Runnable表示就绪状态(万事俱备,只欠CPU),Blocked表示阻塞状态,阻塞状态又有多种情况,可能是因为调用wait()方法进入等待池,也可能是执行同步方法或同步代码块进入等锁池,或者是调用了sleep()方法或join()方法等待休眠或其他线程结束,或是因为发生了I/O中断。

    67、简述synchronized 和java.util.concurrent.locks.Lock的异同?
    答:Lock是Java 5以后引入的新的API,和关键字synchronized相比主要相同点:Lock 能完成synchronized所实现的所有功能;主要不同点:Lock有比synchronized更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且最好在finally 块中释放(这是释放外部资源的最好的地方)。

    68、Java中如何实现序列化,有什么意义?
    答:序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决对象流读写操作时可能引发的问题(如果不进行序列化可能会存在数据乱序的问题)。
    要实现序列化,需要让一个类实现Serializable接口,该接口是一个标识性接口,标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通过writeObject(Object)方法就可以将实现对象写出(即保存其状态);如果需要反序列化则可以用一个输入流建立对象输入流,然后通过readObject方法从流中读取对象。序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆(可以参考第29题)。

    69、Java中有几种类型的流?
    答:字节流和字符流。字节流继承于InputStream、OutputStream,字符流继承于Reader、Writer。在java.io 包中还有许多其他的流,主要是为了提高性能和使用方便。关于Java的I/O需要注意的有两点:一是两种对称性(输入和输出的对称性,字节和字符的对称性);二是两种设计模式(适配器模式和装潢模式)。另外Java中的流不同于C#的是它只有一个维度一个方向。

    面试题 - 编程实现文件拷贝。(这个题目在笔试的时候经常出现,下面的代码给出了两种实现方案)

    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    
    public final class MyUtil {
    
    	private MyUtil() {
    		throw new AssertionError();
    	}
    	
    	public static void fileCopy(String source, String target) throws IOException {
    		try (InputStream in = new FileInputStream(source)) {
    			try (OutputStream out = new FileOutputStream(target)) {
    				byte[] buffer = new byte[4096];
    				int bytesToRead;
    				while((bytesToRead = in.read(buffer)) != -1) {
    					out.write(buffer, 0, bytesToRead);
    				}
    			}
    		}
    	}
    	
    	public static void fileCopyNIO(String source, String target) throws IOException {
    		try (FileInputStream in = new FileInputStream(source)) {
    			try (FileOutputStream out = new FileOutputStream(target)) {
    				FileChannel inChannel = in.getChannel();
    				FileChannel outChannel = out.getChannel();
    				ByteBuffer buffer = ByteBuffer.allocate(4096);
    				while(inChannel.read(buffer) != -1) {
    					buffer.flip();
    					outChannel.write(buffer);
    					buffer.clear();
    				}
    			}
    		}
    	}
    }
    

    **注意:**上面用到Java 7的TWR,使用TWR后可以不用在finally中释放外部资源 ,从而让代码更加优雅。

    70、写一个方法,输入一个文件名和一个字符串,统计这个字符串在这个文件中出现的次数。
    答:代码如下:

    import java.io.BufferedReader;
    import java.io.FileReader;
    
    public final class MyUtil {
    
    	// 工具类中的方法都是静态方式访问的因此将构造器私有不允许创建对象(绝对好习惯)
    	private MyUtil() {
    		throw new AssertionError();
    	}
    
    	/**
    	 * 统计给定文件中给定字符串的出现次数
    	 * 
    	 * @param filename  文件名
    	 * @param word 字符串
    	 * @return 字符串在文件中出现的次数
    	 */
    	public static int countWordInFile(String filename, String word) {
    		int counter = 0;
    		try (FileReader fr = new FileReader(filename)) {
    			try (BufferedReader br = new BufferedReader(fr)) {
    				String line = null;
    				while ((line = br.readLine()) != null) {
    					int index = -1;
    					while (line.length() >= word.length() && (index = line.indexOf(word)) >= 0) {
    						counter++;
    						line = line.substring(index + word.length());
    					}
    				}
    			}
    		} catch (Exception ex) {
    			ex.printStackTrace();
    		}
    		return counter;
    	}
    
    }
    

    71、如何用Java代码列出一个目录下所有的文件?
    答:
    如果只要求列出当前文件夹下的文件,代码如下所示:

    import java.io.File;
    
    class Test12 {
    
    	public static void main(String[] args) {
    		File f = new File("/Users/Hao/Downloads");
    		for(File temp : f.listFiles()) {
    			if(temp.isFile()) {
    				System.out.println(temp.getName());
    			}
    		}
    	}
    }
    

    如果需要对文件夹继续展开,代码如下所示:

    import java.io.File;
    
    class Test12 {
    
    	public static void main(String[] args) {
    		showDirectory(new File("/Users/Hao/Downloads"));
    	}
    	
    	public static void showDirectory(File f) {
    		_walkDirectory(f, 0);
    	}
    	
    	private static void _walkDirectory(File f, int level) {
    		if(f.isDirectory()) {
    			for(File temp : f.listFiles()) {
    				_walkDirectory(temp, level + 1);
    			}
    		}
    		else {
    			for(int i = 0; i < level - 1; i++) {
    				System.out.print("\t");
    			}
    			System.out.println(f.getName());
    		}
    	}
    }
    

    在Java 7中可以使用NIO.2的API来做同样的事情,代码如下所示:

    class ShowFileTest {
    
    	public static void main(String[] args) throws IOException {
    		Path initPath = Paths.get("/Users/Hao/Downloads");
    		Files.walkFileTree(initPath, new SimpleFileVisitor<Path>() {
    
    			@Override
    			public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 
    					throws IOException {
    				System.out.println(file.getFileName().toString());
    				return FileVisitResult.CONTINUE;
    			}
    			
    		});
    	}
    }
    

    72、用Java的套接字编程实现一个多线程的回显(echo)服务器。
    答:

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class EchoServer {
    
    	private static final int ECHO_SERVER_PORT = 6789;
    
    	public static void main(String[] args) {		
    		try(ServerSocket server = new ServerSocket(ECHO_SERVER_PORT)) {
    			System.out.println("服务器已经启动...");
    			while(true) {
    				Socket client = server.accept();
    				new Thread(new ClientHandler(client)).start();
    			}
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    	}
    	
    	private static class ClientHandler implements Runnable {
    		private Socket client;
    		
    		public ClientHandler(Socket client) {
    			this.client = client;
    		}
    		
    		@Override
    		public void run() {
    			try(BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
    					PrintWriter pw = new PrintWriter(client.getOutputStream())) {
    				String msg = br.readLine();
    				System.out.println("收到" + client.getInetAddress() + "发送的: " + msg);
    				pw.println(msg);
    				pw.flush();
    			} catch(Exception ex) {
    				ex.printStackTrace();
    			} finally {
    				try {
    					client.close();
    				} catch (IOException e) {
    					e.printStackTrace();
    				}
    			}
    		}
    	}
    	
    }
    

    **注意:**上面的代码使用了Java 7的TWR语法,由于很多外部资源类都间接的实现了AutoCloseable接口(单方法回调接口),因此可以利用TWR语法在try结束的时候通过回调的方式自动调用外部资源类的close()方法,避免书写冗长的finally代码块。此外,上面的代码用一个静态内部类实现线程的功能,使用多线程可以避免一个用户I/O操作所产生的中断影响其他用户对服务器的访问,简单的说就是一个用户的输入操作不会造成其他用户的阻塞。当然,上面的代码使用线程池可以获得更好的性能,因为频繁的创建和销毁线程所造成的开销也是不可忽视的。

    下面是一段回显客户端测试代码:

    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.Socket;
    import java.util.Scanner;
    
    public class EchoClient {
    
    	public static void main(String[] args) throws Exception {
    		Socket client = new Socket("localhost", 6789);
    		Scanner sc = new Scanner(System.in);
    		System.out.print("请输入内容: ");
    		String msg = sc.nextLine();
    		sc.close();
    		PrintWriter pw = new PrintWriter(client.getOutputStream());
    		pw.println(msg);
    		pw.flush();
    		BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
    		System.out.println(br.readLine());
    		client.close();
    	}
    }
    

    如果希望用NIO的多路复用套接字实现服务器,代码如下所示。NIO的操作虽然带来了更好的性能,但是有些操作是比较底层的,对于初学者来说还是有些难于理解。

    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.CharBuffer;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.util.Iterator;
    
    public class EchoServerNIO {
    
    	private static final int ECHO_SERVER_PORT = 6789;
    	private static final int ECHO_SERVER_TIMEOUT = 5000;
    	private static final int BUFFER_SIZE = 1024;
    	
    	private static ServerSocketChannel serverChannel = null;
    	private static Selector selector = null;	// 多路复用选择器
    	private static ByteBuffer buffer = null;	// 缓冲区
    
    	public static void main(String[] args) {
    		init();
    		listen();
    	}
    	
    	private static void init() {
    		try {
    			serverChannel = ServerSocketChannel.open();
    			buffer = ByteBuffer.allocate(BUFFER_SIZE);
    			serverChannel.socket().bind(new InetSocketAddress(ECHO_SERVER_PORT));
    			serverChannel.configureBlocking(false);
    			selector = Selector.open();
    			serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    		} catch (Exception e) {
    			throw new RuntimeException(e);
    		}
    	}
    	
    	private static void listen() {
    		while (true) {
    			try {
    				if (selector.select(ECHO_SERVER_TIMEOUT) != 0) {
    					Iterator<SelectionKey> it = selector.selectedKeys().iterator();
    					while (it.hasNext()) {
    						SelectionKey key = it.next();
    						it.remove();
    						handleKey(key);
    					}
    				}
    			} catch (Exception e) {
    				e.printStackTrace();
    			}
    		}
    	}
    
    	private static void handleKey(SelectionKey key) throws IOException {
    		SocketChannel channel = null;
    
    		try {
    			if (key.isAcceptable()) {
    				ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
    				channel = serverChannel.accept();
    				channel.configureBlocking(false);
    				channel.register(selector, SelectionKey.OP_READ);
    			} else if (key.isReadable()) {
    				channel = (SocketChannel) key.channel();
    				buffer.clear();
    				if (channel.read(buffer) > 0) {
    					buffer.flip();
    					CharBuffer charBuffer = CharsetHelper.decode(buffer);
    					String msg = charBuffer.toString();
    					System.out.println("收到" + channel.getRemoteAddress() + "的消息:" + msg);
    					channel.write(CharsetHelper.encode(CharBuffer.wrap(msg)));
    				} else {
    					channel.close();
    				}
    			}
    		} catch (Exception e) {
    			e.printStackTrace();
    			if (channel != null) {
    				channel.close();
    			}
    		}
    	}
    
    }
    
    import java.nio.ByteBuffer;
    import java.nio.CharBuffer;
    import java.nio.charset.CharacterCodingException;
    import java.nio.charset.Charset;
    import java.nio.charset.CharsetDecoder;
    import java.nio.charset.CharsetEncoder;
     
    public final class CharsetHelper {
        private static final String UTF_8 = "UTF-8";
        private static CharsetEncoder encoder = Charset.forName(UTF_8).newEncoder();
        private static CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder();
        
        private CharsetHelper() {
        }
         
        public static ByteBuffer encode(CharBuffer in) throws CharacterCodingException{
            return encoder.encode(in);
        }
     
        public static CharBuffer decode(ByteBuffer in) throws CharacterCodingException{
            return decoder.decode(in);
        }
    }
    

    73、XML文档定义有几种形式?它们之间有何本质区别?解析XML文档有哪几种方式?
    答:XML文档定义分为DTD和Schema两种形式,二者都是对XML语法的约束,其本质区别在于Schema本身也是一个XML文件,可以被XML解析器解析,而且可以为XML承载的数据定义类型,约束能力较之DTD更强大。对XML的解析主要有DOM(文档对象模型,Document Object Model)、SAX(Simple API for XML)和StAX(Java 6中引入的新的解析XML的方式,Streaming API for XML),其中DOM处理大型文件时其性能下降的非常厉害,这个问题是由DOM树结构占用的内存较多造成的,而且DOM解析方式必须在解析文件之前把整个文档装入内存,适合对XML的随机访问(典型的用空间换取时间的策略);SAX是事件驱动型的XML解析方式,它顺序读取XML文件,不需要一次全部装载整个文件。当遇到像文件开头,文档结束,或者标签开头与标签结束时,它会触发一个事件,用户通过事件回调代码来处理XML文件,适合对XML的顺序访问;顾名思义,StAX把重点放在流上,实际上StAX与其他解析方式的本质区别就在于应用程序能够把XML作为一个事件流来处理。将XML作为一组事件来处理的想法并不新颖(SAX就是这样做的),但不同之处在于StAX允许应用程序代码把这些事件逐个拉出来,而不用提供在解析器方便时从解析器中接收事件的处理程序。

    74、你在项目中哪些地方用到了XML?
    答:XML的主要作用有两个方面:数据交换和信息配置。在做数据交换时,XML将数据用标签组装成起来,然后压缩打包加密后通过网络传送给接收者,接收解密与解压缩后再从XML文件中还原相关信息进行处理,XML曾经是异构系统间交换数据的事实标准,但此项功能几乎已经被JSON(JavaScript Object Notation)取而代之。当然,目前很多软件仍然使用XML来存储配置信息,我们在很多项目中通常也会将作为配置信息的硬代码写在XML文件中,Java的很多框架也是这么做的,而且这些框架都选择了dom4j作为处理XML的工具,因为Sun公司的官方API实在不怎么好用。

    **补充:**现在有很多时髦的软件(如Sublime)已经开始将配置文件书写成JSON格式,我们已经强烈的感受到XML的另一项功能也将逐渐被业界抛弃。

    75、阐述JDBC操作数据库的步骤。
    答:下面的代码以连接本机的Oracle数据库为例,演示JDBC操作数据库的步骤。

    • 加载驱动。
    	Class.forName("oracle.jdbc.driver.OracleDriver");
    
    • 创建连接。
    	Connection con = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl", "scott", "tiger");
    
    • 创建语句。
    	PreparedStatement ps = con.prepareStatement("select * from emp where sal between ? and ?");
    	ps.setInt(1, 1000);
    	ps.setInt(2, 3000);
    
    • 执行语句。
    	ResultSet rs = ps.executeQuery();
    
    • 处理结果。
    	while(rs.next()) {
    		System.out.println(rs.getInt("empno") + " - " + rs.getString("ename"));
    	}
    
    • 关闭资源。
    	finally {
    		if(con != null) {
    			try {
    				con.close();
    			} catch (SQLException e) {
    				e.printStackTrace();
    			}
    		}
    	}
    

    **提示:**关闭外部资源的顺序应该和打开的顺序相反,也就是说先关闭ResultSet、再关闭Statement、在关闭Connection。上面的代码只关闭了Connection(连接),虽然通常情况下在关闭连接时,连接上创建的语句和打开的游标也会关闭,但不能保证总是如此,因此应该按照刚才说的顺序分别关闭。此外,第一步加载驱动在JDBC 4.0中是可以省略的(自动从类路径中加载驱动),但是我们建议保留。

    76、Statement和PreparedStatement有什么区别?哪个性能更好?
    答:与Statement相比,①PreparedStatement接口代表预编译的语句,它主要的优势在于可以减少SQL的编译错误并增加SQL的安全性(减少SQL注射攻击的可能性);②PreparedStatement中的SQL语句是可以带参数的,避免了用字符串连接拼接SQL语句的麻烦和不安全;③当批量处理SQL或频繁执行相同的查询时,PreparedStatement有明显的性能上的优势,由于数据库可以将编译优化后的SQL语句缓存起来,下次执行相同结构的语句时就会很快(不用再次编译和生成执行计划)。

    **补充:**为了提供对存储过程的调用,JDBC API中还提供了CallableStatement接口。存储过程(Stored Procedure)是数据库中一组为了完成特定功能的SQL语句的集合,经编译后存储在数据库中,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。虽然调用存储过程会在网络开销、安全性、性能上获得很多好处,但是存在如果底层数据库发生迁移时就会有很多麻烦,因为每种数据库的存储过程在书写上存在不少的差别。

    77、使用JDBC操作数据库时,如何提升读取数据的性能?如何提升更新数据的性能?
    答:要提升读取数据的性能,可以指定通过结果集(ResultSet)对象的setFetchSize()方法指定每次抓取的记录数(典型的空间换时间策略);要提升更新数据的性能可以使用PreparedStatement语句构建批处理,将若干SQL语句置于一个批处理中执行。

    78、在进行数据库编程时,连接池有什么作用?
    答:由于创建连接和释放连接都有很大的开销(尤其是数据库服务器不在本地时,每次建立连接都需要进行TCP的三次握手,释放连接需要进行TCP四次握手,造成的开销是不可忽视的),为了提升系统访问数据库的性能,可以事先创建若干连接置于连接池中,需要时直接从连接池获取,使用结束时归还连接池而不必关闭连接,从而避免频繁创建和释放连接所造成的开销,这是典型的用空间换取时间的策略(浪费了空间存储连接,但节省了创建和释放连接的时间)。池化技术在Java开发中是很常见的,在使用线程时创建线程池的道理与此相同。基于Java的开源数据库连接池主要有:C3P0ProxoolDBCPBoneCPDruid等。

    **补充:**在计算机系统中时间和空间是不可调和的矛盾,理解这一点对设计满足性能要求的算法是至关重要的。大型网站性能优化的一个关键就是使用缓存,而缓存跟上面讲的连接池道理非常类似,也是使用空间换时间的策略。可以将热点数据置于缓存中,当用户查询这些数据时可以直接从缓存中得到,这无论如何也快过去数据库中查询。当然,缓存的置换策略等也会对系统性能产生重要影响,对于这个问题的讨论已经超出了这里要阐述的范围。

    79、什么是DAO模式?
    答:DAO(Data Access Object)顾名思义是一个为数据库或其他持久化机制提供了抽象接口的对象,在不暴露底层持久化方案实现细节的前提下提供了各种数据访问操作。在实际的开发中,应该将所有对数据源的访问操作进行抽象化后封装在一个公共API中。用程序设计语言来说,就是建立一个接口,接口中定义了此应用程序中将会用到的所有事务方法。在这个应用程序中,当需要和数据源进行交互的时候则使用这个接口,并且编写一个单独的类来实现这个接口,在逻辑上该类对应一个特定的数据存储。DAO模式实际上包含了两个模式,一是Data Accessor(数据访问器),二是Data Object(数据对象),前者要解决如何访问数据的问题,而后者要解决的是如何用对象封装数据。

    80、事务的ACID是指什么?
    答:

    • 原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会导致整个事务的失败;
    • 一致性(Consistent):事务结束后系统状态是一致的;
    • 隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;
    • 持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败。通过日志和同步备份可以在故障发生后重建数据。

    **补充:**关于事务,在面试中被问到的概率是很高的,可以问的问题也是很多的。首先需要知道的是,只有存在并发数据访问时才需要事务。当多个事务访问同一数据时,可能会存在5类问题,包括3类数据读取问题(脏读、不可重复读和幻读)和2类数据更新问题(第1类丢失更新和第2类丢失更新)。

    脏读(Dirty Read):A事务读取B事务尚未提交的数据并在此基础上操作,而B事务执行回滚,那么A读取到的数据就是脏数据。

    时间转账事务A取款事务B
    T1 开始事务
    T2开始事务 
    T3 查询账户余额为1000元
    T4 取出500元余额修改为500元
    T5查询账户余额为500元(脏读) 
    T6 撤销事务余额恢复为1000元
    T7汇入100元把余额修改为600元 
    T8提交事务 

    不可重复读(Unrepeatable Read):事务A重新读取前面读取过的数据,发现该数据已经被另一个已提交的事务B修改过了。

    时间转账事务A取款事务B
    T1 开始事务
    T2开始事务 
    T3 查询账户余额为1000元
    T4查询账户余额为1000元 
    T5 取出100元修改余额为900元
    T6 提交事务
    T7查询账户余额为900元(不可重复读) 

    幻读(Phantom Read):事务A重新执行一个查询,返回一系列符合查询条件的行,发现其中插入了被事务B提交的行。

    时间统计金额事务A转账事务B
    T1 开始事务
    T2开始事务 
    T3统计总存款为10000元 
    T4 新增一个存款账户存入100元
    T5 提交事务
    T6再次统计总存款为10100元(幻读) 

    第1类丢失更新:事务A撤销时,把已经提交的事务B的更新数据覆盖了。

    时间取款事务A转账事务B
    T1开始事务 
    T2 开始事务
    T3查询账户余额为1000元 
    T4 查询账户余额为1000元
    T5 汇入100元修改余额为1100元
    T6 提交事务
    T7取出100元将余额修改为900元 
    T8撤销事务 
    T9余额恢复为1000元(丢失更新) 

    第2类丢失更新:事务A覆盖事务B已经提交的数据,造成事务B所做的操作丢失。

    时间转账事务A取款事务B
    T1 开始事务
    T2开始事务 
    T3 查询账户余额为1000元
    T4查询账户余额为1000元 
    T5 取出100元将余额修改为900元
    T6 提交事务
    T7汇入100元将余额修改为1100元 
    T8提交事务 
    T9查询账户余额为1100元(丢失更新) 

    数据并发访问所产生的问题,在有些场景下可能是允许的,但是有些场景下可能就是致命的,数据库通常会通过锁机制来解决数据并发访问问题,按锁定对象不同可以分为表级锁和行级锁;按并发事务锁定关系可以分为共享锁和独占锁,具体的内容大家可以自行查阅资料进行了解。
    直接使用锁是非常麻烦的,为此数据库为用户提供了自动锁机制,只要用户指定会话的事务隔离级别,数据库就会通过分析SQL语句然后为事务访问的资源加上合适的锁,此外,数据库还会维护这些锁通过各种手段提高系统的性能,这些对用户来说都是透明的(就是说你不用理解,事实上我确实也不知道)。ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别,如下表所示:

    隔离级别脏读不可重复读幻读第一类丢失更新第二类丢失更新
    READ UNCOMMITED允许允许允许不允许允许
    READ COMMITTED不允许允许允许不允许允许
    REPEATABLE READ不允许不允许允许不允许不允许
    SERIALIZABLE不允许不允许不允许不允许不允许

    需要说明的是,事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高并发性就越差。所以要根据具体的应用来确定合适的事务隔离级别,这个地方没有万能的原则。

    81、JDBC中如何进行事务处理?
    答:Connection提供了事务处理的方法,通过调用setAutoCommit(false)可以设置手动提交事务;当事务完成后用commit()显式提交事务;如果在事务处理过程中发生异常则通过rollback()进行事务回滚。除此之外,从JDBC 3.0中还引入了Savepoint(保存点)的概念,允许通过代码设置保存点并让事务回滚到指定的保存点。
    这里写图片描述

    82、JDBC能否处理Blob和Clob?
    答: Blob是指二进制大对象(Binary Large Object),而Clob是指大字符对象(Character Large Objec),因此其中Blob是为存储大的二进制数据而设计的,而Clob是为存储大的文本数据而设计的。JDBC的PreparedStatement和ResultSet都提供了相应的方法来支持Blob和Clob操作。下面的代码展示了如何使用JDBC操作LOB:
    下面以MySQL数据库为例,创建一个张有三个字段的用户表,包括编号(id)、姓名(name)和照片(photo),建表语句如下:

    create table tb_user
    (
    id int primary key auto_increment,
    name varchar(20) unique not null,
    photo longblob
    );
    

    下面的Java代码向数据库中插入一条记录:

    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.sql.PreparedStatement;
    import java.sql.SQLException;
    
    class JdbcLobTest {
    
    	public static void main(String[] args) {
    		Connection con = null;
    		try {
    			// 1. 加载驱动(Java6以上版本可以省略)
    			Class.forName("com.mysql.jdbc.Driver");
    			// 2. 建立连接
    			con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
    			// 3. 创建语句对象
    			PreparedStatement ps = con.prepareStatement("insert into tb_user values (default, ?, ?)");
    			ps.setString(1, "骆昊");				// 将SQL语句中第一个占位符换成字符串
    			try (InputStream in = new FileInputStream("test.jpg")) {	// Java 7的TWR
    				ps.setBinaryStream(2, in);		// 将SQL语句中第二个占位符换成二进制流
    				// 4. 发出SQL语句获得受影响行数
    				System.out.println(ps.executeUpdate() == 1 ? "插入成功" : "插入失败");
    			} catch(IOException e) {
    				System.out.println("读取照片失败!");
    			}
    		} catch (ClassNotFoundException | SQLException e) {		// Java 7的多异常捕获
    			e.printStackTrace();
    		} finally {	// 释放外部资源的代码都应当放在finally中保证其能够得到执行
    			try {
    				if(con != null && !con.isClosed()) {
    					con.close();	// 5. 释放数据库连接 
    					con = null;     // 指示垃圾回收器可以回收该对象
    				}
    			} catch (SQLException e) {
    				e.printStackTrace();
    			}
    		}
    	}
    }
    

    83、简述正则表达式及其用途。
    答:在编写处理字符串的程序时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。

    **说明:**计算机诞生初期处理的信息几乎都是数值,但是时过境迁,今天我们使用计算机处理的信息更多的时候不是数值而是字符串,正则表达式就是在进行字符串匹配和处理的时候最为强大的工具,绝大多数语言都提供了对正则表达式的支持。

    84、Java中是如何支持正则表达式操作的?
    答:Java中的String类提供了支持正则表达式操作的方法,包括:matches()、replaceAll()、replaceFirst()、split()。此外,Java中可以用Pattern类表示正则表达式对象,它提供了丰富的API进行各种正则表达式操作,请参考下面面试题的代码。

    面试题: - 如果要从字符串中截取第一个英文左括号之前的字符串,例如:北京市(朝阳区)(西城区)(海淀区),截取结果为:北京市,那么正则表达式怎么写?

    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    class RegExpTest {
    
    	public static void main(String[] args) {
    		String str = "北京市(朝阳区)(西城区)(海淀区)";
    		Pattern p = Pattern.compile(".*?(?=\\()");
    		Matcher m = p.matcher(str);
    		if(m.find()) {
    			System.out.println(m.group());
    		}
    	}
    }
    

    **说明:**上面的正则表达式中使用了懒惰匹配和前瞻,如果不清楚这些内容,推荐读一下网上很有名的《正则表达式30分钟入门教程》

    85、获得一个类的类对象有哪些方式?
    答:

    • 方法1:类型.class,例如:String.class
    • 方法2:对象.getClass(),例如:"hello".getClass()
    • 方法3:Class.forName(),例如:Class.forName("java.lang.String")

    86、如何通过反射创建对象?
    答:

    • 方法1:通过类对象调用newInstance()方法,例如:String.class.newInstance()
    • 方法2:通过类对象的getConstructor()或getDeclaredConstructor()方法获得构造器(Constructor)对象并调用其newInstance()方法创建对象,例如:String.class.getConstructor(String.class).newInstance("Hello");

    87、如何通过反射获取和设置对象私有字段的值?
    答:可以通过类对象的getDeclaredField()方法字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。下面的代码实现了一个反射的工具类,其中的两个静态方法分别用于获取和设置私有字段的值,字段可以是基本类型也可以是对象类型且支持多级对象操作,例如ReflectionUtil.get(dog, "owner.car.engine.id");可以获得dog对象的主人的汽车的引擎的ID号。

    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.lang.reflect.Modifier;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 反射工具类
     * @author 骆昊
     *
     */
    public class ReflectionUtil {
    
    	private ReflectionUtil() {
    		throw new AssertionError();
    	}
    
    	/**
    	 * 通过反射取对象指定字段(属性)的值
    	 * @param target 目标对象
    	 * @param fieldName 字段的名字
    	 * @throws 如果取不到对象指定字段的值则抛出异常
    	 * @return 字段的值
    	 */
    	public static Object getValue(Object target, String fieldName) {
    		Class<?> clazz = target.getClass();
    		String[] fs = fieldName.split("\\.");
    		
    		try {
    			for(int i = 0; i < fs.length - 1; i++) {
    				Field f = clazz.getDeclaredField(fs[i]);
    				f.setAccessible(true);
    				target = f.get(target);
    				clazz = target.getClass();
    			}
    		
    			Field f = clazz.getDeclaredField(fs[fs.length - 1]);
    			f.setAccessible(true);
    			return f.get(target);
    		}
    		catch (Exception e) {
    			throw new RuntimeException(e);
    		}
    	}
    	
    	/**
    	 * 通过反射给对象的指定字段赋值
    	 * @param target 目标对象
    	 * @param fieldName 字段的名称
    	 * @param value 值
    	 */
    	public static void setValue(Object target, String fieldName, Object value) {
    		Class<?> clazz = target.getClass();
    		String[] fs = fieldName.split("\\.");
    		try {
    			for(int i = 0; i < fs.length - 1; i++) {
    				Field f = clazz.getDeclaredField(fs[i]);
    				f.setAccessible(true);
    				Object val = f.get(target);
    				if(val == null) {
    					Constructor<?> c = f.getType().getDeclaredConstructor();
    					c.setAccessible(true);
    					val = c.newInstance();
    					f.set(target, val);
    				}
    				target = val;
    				clazz = target.getClass();
    			}
    		
    			Field f = clazz.getDeclaredField(fs[fs.length - 1]);
    			f.setAccessible(true);
    			f.set(target, value);
    		}
    		catch (Exception e) {
    			throw new RuntimeException(e);
    		}
    	}
    	
    }
    

    88、如何通过反射调用对象的方法?
    答:请看下面的代码:

    import java.lang.reflect.Method;
    
    class MethodInvokeTest {
    
    	public static void main(String[] args) throws Exception {
    		String str = "hello";
    		Method m = str.getClass().getMethod("toUpperCase");
    		System.out.println(m.invoke(str));	// HELLO
    	}
    }
    

    89、简述一下面向对象的"六原则一法则"。
    答:

    • 单一职责原则:一个类只做它该做的事情。(单一职责原则想表达的就是"高内聚",写代码最终极的原则只有六个字"高内聚、低耦合",就如同葵花宝典或辟邪剑谱的中心思想就八个字"欲练此功必先自宫",所谓的高内聚就是一个代码模块只完成一项功能,在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。我们都知道一句话叫"因为专注,所以专业",一个对象如果承担太多的职责,那么注定它什么都做不好。这个世界上任何好的东西都有两个特征,一个是功能单一,好的相机绝对不是电视购物里面卖的那种一个机器有一百多种功能的,它基本上只能照相;另一个是模块化,好的自行车是组装车,从减震叉、刹车到变速器,所有的部件都是可以拆卸和重新组装的,好的乒乓球拍也不是成品拍,一定是底板和胶皮可以拆分和自行组装的,一个好的软件系统,它里面的每个功能模块也应该是可以轻易的拿到其他系统中使用的,这样才能实现软件复用的目标。)
    • 开闭原则:软件实体应当对扩展开放,对修改关闭。(在理想的状态下,当我们需要为一个软件系统增加新功能时,只需要从原来的系统派生出一些新类就可以,不需要修改原来的任何一行代码。要做到开闭有两个要点:①抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点;②封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂而换乱,如果不清楚如何封装可变性,可以参考《设计模式精解》一书中对桥梁模式的讲解的章节。)
    • 依赖倒转原则:面向接口编程。(该原则说得直白和具体一些就是声明方法的参数类型、方法的返回类型、变量的引用类型时,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代,请参考下面的里氏替换原则。)
      里氏替换原则:任何时候都可以用子类型替换掉父类型。(关于里氏替换原则的描述,Barbara Liskov女士的描述比这个要复杂得多,但简单的说就是能用父类型的地方就一定能使用子类型。里氏替换原则可以检查继承关系是否合理,如果一个继承关系违背了里氏替换原则,那么这个继承关系一定是错误的,需要对代码进行重构。例如让猫继承狗,或者狗继承猫,又或者让正方形继承长方形都是错误的继承关系,因为你很容易找到违反里氏替换原则的场景。需要注意的是:子类一定是增加父类的能力而不是减少父类的能力,因为子类比父类的能力更多,把能力多的对象当成能力少的对象来用当然没有任何问题。)
    • 接口隔离原则:接口要小而专,绝不能大而全。(臃肿的接口是对接口的污染,既然接口表示能力,那么一个接口只应该描述一种能力,接口也应该是高度内聚的。例如,琴棋书画就应该分别设计为四个接口,而不应设计成一个接口中的四个方法,因为如果设计成一个接口中的四个方法,那么这个接口很难用,毕竟琴棋书画四样都精通的人还是少数,而如果设计成四个接口,会几项就实现几个接口,这样的话每个接口被复用的可能性是很高的。Java中的接口代表能力、代表约定、代表角色,能否正确的使用接口一定是编程水平高低的重要标识。)
    • 合成聚合复用原则:优先使用聚合或合成关系复用代码。(通过继承来复用代码是面向对象程序设计中被滥用得最多的东西,因为所有的教科书都无一例外的对继承进行了鼓吹从而误导了初学者,类与类之间简单的说有三种关系,Is-A关系、Has-A关系、Use-A关系,分别代表继承、关联和依赖。其中,关联关系根据其关联的强度又可以进一步划分为关联、聚合和合成,但说白了都是Has-A关系,合成聚合复用原则想表达的是优先考虑Has-A关系而不是Is-A关系复用代码,原因嘛可以自己从百度上找到一万个理由,需要说明的是,即使在Java的API中也有不少滥用继承的例子,例如Properties类继承了Hashtable类,Stack类继承了Vector类,这些继承明显就是错误的,更好的做法是在Properties类中放置一个Hashtable类型的成员并且将其键和值都设置为字符串来存储数据,而Stack类的设计也应该是在Stack类中放一个Vector对象来存储数据。记住:任何时候都不要继承工具类,工具是可以拥有并可以使用的,而不是拿来继承的。)
    • 迪米特法则:迪米特法则又叫最少知识原则,一个对象应当对其他对象有尽可能少的了解。(迪米特法则简单的说就是如何做到"低耦合",门面模式和调停者模式就是对迪米特法则的践行。对于门面模式可以举一个简单的例子,你去一家公司洽谈业务,你不需要了解这个公司内部是如何运作的,你甚至可以对这个公司一无所知,去的时候只需要找到公司入口处的前台美女,告诉她们你要做什么,她们会找到合适的人跟你接洽,前台的美女就是公司这个系统的门面。再复杂的系统都可以为用户提供一个简单的门面,Java Web开发中作为前端控制器的Servlet或Filter不就是一个门面吗,浏览器对服务器的运作方式一无所知,但是通过前端控制器就能够根据你的请求得到相应的服务。调停者模式也可以举一个简单的例子来说明,例如一台计算机,CPU、内存、硬盘、显卡、声卡各种设备需要相互配合才能很好的工作,但是如果这些东西都直接连接到一起,计算机的布线将异常复杂,在这种情况下,主板作为一个调停者的身份出现,它将各个设备连接在一起而不需要每个设备之间直接交换数据,这样就减小了系统的耦合度和复杂度,如下图所示。迪米特法则用通俗的话来将就是不要和陌生人打交道,如果真的需要,找一个自己的朋友,让他替你和陌生人打交道。)

    这里写图片描述
    这里写图片描述

    90、简述一下你了解的设计模式。
    答:所谓设计模式,就是一套被反复使用的代码设计经验的总结(情境中一个问题经过证实的一个解决方案)。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使人们可以更加简单方便的复用成功的设计和体系结构。将已证实的技术表述成设计模式也会使新系统开发者更加容易理解其设计思路。
    在GoF的《Design Patterns: Elements of Reusable Object-Oriented Software》中给出了三类(创建型[对类的实例化过程的抽象化]、结构型[描述如何将类或对象结合在一起形成更大的结构]、行为型[对在不同的对象之间划分责任和算法的抽象化])共23种设计模式,包括:Abstract Factory(抽象工厂模式),Builder(建造者模式),Factory Method(工厂方法模式),Prototype(原始模型模式),Singleton(单例模式);Facade(门面模式),Adapter(适配器模式),Bridge(桥梁模式),Composite(合成模式),Decorator(装饰模式),Flyweight(享元模式),Proxy(代理模式);Command(命令模式),Interpreter(解释器模式),Visitor(访问者模式),Iterator(迭代子模式),Mediator(调停者模式),Memento(备忘录模式),Observer(观察者模式),State(状态模式),Strategy(策略模式),Template Method(模板方法模式), Chain Of Responsibility(责任链模式)。
    面试被问到关于设计模式的知识时,可以拣最常用的作答,例如:

    • 工厂模式:工厂类可以根据条件生成不同的子类实例,这些子类有一个公共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作(多态方法)。当得到子类的实例后,开发人员可以调用基类中的方法而不必考虑到底返回的是哪一个子类的实例。
    • 代理模式:给一个对象提供一个代理对象,并由代理对象控制原对象的引用。实际开发中,按照使用目的的不同,代理可以分为:远程代理、虚拟代理、保护代理、Cache代理、防火墙代理、同步化代理、智能引用代理。
    • 适配器模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起使用的类能够一起工作。
    • 模板方法模式:提供一个抽象类,将部分逻辑以具体方法或构造器的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法(多态实现),从而实现不同的业务逻辑。
      除此之外,还可以讲讲上面提到的门面模式、桥梁模式、单例模式、装潢模式(Collections工具类和I/O系统中都使用装潢模式)等,反正基本原则就是拣自己最熟悉的、用得最多的作答,以免言多必失。

    91、用Java写一个单例类。
    答:

    • 饿汉式单例
    public class Singleton {
        private Singleton(){}
        private static Singleton instance = new Singleton();
        public static Singleton getInstance(){
            return instance;
        }
    }
    
    • 懒汉式单例
    public class Singleton {
        private static Singleton instance = null;
        private Singleton() {}
        public static synchronized Singleton getInstance(){
            if (instance == null) instance = new Singleton();
            return instance;
        }
    }
    

    **注意:**实现一个单例有两点注意事项,①将构造器私有,不允许外界通过构造器创建对象;②通过公开的静态方法向外界返回类的唯一实例。这里有一个问题可以思考:Spring的IoC容器可以为普通的类创建单例,它是怎么做到的呢?

    92、什么是UML?
    答:UML是统一建模语言(Unified Modeling Language)的缩写,它发表于1997年,综合了当时已经存在的面向对象的建模语言、方法和过程,是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支持。使用UML可以帮助沟通与交流,辅助应用设计和文档的生成,还能够阐释系统的结构和行为。

    93、UML中有哪些常用的图?
    答:UML定义了多种图形化的符号来描述软件系统部分或全部的静态结构和动态结构,包括:用例图(use case diagram)、类图(class diagram)、时序图(sequence diagram)、协作图(collaboration diagram)、状态图(statechart diagram)、活动图(activity diagram)、构件图(component diagram)、部署图(deployment diagram)等。在这些图形化符号中,有三种图最为重要,分别是:用例图(用来捕获需求,描述系统的功能,通过该图可以迅速的了解系统的功能模块及其关系)、类图(描述类以及类与类之间的关系,通过该图可以快速了解系统)、时序图(描述执行特定任务时对象之间的交互关系以及执行顺序,通过该图可以了解对象能接收的消息也就是说对象能够向外界提供的服务)。
    用例图:
    这里写图片描述
    类图:
    这里写图片描述
    时序图:
    这里写图片描述

    94、用Java写一个冒泡排序。
    答:冒泡排序几乎是个程序员都写得出来,但是面试的时候如何写一个逼格高的冒泡排序却不是每个人都能做到,下面提供一个参考代码:

    import java.util.Comparator;
     
    /**
     * 排序器接口(策略模式: 将算法封装到具有共同接口的独立的类中使得它们可以相互替换)
     * @author骆昊
     *
     */
    public interface Sorter {
      
       /**
        * 排序
        * @param list 待排序的数组
        */
       public <T extends Comparable<T>> void sort(T[] list);
      
       /**
        * 排序
        * @param list 待排序的数组
        * @param comp 比较两个对象的比较器
        */
       public <T> void sort(T[] list, Comparator<T> comp);
    }
    
    import java.util.Comparator;
    
    /**
     * 冒泡排序
     * 
     * @author骆昊
     *
     */
    public class BubbleSorter implements Sorter {
    
    	@Override
    	public <T extends Comparable<T>> void sort(T[] list) {
    		boolean swapped = true;
    		for (int i = 1, len = list.length; i < len && swapped; ++i) {
    			swapped = false;
    			for (int j = 0; j < len - i; ++j) {
    				if (list[j].compareTo(list[j + 1]) > 0) {
    					T temp = list[j];
    					list[j] = list[j + 1];
    					list[j + 1] = temp;
    					swapped = true;
    				}
    			}
    		}
    	}
    
    	@Override
    	public <T> void sort(T[] list, Comparator<T> comp) {
    		boolean swapped = true;
    		for (int i = 1, len = list.length; i < len && swapped; ++i) {
    			swapped = false;
    			for (int j = 0; j < len - i; ++j) {
    				if (comp.compare(list[j], list[j + 1]) > 0) {
    					T temp = list[j];
    					list[j] = list[j + 1];
    					list[j + 1] = temp;
    					swapped = true;
    				}
    			}
    		}
    	}
    }
    

    95、用Java写一个折半查找。
    答:折半查找,也称二分查找、二分搜索,是一种在有序数组中查找某一特定元素的搜索算法。搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组已经为空,则表示找不到指定的元素。这种搜索算法每一次比较都使搜索范围缩小一半,其时间复杂度是O(logN)。

    import java.util.Comparator;
     
    public class MyUtil {
     
       public static <T extends Comparable<T>> int binarySearch(T[] x, T key) {
          return binarySearch(x, 0, x.length- 1, key);
       }
      
       // 使用循环实现的二分查找
       public static <T> int binarySearch(T[] x, T key, Comparator<T> comp) {
          int low = 0;
          int high = x.length - 1;
          while (low <= high) {
              int mid = (low + high) >>> 1;
              int cmp = comp.compare(x[mid], key);
              if (cmp < 0) {
                low= mid + 1;
              }
              else if (cmp > 0) {
                high= mid - 1;
              }
              else {
                return mid;
              }
          }
          return -1;
       }
      
       // 使用递归实现的二分查找
       private static<T extends Comparable<T>> int binarySearch(T[] x, int low, int high, T key) {
          if(low <= high) {
            int mid = low + ((high -low) >> 1);
            if(key.compareTo(x[mid])== 0) {
               return mid;
            }
            else if(key.compareTo(x[mid])< 0) {
               return binarySearch(x,low, mid - 1, key);
            }
            else {
               return binarySearch(x,mid + 1, high, key);
            }
          }
          return -1;
       }
    }
    

    **说明:**上面的代码中给出了折半查找的两个版本,一个用递归实现,一个用循环实现。需要注意的是计算中间位置时不应该使用(high+ low) / 2的方式,因为加法运算可能导致整数越界,这里应该使用以下三种方式之一:low + (high - low) / 2或low + (high – low) >> 1或(low + high) >>> 1(>>>是逻辑右移,是不带符号位的右移)

    展开全文
  • 网络编程面试题(2020最新版)

    万次阅读 多人点赞 2020-03-16 17:36:28
    TCP/IP 协议族应用层运输层网络层数据链路层物理层TCP/IP 协议族TCP的三次握手四次挥手TCP报文的头部结构三次握手四次挥手常见面试题为什么TCP连接的时候是3次?2次不可以吗?为什么TCP连接的时候是3次,关闭的时候...

    Java面试总结(2021优化版)已发布在个人微信公众号【技术人成长之路】,优化版首先修正了读者反馈的部分答案存在的错误,同时根据最新面试总结,删除了低频问题,添加了一些常见面试题,对文章进行了精简优化,欢迎大家关注!😊😊

    【技术人成长之路】,助力技术人成长!更多精彩文章第一时间在公众号发布哦!

    计算机网络体系结构

    在计算机网络的基本概念中,分层次的体系结构是最基本的。计算机网络体系结构的抽象概念较多,在学习时要多思考。这些概念对后面的学习很有帮助。

    网络协议是什么?

    在计算机网络要做到有条不紊地交换数据,就必须遵守一些事先约定好的规则,比如交换数据的格式、是否需要发送一个应答信息。这些规则被称为网络协议。

    为什么要对网络协议分层?

    • 简化问题难度和复杂度。由于各层之间独立,我们可以分割大问题为小问题。
    • 灵活性好。当其中一层的技术变化时,只要层间接口关系保持不变,其他层不受影响。
    • 易于实现和维护。
    • 促进标准化工作。分开后,每层功能可以相对简单地被描述。

    网络协议分层的缺点: 功能可能出现在多个层里,产生了额外开销。

    为了使不同体系结构的计算机网络都能互联,国际标准化组织 ISO 于1977年提出了一个试图使各种计算机在世界范围内互联成网的标准框架,即著名的开放系统互联基本参考模型 OSI/RM,简称为OSI。

    OSI 的七层协议体系结构的概念清楚,理论也较完整,但它既复杂又不实用,TCP/IP 体系结构则不同,但它现在却得到了非常广泛的应用。TCP/IP 是一个四层体系结构,它包含应用层,运输层,网际层和网络接口层(用网际层这个名字是强调这一层是为了解决不同网络的互连问题),不过从实质上讲,TCP/IP 只有最上面的三层,因为最下面的网络接口层并没有什么具体内容,因此在学习计算机网络的原理时往往采用折中的办法,即综合 OSI 和 TCP/IP 的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚,有时为了方便,也可把最底下两层称为网络接口层。

    四层协议,五层协议和七层协议的关系如下:

    • TCP/IP是一个四层的体系结构,主要包括:应用层、运输层、网际层和网络接口层。
    • 五层协议的体系结构主要包括:应用层、运输层、网络层,数据链路层和物理层。
    • OSI七层协议模型主要包括是:应用层(Application)、表示层(Presentation)、会话层(Session)、运输层(Transport)、网络层(Network)、数据链路层(Data Link)、物理层(Physical)。

    在这里插入图片描述

    注:五层协议的体系结构只是为了介绍网络原理而设计的,实际应用还是 TCP/IP 四层体系结构。

    TCP/IP 协议族

    应用层

    应用层( application-layer )的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程(进程:主机中正在运行的程序)间的通信和交互的规则。

    对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域名系统 DNS,支持万维网应用的 HTTP 协议,支持电子邮件的 SMTP 协议等等。

    运输层

    运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。

    运输层主要使用一下两种协议

    1. 传输控制协议-TCP:提供面向连接的,可靠的数据传输服务。
    2. 用户数据协议-UDP:提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。
    UDPTCP
    是否连接无连接面向连接
    是否可靠不可靠传输,不使用流量控制和拥塞控制可靠传输,使用流量控制和拥塞控制
    连接对象个数支持一对一,一对多,多对一和多对多交互通信只能是一对一通信
    传输方式面向报文面向字节流
    首部开销首部开销小,仅8字节首部最小20字节,最大60字节
    场景适用于实时应用(IP电话、视频会议、直播等)适用于要求可靠传输的应用,例如文件传输

    每一个应用层(TCP/IP参考模型的最高层)协议一般都会使用到两个传输层协议之一:

    运行在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地址。

    运行在TCPUDP协议上:

    • DNS(Domain Name Service,域名服务),用于完成地址查找,邮件转发等工作。

    网络层

    网络层的任务就是选择合适的网间路由和交换结点,确保计算机通信的数据及时传送。在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报 ,简称数据报。

    互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Intert Prococol)和许多路由选择协议,因此互联网的网络层也叫做网际层或 IP 层。

    数据链路层

    数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。

    在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。

    在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结束。

    一般的web应用的通信传输流是这样的:

    img

    发送端在层与层之间传输数据时,每经过一层时会被打上一个该层所属的首部信息。反之,接收端在层与层之间传输数据时,每经过一层时会把对应的首部信息去除。

    物理层

    在物理层上所传送的数据单位是比特。 物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。

    TCP/IP 协议族

    在互联网使用的各种协议中最重要和最著名的就是 TCP/IP 两个协议。现在人们经常提到的 TCP/IP 并不一定是单指 TCP 和 IP 这两个具体的协议,而往往是表示互联网所使用的整个 TCP/IP 协议族。

    img

    互联网协议套件(英语:Internet Protocol Suite,缩写IPS)是一个网络通讯模型,以及一整个网络传输协议家族,为网际网络的基础通讯架构。它常被通称为TCP/IP协议族(英语:TCP/IP Protocol Suite,或TCP/IP Protocols),简称TCP/IP。因为该协定家族的两个核心协定:TCP(传输控制协议)和IP(网际协议),为该家族中最早通过的标准。

    划重点:

    TCP(传输控制协议)和IP(网际协议) 是最先定义的两个核心协议,所以才统称为TCP/IP协议族

    TCP的三次握手四次挥手

    TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务端保存的一份关于对方的信息,如ip地址、端口号等。

    TCP可以看成是一种字节流,它会处理IP层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在TCP头部。

    一个TCP连接由一个4元组构成,分别是两个IP地址和两个端口号。一个TCP连接通常分为三个阶段:连接、数据传输、退出(关闭)。通过三次握手建立一个链接,通过四次挥手来关闭一个连接

    当一个连接被建立或被终止时,交换的报文段只包含TCP头部,而没有数据

    TCP报文的头部结构

    在了解TCP连接之前先来了解一下TCP报文的头部结构。

    TCPHeader.png

    上图中有几个字段需要重点介绍下:

    (1)序号:seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。

    (2)确认序号:ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,ack=seq+1。

    (3)标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:

    • ACK:确认序号有效。
    • FIN:释放一个连接。
    • PSH:接收方应该尽快将这个报文交给应用层。
    • RST:重置连接。
    • SYN:发起一个新连接。
    • URG:紧急指针(urgent pointer)有效。

    需要注意的是:

    • 不要将确认序号ack与标志位中的ACK搞混了。
    • 确认方ack=发起方seq+1,两端配对。

    三次握手

    三次握手的本质是确认通信双方收发数据的能力

    首先,我让信使运输一份信件给对方,对方收到了,那么他就知道了我的发件能力和他的收件能力是可以的

    于是他给我回信,我若收到了,我便知我的发件能力和他的收件能力是可以的,并且他的发件能力和我的收件能力是可以

    然而此时他还不知道他的发件能力和我的收件能力到底可不可以,于是我最后回馈一次,他若收到了,他便清楚了他的发件能力和我的收件能力是可以的

    这,就是三次握手,这样说,你理解了吗?

    三次握手.png

    • 第一次握手:客户端要向服务端发起连接请求,首先客户端随机生成一个起始序列号ISN(比如是100),那客户端向服务端发送的报文段包含SYN标志位(也就是SYN=1),序列号seq=100。
    • 第二次握手:服务端收到客户端发过来的报文后,发现SYN=1,知道这是一个连接请求,于是将客户端的起始序列号100存起来,并且随机生成一个服务端的起始序列号(比如是300)。然后给客户端回复一段报文,回复报文包含SYN和ACK标志(也就是SYN=1,ACK=1)、序列号seq=300、确认号ack=101(客户端发过来的序列号+1)。
    • 第三次握手:客户端收到服务端的回复后发现ACK=1并且ack=101,于是知道服务端已经收到了序列号为100的那段报文;同时发现SYN=1,知道了服务端同意了这次连接,于是就将服务端的序列号300给存下来。然后客户端再回复一段报文给服务端,报文包含ACK标志位(ACK=1)、ack=301(服务端序列号+1)、seq=101(第一次握手时发送报文是占据一个序列号的,所以这次seq就从101开始,需要注意的是不携带数据的ACK报文是不占据序列号的,所以后面第一次正式发送数据时seq还是101)。当服务端收到报文后发现ACK=1并且ack=301,就知道客户端收到序列号为300的报文了,就这样客户端和服务端通过TCP建立了连接。

    四次挥手

    四次挥手的目的是关闭一个连接

    四次挥手.jpeg

    比如客户端初始化的序列号ISA=100,服务端初始化的序列号ISA=300。TCP连接成功后客户端总共发送了1000个字节的数据,服务端在客户端发FIN报文前总共回复了2000个字节的数据。

    • 第一次挥手:当客户端的数据都传输完成后,客户端向服务端发出连接释放报文(当然数据没发完时也可以发送连接释放报文并停止发送数据),释放连接报文包含FIN标志位(FIN=1)、序列号seq=1101(100+1+1000,其中的1是建立连接时占的一个序列号)。需要注意的是客户端发出FIN报文段后只是不能发数据了,但是还可以正常收数据;另外FIN报文段即使不携带数据也要占据一个序列号。
    • 第二次挥手:服务端收到客户端发的FIN报文后给客户端回复确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=1102(客户端FIN报文序列号1101+1)、序列号seq=2300(300+2000)。此时服务端处于关闭等待状态,而不是立马给客户端发FIN报文,这个状态还要持续一段时间,因为服务端可能还有数据没发完。
    • 第三次挥手:服务端将最后数据(比如50个字节)发送完毕后就向客户端发出连接释放报文,报文包含FIN和ACK标志位(FIN=1,ACK=1)、确认号和第二次挥手一样ack=1102、序列号seq=2350(2300+50)。
    • 第四次挥手:客户端收到服务端发的FIN报文后,向服务端发出确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=2351、序列号seq=1102。注意客户端发出确认报文后不是立马释放TCP连接,而是要经过2MSL(最长报文段寿命的2倍时长)后才释放TCP连接。而服务端一旦收到客户端发出的确认报文就会立马释放TCP连接,所以服务端结束TCP连接的时间要比客户端早一些。

    常见面试题

    为什么TCP连接的时候是3次?2次不可以吗?

    因为需要考虑连接时丢包的问题,如果只握手2次,第二次握手时如果服务端发给客户端的确认报文段丢失,此时服务端已经准备好了收发数(可以理解服务端已经连接成功)据,而客户端一直没收到服务端的确认报文,所以客户端就不知道服务端是否已经准备好了(可以理解为客户端未连接成功),这种情况下客户端不会给服务端发数据,也会忽略服务端发过来的数据。

    如果是三次握手,即便发生丢包也不会有问题,比如如果第三次握手客户端发的确认ack报文丢失,服务端在一段时间内没有收到确认ack报文的话就会重新进行第二次握手,也就是服务端会重发SYN报文段,客户端收到重发的报文段后会再次给服务端发送确认ack报文。

    为什么TCP连接的时候是3次,关闭的时候却是4次?

    因为只有在客户端和服务端都没有数据要发送的时候才能断开TCP。而客户端发出FIN报文时只能保证客户端没有数据发了,服务端还有没有数据发客户端是不知道的。而服务端收到客户端的FIN报文后只能先回复客户端一个确认报文来告诉客户端我服务端已经收到你的FIN报文了,但我服务端还有一些数据没发完,等这些数据发完了服务端才能给客户端发FIN报文(所以不能一次性将确认报文和FIN报文发给客户端,就是这里多出来了一次)。

    为什么客户端发出第四次挥手的确认报文后要等2MSL的时间才能释放TCP连接?

    这里同样是要考虑丢包的问题,如果第四次挥手的报文丢失,服务端没收到确认ack报文就会重发第三次挥手的报文,这样报文一去一回最长时间就是2MSL,所以需要等这么长时间来确认服务端确实已经收到了。

    如果已经建立了连接,但是客户端突然出现故障了怎么办?

    TCP设有一个保活计时器,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

    什么是HTTP,HTTP 与 HTTPS 的区别

    HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范

    区别HTTPHTTPS
    协议运行在 TCP 之上,明文传输,客户端与服务器端都无法验证对方的身份身披 SSL( Secure Socket Layer )外壳的 HTTP,运行于 SSL 上,SSL 运行于 TCP 之上, 是添加了加密和认证机制的 HTTP
    端口80443
    资源消耗较少由于加解密处理,会消耗更多的 CPU 和内存资源
    开销无需证书需要证书,而证书一般需要向认证机构购买
    加密机制共享密钥加密和公开密钥加密并用的混合加密机制
    安全性由于加密机制,安全性强

    常用HTTP状态码

    HTTP状态码表示客户端HTTP请求的返回结果、标识服务器处理是否正常、表明请求出现的错误等。

    状态码的类别:

    类别原因短语
    1XXInformational(信息性状态码) 接受的请求正在处理
    2XXSuccess(成功状态码) 请求正常处理完毕
    3XXRedirection(重定向状态码) 需要进行附加操作以完成请求
    4XXClient Error(客户端错误状态码) 服务器无法处理请求
    5XXServer Error(服务器错误状态码) 服务器处理请求出错

    常用HTTP状态码:

    2XX成功(这系列表明请求被正常处理了)
    200OK,表示从客户端发来的请求在服务器端被正确处理
    204No content,表示请求成功,但响应报文不含实体的主体部分
    206Partial Content,进行范围请求成功
    3XX重定向(表明浏览器要执行特殊处理)
    301moved permanently,永久性重定向,表示资源已被分配了新的 URL
    302found,临时性重定向,表示资源临时被分配了新的 URL
    303see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源(对于301/302/303响应,几乎所有浏览器都会删除报文主体并自动用GET重新请求)
    304not modified,表示服务器允许访问资源,但请求未满足条件的情况(与重定向无关)
    307temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求
    4XX客户端错误
    400bad request,请求报文存在语法错误
    401unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
    403forbidden,表示对请求资源的访问被服务器拒绝,可在实体主体部分返回原因描述
    404not found,表示在服务器上没有找到请求的资源
    5XX服务器错误
    500internal sever error,表示服务器端在执行请求时发生了错误
    501Not Implemented,表示服务器不支持当前请求所需要的某个功能
    503service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

    GET和POST区别

    说道GET和POST,就不得不提HTTP协议,因为浏览器和服务器的交互是通过HTTP协议执行的,而GET和POST也是HTTP协议中的两种方法。

    HTTP全称为Hyper Text Transfer Protocol,中文翻译为超文本传输协议,目的是保证浏览器与服务器之间的通信。HTTP的工作方式是客户端与服务器之间的请求-应答协议。

    HTTP协议中定义了浏览器和服务器进行交互的不同方法,基本方法有4种,分别是GET,POST,PUT,DELETE。这四种方法可以理解为,对服务器资源的查,改,增,删。

    • GET:从服务器上获取数据,也就是所谓的查,仅仅是获取服务器资源,不进行修改。
    • POST:向服务器提交数据,这就涉及到了数据的更新,也就是更改服务器的数据。
    • PUT:英文含义是放置,也就是向服务器新添加数据,就是所谓的增。
    • DELETE:从字面意思也能看出,这种方式就是删除服务器数据的过程。

    GET和POST区别

    1. Get是不安全的,因为在传输过程,数据被放在请求的URL中;Post的所有操作对用户来说都是不可见的。 但是这种做法也不时绝对的,大部分人的做法也是按照上面的说法来的,但是也可以在get请求加上 request body,给 post请求带上 URL 参数。

    2. Get请求提交的url中的数据最多只能是2048字节,这个限制是浏览器或者服务器给添加的,http协议并没有对url长度进行限制,目的是为了保证服务器和浏览器能够正常运行,防止有人恶意发送请求。Post请求则没有大小限制。

    3. Get限制Form表单的数据集的值必须为ASCII字符;而Post支持整个ISO10646字符集。

    4. Get执行效率却比Post方法好。Get是form提交的默认方法。

    5. GET产生一个TCP数据包;POST产生两个TCP数据包。

      对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

      而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

    什么是对称加密与非对称加密

    对称密钥加密是指加密和解密使用同一个密钥的方式,这种方式存在的最大问题就是密钥发送问题,即如何安全地将密钥发给对方;

    而非对称加密是指使用一对非对称密钥,即公钥和私钥,公钥可以随意发布,但私钥只有自己知道。发送密文的一方使用对方的公钥进行加密处理,对方接收到加密信息后,使用自己的私钥进行解密。
    由于非对称加密的方式不需要发送用来解密的私钥,所以可以保证安全性;但是和对称加密比起来,非常的慢

    什么是HTTP2

    HTTP2 可以提高了网页的性能。

    在 HTTP1 中浏览器限制了同一个域名下的请求数量(Chrome 下一般是六个),当在请求很多资源的时候,由于队头阻塞当浏览器达到最大请求数量时,剩余的资源需等待当前的六个请求完成后才能发起请求。

    HTTP2 中引入了多路复用的技术,这个技术可以只通过一个 TCP 连接就可以传输所有的请求数据。多路复用可以绕过浏览器限制同一个域名下的请求数量的问题,进而提高了网页的性能。

    Session、Cookie和Token的主要区别

    HTTP协议本身是无状态的。什么是无状态呢,即服务器无法判断用户身份。

    什么是cookie

    cookie是由Web服务器保存在用户浏览器上的小文件(key-value格式),包含用户相关的信息。客户端向服务器发起请求,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户身份。

    什么是session

    session是依赖Cookie实现的。session是服务器端对象

    session 是浏览器和服务器会话过程中,服务器分配的一块储存空间。服务器默认为浏览器在cookie中设置 sessionid,浏览器在向服务器请求过程中传输 cookie 包含 sessionid ,服务器根据 sessionid 获取出会话中存储的信息,然后确定会话的身份信息。

    cookie与session区别

    • 存储位置与安全性:cookie数据存放在客户端上,安全性较差,session数据放在服务器上,安全性相对更高;
    • 存储空间:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,session无此限制
    • 占用服务器资源:session一定时间内保存在服务器上,当访问增多,占用服务器性能,考虑到服务器性能方面,应当使用cookie。

    什么是Token

    Token的引入:Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生。

    Token的定义:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。

    使用Token的目的:Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。

    Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位

    session与token区别

    • session机制存在服务器压力增大,CSRF跨站伪造请求攻击,扩展性不强等问题;
    • session存储在服务器端,token存储在客户端
    • token提供认证和授权功能,作为身份认证,token安全性比session好;
    • session这种会话存储方式方式只适用于客户端代码和服务端代码运行在同一台服务器上,token适用于项目级的前后端分离(前后端代码运行在不同的服务器下)

    Servlet是线程安全的吗

    Servlet不是线程安全的,多线程并发的读写会导致数据不同步的问题。

    解决的办法是尽量不要定义name属性,而是要把name变量分别定义在doGet()和doPost()方法内。虽然使用synchronized(name){}语句块可以解决问题,但是会造成线程的等待,不是很科学的办法。

    注意:多线程的并发的读写Servlet类属性会导致数据不同步。但是如果只是并发地读取属性而不写入,则不存在数据不同步的问题。因此Servlet里的只读属性最好定义为final类型的。

    Servlet接口中有哪些方法及Servlet生命周期探秘

    在Java Web程序中,Servlet主要负责接收用户请求HttpServletRequest,在doGet()doPost()中做相应的处理,并将回应HttpServletResponse反馈给用户。Servlet可以设置初始化参数,供Servlet内部使用。

    Servlet接口定义了5个方法,其中前三个方法与Servlet生命周期相关

    • void init(ServletConfig config) throws ServletException
    • void service(ServletRequest req, ServletResponse resp) throws ServletException, java.io.IOException
    • void destory()
    • java.lang.String getServletInfo()
    • ServletConfig getServletConfig()

    生命周期:

    Web容器加载Servlet并将其实例化后,Servlet生命周期开始,容器运行其init()方法进行Servlet的初始化;

    请求到达时调用Servlet的service()方法,service()方法会根据需要调用与请求对应的doGet或doPost等方法;

    当服务器关闭或项目被卸载时服务器会将Servlet实例销毁,此时会调用Servlet的destroy()方法

    init方法和destory方法只会执行一次,service方法客户端每次请求Servlet都会执行。Servlet中有时会用到一些需要初始化与销毁的资源,因此可以把初始化资源的代码放入init方法中,销毁资源的代码放入destroy方法中,这样就不需要每次处理客户端的请求都要初始化与销毁资源。

    如果客户端禁止 cookie 能实现 session 还能用吗?

    Cookie 与 Session,一般认为是两个独立的东西,Session采用的是在服务器端保持状态的方案,而Cookie采用的是在客户端保持状态的方案。

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

    假定用户关闭Cookie的情况下使用Session,其实现途径有以下几种:

    1. 手动通过URL传值、隐藏表单传递Session ID。
    2. 用文件、数据库等形式保存Session ID,在跨页过程中手动调用。
    展开全文
  • MySQL数据库面试题(2020最新版)

    万次阅读 多人点赞 2020-03-10 17:20:40
    文章目录数据库基础知识为什么要使用数据库什么是SQL?什么是MySQL?数据库三大范式是什么mysql有关权限的表都有哪几个MySQL的binlog有有几种录入格式?分别有什么区别?数据类型mysql有哪些数据类型引擎MySQL存储...

    Java面试总结(2021优化版)已发布在个人微信公众号【技术人成长之路】,优化版首先修正了读者反馈的部分答案存在的错误,同时根据最新面试总结,删除了低频问题,添加了一些常见面试题,对文章进行了精简优化,欢迎大家关注!😊😊

    【技术人成长之路】,助力技术人成长!更多精彩文章第一时间在公众号发布哦!

    文章目录

    Java面试总结汇总,整理了包括Java基础知识,集合容器,并发编程,JVM,常用开源框架Spring,MyBatis,数据库,中间件等,包含了作为一个Java工程师在面试中需要用到或者可能用到的绝大部分知识。欢迎大家阅读,本人见识有限,写的博客难免有错误或者疏忽的地方,还望各位大佬指点,在此表示感激不尽。文章持续更新中…

    序号内容链接地址
    1Java基础知识面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104390612
    2Java集合容器面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104588551
    3Java异常面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104390689
    4并发编程面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104863992
    5JVM面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104390752
    6Spring面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397516
    7Spring MVC面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397427
    8Spring Boot面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397299
    9Spring Cloud面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397367
    10MyBatis面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/101292950
    11Redis面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/103522351
    12MySQL数据库面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104778621
    13消息中间件MQ与RabbitMQ面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104588612
    14Dubbo面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104390006
    15Linux面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104588679
    16Tomcat面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397665
    17ZooKeeper面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104397719
    18Netty面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/104391081
    19架构设计&分布式&数据结构与算法面试题(2020最新版)https://thinkwon.blog.csdn.net/article/details/105870730

    数据库基础知识

    为什么要使用数据库

    数据保存在内存

    优点: 存取速度快

    缺点: 数据不能永久保存

    数据保存在文件

    优点: 数据永久保存

    缺点:1)速度比内存操作慢,频繁的IO操作。2)查询数据不方便

    数据保存在数据库

    1)数据永久保存

    2)使用SQL语句,查询方便效率高。

    3)管理数据方便

    什么是SQL?

    结构化查询语言(Structured Query Language)简称SQL,是一种数据库查询语言。

    作用:用于存取数据、查询、更新和管理关系数据库系统。

    什么是MySQL?

    MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件之一。在Java企业级开发中非常常用,因为 MySQL 是开源免费的,并且方便扩展。

    数据库三大范式是什么

    第一范式:每个列都不可以再拆分。

    第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。

    第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。

    在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。

    mysql有关权限的表都有哪几个

    MySQL服务器通过权限表来控制用户对数据库的访问,权限表存放在mysql数据库里,由mysql_install_db脚本初始化。这些权限表分别user,db,table_priv,columns_priv和host。下面分别介绍一下这些表的结构和内容:

    • user权限表:记录允许连接到服务器的用户帐号信息,里面的权限是全局级的。
    • db权限表:记录各个帐号在各个数据库上的操作权限。
    • table_priv权限表:记录数据表级的操作权限。
    • columns_priv权限表:记录数据列级的操作权限。
    • host权限表:配合db权限表对给定主机上数据库级操作权限作更细致的控制。这个权限表不受GRANT和REVOKE语句的影响。

    MySQL的binlog有有几种录入格式?分别有什么区别?

    有三种格式,statement,row和mixed。

    • statement模式下,每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。由于sql的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制。
    • row级别下,不记录sql语句上下文相关信息,仅保存哪条记录被修改。记录单元为每一行的改动,基本是可以全部记下来但是由于很多操作,会导致大量行的改动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大。
    • mixed,一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row。

    此外,新版的MySQL中对row级别也做了一些优化,当表结构发生变化的时候,会记录语句而不是逐行记录。

    数据类型

    mysql有哪些数据类型

    分类类型名称说明
    整数类型tinyInt很小的整数(8位二进制)
    smallint小的整数(16位二进制)
    mediumint中等大小的整数(24位二进制)
    int(integer)普通大小的整数(32位二进制)
    小数类型float单精度浮点数
    double双精度浮点数
    decimal(m,d)压缩严格的定点数
    日期类型yearYYYY 1901~2155
    timeHH:MM:SS -838:59:59~838:59:59
    dateYYYY-MM-DD 1000-01-01~9999-12-3
    datetimeYYYY-MM-DD HH:MM:SS 1000-01-01 00:00:00~ 9999-12-31 23:59:59
    timestampYYYY-MM-DD HH:MM:SS 19700101 00:00:01 UTC~2038-01-19 03:14:07UTC
    文本、二进制类型CHAR(M)M为0~255之间的整数
    VARCHAR(M)M为0~65535之间的整数
    TINYBLOB允许长度0~255字节
    BLOB允许长度0~65535字节
    MEDIUMBLOB允许长度0~167772150字节
    LONGBLOB允许长度0~4294967295字节
    TINYTEXT允许长度0~255字节
    TEXT允许长度0~65535字节
    MEDIUMTEXT允许长度0~167772150字节
    LONGTEXT允许长度0~4294967295字节
    VARBINARY(M)允许长度0~M个字节的变长字节字符串
    BINARY(M)允许长度0~M个字节的定长字节字符串
    • 1、整数类型,包括TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT,分别表示1字节、2字节、3字节、4字节、8字节整数。任何整数类型都可以加上UNSIGNED属性,表示数据是无符号的,即非负整数。
      长度:整数类型可以被指定长度,例如:INT(11)表示长度为11的INT类型。长度在大多数场景是没有意义的,它不会限制值的合法范围,只会影响显示字符的个数,而且需要和UNSIGNED ZEROFILL属性配合使用才有意义。
      例子,假定类型设定为INT(5),属性为UNSIGNED ZEROFILL,如果用户插入的数据为12的话,那么数据库实际存储数据为00012。

    • 2、实数类型,包括FLOAT、DOUBLE、DECIMAL。
      DECIMAL可以用于存储比BIGINT还大的整型,能存储精确的小数。
      而FLOAT和DOUBLE是有取值范围的,并支持使用标准的浮点进行近似计算。
      计算时FLOAT和DOUBLE相比DECIMAL效率更高一些,DECIMAL你可以理解成是用字符串进行处理。

    • 3、字符串类型,包括VARCHAR、CHAR、TEXT、BLOB
      VARCHAR用于存储可变长字符串,它比定长类型更节省空间。
      VARCHAR使用额外1或2个字节存储字符串长度。列长度小于255字节时,使用1字节表示,否则使用2字节表示。
      VARCHAR存储的内容超出设置的长度时,内容会被截断。
      CHAR是定长的,根据定义的字符串长度分配足够的空间。
      CHAR会根据需要使用空格进行填充方便比较。
      CHAR适合存储很短的字符串,或者所有值都接近同一个长度。
      CHAR存储的内容超出设置的长度时,内容同样会被截断。

      使用策略:
      对于经常变更的数据来说,CHAR比VARCHAR更好,因为CHAR不容易产生碎片。
      对于非常短的列,CHAR比VARCHAR在存储空间上更有效率。
      使用时要注意只分配需要的空间,更长的列排序时会消耗更多内存。
      尽量避免使用TEXT/BLOB类型,查询时会使用临时表,导致严重的性能开销。

    • 4、枚举类型(ENUM),把不重复的数据存储为一个预定义的集合。
      有时可以使用ENUM代替常用的字符串类型。
      ENUM存储非常紧凑,会把列表值压缩到一个或两个字节。
      ENUM在内部存储时,其实存的是整数。
      尽量避免使用数字作为ENUM枚举的常量,因为容易混乱。
      排序是按照内部存储的整数

    • 5、日期和时间类型,尽量使用timestamp,空间效率高于datetime,
      用整数保存时间戳通常不方便处理。
      如果需要存储微妙,可以使用bigint存储。
      看到这里,这道真题是不是就比较容易回答了。

    引擎

    MySQL存储引擎MyISAM与InnoDB区别

    存储引擎Storage engine:MySQL中的数据、索引以及其他对象是如何存储的,是一套文件系统的实现。

    常用的存储引擎有以下:

    • Innodb引擎:Innodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统。
    • MyIASM引擎(原本Mysql的默认引擎):不提供事务的支持,也不支持行级锁和外键。
    • MEMORY引擎:所有的数据都在内存中,数据的处理速度快,但是安全性不高。

    MyISAM与InnoDB区别

    MyISAMInnodb
    存储结构每张表被存放在三个文件:frm-表格定义、MYD(MYData)-数据文件、MYI(MYIndex)-索引文件所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB
    存储空间MyISAM可被压缩,存储空间较小InnoDB的表需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引
    可移植性、备份及恢复由于MyISAM的数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作免费的方案可以是拷贝数据文件、备份 binlog,或者用 mysqldump,在数据量达到几十G的时候就相对痛苦了
    文件格式数据和索引是分别存储的,数据.MYD,索引.MYI数据和索引是集中存储的,.ibd
    记录存储顺序按记录插入顺序保存按主键大小有序插入
    外键不支持支持
    事务不支持支持
    锁支持(锁是避免资源争用的一个机制,MySQL锁对用户几乎是透明的)表级锁定行级锁定、表级锁定,锁定力度小并发能力高
    SELECTMyISAM更优
    INSERT、UPDATE、DELETEInnoDB更优
    select count(*)myisam更快,因为myisam内部维护了一个计数器,可以直接调取。
    索引的实现方式B+树索引,myisam 是堆表B+树索引,Innodb 是索引组织表
    哈希索引不支持支持
    全文索引支持不支持

    MyISAM索引与InnoDB索引的区别?

    • InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
    • InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
    • MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
    • InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。

    InnoDB引擎的4大特性

    • 插入缓冲(insert buffer)

    • 二次写(double write)

    • 自适应哈希索引(ahi)

    • 预读(read ahead)

    存储引擎选择

    如果没有特别的需求,使用默认的Innodb即可。

    MyISAM:以读写插入为主的应用程序,比如博客系统、新闻门户网站。

    Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键。比如OA自动化办公系统。

    索引

    什么是索引?

    索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。

    索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。

    更通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。索引是一个文件,它是要占据物理空间的。

    索引有哪些优缺点?

    索引的优点

    • 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
    • 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

    索引的缺点

    • 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;
    • 空间方面:索引需要占物理空间。

    索引使用场景(重点)

    where

    img

    上图中,根据id查询记录,因为id字段仅建立了主键索引,因此此SQL执行可选的索引只有主键索引,如果有多个,最终会选一个较优的作为检索的依据。

    -- 增加一个没有建立索引的字段
    alter table innodb1 add sex char(1);
    -- 按sex检索时可选的索引为null
    EXPLAIN SELECT * from innodb1 where sex='男';
    

    img

    可以尝试在一个字段未建立索引时,根据该字段查询的效率,然后对该字段建立索引(alter table 表名 add index(字段名)),同样的SQL执行的效率,你会发现查询效率会有明显的提升(数据量越大越明显)。

    order by

    当我们使用order by将查询结果按照某个字段排序时,如果该字段没有建立索引,那么执行计划会将查询出的所有数据使用外部排序(将数据从硬盘分批读取到内存使用内部排序,最后合并排序结果),这个操作是很影响性能的,因为需要将查询涉及到的所有数据从磁盘中读到内存(如果单条数据过大或者数据量过多都会降低效率),更无论读到内存之后的排序了。

    但是如果我们对该字段建立索引alter table 表名 add index(字段名),那么由于索引本身是有序的,因此直接按照索引的顺序和映射关系逐条取出数据即可。而且如果分页的,那么只用取出索引表某个范围内的索引对应的数据,而不用像上述那取出所有数据进行排序再返回某个范围内的数据。(从磁盘取数据是最影响性能的)

    join

    join语句匹配关系(on)涉及的字段建立索引能够提高效率

    索引覆盖

    如果要查询的字段都建立过索引,那么引擎会直接在索引表中查询而不会访问原始数据(否则只要有一个字段没有建立索引就会做全表扫描),这叫索引覆盖。因此我们需要尽可能的在select只写必要的查询字段,以增加索引覆盖的几率。

    这里值得注意的是不要想着为每个字段建立索引,因为优先使用索引的优势就在于其体积小。

    索引有哪几种类型?

    主键索引: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。

    唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。

    • 可以通过 ALTER TABLE table_name ADD UNIQUE (column); 创建唯一索引

    • 可以通过 ALTER TABLE table_name ADD UNIQUE (column1,column2); 创建唯一组合索引

    普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。

    • 可以通过ALTER TABLE table_name ADD INDEX index_name (column);创建普通索引

    • 可以通过ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);创建组合索引

    全文索引: 是目前搜索引擎使用的一种关键技术。

    • 可以通过ALTER TABLE table_name ADD FULLTEXT (column);创建全文索引

    索引的数据结构(b树,hash)

    索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有Hash索引B+树索引等,而我们经常使用的InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。

    1)B树索引

    mysql通过存储引擎取数据,基本上90%的人用的就是InnoDB了,按照实现方式分,InnoDB的索引类型目前只有两种:BTREE(B树)索引和HASH索引。B树索引是Mysql数据库中使用最频繁的索引类型,基本所有存储引擎都支持BTree索引。通常我们说的索引不出意外指的就是(B树)索引(实际是用B+树实现的,因为在查看表索引时,mysql一律打印BTREE,所以简称为B树索引)

    img

    查询方式:

    主键索引区:PI(关联保存的时数据的地址)按主键查询,

    普通索引区:si(关联的id的地址,然后再到达上面的地址)。所以按主键查询,速度最快

    B+tree性质:

    1.)n棵子tree的节点包含n个关键字,不用来保存数据而是保存数据的索引。

    2.)所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。

    3.)所有的非终端结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。

    4.)B+ 树中,数据对象的插入和删除仅在叶节点上进行。

    5.)B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点。

    2)哈希索引

    简要说下,类似于数据结构中简单实现的HASH表(散列表)一样,当我们在mysql中用哈希索引时,主要就是通过Hash算法(常见的Hash算法有直接定址法、平方取中法、折叠法、除数取余法、随机数法),将数据库字段数据转换成定长的Hash值,与这条数据的行指针一并存入Hash表的对应位置;如果发生Hash碰撞(两个不同关键字的Hash值相同),则在对应Hash键下以链表形式存储。当然这只是简略模拟图。

    img

    索引的基本原理

    索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。

    索引的原理很简单,就是把无序的数据变成有序的查询

    1. 把创建了索引的列的内容进行排序

    2. 对排序结果生成倒排表

    3. 在倒排表内容上拼上数据地址链

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

    索引算法有哪些?

    索引算法有 BTree算法和Hash算法

    BTree算法

    BTree是最常用的mysql数据库索引算法,也是mysql默认的算法。因为它不仅可以被用在=,>,>=,<,<=和between这些比较操作符上,而且还可以用于like操作符,只要它的查询条件是一个不以通配符开头的常量, 例如:

    -- 只要它的查询条件是一个不以通配符开头的常量
    select * from user where name like 'jack%'; 
    -- 如果一通配符开头,或者没有使用常量,则不会使用索引,例如: 
    select * from user where name like '%jack'; 
    

    Hash算法

    Hash Hash索引只能用于对等比较,例如=,<=>(相当于=)操作符。由于是一次定位数据,不像BTree索引需要从根节点到枝节点,最后才能访问到页节点这样多次IO访问,所以检索效率远高于BTree索引。

    索引设计的原则?

    1. 适合索引的列是出现在where子句中的列,或者连接子句中指定的列
    2. 基数较小的类,索引效果较差,没有必要在此列建立索引
    3. 使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间
    4. 不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。

    创建索引的原则(重中之重)

    索引虽好,但也不是无限制的使用,最好符合一下几个原则

    1) 最左前缀匹配原则,组合索引非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。

    2)较频繁作为查询条件的字段才去创建索引

    3)更新频繁字段不适合创建索引

    4)若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)

    5)尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

    6)定义有外键的数据列一定要建立索引。

    7)对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。

    8)对于定义为text、image和bit的数据类型的列不要建立索引。

    创建索引的三种方式,删除索引

    第一种方式:在执行CREATE TABLE时创建索引

    CREATE TABLE user_index2 (
    	id INT auto_increment PRIMARY KEY,
    	first_name VARCHAR (16),
    	last_name VARCHAR (16),
    	id_card VARCHAR (18),
    	information text,
    	KEY name (first_name, last_name),
    	FULLTEXT KEY (information),
    	UNIQUE KEY (id_card)
    );
    

    第二种方式:使用ALTER TABLE命令去增加索引

    ALTER TABLE table_name ADD INDEX index_name (column_list);
    

    ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。

    其中table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。

    索引名index_name可自己命名,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。

    第三种方式:使用CREATE INDEX命令创建

    CREATE INDEX index_name ON table_name (column_list);
    

    CREATE INDEX可对表增加普通索引或UNIQUE索引。(但是,不能创建PRIMARY KEY索引)

    删除索引

    根据索引名删除普通索引、唯一索引、全文索引:alter table 表名 drop KEY 索引名

    alter table user_index drop KEY name;
    alter table user_index drop KEY id_card;
    alter table user_index drop KEY information;
    

    删除主键索引:alter table 表名 drop primary key(因为主键只有一个)。这里值得注意的是,如果主键自增长,那么不能直接执行此操作(自增长依赖于主键索引):

    img

    需要取消自增长再行删除:

    alter table user_index
    -- 重新定义字段
    MODIFY id int,
    drop PRIMARY KEY
    

    但通常不会删除主键,因为设计主键一定与业务逻辑无关。

    创建索引时需要注意什么?

    • 非空字段:应该指定列为NOT NULL,除非你想存储NULL。在mysql中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值;
    • 取值离散大的字段:(变量各个取值之间的差异程度)的列放到联合索引的前面,可以通过count()函数查看字段的差异值,返回值越大说明字段的唯一值越多字段的离散程度高;
    • 索引字段越小越好:数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高。

    使用索引查询一定能提高查询的性能吗?为什么

    通常,通过索引查询数据比全表扫描要快。但是我们也必须注意到它的代价。

    • 索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改。 这意味着每条记录的INSERT,DELETE,UPDATE将为此多付出4,5 次的磁盘I/O。 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢。使用索引查询不一定能提高查询性能,索引范围查询(INDEX RANGE SCAN)适用于两种情况:
    • 基于一个范围的检索,一般查询返回结果集小于表中记录数的30%
    • 基于非唯一性索引的检索

    百万级别或以上的数据如何删除

    关于索引:由于索引需要额外的维护成本,因为索引文件是单独存在的文件,所以当我们对数据的增加,修改,删除,都会产生额外的对索引文件的操作,这些操作需要消耗额外的IO,会降低增/改/删的执行效率。所以,在我们删除数据库百万级别数据的时候,查询MySQL官方手册得知删除数据的速度和创建的索引数量是成正比的。

    1. 所以我们想要删除百万数据的时候可以先删除索引(此时大概耗时三分多钟)
    2. 然后删除其中无用数据(此过程需要不到两分钟)
    3. 删除完成后重新创建索引(此时数据较少了)创建索引也非常快,约十分钟左右。
    4. 与之前的直接删除绝对是要快速很多,更别说万一删除中断,一切删除会回滚。那更是坑了。

    前缀索引

    语法:index(field(10)),使用字段值的前10个字符建立索引,默认是使用字段的全部内容建立索引。

    前提:前缀的标识度高。比如密码就适合建立前缀索引,因为密码几乎各不相同。

    实操的难度:在于前缀截取的长度。

    我们可以利用select count(*)/count(distinct left(password,prefixLen));,通过从调整prefixLen的值(从1自增)查看不同前缀长度的一个平均匹配度,接近1时就可以了(表示一个密码的前prefixLen个字符几乎能确定唯一一条记录)

    什么是最左前缀原则?什么是最左匹配原则

    • 顾名思义,就是最左优先,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。
    • 最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
    • =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式

    B树和B+树的区别

    • 在B树中,你可以将键和值存放在内部节点和叶子节点;但在B+树中,内部节点都是键,没有值,叶子节点同时存放键和值。

    • B+树的叶子节点有一条链相连,而B树的叶子节点各自独立。

      img

    使用B树的好处

    B树可以在内部节点同时存储键和值,因此,把频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率。这种特性使得B树在特定数据重复多次查询的场景中更加高效。

    使用B+树的好处

    由于B+树的内部节点只存放键,不存放值,因此,一次读取,可以在内存页中获取更多的键,有利于更快地缩小查找范围。 B+树的叶节点由一条链相连,因此,当需要进行一次全数据遍历的时候,B+树只需要使用O(logN)时间找到最小的一个节点,然后通过链进行O(N)的顺序遍历即可。而B树则需要对树的每一层进行遍历,这会需要更多的内存置换次数,因此也就需要花费更多的时间

    Hash索引和B+树所有有什么区别或者说优劣呢?

    首先要知道Hash索引和B+树索引的底层实现原理:

    hash索引底层就是hash表,进行查找时,调用一次hash函数就可以获取到相应的键值,之后进行回表查询获得实际数据。B+树底层实现是多路平衡查找树。对于每一次的查询都是从根节点出发,查找到叶子节点方可以获得所查键值,然后根据查询判断是否需要回表查询数据。

    那么可以看出他们有以下的不同:

    • hash索引进行等值查询更快(一般情况下),但是却无法进行范围查询。

    因为在hash索引中经过hash函数建立索引之后,索引的顺序与原顺序无法保持一致,不能支持范围查询。而B+树的的所有节点皆遵循(左节点小于父节点,右节点大于父节点,多叉树也类似),天然支持范围。

    • hash索引不支持使用索引进行排序,原理同上。
    • hash索引不支持模糊查询以及多列索引的最左前缀匹配。原理也是因为hash函数的不可预测。AAAA和AAAAB的索引没有相关性。
    • hash索引任何时候都避免不了回表查询数据,而B+树在符合某些条件(聚簇索引,覆盖索引等)的时候可以只通过索引完成查询。
    • hash索引虽然在等值查询上较快,但是不稳定。性能不可预测,当某个键值存在大量重复的时候,发生hash碰撞,此时效率可能极差。而B+树的查询效率比较稳定,对于所有的查询都是从根节点到叶子节点,且树的高度较低。

    因此,在大多数情况下,直接选择B+树索引可以获得稳定且较好的查询速度。而不需要使用hash索引。

    数据库为什么使用B+树而不是B树

    • B树只适合随机检索,而B+树同时支持随机检索和顺序检索;
    • B+树空间利用率更高,可减少I/O次数,磁盘读写代价更低。一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗。B+树的内部结点并没有指向关键字具体信息的指针,只是作为索引使用,其内部结点比B树小,盘块能容纳的结点中关键字数量更多,一次性读入内存中可以查找的关键字也就越多,相对的,IO读写次数也就降低了。而IO读写次数是影响索引检索效率的最大因素;
    • B+树的查询效率更加稳定。B树搜索有可能会在非叶子结点结束,越靠近根节点的记录查找时间越短,只要找到关键字即可确定记录的存在,其性能等价于在关键字全集内做一次二分查找。而在B+树中,顺序检索比较明显,随机检索时,任何关键字的查找都必须走一条从根节点到叶节点的路,所有关键字的查找路径长度相同,导致每一个关键字的查询效率相当。
    • B-树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。B+树的叶子节点使用指针顺序连接在一起,只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作。
    • 增删文件(节点)时,效率更高。因为B+树的叶子节点包含所有关键字,并以有序的链表结构存储,这样可很好提高增删效率。

    B+树在满足聚簇索引和覆盖索引的时候不需要回表查询数据,

    在B+树的索引中,叶子节点可能存储了当前的key值,也可能存储了当前的key值以及整行的数据,这就是聚簇索引和非聚簇索引。 在InnoDB中,只有主键索引是聚簇索引,如果没有主键,则挑选一个唯一键建立聚簇索引。如果没有唯一键,则隐式的生成一个键来建立聚簇索引。

    当查询使用聚簇索引时,在对应的叶子节点,可以获取到整行数据,因此不用再次进行回表查询。

    什么是聚簇索引?何时使用聚簇索引与非聚簇索引

    • 聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据
    • 非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因

    澄清一个概念:innodb中,在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值

    何时使用聚簇索引与非聚簇索引

    img

    非聚簇索引一定会回表查询吗?

    不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。

    举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行select age from employee where age < 20的查询时,在索引的叶子节点上,已经包含了age信息,不会再次进行回表查询。

    联合索引是什么?为什么需要注意联合索引中的顺序?

    MySQL可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。

    具体原因为:

    MySQL使用索引时需要索引有序,假设现在建立了"name,age,school"的联合索引,那么索引的排序为: 先按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序。

    当进行查询时,此时索引仅仅按照name严格有序,因此必须首先使用name字段进行等值查询,之后对于匹配到的列而言,其按照age字段严格有序,此时可以使用age字段用做索引查找,以此类推。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。

    事务

    什么是数据库事务?

    事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。

    事务最经典也经常被拿出来说例子就是转账了。

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

    事物的四大特性(ACID)介绍一下?

    关系性数据库需要遵循ACID规则,具体内容如下:

    事务的特性

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

    什么是脏读?幻读?不可重复读?

    • 脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
    • 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
    • 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。

    什么是事务的隔离级别?MySQL的默认隔离级别是什么?

    为了达到事务的四大特性,数据库定义了4种不同的事务隔离级别,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。

    隔离级别脏读不可重复读幻影读
    READ-UNCOMMITTED
    READ-COMMITTED×
    REPEATABLE-READ××
    SERIALIZABLE×××

    SQL 标准定义了四个隔离级别:

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

    这里需要注意的是:Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别

    事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。

    因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)**并不会有任何性能损失。

    InnoDB 存储引擎在 分布式事务 的情况下一般会用到**SERIALIZABLE(可串行化)**隔离级别。

    对MySQL的锁了解吗

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

    就像酒店的房间,如果大家随意进出,就会出现多人抢夺同一个房间的情况,而在房间上装上锁,申请到钥匙的人才可以入住并且将房间锁起来,其他人只有等他使用完毕才可以再次使用。

    隔离级别与锁的关系

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

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

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

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

    按照锁的粒度分数据库锁有哪些?锁机制与InnoDB锁算法

    在关系型数据库中,可以按照锁的粒度把数据库锁分为行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎 )。

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

    • MyISAM采用表级锁(table-level locking)。
    • InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁

    行级锁,表级锁和页级锁对比

    行级锁 行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。

    特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

    表级锁 表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。

    特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。

    页级锁 页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。

    特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

    从锁的类别上分MySQL都有哪些锁呢?像上面那样子进行锁定岂不是有点阻碍并发效率了

    从锁的类别上来讲,有共享锁和排他锁。

    共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。

    排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。

    用上面的例子来说就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的。 一种是真正的入住一晚,在这期间,无论是想入住的还是想看房的都不可以。

    锁的粒度取决于具体的存储引擎,InnoDB实现了行级锁,页级锁,表级锁。

    他们的加锁开销从大到小,并发能力也是从大到小。

    MySQL中InnoDB引擎的行锁是怎么实现的?

    答:InnoDB是基于索引来完成行锁

    例: select * from tab_with_index where id = 1 for update;

    for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么InnoDB将完成表锁,并发将无从谈起

    InnoDB存储引擎的锁的算法有三种

    • Record lock:单个行记录上的锁
    • Gap lock:间隙锁,锁定一个范围,不包括记录本身
    • Next-key lock:record+gap 锁定一个范围,包含记录本身

    相关知识点:

    1. innodb对于行的查询使用next-key lock
    2. Next-locking keying为了解决Phantom Problem幻读问题
    3. 当查询的索引含有唯一属性时,将next-key lock降级为record key
    4. Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生
    5. 有两种方式显式关闭gap锁:(除了外键约束和唯一性检查外,其余情况仅使用record lock) A. 将事务隔离级别设置为RC B. 将参数innodb_locks_unsafe_for_binlog设置为1

    什么是死锁?怎么解决?

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

    常见的解决死锁的方法

    1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。

    2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;

    3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;

    如果业务处理不好可以用分布式事务锁或者使用乐观锁

    数据库的乐观锁和悲观锁是什么?怎么实现的?

    数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

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

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

    两种锁的使用场景

    从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。

    但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

    视图

    为什么要使用视图?什么是视图?

    为了提高复杂SQL语句的复用性和表操作的安全性,MySQL数据库管理系统提供了视图特性。所谓视图,本质上是一种虚拟表,在物理上是不存在的,其内容与真实的表相似,包含一系列带有名称的列和行数据。但是,视图并不在数据库中以储存的数据值形式存在。行和列数据来自定义视图的查询所引用基本表,并且在具体引用视图时动态生成。

    视图使开发者只关心感兴趣的某些特定数据和所负责的特定任务,只能看到视图中所定义的数据,而不是视图所引用表中的数据,从而提高了数据库中数据的安全性。

    视图有哪些特点?

    视图的特点如下:

    • 视图的列可以来自不同的表,是表的抽象和在逻辑意义上建立的新关系。

    • 视图是由基本表(实表)产生的表(虚表)。

    • 视图的建立和删除不影响基本表。

    • 对视图内容的更新(添加,删除和修改)直接影响基本表。

    • 当视图来自多个基本表时,不允许添加和删除数据。

    视图的操作包括创建视图,查看视图,删除视图和修改视图。

    视图的使用场景有哪些?

    视图根本用途:简化sql查询,提高开发效率。如果说还有另外一个用途那就是兼容老的表结构。

    下面是视图的常见使用场景:

    • 重用SQL语句;

    • 简化复杂的SQL操作。在编写查询后,可以方便的重用它而不必知道它的基本查询细节;

    • 使用表的组成部分而不是整个表;

    • 保护数据。可以给用户授予表的特定部分的访问权限而不是整个表的访问权限;

    • 更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。

    视图的优点

    1. 查询简单化。视图能简化用户的操作
    2. 数据安全性。视图使用户能以多种角度看待同一数据,能够对机密数据提供安全保护
    3. 逻辑数据独立性。视图对重构数据库提供了一定程度的逻辑独立性

    视图的缺点

    1. 性能。数据库必须把视图的查询转化成对基本表的查询,如果这个视图是由一个复杂的多表查询所定义,那么,即使是视图的一个简单查询,数据库也把它变成一个复杂的结合体,需要花费一定的时间。

    2. 修改限制。当用户试图修改视图的某些行时,数据库必须把它转化为对基本表的某些行的修改。事实上,当从视图中插入或者删除时,情况也是这样。对于简单视图来说,这是很方便的,但是,对于比较复杂的视图,可能是不可修改的

      这些视图有如下特征:1.有UNIQUE等集合操作符的视图。2.有GROUP BY子句的视图。3.有诸如AVG\SUM\MAX等聚合函数的视图。 4.使用DISTINCT关键字的视图。5.连接表的视图(其中有些例外)

    什么是游标?

    游标是系统为用户开设的一个数据缓冲区,存放SQL语句的执行结果,每个游标区都有一个名字。用户可以通过游标逐一获取记录并赋给主变量,交由主语言进一步处理。

    存储过程与函数

    什么是存储过程?有哪些优缺点?

    存储过程是一个预编译的SQL语句,优点是允许模块化的设计,就是说只需要创建一次,以后在该程序中就可以调用多次。如果某次操作需要执行多次SQL,使用存储过程比单纯SQL语句执行要快。

    优点

    1)存储过程是预编译过的,执行效率高。

    2)存储过程的代码直接存放于数据库中,通过存储过程名直接调用,减少网络通讯。

    3)安全性高,执行存储过程需要有一定权限的用户。

    4)存储过程可以重复使用,减少数据库开发人员的工作量。

    缺点

    1)调试麻烦,但是用 PL/SQL Developer 调试很方便!弥补这个缺点。

    2)移植问题,数据库端代码当然是与数据库相关的。但是如果是做工程型项目,基本不存在移植问题。

    3)重新编译问题,因为后端代码是运行前编译的,如果带有引用关系的对象发生改变时,受影响的存储过程、包将需要重新编译(不过也可以设置成运行时刻自动编译)。

    4)如果在一个程序系统中大量的使用存储过程,到程序交付使用的时候随着用户需求的增加会导致数据结构的变化,接着就是系统的相关问题了,最后如果用户想维护该系统可以说是很难很难、而且代价是空前的,维护起来更麻烦。

    触发器

    什么是触发器?触发器的使用场景有哪些?

    触发器是用户定义在关系表上的一类由事件驱动的特殊的存储过程。触发器是指一段代码,当触发某个事件时,自动执行这些代码。

    使用场景

    • 可以通过数据库中的相关表实现级联更改。
    • 实时监控某张表中的某个字段的更改而需要做出相应的处理。
    • 例如可以生成某些业务的编号。
    • 注意不要滥用,否则会造成数据库及应用程序的维护困难。
    • 大家需要牢记以上基础知识点,重点是理解数据类型CHAR和VARCHAR的差异,表存储引擎InnoDB和MyISAM的区别。

    MySQL中都有哪些触发器?

    在MySQL数据库中有如下六种触发器:

    • Before Insert
    • After Insert
    • Before Update
    • After Update
    • Before Delete
    • After Delete

    常用SQL语句

    SQL语句主要分为哪几类

    数据定义语言DDL(Data Ddefinition Language)CREATE,DROP,ALTER

    主要为以上操作 即对逻辑结构等有操作的,其中包括表结构,视图和索引。

    数据查询语言DQL(Data Query Language)SELECT

    这个较为好理解 即查询操作,以select关键字。各种简单查询,连接查询等 都属于DQL。

    数据操纵语言DML(Data Manipulation Language)INSERT,UPDATE,DELETE

    主要为以上操作 即对数据进行操作的,对应上面所说的查询操作 DQL与DML共同构建了多数初级程序员常用的增删改查操作。而查询是较为特殊的一种 被划分到DQL中。

    数据控制功能DCL(Data Control Language)GRANT,REVOKE,COMMIT,ROLLBACK

    主要为以上操作 即对数据库安全性完整性等有操作的,可以简单的理解为权限控制等。

    超键、候选键、主键、外键分别是什么?

    • 超键:在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以为作为一个超键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键。
    • 候选键:是最小超键,即没有冗余元素的超键。
    • 主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。
    • 外键:在一个表中存在的另一个表的主键称此表的外键。

    SQL 约束有哪几种?

    SQL 约束有哪几种?

    • NOT NULL: 用于控制字段的内容一定不能为空(NULL)。
    • UNIQUE: 控件字段内容不能重复,一个表允许有多个 Unique 约束。
    • PRIMARY KEY: 也是用于控件字段内容不能重复,但它在一个表只允许出现一个。
    • FOREIGN KEY: 用于预防破坏表之间连接的动作,也能防止非法数据插入外键列,因为它必须是它指向的那个表中的值之一。
    • CHECK: 用于控制字段的值范围。

    六种关联查询

    • 交叉连接(CROSS JOIN)
    • 内连接(INNER JOIN)
    • 外连接(LEFT JOIN/RIGHT JOIN)
    • 联合查询(UNION与UNION ALL)
    • 全连接(FULL JOIN)
    • 交叉连接(CROSS JOIN)
    SELECT * FROM A,B(,C)或者SELECT * FROM A CROSS JOIN B (CROSS JOIN C)#没有任何关联条件,结果是笛卡尔积,结果集会很大,没有意义,很少使用内连接(INNER JOIN)SELECT * FROM A,B WHERE A.id=B.id或者SELECT * FROM A INNER JOIN B ON A.id=B.id多表中同时符合某种条件的数据记录的集合,INNER JOIN可以缩写为JOIN
    

    内连接分为三类

    • 等值连接:ON A.id=B.id
    • 不等值连接:ON A.id > B.id
    • 自连接:SELECT * FROM A T1 INNER JOIN A T2 ON T1.id=T2.pid

    外连接(LEFT JOIN/RIGHT JOIN)

    • 左外连接:LEFT OUTER JOIN, 以左表为主,先查询出左表,按照ON后的关联条件匹配右表,没有匹配到的用NULL填充,可以简写成LEFT JOIN
    • 右外连接:RIGHT OUTER JOIN, 以右表为主,先查询出右表,按照ON后的关联条件匹配左表,没有匹配到的用NULL填充,可以简写成RIGHT JOIN

    联合查询(UNION与UNION ALL)

    SELECT * FROM A UNION SELECT * FROM B UNION ...
    
    • 就是把多个结果集集中在一起,UNION前的结果为基准,需要注意的是联合查询的列数要相等,相同的记录行会合并
    • 如果使用UNION ALL,不会合并重复的记录行
    • 效率 UNION 高于 UNION ALL

    全连接(FULL JOIN)

    • MySQL不支持全连接
    • 可以使用LEFT JOIN 和UNION和RIGHT JOIN联合使用
    SELECT * FROM A LEFT JOIN B ON A.id=B.id UNIONSELECT * FROM A RIGHT JOIN B ON A.id=B.id
    

    表连接面试题

    有2张表,1张R、1张S,R表有ABC三列,S表有CD两列,表中各有三条记录。

    R表

    ABC
    a1b1c1
    a2b2c2
    a3b3c3

    S表

    CD
    c1d1
    c2d2
    c4d3
    1. 交叉连接(笛卡尔积):

    select r.*,s.* from r,s

    ABCCD
    a1b1c1c1d1
    a2b2c2c1d1
    a3b3c3c1d1
    a1b1c1c2d2
    a2b2c2c2d2
    a3b3c3c2d2
    a1b1c1c4d3
    a2b2c2c4d3
    a3b3c3c4d3
    1. 内连接结果:

      select r.*,s.* from r inner join s on r.c=s.c

    ABCCD
    a1b1c1c1d1
    a2b2c2c2d2
    1. 左连接结果:

      select r.*,s.* from r left join s on r.c=s.c

    ABCCD
    a1b1c1c1d1
    a2b2c2c2d2
    a3b3c3
    1. 右连接结果:

      select r.*,s.* from r right join s on r.c=s.c

    ABCCD
    a1b1c1c1d1
    a2b2c2c2d2
    c4d3
    1. 全表连接的结果(MySql不支持,Oracle支持):

      select r.*,s.* from r full join s on r.c=s.c

    ABCCD
    a1b1c1c1d1
    a2b2c2c2d2
    a3b3c3
    c4d3

    什么是子查询

    1. 条件:一条SQL语句的查询结果做为另一条查询语句的条件或查询结果

    2. 嵌套:多条SQL语句嵌套使用,内部的SQL查询语句称为子查询。

    子查询的三种情况

    1. 子查询是单行单列的情况:结果集是一个值,父查询使用:=、 <、 > 等运算符
    -- 查询工资最高的员工是谁? 
    select  * from employee where salary=(select max(salary) from employee);   
    
    1. 子查询是多行单列的情况:结果集类似于一个数组,父查询使用:in 运算符
    -- 查询工资最高的员工是谁? 
    select  * from employee where salary=(select max(salary) from employee);    
    
    1. 子查询是多行多列的情况:结果集类似于一张虚拟表,不能用于where条件,用于select子句中做为子表
    -- 1) 查询出2011年以后入职的员工信息
    -- 2) 查询所有的部门信息,与上面的虚拟表中的信息比对,找出所有部门ID相等的员工。
    select * from dept d,  (select * from employee where join_date > '2011-1-1') e where e.dept_id =  d.id;    
    
    -- 使用表连接:
    select d.*, e.* from  dept d inner join employee e on d.id = e.dept_id where e.join_date >  '2011-1-1'  
    

    mysql中 in 和 exists 区别

    mysql中的in语句是把外表和内表作hash 连接,而exists语句是对外表作loop循环,每次loop循环再对内表进行查询。一直大家都认为exists比in语句的效率要高,这种说法其实是不准确的。这个是要区分环境的。

    1. 如果查询的两个表大小相当,那么用in和exists差别不大。
    2. 如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in。
    3. not in 和not exists:如果查询语句使用了not in,那么内外表都进行全表扫描,没有用到索引;而not extsts的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快。

    varchar与char的区别

    char的特点

    • char表示定长字符串,长度是固定的;

    • 如果插入数据的长度小于char的固定长度时,则用空格填充;

    • 因为长度固定,所以存取速度要比varchar快很多,甚至能快50%,但正因为其长度固定,所以会占据多余的空间,是空间换时间的做法;

    • 对于char来说,最多能存放的字符个数为255,和编码无关

    varchar的特点

    • varchar表示可变长字符串,长度是可变的;

    • 插入的数据是多长,就按照多长来存储;

    • varchar在存取方面与char相反,它存取慢,因为长度不固定,但正因如此,不占据多余的空间,是时间换空间的做法;

    • 对于varchar来说,最多能存放的字符个数为65532

    总之,结合性能角度(char更快)和节省磁盘空间角度(varchar更小),具体情况还需具体来设计数据库才是妥当的做法。

    varchar(50)中50的涵义

    最多存放50个字符,varchar(50)和(200)存储hello所占空间一样,但后者在排序时会消耗更多内存,因为order by col采用fixed_length计算col长度(memory引擎也一样)。在早期 MySQL 版本中, 50 代表字节数,现在代表字符数。

    int(20)中20的涵义

    是指显示字符的长度。20表示最大显示宽度为20,但仍占4字节存储,存储范围不变;

    不影响内部存储,只是影响带 zerofill 定义的 int 时,前面补多少个 0,易于报表展示

    mysql为什么这么设计

    对大多数应用没有意义,只是规定一些工具用来显示字符的个数;int(1)和int(20)存储和计算均一样;

    mysql中int(10)和char(10)以及varchar(10)的区别

    • int(10)的10表示显示的数据的长度,不是存储数据的大小;chart(10)和varchar(10)的10表示存储数据的大小,即表示存储多少个字符。

      int(10) 10位的数据长度 9999999999,占32个字节,int型4位
      char(10) 10位固定字符串,不足补空格 最多10个字符
      varchar(10) 10位可变字符串,不足补空格 最多10个字符

    • char(10)表示存储定长的10个字符,不足10个就用空格补齐,占用更多的存储空间

    • varchar(10)表示存储10个变长的字符,存储多少个就是多少个,空格也按一个字符存储,这一点是和char(10)的空格不同的,char(10)的空格表示占位不算一个字符

    FLOAT和DOUBLE的区别是什么?

    • FLOAT类型数据可以存储至多8位十进制数,并在内存中占4字节。
    • DOUBLE类型数据可以存储至多18位十进制数,并在内存中占8字节。

    drop、delete与truncate的区别

    三者都表示删除,但是三者有一些差别:

    DeleteTruncateDrop
    类型属于DML属于DDL属于DDL
    回滚可回滚不可回滚不可回滚
    删除内容表结构还在,删除表的全部或者一部分数据行表结构还在,删除表中的所有数据从数据库中删除表,所有的数据行,索引和权限也会被删除
    删除速度删除速度慢,需要逐行删除删除速度快删除速度最快

    因此,在不再需要一张表的时候,用drop;在想删除部分数据行时候,用delete;在保留表而删除所有数据的时候用truncate。

    UNION与UNION ALL的区别?

    • 如果使用UNION ALL,不会合并重复的记录行
    • 效率 UNION 高于 UNION ALL

    SQL优化

    如何定位及优化SQL语句的性能问题?创建的索引有没有被使用到?或者说怎么才可以知道这条语句运行很慢的原因?

    对于低性能的SQL语句的定位,最重要也是最有效的方法就是使用执行计划,MySQL提供了explain命令来查看语句的执行计划。 我们知道,不管是哪种数据库,或者是哪种数据库引擎,在对一条SQL语句进行执行的过程中都会做很多相关的优化,对于查询语句,最重要的优化方式就是使用索引。 而执行计划,就是显示数据库引擎对于SQL语句的执行的详细情况,其中包含了是否使用索引,使用什么索引,使用的索引的相关信息等

    在这里插入图片描述

    执行计划包含的信息 id 有一组数字组成。表示一个查询中各个子查询的执行顺序;

    • id相同执行顺序由上至下。
    • id不同,id值越大优先级越高,越先被执行。
    • id为null时表示一个结果集,不需要使用它查询,常出现在包含union等查询语句中。

    select_type 每个子查询的查询类型,一些常见的查询类型。

    idselect_typedescription
    1SIMPLE不包含任何子查询或union等查询
    2PRIMARY包含子查询最外层查询就显示为 PRIMARY
    3SUBQUERY在select或 where字句中包含的查询
    4DERIVEDfrom字句中包含的查询
    5UNION出现在union后的查询语句中
    6UNION RESULT从UNION中获取结果集,例如上文的第三个例子

    table 查询的数据表,当从衍生表中查数据时会显示 x 表示对应的执行计划id partitions 表分区、表创建的时候可以指定通过那个列进行表分区。 举个例子:

    create table tmp (
        id int unsigned not null AUTO_INCREMENT,
        name varchar(255),
        PRIMARY KEY (id)
    ) engine = innodb
    partition by key (id) partitions 5;
    

    type(非常重要,可以看到有没有走索引) 访问类型

    • ALL 扫描全表数据
    • index 遍历索引
    • range 索引范围查找
    • index_subquery 在子查询中使用 ref
    • unique_subquery 在子查询中使用 eq_ref
    • ref_or_null 对Null进行索引的优化的 ref
    • fulltext 使用全文索引
    • ref 使用非唯一索引查找数据
    • eq_ref 在join查询中使用PRIMARY KEYorUNIQUE NOT NULL索引关联。

    possible_keys 可能使用的索引,注意不一定会使用。查询涉及到的字段上若存在索引,则该索引将被列出来。当该列为 NULL时就要考虑当前的SQL是否需要优化了。

    key 显示MySQL在查询中实际使用的索引,若没有使用索引,显示为NULL。

    TIPS:查询中若使用了覆盖索引(覆盖索引:索引的数据覆盖了需要查询的所有数据),则该索引仅出现在key列表中

    key_length 索引长度

    ref 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值

    rows 返回估算的结果集数目,并不是一个准确的值。

    extra 的信息非常丰富,常见的有:

    1. Using index 使用覆盖索引
    2. Using where 使用了用where子句来过滤结果集
    3. Using filesort 使用文件排序,使用非索引列进行排序时出现,非常消耗性能,尽量优化。
    4. Using temporary 使用了临时表 sql优化的目标可以参考阿里开发手册
    【推荐】SQL性能优化的目标:至少要达到 range 级别,要求是ref级别,如果可以是consts最好。 
    说明: 
    1) consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。 
    2) ref 指的是使用普通的索引(normal index)。 
    3) range 对索引进行范围检索。 
    反例:explain表的结果,type=index,索引物理文件全扫描,速度非常慢,这个index级别比较range还低,与全表扫描是小巫见大巫。
    

    SQL的生命周期?

    1. 应用服务器与数据库服务器建立一个连接

    2. 数据库进程拿到请求sql

    3. 解析并生成执行计划,执行

    4. 读取数据到内存并进行逻辑处理

    5. 通过步骤一的连接,发送结果到客户端

    6. 关掉连接,释放资源

      在这里插入图片描述

    大表数据查询,怎么优化

    1. 优化shema、sql语句+索引;
    2. 第二加缓存,memcached, redis;
    3. 主从复制,读写分离;
    4. 垂直拆分,根据你模块的耦合度,将一个大的系统分为多个小的系统,也就是分布式系统;
    5. 水平切分,针对数据量大的表,这一步最麻烦,最能考验技术水平,要选择一个合理的sharding key, 为了有好的查询效率,表结构也要改动,做一定的冗余,应用也要改,sql中尽量带sharding key,将数据定位到限定的表上去查,而不是扫描全部的表;

    超大分页怎么处理?

    超大的分页一般从两个方向上来解决.

    • 数据库层面,这也是我们主要集中关注的(虽然收效没那么大),类似于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开发手册》中,对超大分页的解决办法是类似于上面提到的第一种.

    【推荐】利用延迟关联或者子查询优化超多分页场景。 
    
    说明:MySQL并不是跳过offset行,而是取offset+N行,然后返回放弃前offset行,返回N行,那当offset特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行SQL改写。 
    
    正例:先快速定位需要获取的id段,然后再关联: 
    
    SELECT a.* FROM1 a, (select id from1 where 条件 LIMIT 100000,20 ) b where a.id=b.id
    

    mysql 分页

    LIMIT 子句可以被用于强制 SELECT 语句返回指定的记录数。LIMIT 接受一个或两个数字参数。参数必须是一个整数常量。如果给定两个参数,第一个参数指定第一个返回记录行的偏移量,第二个参数指定返回记录行的最大数目。初始记录行的偏移量是 0(而不是 1)

    mysql> SELECT * FROM table LIMIT 5,10; // 检索记录行 6-15 
    

    为了检索从某一个偏移量到记录集的结束所有的记录行,可以指定第二个参数为 -1:

    mysql> SELECT * FROM table LIMIT 95,-1; // 检索记录行 96-last. 
    

    如果只给定一个参数,它表示返回最大的记录行数目:

    mysql> SELECT * FROM table LIMIT 5; //检索前 5 个记录行 
    

    换句话说,LIMIT n 等价于 LIMIT 0,n。

    慢查询日志

    用于记录执行时间超过某个临界值的SQL日志,用于快速定位慢查询,为我们的优化做参考。

    开启慢查询日志

    配置项:slow_query_log

    可以使用show variables like ‘slov_query_log’查看是否开启,如果状态值为OFF,可以使用set GLOBAL slow_query_log = on来开启,它会在datadir下产生一个xxx-slow.log的文件。

    设置临界时间

    配置项:long_query_time

    查看:show VARIABLES like 'long_query_time',单位秒

    设置:set long_query_time=0.5

    实操时应该从长时间设置到短的时间,即将最慢的SQL优化掉

    查看日志,一旦SQL超过了我们设置的临界时间就会被记录到xxx-slow.log

    关心过业务系统里面的sql耗时吗?统计过慢查询吗?对慢查询都怎么优化过?

    在业务系统中,除了使用主键进行的查询,其他的我都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。

    慢查询的优化首先要搞明白慢的原因是什么? 是查询条件没有命中索引?是load了不需要的数据列?还是数据量太大?

    所以优化也是针对这三个方向来的,

    • 首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。
    • 分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。
    • 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。

    为什么要尽量设定一个主键?

    主键是数据库确保数据行在整张表唯一性的保障,即使业务上本张表没有主键,也建议添加一个自增长的ID列作为主键。设定了主键之后,在后续的删改查的时候可能更加快速以及确保操作数据范围安全。

    主键使用自增ID还是UUID?

    推荐使用自增ID,不要使用UUID。

    因为在InnoDB存储引擎中,主键索引是作为聚簇索引存在的,也就是说,主键索引的B+树叶子节点上存储了主键索引以及全部的数据(按照顺序),如果主键索引是自增ID,那么只需要不断向后排列即可,如果是UUID,由于到来的ID与原来的大小不确定,会造成非常多的数据插入,数据移动,然后导致产生很多的内存碎片,进而造成插入性能的下降。

    总之,在数据量大一些的情况下,用自增主键性能会好一些。

    关于主键是聚簇索引,如果没有主键,InnoDB会选择一个唯一键来作为聚簇索引,如果没有唯一键,会生成一个隐式的主键。

    字段为什么要求定义为not null?

    null值会占用更多的字节,且会在程序中造成很多与预期不符的情况。

    如果要存储用户的密码散列,应该使用什么字段进行存储?

    密码散列,盐,用户身份证号等固定长度的字符串应该使用char而不是varchar来存储,这样可以节省空间且提高检索效率。

    优化查询过程中的数据访问

    • 访问数据太多导致查询性能下降
    • 确定应用程序是否在检索大量超过需要的数据,可能是太多行或列
    • 确认MySQL服务器是否在分析大量不必要的数据行
    • 避免犯如下SQL语句错误
    • 查询不需要的数据。解决办法:使用limit解决
    • 多表关联返回全部列。解决办法:指定列名
    • 总是返回全部列。解决办法:避免使用SELECT *
    • 重复查询相同的数据。解决办法:可以缓存数据,下次直接读取缓存
    • 是否在扫描额外的记录。解决办法:
    • 使用explain进行分析,如果发现查询需要扫描大量的数据,但只返回少数的行,可以通过如下技巧去优化:
    • 使用索引覆盖扫描,把所有的列都放到索引中,这样存储引擎不需要回表获取对应行就可以返回结果。
    • 改变数据库和表的结构,修改数据表范式
    • 重写SQL语句,让优化器可以以更优的方式执行查询。

    优化长难的查询语句

    • 一个复杂查询还是多个简单查询
    • MySQL内部每秒能扫描内存中上百万行数据,相比之下,响应数据给客户端就要慢得多
    • 使用尽可能小的查询是好的,但是有时将一个大的查询分解为多个小的查询是很有必要的。
    • 切分查询
    • 将一个大的查询分为多个小的相同的查询
    • 一次性删除1000万的数据要比一次删除1万,暂停一会的方案更加损耗服务器开销。
    • 分解关联查询,让缓存的效率更高。
    • 执行单个查询可以减少锁的竞争。
    • 在应用层做关联更容易对数据库进行拆分。
    • 查询效率会有大幅提升。
    • 较少冗余记录的查询。

    优化特定类型的查询语句

    • count(*)会忽略所有的列,直接统计所有列数,不要使用count(列名)
    • MyISAM中,没有任何where条件的count(*)非常快。
    • 当有where条件时,MyISAM的count统计不一定比其它引擎快。
    • 可以使用explain查询近似值,用近似值替代count(*)
    • 增加汇总表
    • 使用缓存

    优化关联查询

    • 确定ON或者USING子句中是否有索引。
    • 确保GROUP BY和ORDER BY只有一个表中的列,这样MySQL才有可能使用索引。

    优化子查询

    • 用关联查询替代
    • 优化GROUP BY和DISTINCT
    • 这两种查询据可以使用索引来优化,是最有效的优化方法
    • 关联查询中,使用标识列分组的效率更高
    • 如果不需要ORDER BY,进行GROUP BY时加ORDER BY NULL,MySQL不会再进行文件排序。
    • WITH ROLLUP超级聚合,可以挪到应用程序处理

    优化LIMIT分页

    • LIMIT偏移量大的时候,查询效率较低
    • 可以记录上次查询的最大ID,下次查询时直接根据该ID来查询

    优化UNION查询

    • UNION ALL的效率高于UNION

    优化WHERE子句

    解题方法

    对于此类考题,先说明如何定位低效SQL语句,然后根据SQL语句可能低效的原因做排查,先从索引着手,如果索引没有问题,考虑以上几个方面,数据访问的问题,长难查询句的问题还是一些特定类型优化的问题,逐一回答。

    SQL语句优化的一些方法?

    • 1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
    • 2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
    select id from t where num is null
    -- 可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
    select id from t where num=
    
    • 3.应尽量避免在 where 子句中使用!=或<>操作符,否则引擎将放弃使用索引而进行全表扫描。
    • 4.应尽量避免在 where 子句中使用or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:
    select id from t where num=10 or num=20
    -- 可以这样查询:
    select id from t where num=10 union all select id from t where num=20
    
    • 5.in 和 not in 也要慎用,否则会导致全表扫描,如:
    select id from t where num in(1,2,3) 
    -- 对于连续的数值,能用 between 就不要用 in 了:
    select id from t where num between 1 and 3
    
    • 6.下面的查询也将导致全表扫描:select id from t where name like ‘%李%’若要提高效率,可以考虑全文检索。
    • 7.如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然 而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:
    select id from t where num=@num
    -- 可以改为强制查询使用索引:
    select id from t with(index(索引名)) where num=@num
    
    • 8.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:
    select id from t where num/2=100
    -- 应改为:
    select id from t where num=100*2
    
    • 9.应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:
    select id from t where substring(name,1,3)=’abc’
    -- name以abc开头的id应改为:
    select id from t where name like ‘abc%
    • 10.不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。

    数据库优化

    为什么要优化

    • 系统的吞吐量瓶颈往往出现在数据库的访问速度上
    • 随着应用程序的运行,数据库的中的数据会越来越多,处理时间会相应变慢
    • 数据是存放在磁盘上的,读写速度无法和内存相比

    优化原则:减少系统瓶颈,减少资源占用,增加系统的反应速度。

    数据库结构优化

    一个好的数据库设计方案对于数据库的性能往往会起到事半功倍的效果。

    需要考虑数据冗余、查询和更新的速度、字段的数据类型是否合理等多方面的内容。

    将字段很多的表分解成多个表

    对于字段较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表。

    因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。

    增加中间表

    对于需要经常联合查询的表,可以建立中间表以提高查询效率。

    通过建立中间表,将需要通过联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询。

    增加冗余字段

    设计数据表时应尽量遵循范式理论的规约,尽可能的减少冗余字段,让数据库设计看起来精致、优雅。但是,合理的加入冗余字段可以提高查询速度。

    表的规范化程度越高,表和表之间的关系越多,需要连接查询的情况也就越多,性能也就越差。

    注意:

    冗余字段的值在一个表中修改了,就要想办法在其他表中更新,否则就会导致数据不一致的问题。

    MySQL数据库cpu飙升到500%的话他怎么处理?

    当 cpu 飙升到 500%时,先用操作系统命令 top 命令观察是不是 mysqld 占用导致的,如果不是,找出占用高的进程,并进行相关处理。

    如果是 mysqld 造成的, show processlist,看看里面跑的 session 情况,是不是有消耗资源的 sql 在运行。找出消耗高的 sql,看看执行计划是否准确, index 是否缺失,或者实在是数据量太大造成。

    一般来说,肯定要 kill 掉这些线程(同时观察 cpu 使用率是否下降),等进行相应的调整(比如说加索引、改 sql、改内存参数)之后,再重新跑这些 SQL。

    也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等

    大表怎么优化?某个表有近千万数据,CRUD比较慢,如何优化?分库分表了是怎么做的?分表分库了有什么问题?有用到中间件么?他们的原理知道么?

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

    1. 限定数据的范围: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内。;
    2. 读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读;
    3. 缓存: 使用MySQL的缓存,另外对重量级、更新少的数据可以考虑使用应用级别的缓存;

    还有就是通过分库分表的方式进行优化,主要有垂直分表和水平分表

    1. 垂直分区:

      根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。

      简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。 如下图所示,这样来说大家应该就更容易理解了。

      img

      垂直拆分的优点: 可以使得行数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。

      垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;

      垂直分表

      把主键和一些列放在一个表,然后把主键和另外的列放在另一个表中

      img

      适用场景
      • 1、如果一个表中某些列常用,另外一些列不常用
      • 2、可以使数据行变小,一个数据页能存储更多数据,查询时减少I/O次数
      缺点
      • 有些分表的策略基于应用层的逻辑算法,一旦逻辑算法改变,整个分表逻辑都会改变,扩展性较差
      • 对于应用层来说,逻辑算法增加开发成本
      • 管理冗余列,查询所有数据需要join操作
    2. 水平分区:

      保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。

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

      数据库水平拆分

      水品拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分最好分库

      水平拆分能够 支持非常大的数据量存储,应用端改造也少,但 分片事务难以解决 ,跨界点Join性能较差,逻辑复杂。

      《Java工程师修炼之道》的作者推荐 尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度 ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。

      水平分表:

      表很大,分割后可以降低在查询时需要读的数据和索引的页数,同时也降低了索引的层数,提高查询次数

      img

      适用场景
      • 1、表中的数据本身就有独立性,例如表中分表记录各个地区的数据或者不同时期的数据,特别是有些数据常用,有些不常用。
      • 2、需要把数据存放在多个介质上。
      水平切分的缺点
      • 1、给应用增加复杂度,通常查询时需要多个表名,查询所有数据都需UNION操作
      • 2、在许多数据库应用中,这种复杂度会超过它带来的优点,查询时会增加读一个索引层的磁盘次数

      下面补充一下数据库分片的两种常见方案:

      • 客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
      • 中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。

    分库分表后面临的问题

    • 事务支持 分库分表后,就成了分布式事务了。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价; 如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。

    • 跨库join

      只要是进行切分,跨节点Join的问题是不可避免的。但是良好的设计和切分却可以减少此类情况的发生。解决这一问题的普遍做法是分两次查询实现。在第一次查询的结果集中找出关联数据的id,根据这些id发起第二次请求得到关联数据。 分库分表方案产品

    • 跨节点的count,order by,group by以及聚合函数问题 这些是一类问题,因为它们都需要基于全部数据集合进行计算。多数的代理都不会自动处理合并工作。解决方案:与解决跨节点join问题的类似,分别在各个节点上得到结果后在应用程序端进行合并。和join不同的是每个结点的查询可以并行执行,因此很多时候它的速度要比单一大表快很多。但如果结果集很大,对应用程序内存的消耗是一个问题。

    • 数据迁移,容量规划,扩容等问题 来自淘宝综合业务平台团队,它利用对2的倍数取余具有向前兼容的特性(如对4取余得1的数对2取余也是1)来分配数据,避免了行级别的数据迁移,但是依然需要进行表级别的迁移,同时对扩容规模和分表数量都有限制。总得来说,这些方案都不是十分的理想,多多少少都存在一些缺点,这也从一个侧面反映出了Sharding扩容的难度。

    • ID问题

    • 一旦数据库被切分到多个物理结点上,我们将不能再依赖数据库自身的主键生成机制。一方面,某个分区数据库自生成的ID无法保证在全局上是唯一的;另一方面,应用程序在插入数据之前需要先获得ID,以便进行SQL路由. 一些常见的主键生成策略

    UUID 使用UUID作主键是最简单的方案,但是缺点也是非常明显的。由于UUID非常的长,除占用大量存储空间外,最主要的问题是在索引上,在建立索引和基于索引进行查询时都存在性能问题。 Twitter的分布式自增ID算法Snowflake 在分布式系统中,需要生成全局UID的场合还是比较多的,twitter的snowflake解决了这种需求,实现也还是很简单的,除去配置信息,核心代码就是毫秒级时间41位 机器ID 10位 毫秒内序列12位。

    • 跨分片的排序分页

      般来讲,分页时需要按照指定字段进行排序。当排序字段就是分片字段的时候,我们通过分片规则可以比较容易定位到指定的分片,而当排序字段非分片字段的时候,情况就会变得比较复杂了。为了最终结果的准确性,我们需要在不同的分片节点中将数据进行排序并返回,并将不同分片返回的结果集进行汇总和再次排序,最后再返回给用户。如下图所示:

      在这里插入图片描述

    MySQL的复制原理以及流程

    主从复制:将主数据库中的DDL和DML操作通过二进制日志(BINLOG)传输到从数据库上,然后将这些日志重新执行(重做);从而使得从数据库的数据与主数据库保持一致。

    主从复制的作用

    1. 主数据库出现问题,可以切换到从数据库。
    2. 可以进行数据库层面的读写分离。
    3. 可以在从数据库上进行日常备份。

    MySQL主从复制解决的问题

    • 数据分布:随意开始或停止复制,并在不同地理位置分布数据备份
    • 负载均衡:降低单个服务器的压力
    • 高可用和故障切换:帮助应用程序避免单点失败
    • 升级测试:可以用更高版本的MySQL作为从库

    MySQL主从复制工作原理

    • 在主库上把数据更高记录到二进制日志
    • 从库将主库的日志复制到自己的中继日志
    • 从库读取中继日志的事件,将其重放到从库数据中

    基本原理流程,3个线程以及之间的关联

    :binlog线程——记录下所有改变了数据库数据的语句,放进master上的binlog中;

    :io线程——在使用start slave 之后,负责从master上拉取 binlog 内容,放进自己的relay log中;

    :sql执行线程——执行relay log中的语句;

    复制过程

    img

    Binary log:主数据库的二进制日志

    Relay log:从服务器的中继日志

    第一步:master在每个事务更新数据完成之前,将该操作记录串行地写入到binlog文件中。

    第二步:salve开启一个I/O Thread,该线程在master打开一个普通连接,主要工作是binlog dump process。如果读取的进度已经跟上了master,就进入睡眠状态并等待master产生新的事件。I/O线程最终的目的是将这些事件写入到中继日志中。

    第三步:SQL Thread会读取中继日志,并顺序执行该日志中的SQL事件,从而与主数据库中的数据保持一致。

    读写分离有哪些解决方案?

    读写分离是依赖于主从复制,而主从复制又是为读写分离服务的。因为主从复制要求slave不能写只能读(如果对slave执行写操作,那么show slave status将会呈现Slave_SQL_Running=NO,此时你需要按照前面提到的手动同步一下slave)。

    方案一

    使用mysql-proxy代理

    优点:直接实现读写分离和负载均衡,不用修改代码,master和slave用一样的帐号,mysql官方不建议实际生产中使用

    缺点:降低性能, 不支持事务

    方案二

    使用AbstractRoutingDataSource+aop+annotation在dao层决定数据源。
    如果采用了mybatis, 可以将读写分离放在ORM层,比如mybatis可以通过mybatis plugin拦截sql语句,所有的insert/update/delete都访问master库,所有的select 都访问salve库,这样对于dao层都是透明。 plugin实现时可以通过注解或者分析语句是读写方法来选定主从库。不过这样依然有一个问题, 也就是不支持事务, 所以我们还需要重写一下DataSourceTransactionManager, 将read-only的事务扔进读库, 其余的有读有写的扔进写库。

    方案三

    使用AbstractRoutingDataSource+aop+annotation在service层决定数据源,可以支持事务.

    缺点:类内部方法通过this.xx()方式相互调用时,aop不会进行拦截,需进行特殊处理。

    备份计划,mysqldump以及xtranbackup的实现原理

    (1)备份计划

    视库的大小来定,一般来说 100G 内的库,可以考虑使用 mysqldump 来做,因为 mysqldump更加轻巧灵活,备份时间选在业务低峰期,可以每天进行都进行全量备份(mysqldump 备份出来的文件比较小,压缩之后更小)。

    100G 以上的库,可以考虑用 xtranbackup 来做,备份速度明显要比 mysqldump 要快。一般是选择一周一个全备,其余每天进行增量备份,备份时间为业务低峰期。

    (2)备份恢复时间

    物理备份恢复快,逻辑备份恢复慢

    这里跟机器,尤其是硬盘的速率有关系,以下列举几个仅供参考

    20G的2分钟(mysqldump)

    80G的30分钟(mysqldump)

    111G的30分钟(mysqldump)

    288G的3小时(xtra)

    3T的4小时(xtra)

    逻辑导入时间一般是备份时间的5倍以上

    (3)备份恢复失败如何处理

    首先在恢复之前就应该做足准备工作,避免恢复的时候出错。比如说备份之后的有效性检查、权限检查、空间检查等。如果万一报错,再根据报错的提示来进行相应的调整。

    (4)mysqldump和xtrabackup实现原理

    mysqldump

    mysqldump 属于逻辑备份。加入–single-transaction 选项可以进行一致性备份。后台进程会先设置 session 的事务隔离级别为 RR(SET SESSION TRANSACTION ISOLATION LEVELREPEATABLE READ),之后显式开启一个事务(START TRANSACTION /*!40100 WITH CONSISTENTSNAPSHOT */),这样就保证了该事务里读到的数据都是事务事务时候的快照。之后再把表的数据读取出来。如果加上–master-data=1 的话,在刚开始的时候还会加一个数据库的读锁(FLUSH TABLES WITH READ LOCK),等开启事务后,再记录下数据库此时 binlog 的位置(showmaster status),马上解锁,再读取表的数据。等所有的数据都已经导完,就可以结束事务

    Xtrabackup:

    xtrabackup 属于物理备份,直接拷贝表空间文件,同时不断扫描产生的 redo 日志并保存下来。最后完成 innodb 的备份后,会做一个 flush engine logs 的操作(老版本在有 bug,在5.6 上不做此操作会丢数据),确保所有的 redo log 都已经落盘(涉及到事务的两阶段提交

    概念,因为 xtrabackup 并不拷贝 binlog,所以必须保证所有的 redo log 都落盘,否则可能会丢最后一组提交事务的数据)。这个时间点就是 innodb 完成备份的时间点,数据文件虽然不是一致性的,但是有这段时间的 redo 就可以让数据文件达到一致性(恢复的时候做的事

    情)。然后还需要 flush tables with read lock,把 myisam 等其他引擎的表给备份出来,备份完后解锁。这样就做到了完美的热备。

    数据表损坏的修复方式有哪些?

    使用 myisamchk 来修复,具体步骤:

    • 1)修复前将mysql服务停止。
    • 2)打开命令行方式,然后进入到mysql的/bin目录。
    • 3)执行myisamchk –recover 数据库所在路径/*.MYI

    使用repair table 或者 OPTIMIZE table命令来修复,REPAIR TABLE table_name 修复表 OPTIMIZE TABLE table_name 优化表 REPAIR TABLE 用于修复被破坏的表。 OPTIMIZE TABLE 用于回收闲置的数据库空间,当表上的数据行被删除时,所占据的磁盘空间并没有立即被回收,使用了OPTIMIZE TABLE命令后这些空间将被回收,并且对磁盘上的数据行进行重排(注意:是磁盘上,而非数据库)

    展开全文
  • 40 道基础Dubbo 面试题及答案

    万次阅读 多人点赞 2018-10-01 23:59:41
    转载自史上最全 40 道 Dubbo 面试题及答案,看完碾压面试官 想往高处走,怎么能不懂 Dubbo? Dubbo是国内最出名的分布式服务框架,也是 Java 程序员必备的必会的框架之一。Dubbo 更是中高级面试过程中经常会问的...
  • SpringMVC常见面试题总结(超详细回答)

    万次阅读 多人点赞 2018-07-08 22:51:43
    相关阅读: Spring常见面试题总结 SpringMVC常见面试题总结 Mybatis常见面试题总结 MySQL常见面试题总结 Redis常见面试题总结 RabbitMQ消息队列常见面试题总结 ElasticSearch搜索引擎常见面试题总结 计算机网络常见...
  • a = 20 b = 10 num1 = 10 num2 = 20
  • Java 最常见的 200+ 面试题:面试必备

    万次阅读 多人点赞 2019-07-10 17:41:50
    这份面试清单是从我 2015 年做了 TeamLeader 之后开始收集的,一方面是给公司招聘用,另一方面是想用它来挖掘在 Java 技术栈中,还有那些知识点是我不知道的,我想找到这些技术盲点,然后修复它,以此来提高自己的...
  • Spring面试题(2020最新版)

    万次阅读 多人点赞 2020-02-19 17:58:15
    文章目录Spring概述(10)什么是spring?Spring框架的设计目标,设计理念,和核心是什么Spring的优缺点是什么?Spring有哪些应用场景Spring由哪些模块组成?Spring 框架中都用到了哪些设计模式?...
  • Redis面试题(2020最新版)

    万次阅读 多人点赞 2019-12-13 10:38:01
    文章目录概述什么是RedisRedis有哪些数据类型Redis有哪些优缺点Redis的应用场景为什么要用 Redis /为什么要用缓存为什么要用 Redis 而不用 map/guava 做缓存?Redis为什么这么快持久化什么是Redis持久化?...
  • Linux面试题(2020最新版)

    万次阅读 多人点赞 2020-03-01 11:14:38
    文章目录Linux 概述什么是LinuxUnix和Linux有什么区别?什么是 Linux 内核?Linux的基本组件是什么?Linux 的体系结构BASH和DOS之间的基本区别是什么?Linux 开机启动过程?Linux系统缺省的运行级别?...
  • Spring Boot面试题(2020最新版)

    万次阅读 多人点赞 2020-02-19 17:48:42
    文章目录概述什么是 Spring Boot?Spring Boot 有哪些优点?Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?配置什么是 JavaConfig?Spring Boot 自动配置原理是什么?你如何理解 Spring Boot 配置加载...
  • java 入门面试题

    万次阅读 多人点赞 2019-04-04 14:13:55
    每次自己想跳槽的时候,内心总是担忧着那些面试题怎么解答。 很多问题在实际工作中并不会遇到,没有实际的解决问题经验,看过也记不住。 假如我明年需要换工作,那现在把下次的面试准备工作,拆分、融入到平时的...
  • Java集合容器面试题(2020最新版)

    万次阅读 多人点赞 2020-03-01 11:08:34
    文章目录集合容器概述什么是集合集合的特点集合和数组的区别使用集合框架的好处常用的集合类有哪些?List,Set,Map三者的区别?List、Set、Map 是否继承自 Collection 接口?List、Map、Set 三个接口存取元素时,各...
  • Spring常见面试题总结(超详细回答)

    万次阅读 多人点赞 2018-07-08 15:36:08
    1、Spring是什么? Spring是一个轻量级的IoC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。常见的配置方式有三种:基于XML的配置...
  • JAVA经典面试题附答案(持续更新版)

    万次阅读 多人点赞 2019-01-18 01:31:16
    JAVA基础 JAVA中的几种基本类型,各占用多少字节? 下图单位是bit,非字节 1B=8bit String能被继承吗?为什么? 不可以,因为String类有final修饰符,而final修饰的类是不能被继承的,实现细节不允许改变。...
  • 面试题总结 —— JAVA高级工程师

    万次阅读 多人点赞 2016-03-03 12:03:31
    面试题总结——JAVA高级工程师 近期考虑换工作的问题,于是投简历面试,面试5家公司的高级Java工程师,有4家给了我offer,想着总结一下面试经验,方便最近正在寻求机会的你们 一、无笔试题 不知道是不是职位...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 715,633
精华内容 286,253
关键字:

面试题