Java面试总纲

面试

文章目录

  • 面试
  • 自我介绍
  • 基础
    • int类型的取值范围
    • String底层为什么是final修饰的
    • final关键字
    • 面向对象
    • 向上转型和向下转型
    • Java自动类型转换
    • Java数据类型自动提升(注意以下讨论的是二元操作符)
    • 抽象类和接口的区别
    • 静态代码块,构造代码块和构造函数的执行顺序
    • 反射
      • Java中利用反射获取对象的方式有:
    • 异常
  • 集合
    • ArrayList动态数组扩容机制
    • HashMap的结构
    • hashTable, hashMap, concerrentHashMap
    • HashMap的遍历
    • HashMap的动态删除
    • 理解和使用Java8中的时间API
  • JVM
    • 可达性分析
    • 双亲委派模型
    • Java对象的创建过程
    • Java类的生命周期
    • 垃圾回收算法与垃圾回收器
    • 调优命令
    • 逃逸分析和栈上分配
      • 栈上分配
      • 逃逸分析
    • 什么是Stop The World? 什么是OopMap? 什么是安全点?
    • 什么是指针碰撞?什么是空闲列表?什么是TLAB?
  • 多线程
    • Java中实现多线程有几种方法
    • 线程池
    • 线程死锁
    • wait()和notify()
    • wait()和sleep()区别
    • sychronized关键字
    • volitle关键字
    • ThreadLocal关键字
    • Lock
    • countDownLatch
  • 框架
    • JDBC
    • Mybatis
    • Servlet
    • SpringMVC
    • Spring
      • Bean的生命周期
      • IOC
      • 循环依赖
      • AOP
  • 数据库
    • 数据库引擎 Innodb 和 myisam 区别
    • InnoDB的B+树结构(mysq数据库中的索引结构)
    • 深入了解MySQL数据库的索引及底层结构
      • 导致SQL执行慢的原因?
      • 慢sql问题解决入口:
      • 什么是索引?
      • 索引类型:
      • 索引的种类:
      • MySQL聚集索引和非聚集索引的区别?
      • 哈希索引:
      • B+TREE索引:
      • 联合索引以及联合索引的失效问题
    • 数据库连接池只有100个连接,有3000个连接请求
    • 数据库锁
      • MySQL锁概述
      • 数据库的三种锁特性
      • 什么是锁?
      • 什么是死锁
      • 数据库死锁出现的案列
      • 如何处理死锁?
      • 如何避免死锁?
      • innodb默认是如何对待死锁的?
      • 如何开启死锁检测?
      • 什么是全局锁?它的应用场景有哪些?
      • 使用全局锁会导致的问题?
      • 如何处理逻辑备份时,整个数据库不能插入的情况?
      • innodb如何实现行锁?
      • 什么是共享锁?
      • 什么是排他锁?
      • 悲观锁和乐观锁的区别?
      • 乐观锁有什么优点和缺点?
      • innodb存储引擎有几种算法?
      • 优化锁方面的意见?
    • mysql数据库读写分离原理
    • mysql主从复制机制
    • redis中的数据类型和数据结构
    • redis主从复制机制
  • 并发
    • redis和数据库双写一致性问题
      • 1.先更新数据库,再更新缓存(有脏数据,不采用)
      • 2.先删缓存,再更新数据库(有脏数据,不采用,可用延时双删策略解决)
      • 3.延时双删策略(可以采用)
      • 4.先更新数据库,再删缓存(可以采用)
    • 高并发情况下如何保证数据安全
    • 高并发情况下如何保证系统安全
    • 高并发情况下如何解决redis雪崩和穿透
  • 事务
    • ACID
    • Spring 和 数据库 事务的隔离级别
    • 传播行为
    • Spring事务什么时候会失效?
    • 分布式事务控制
      • TPC
      • TCC
    • 幂等性
  • 工具
    • Nginx
    • rabbitMQ
      • rabbitmq生产
      • rabbitmq消费
    • kafka
      • kafka生产
      • kafka消费
    • kafka如何确保数据不丢失
    • kafka怎么保证消息的消费顺序?
      • kafka保证消息顺序有2种方法。
    • RabbitMQ和Kafka的区别
    • kafka的优势
    • Zookeeper
      • 什么是ZooKeeper
      • zookeeper 都有哪些功能?
      • zookeeper 有三种部署模式:
      • zookeeper 怎么保证主从节点的状态同步?
  • 设计模式
  • 网络
    • HTTP和HTTPS的区别
    • HTTP和TCP的区别和联系
      • TCP连接
      • HTTP连接
      • 相互联系和区别


自我介绍


At first, Thank you so much for giving me this opportunity for this interview.

My name is XXX, and you can call me Alex Leon which is my English name.

I graduated from Shanghai Maritime University with bachelor’s degree at 2016, I have worked for two companies, have been engaged in Java development for about five years

I passed CET4 during my college years,and I got Java Software Development Special Skill Certificate at 2019 , which is issued by the (MIIT)Ministry of industry and information technology, Now I am studying and preparing for the exam of software designer.

I have good foundation and coding practice of Java, and also I know the skills of Groovy, Mysql and Oracle, Im familiar with popular framework such as Spring, SpringBoot, SpringMVC, SpringCloud, Mybatis, and Grails, and I often use the tools like Kafka, RabbitMq, Redis and Nginx

The latest project I participated is SPDB(Shanghai Pudong Development Bank) ecosystem marketing project, with framework springBoot, springCloud and grails. it is distributed and microserviced.

Im good at learning new technologies, I love coding, I love programming, and I always keep a good self-drive for learning.

CitiBank is a large and international company, on the other hand, I have similar project experience of bank, so I really hope to join Citibank

Thank you so much


各位面试官好, 我叫XXX,16年毕业于上海海事大学毕业, 毕业之后一直在上海发展,一共呆过两家公司,从事java开发工作5年左右

我的大概情况是: 参加工作前两年从事企业传统项目,主要是ERP\CRM这些生产管理系统,用到的技术点主要是传统的单体框架,SSM框架,数据库是mysql。
后来这两年参与浦发银行生态圈项目,涉及分布式和微服务的架构

我近期参与的项目是浦发银行生态圈营销系统
主要功能是为浦发银行所有生态产品,比如手机银行app、浦惠到家app、浦慧app、甜橘app等,为这些产品提供制券和活动页面配置的管理端系统,以及这些h5活动页面的运行时服务支持
采用微服务分布式架构,开发语言采用的是groovy, 框架使用的是grails框架,包管理工具使用的是gradle, 同时集成了springCloud的相关组件

角色情况,前期作为初级开发工程师,主要以开发功能模块为主,近一年作为开发组长, 带领8个人的小团队,也参与到需求分析评估、项目流程管理,包括ci cd发布流程、以及代码审核的工作。

平时我喜欢看一些源码,会去github或者gitee逛一些开源项目, 也租了服务器购买和备案域名并搭建了一些个人项目,比如主页,在线简历,浏览器搜索页等

19年通过了工信部专项技能认证java开发工程师的考试, 21年去年我参加了国家软考软件设计师考试, 下午题目考了62发挥不错,上午题目就差5分就能通过
所以今年第一个目标就是换个工作,第二个目标就是能通过软考


基础


int类型的取值范围

-2^31 ~ 2^31 - 1

String底层为什么是final修饰的

  • 1.为了实现字符串池

只有字符串是不可变的,字符串池才有可能实现。不同的字符串变量可以指向池中的同一个字符串,节省heap空间。但如果字符串是可变的,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。

  • 2.为了线程安全

只有字符串是不可变的,多线程才安全,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步,字符串本身就是线程安全的。

  • 3.为了实现String可以创建HashCode不可变性

只有字符串是不可变的,则在它创建的时候HashCode就可以被允许缓存,并且不会在每次调用 String 的 hashcode 方法时重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

  • 4.为了系统安全

而且String类中的很多方法的实现不是Java代码,而是调用操作系统的本地方法来完成的,如果String类不被final修饰,被继承重写方法的话,系统会很不安全。

虽然final修饰代表了不可变,但仅仅是引用地址不可变,并不代表了数组本身不会变

final关键字

final关键字可以修饰类,方法和变量:

  • 被final修饰的类不能被继承,即它不能拥有自己的子类;
  • 被final修饰的方法不能被重写;
  • final修饰的变量,无论是类变量、实例变量、参数变量(形参)还是局部变量,都需要进行初始化操作。

面向对象

重视对象思维,关注每个对象需要做什么,而不是关注过程和步骤

  1. 封装:

    明确标识出允许外部使用的所有成员函数和数据项

    内部细节对外部调用者透明,外部调用无需修改或者关心内部实现的细节

  2. 继承

    继承基类的方法,并作出自己的改变和扩展

    子类共性的方法或者属性(抽取出来)直接使用继承的父类的,不需要自己再定义,只需扩展自己个性化的

  3. 多态

    基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同,使得程序更易扩展

    多态有个条件就是继承,多态和继承是一脉相承的

    多态的条件:继承,方法重写,父类引用指向子类对象

使用引用变量调用的方法实际上是子类重写的方法,而不是父类的

弊端:多态调用方法不能是子类特有/独有的方法,因为能调用的方法必须是重写父类的方法,所以父类中没有的方法不能调用。

向上转型和向下转型

所以我们常说的向上转型,其实就是多态,即父类引用指向子类对象,此时调用方法实际上使用的是子类的实现, 而子类独有的方法是无法调用的

但如果将该变量强制类型转换成子类(向下转型)后,就可以使用子类特有的方法

Java自动类型转换

  1. 两种类型是彼此兼容的
  2. 转换的目的类型占得空间范围一定要大于转化的源类型

正向过程:由低字节向高字节自动转换

byte->short->int->long->float->double

逆向过程:使用强制转换,可能丢失精度。

int a=(int)3.14;

Java数据类型自动提升(注意以下讨论的是二元操作符)

Java定义了若干使用于表达式的类型提升规则:

  1. 所有的byte型. short型和char型运算后将被提升到int型(例外: final修饰的short, char变量相加后不会被自动提升。)
  2. 如果一个操作数是long形 计算结果就是long型;
  3. 如果一个操作数是float型,计算结果就是float型;
  4. 如果一个操作数是double型,计算结果就是double型;

另一种归纳方式(《Java核心技术卷I》P43):

  1. 如果两个操作数其中有一个是double类型,另一个操作就会转换为double类型。
  2. 否则,如果其中一个操作数是float类型,另一个将会转换为float类型。
  3. 否则,如果其中一个操作数是long类型,另一个会转换为long类型。
  4. 否则,两个操作数都转换为int类型。

抽象类和接口的区别

区别 抽象类 接口
默认的方法实现 它可以有默认的方法实现 接口完全是抽象的,所有方法都必须是抽象的,java1.8之后允许接口有默认实现
实现方式 子类使用extends关键字来继承一个抽象类,如果子类不是抽象类的话,那么子类必须实现父类所有的抽象方法的具体实现 实现类使用implements关键字来实现接口,它需要提供接口中所有声明的方法的具体实现
构造器 抽象类可以有构造器 接口不能有构造器
与正常Java类的区别 除了你不能实例化抽象类之外,它几乎和正常的类没有任何区别 接口是完全不同的类型
访问修饰符 抽象方法可以是public、protected和default这些修饰符 接口里的方法默认修饰符是public,也只能是public
普通变量 抽象类可以对变量没有限制,和正常类一样 接口中的变量必须是public static final的
多继承性 由于java单继承局限,当继承了抽象类,就不能继承其他的类了 一个类可以实现多个接口,并且对你继承另一个类时,没有限制
添加新的方法 你可以往抽象类中添加新的正常方法,并且你不需要改变你现在的代码 如果你往接口中添加新的方法,那么你必须在实现了该接口的类中实现接口的新方法

静态代码块,构造代码块和构造函数的执行顺序

静态代码块:最早执行,类被载入内存时执行,只执行一次。没有名字、参数和返回值,有关键字static。

构造代码块:执行时间比静态代码块晚,比构造函数早,和构造函数一样,只在对象初始化的时候运行。没有名字、参数和返回值。

构造函数:执行时间比构造代码块时间晚,也是在对象初始化的时候运行。没有返回值,构造函数名称和类名一致。

注意:静态代码块在类加载的时候就执行,所以的它优先级高于main()方法。

下面我们看一下有继承时的情况:

public class Parent {

    public Parent() {
        System.out.println("Parent的构造方法");
    }

    static {
        System.out.println("Parent的静态代码块");
    }

    {
        System.out.println("Parent的构造代码块");
    }
}

public class Son extends Parent {
    public Son() {
        System.out.println("Son的构造方法");
    }

    static {
        System.out.println("Son的静态代码块");
    }

    {
        System.out.println("Son的构造代码块");
    }

    public static void main(String[] args) {
        System.out.println("main方法");
        new Son();
    }
}
Parent的静态代码块
        Son的静态代码块
        main方法
        Parent的构造代码块
        Parent的构造方法
        Son的构造代码块
        Son的构造方法

可以看出:父类始终先调用(继承先调用父类),并且这三者之间的相对顺序始终保持不变。

到此貌似没什么问题,但是请看如下变形:

public class B {
    public static B t1 = new B();
    public static B t2 = new B();

    {
        System.out.println("构造代码块");
    }

    public B() {
        System.out.println("构造函数");
    }

    static {
        System.out.println("静态代码块");
    }

    public static B t3 = new B();

    public static void main(String[] args) {
        new B();
    }
}
构造代码块
        构造函数
        构造代码块
        构造函数
        静态代码块
        构造代码块
        构造函数
        构造代码块
        构造函数

因为b1、b2、b3用static修饰,与静态块处于同一优先级,同一优先级就按先后顺序来执行。

反射

在运行时动态获取调用或修改类信息,属性,方法。

Java中利用反射获取对象的方式有:

  • a)类名.class,不会加执行态代码块
Class<Object> c1 =Object.class
  • b)Class.forName(“包名.类名”) ,会执行静态代码块
Class<?> c2 = Class.forName("java.lang.Object");
  • c)类的实例对象.getClass(),会执行静态代码块
Class<?> c3 = new Object().getClass();
  • d)Class.forName(“包名.类名”, boolean,loader)
Class<?> c4 = Class.forName("com.java.oop.ClassA", false, ClassLoader.getSystemClassLoader());
  • e)类加载器.load(“包名+类名”) 不会执行静态代码块
ClassLoader loader = ClassLoader.getSystemClassLoader();
        Class<?> c5 = loader.loadClass("com.java.oop.ClassA");//不会执行静态代码块。

异常

Java面试总纲_第1张图片


集合

Java面试总纲_第2张图片

ArrayList动态数组扩容机制

在JDK1.8中,如果通过无参构造的话,初始数组容量为0,当真正对数组进行添加时(即添加第一个元素时),才真正分配容量,默认分配容量为10;

当容量不足时(容量为size,添加第size+1个元素时),先判断按照1.5倍(位运算)的比例扩容能否满足最低容量要求,若能,则以1.5倍扩容,否则以最低容量要求进行扩容。

执行add(E e)方法时,先判断ArrayList当前容量是否满足size+1的容量;在判断是否满足size+1的容量时,先判断ArrayList是否为空,若为空,则先初始化ArrayList初始容量为10,再判断初始容量是否满足最低容量要求;若不为空,则直接判断当前容量是否满足最低容量要求;若满足最低容量要求,则直接添加;若不满足,则先扩容,再添加。

ArrayList的最大容量为Integer.MAX_VALUE

ArrayList扩容的例子:ArrayList相当于在没指定initialCapacity时就是会使用延迟分配对象数组空间,当第一次插入元素时才分配10(默认)个对象空间。

假如有20个数据需要添加,那么会分别在第一次的时候,将ArrayList的容量变为10 (如下图一);之后扩容会按照1.5倍增长。也就是当添加第11个数据的时候,Arraylist继续扩容变为10*1.5=15(如下图二);当添加第16个数据时,继续扩容变为15 * 1.5 =22个。

HashMap的结构

Java面试总纲_第3张图片

HashMap是数组➕单向链表的数据结构

数组中保存的不是key value, 严格意义上讲保存的是一个Node实现了Map.Entry接口

我可以围绕它源码的三个主要方法来讲一下
put() get()和resize()

put()方法时, 先对key进行hashCode(), 得到的值再去与数组容量进行与操作,得到一个哈希值

这步操作是为了使得key的哈希值都在数组下标范围内,定位到数组下标的bucket

当这个bucket为空时,直接将这个node放进去,所以多线程下线程不安全,多条线程同时判断到bucket为空,同时放入node导致有些数据没了,解决办法有Collections.Sychronized,或使用concurrentHashMap

当这个bucket中已经有值,说明存在hash冲突,此时遍历链表对比key.equals()

如果链表中已有则覆盖oldValue,如果没有则在链表的尾部(尾插法)进行add,1.8之前是头插法,重新赋值第一个节点然后指向前一个节点,多线程情况下可能导致next节点永不为空从而造成死链

当链表长度大于8时,会转换成红黑树,利用红黑树的左旋右旋来提高效率,当小于6时又会转换成链表

get()方法时,先对key哈希,找到数组的bucket,然后遍历链表查询key.equals()是否存在

resize()方法,数组长度默认起始是16,默认负载因子为0.75f,所以当数组大小超过16*0.75=12时,会对数组进行双倍扩容

hashTable, hashMap, concerrentHashMap

hashtable中不能有null key或者value, hashmap中允许

Hashtable中使用了sycronize同步,效率较低,虽然多线程中相对安全,但也不常使用

因为可以使用Collections.sycronized去实现

或者直接使用concurrentHashMap, 它在hashMap的基础上外层多维护了一个segment

是分段进行加锁的,所以多线程时安全又提高了效率

concurrentHashMap中通过自旋锁和CAS确保不同线程获取到的是同一个segment对象

Java面试总纲_第4张图片

Java面试总纲_第5张图片

HashMap的遍历

  • 声明一个map
HashMap<String, String> map = new HashMap<>();
map.put("a","123");
map.put("b","456");
map.put("c","789");
  • 方法1:普通的foreach, 遍历的是key或者value
for (String val : map.values()){
System.out.println("method1_foreach value:"+val);
}
for(String key : map.keySet()){
System.out.println("method1_foreach key:"+ key + "; value:" + map.get(key));
}
  • 方法2:迭代器装载entry, 或者迭代器装载keySet, 可以在遍历中同时使用map(动态删除首选)
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String,String> entry = iterator.next();
System.out.println("method2_iterator: key:" + entry.getKey() + "; value:" + entry.getValue());
}
  • 方法3:entrySet foreach, 非常推荐
for (Map.Entry<String,String> entry : map.entrySet()){
System.out.println("method3_entrySetForeach: key:" + entry.getKey() + "; value:"+entry.getValue());
}
  • 方法4:lamda表达式
map.forEach((key, value) ->{
	System.out.println(key + ": " + value);
});
  • 方法5: stream API 单线程
map.entrySet().stream().forEach((entry) ->{
	System.out.println(entry.getKey() + ": " + entry.getValue());
});

HashMap的动态删除

只能使用迭代器的方式(迭代器装载entrySet或者装载keySet),否则报异常ConcurrentModificationException

  • 迭代器装载keySet实现动态删除
Iterator<Integer> iter = map.keySet().iterator();
while(iter.hasNext()) {
	int key = iter.next();
	System.out.println(key + ": " + map.get(key));
	if(key == 2) {
		iter.remove();
	}
}
  • 迭代器装载entrySet实现动态删除
Iterator<Map.Entry<Integer, String>> mapiter = map.entrySet().iterator();
while(mapiter.hasNext()) {
	@SuppressWarnings("unchecked")
	Map.Entry<Integer, String> entry = mapiter.next();
	System.out.println(entry.getKey() + ": " + entry.getValue());
	if(entry.getKey() == 2) {
		mapiter.remove();
	}
}

理解和使用Java8中的时间API

hoohack


JVM

主要实现了Java的跨系统,不同系统由JVM编译处理成不同的机器码,所以不同的系统对应的JVM版本也不同

主要分为 类装载子系统、字节码执行引擎、运行时数据区

类装载子系统用于加载字节码

字节码执行引擎主要有三个作用

  • 执行字节码

  • 修改程序计数器

  • 创建和管理垃圾回收线程

    最重要的是运行时数据区,主要分为线程公有区和线程私有区

    线程公有区包含堆和方法区(元数据区)

  • 堆是存放对象的

  • 方法区用来存放常量、静态变量、类元信息

    线程私有区包含线程栈、本地方法栈、程序计数器

  • 当程序执行到native关键字修饰的本地方法的时候,会由本地方法栈分配空间

  • 程序计数器用于记录当前字节码执行到的位置,因为线程是交替获取cpu资源进行执行的,需要知道该从哪里执行

  • 线程栈中包含多个栈帧,每个线程分配一个线程栈,而线程中的方法又会分配不同的栈帧

    • 局部变量表:存放方法的局部变量
    • 操作数栈:为加减乘除等运算提供内存空间,变量先复制到操作数栈,运算完再将结果赋给相应变量. 所以i=i++还是等于原来的值, 是因为jvm先执行了压栈,将i压入操作数栈顶,然后执行自增操作,这个时候栈顶的i为1,本地变量i为2,但是又将栈顶的i弹出操作数栈并赋值给本地变量i了,所以本地变量i最终不变
    • 动态链接:我们调用一个方法的时候,那些方法名称包括括号实际上是"符号",比如math.compute(), "compute()"实际上是一种符号,动态链接就是根据这些东西去找到它的内存地址,从而找到对应的代码
    • 方法出口:一个方法执行完后,需要找到调用他的上一个方法中的位置好继续执行

Java面试总纲_第6张图片

当伊甸园区满了,会触发minor gc, minor gc会回收整个年轻代

幸存的对象会从伊甸园区 移动到 其中一个幸存区s0, 当再次触发minor gc, 幸存对象又会被挪到另一个空的幸存区s1, 然后s0会被清空

所以当一个对象如果一直幸存,它会在幸存区 s0 和 s1 之间反复横跳

每经历一次gc,对象的分代年龄会加1, 当加到15, 这个对象会被移动到老年代

如果幸存对象在幸存区放不下,gc后也会被直接放到老年代

当老年代放满之后,jvm会再开启一个垃圾回收线程,专门进行full gc, full gc会将年轻代和老年代都回收

当full gc之后还是没法腾出足够空间,就会内存溢出OOM, OutOfMemeryException

可达性分析

GCRoot根结点:线程栈的本地变量、静态变量、本地方法栈的变量。

将GCRoot作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余为标记的对象都是垃圾对象。

双亲委派模型

其实就是一种类加载器的层次关系

Java面试总纲_第7张图片

Java面试总纲_第8张图片

Java对象的创建过程

Java面试总纲_第9张图片

Java类的生命周期

当我们编写一个java的源文件后,经过编译会生成一个后缀名为class的文件,这种文件叫做字节码文件,只有这种字节码文件才能够在java虚拟机中运行,java类的生命周期就是指一个class文件从加载到卸载的全过程。

一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况

Java面试总纲_第10张图片

加载

就是找到需要加载的类并把类的信息加载到jvm的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。

类的加载方式比较灵活,我们最常用的加载方式有两种,一种是根据类的全路径名找到相应的class文件,然后从class文件中读取文件内容;另一种是从jar文件中读取

连接

连接阶段比较复杂,一般会跟加载阶段和初始化阶段交叉进行,这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,可以细分为三个步骤:验证、准备和解析。

  • 验证:当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被jvm所运行。

  • 准备:准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认的初值是这样的:

    • 基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0。
    • 引用类型的默认值为null。
    • 常量的默认值为我们程序中设定的值,比如我们在程序中定义final static int a = 100,则准备阶段中a的初值就是100。
  • 解析:这一阶段的任务就是把常量池中的符号引用转换为直接引用。

那么什么是符号引用,什么又是直接引用呢?

我们来举个例子:我们要找一个人,我们现有的信息是这个人的身份证号是1234567890。只有这个信息我们显然找不到这个人,但是通过公安局的身份系统,我们输入1234567890这个号之后,就会得到它的全部信息:比如安徽省黄山市余暇村18号张三,通过这个信息我们就能找到这个人了。这里,123456790就好比是一个符号引用,而安徽省黄山市余暇村18号张三就是直接引用。在内存中也是一样,比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。

连接阶段完成之后会根据使用的情况(直接引用还是被动引用)来选择是否对类进行初始化。

初始化

如果一个类被直接引用,就会触发类的初始化。在java中,直接引用的情况有:

  • 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
  • 通过反射方式执行以上三种行为。
  • 初始化子类的时候,会触发父类的初始化。
  • 作为程序入口直接运行时(也就是直接调用main方法)。

垃圾回收算法与垃圾回收器

垃圾回收算法:

  1. 标记清除算法:将所有需要回收的对象进行标记,标记结束后对标记的对象进行回收,但是效率低,会造成大量的碎片。
  2. 复制算法: 复制算法将空间分为两部分,每次使用其中的一部分。当一块内存用完了,就将这块的所有对象复制到另一块,将已使用的块清除。不会产生碎片,但是会浪费一定的内存空间。 在堆中的年轻代使用该算法,因为年轻代的对象多为生存周期比较短的对象。年轻代将内存分为一个Eden,两个survivor。每次使用Eden与一个survivor。当回收时,将survivor与Eden中存活的对象复制到另一个survivor,最后清理掉Eden与survivor。当survivor与Eden中存活的对象大小超过另一个survivor,则需要老年代来担保。
  3. 标记整理算法:复制算法在对象存活率较高时,复制会使得效率降低。根据老年代的特点,使用标记整理算法。标记之后将所有存活的对象移向一端,将其他的清理。解决了碎片问题。
  4. 分代收集算法:在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

垃圾回收器:(Serial、ParNew、Parallel Scavaenge 、Serial Old、Parallel Old、CMS、G1)

Serial收集器是单线程的收集器,在进行垃圾回收时,需要停止其他的所有工作线程。

ParNew收集器时Serial的多线程版本。在单线程的环境下,parnew绝不比serial收集器具有更改的效果,因为存在着线程的开销,但是随着cpu的增加,便可以体现出优势。默认情况下线程个数与cpu数量相同。

Parallel Scavenge收集器:年轻代收集器,多线程并行收集,使用复制算法,与parnew相似。CMS,Parnew,Serial的设计目标是为了缩短用户线程的停顿时间。但是parallel scavenge的设计目标时实现一个可控的吞吐量(cpu运行用户代码时间/cpu消耗的总时间)。可以设置两个参数最大垃圾收集停顿时间、吞吐量大小,但是最大垃圾收集停顿

时间越小,系统设置的新生代越小,GC频率增加。

Serial Old 收集器:是serial在老年代的版本。

CMS:是一种获取最短停顿时间为目标的收集器。基于标记清除(老年代唯一一个基于标记清除的算法,除G1外)的算法实现。整个过程有四个步骤:初始标记、并发标记、重新标记、并发清除,其中初始标记与重新标记仍要停顿所有用户线程。初始阶段,主要负责标记gcroot能直接关联的对象,速度很快;并发标记是从GCRoot开始继续向下标记;重新标记是统计那些在并发标记过程中发生变化的标记;这个阶段的时间要比初始标记长,但是低于并发标记。并发清除是清除老年代中的垃圾。

CMS存在缺点:1.采用标记清除的算法(老年代唯一一个采用标记清除的算法),会产生碎片。 2.不能处理浮动垃圾(浮动垃圾:在并发清除时,用户线程还在运行,还会有新的垃圾产生,这部分只能等到下次GC时清理)。 3.对cpu特别敏感。由于CMS在GC时,最耗时的并发标记与并发 清除是与用户线程同时执行的,因此可以降低停顿时间,但是并发标记时会占用一部分的cpu资源,导致应用程序变慢。

G1收集器:唯一一个可以同时用于年轻代与老年代的垃圾收集器。G1收集器采用标记整理的算法,避免碎片。使用该收集器时,其堆的内存布局就发生变化,将堆分为不同的大小相等的region(每个region有一个remembered Set,为了避免做可达性分析是扫描这个堆,当引用在不同的region之间时,则将相关引用信息记录到remembered Set中),避免在整个堆中进行全区域的垃圾收集,能建立可预测的停顿时间模型。整个过程包括如下四个步骤:初始标记、并发标记、最终标记、筛选回收。初始标记与并发标记与CMS相似;最终标记:将并发标记阶段那些发生变化的对象的变化记录写入线程remembered set log,同时与remembered set合并;筛选回收阶段:通过对每个region的价值和成本进行筛选,已得到一个最好的回收方案,并回收。


调优命令

Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo

  • jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
  • jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
  • jmap,JVM Memory Map命令用于生成heap dump文件
  • jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
  • jstack,用于生成java虚拟机当前时刻的线程快照。
  • jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。

常用调优工具jconsole,jvisualvm

性能调优参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SdISCxkA-1648185335899)(https://blog.csdn.net/rodesad/article/details/51544977)]

逃逸分析和栈上分配

栈上分配

什么对象可以被栈上分配呢?没有被逃逸的小对象可以栈上分配

class Stack{}
public class TestStackMemory01 {
	public static void main(String[] args) {
     //小对象(占用内存资源比较少),未被逃逸.
     //栈上分配(这样的对象在方法结束之后,生命周期就结束)
		Stack s1 = new Stack();
	}
}

小对象有可能在内存分配时会存储在栈上。
堆中分配对象的生命周期会长一些,而且对象要想被销毁还得启用GC,而GC操作有可能导致系统暂停回收垃圾(毫秒级)。
如果将小对象分配到栈上(线程私有),方法执行结束会出栈,对象会销毁。声明周期短而且不用启用GC

逃逸分析

package com.java.memory;

class Container{
	int[] array = new int[1024];//1024*4个字节	
	@Override
	protected void finalize() throws Throwable {
		System.out.println("finalilze()...");
		//此方法属于Object.lang下面,gc回收之前会调用这个方法,我们通过此方法来观察对象是否被回收
	}
}
public class TestHeapMemory01 {
	static Container c2;
	public static void main(String[] args) {
		Container c1 = new Container();
		c2=new Container();
		c1=null;
		System.gc();//启动gc回收机制
	}
}

对于c1,如果没有16行代码c1=null,Container对象有引用指向,这时候GC不会回收
当加上c1=null的代码,此时就没有引用指向此对象,调用GC时会被回收,并在回收前调用finalize方法,这是堆区的GC回收机制

对于c2,方法外部有引用指向这个对象,属于逃逸对象,方法执行结束也不会被回收

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。

例如以下代码:

public static StringBuffer craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}

public static String createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}

第一段代码中的sb就逃逸了,而第二段代码中的sb就没有逃逸。

使用逃逸分析,编译器可以对代码做如下优化:

  • 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
  • 将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
  • 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

在Java代码运行时,通过JVM参数可指定是否开启逃逸分析,

  • -XX:+DoEscapeAnalysis : 表示开启逃逸分析
  • -XX:-DoEscapeAnalysis : 表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis

什么是Stop The World? 什么是OopMap? 什么是安全点?

进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为Stop The World。

在HotSpot中,有个数据结构(映射表)称为OopMap。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过程中,也会在特定的位置生成 OopMap,记录下栈上和寄存器里哪些位置是引用。

这些特定的位置主要在:

  • 循环的末尾(非 counted 循环)
  • 方法临返回前 / 调用方法的call指令后
  • 可能抛异常的位置

这些位置就叫作安全点(safepoint)。 用户程序执行时并非在代码指令流的任意位置都能够在停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。

什么是指针碰撞?什么是空闲列表?什么是TLAB?

  • 指针碰撞

一般情况下,JVM的对象都放在堆内存中(发生逃逸分析除外)。当类加载检查通过后,Java虚拟机开始为新生对象分配内存。如果Java堆中内存是绝对规整的,所有被使用过的的内存都被放到一边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是指针碰撞

  • 空闲列表

如果Java堆内存中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,不可以进行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是空闲列表

对象创建在虚拟机中是非常频繁的行为,可能存在线性安全问题。如果一个线程正在给A对象分配内存,指针还没有来的及修改,同时另一个为B对象分配内存的线程,仍引用这之前的指针指向,这就出「问题」了。

  • TLAB

可以把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块内存,这就是TLAB(Thread Local Allocation Buffer,本地线程分配缓存)。虚拟机通过-XX:UseTLAB设定它的。


多线程

Java中实现多线程有几种方法

  1. 继承Thread类重写run方法,使用thread.start()调用
  2. 实现runable接口,实现run方法,使用thread.start()调用
  3. 实现callable接口,重写call方法,使用future.get()调用

线程池

通过ThreadPoolExecutor类,可以构造出各种需求的线程池。底层是通过workQueue实现

实际应用中直接用静态类Executor

  • Executors.newCachedThreadPool(); //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE
  • Executors.newSingleThreadExecutor(); //创建容量为1的缓冲池
  • Executors.newFixedThreadPool(int); //创建固定容量大小的缓冲池

线程死锁

多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放。

由于线程被无限期地阻塞,因此程序不可能正常终止,最终导致死锁产生。

线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会因互相等待而进入死锁状态。

学过操作系统的朋友都知道,产生死锁必须具备以下四个条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用;
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源持有不释放;
  3. 不剥夺条件:线程已获得的资源,在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源;
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

同理,只要任意破坏产生死锁的四个条件中的其中一个就可以了:

  1. 破坏互斥条件

    该条件没有办法破坏,因为用锁的意义本来就是想让他们互斥的(临界资源需要互斥访问)

  2. 破坏请求与保持条件

    一次性申请所有的资源;

  3. 破坏不剥夺条件

    占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源;

  4. 破坏循环等待条件

    靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。

wait()和notify()

Object.wait() -挂起一个线程

Object.notify() -唤醒一个线程

唤醒是根据线程优先级来选择的

class Source {
    public int count = 0;
    public boolean flag = false; // 是否有数据
}

class Producer implements Runnable {

    private Source source;

    public Producer(Source source) {
        this.source = source;
    }

    @Override
    public void run() {
        while (true) {
            synchronized(source) {
                if(!source.flag) {
                    source.count++;
                    source.flag = true;
                    System.out.println("生产商品:"+source.count);
                    source.notify(); // 唤醒另一线程
                } else {
                    try {
                        source.wait(); // 等前线程等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

class Comsumer implements Runnable {
    private Source source;

    public Comsumer(Source source) {
        this.source = source;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (source) {
                if(source.flag) {
                    source.flag = false;
                    System.out.println("消费商品:"+source.count);
                    source.notify(); // 唤醒另一线程
                } else {
                    try {
                        source.wait();  // 等前线程等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

public class MQTest {
    public static void main(String[] args) {
        Source source = new Source();
        Producer producer = new Producer(source);
        Comsumer comsumer = new Comsumer(source);
        Thread t1 = new Thread(producer);
        Thread t2 = new Thread(comsumer);
        t1.start();
        t2.start();
    }
}

wait()和sleep()区别

  1. wait()来自Object类,sleep()来自Thread类
  2. 调用 sleep()方法,线程不会释放对象锁。而调用 wait() 方法线程会释放对象锁;
  3. sleep()睡眠后不出让系统资源,wait()让其他线程可以占用 CPU;
  4. sleep(millionseconds)需要指定一个睡眠时间,时间一到会自然唤醒。而wait()需要配合notify()或者notifyAll()使用

sychronized关键字

  • 原子性

synchronized经过编译之后,对应的是class文件中的monitorenter和monitorexit这两个字节码指令。

这两个字节码对应的内存模型的操作是lock(上锁)和unlock(解锁)。因为这两个操作之间运行的都是原子的(这个操作保证了变量为一个线程独占的,也就是说只有获得锁的线程才能够操作被锁定的内存区域),所以synchronized也具有原子性。

这两个字节码都需要一个对象来作为锁。因此,

  1. 如果synchronized修饰的是实例方法,则会传入this作为参数(相当于对调用方法的对象加锁)
  2. 如果修饰的是静态方法,则会传入class类对象作为参数。
  3. 如果只是一个同步块,那么锁就是括号里配置的对象

执行monitorenter字节码时, 如果这个对象没有被上锁,或者当前线程已经持有了该锁,那么锁的计数器会+1,

而在执行monitorexit字节码时,锁的计数器会-1,当计数器为0时,锁被释放。

如果获取对象的锁失败,那么该线程会被阻塞等待,直到之前把这个对象上锁的线程释放这个锁为止。

每个对象都有一个monitor(监视器)与之关联,所谓的上锁,就是获得对象的monitor的独占权(因为只用获得monitor才能访问这个对象)

执行monitorenter字节码的时候,线程就会尝试获得monitor的所有权,也就是尝试获得对象的锁

只有获得了monitor,才能进入同步块,或者执行同步方法

独占对象的本质是独占对象的monitor

  • 可见性

此外,synchronized也具有可见性,因为它调用的unlock解锁这个操作规定,放开对某个变量的锁的之前,需要把这个变量从缓存更新到主内存,因此它也具有可见性

  • 有序性

为什么synchronized无法禁止指令重排,却能保证有序性??

因为在一个线程内部,他不管怎么指令重排,他都是as if serial的

也就是说单线程即使重排序之后的运行结果和串行运行的结果是一样的,是类似串行的语义。

而当线程运行到同步块时,会加锁,其他线程无法获得锁,也就是说此时同步块内的方法是单线程的

根据as if serial,可以认为他是有序的

而指令重排序导致线程不安全是多线程运行的时候,不是单线程运行的时候

因此多线程运行时禁止指令重排序也可以实现有序性,这就是volatile。

volitle关键字

保证了变量在多线程中的可见性、有序性、但不保证原子性

  • 可见性

Volitle修饰的变量,在一个线程中被改变,会立刻通知总线,并通知其他线程

实现原理:如果使用这个修饰符,对该变量进行写操作之后,会立即执行store和write操作(对应的汇编代码中会加上一个lock前缀),立即将该变量从工作内存(或者说缓存)写入主内存,保证了对别的线程立即可见(因为这会导致别的线程的工作内存中该变量的缓存会失效),并且同时其他的cpu的工作内存中的值无效,直接从主内存读取并刷新工作内存。

  • 有序性

第二个特征是禁止指令重排序优化,也就是保证volatile修饰的变量不会被指令重排序优化,从而保证代码的执行顺序和程序顺序相同,保证了有序性。

实现是当变量被声明为volatile时,通过在生成的字节码中插入“内存屏障”,来禁止特定类型的指令重排序(定义了很多情况下禁止指令重排序)。

举个例子:每个volatile变量在写操作之前会有一个“写写屏障”,这表示这个写操作之前的写操作和它禁止重排序,后面会有一个“写读屏障”,这表示这个写操作和后面的读操作不能重排序。

  • 原子性不满足

但volatile关键字不保证对变量操作的原子性(synchronized可以保证原子性)

比如i++操作,这不是一个原子操作,它包括四个字节码指令,首先把i放到操作数栈栈顶,然后把int类型1放到栈顶,两个出栈相加,再入栈;而如果相加之前别线程修改了i的值,栈顶的i就是过期的,会发生错误。因此线程不安全。

也因此,使用volatile而不会引起线程不安全的前提是:1、对该变量的运算不依赖于该变量的值,或者只有一个线程能修改该变量的值。2、变量不需要与其他状态变量共同参与不变约束。

ThreadLocal关键字

使用ThreadLocal声明的变量

ThreadLocal num = new ThreadLocal(1);

会在线程中维护一个map, key是这个ThreadLocal对象,value是所真正保存的值

可以保证当前线程获取的这个变量值一定是它自己的,不会获取到其他线程的

但是有个弊端,如果使用线程池的话, 线程处理完业务后并不会被销毁,而是放到线程池中会被再取出用于其他业务处理。虽然数据上不会取到之前的,但是之前的那个ThreadLocal对象一直在被引用没有被销毁掉,可能导致oom, 解决办法处理完业务后,最后要将ThreadLocal修饰的变量手动清除引用,比如赋值为null

Lock

csdn

countDownLatch

concurrent包里面的CountDownLatch其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是同时只能有一个线程去减这个计数器里面的值。

CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止。

举一个我们项目中的例子:之前我们有个活动,月享实惠,根据每月消费总额之类的等等条件,去判断各个集卡模块是否达标
每个页面需要异步发十几个请求到我们后端,然后我们后端处理后还要再发请求到下游接口 去真正查询客户的达标状态
如果采用countDownLatch,一个线程等待收集其他线程的结果,然后再向下处理,这样就能减轻我们系统的并发压力

public class Worker implements Runnable{

    private CountDownLatch downLatch;
    private String name;

    public Worker(CountDownLatch downLatch, String name){
        this.downLatch = downLatch;
        this.name = name;
    }

    public void run() {
        this.doWork();
        try
        {
            TimeUnit.SECONDS.sleep(new Random().nextInt(10));
        }catch(InterruptedException ie){
        }
        System.out.println(this.name + "活干完了!");
        this.downLatch.countDown();

    }

    private void doWork()
    {
        System.out.println(this.name + "正在干活!");
    }

} 

public class Boss implements Runnable {

    private CountDownLatch downLatch;

    public Boss(CountDownLatch downLatch){
        this.downLatch = downLatch;
    }

    public void run() {
        System.out.println("老板正在等所有的工人干完活......");
        try {
            this.downLatch.await();
        } catch (InterruptedException e) {
        }
        System.out.println("工人活都干完了,老板开始检查了!");
    }

} 

public class CountDownLatchDemo {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();

        CountDownLatch latch = new CountDownLatch(3);

        Worker w1 = new Worker(latch,"张三");
        Worker w2 = new Worker(latch,"李四");
        Worker w3 = new Worker(latch,"王二");

        Boss boss = new Boss(latch);

        executor.execute(w3);
        executor.execute(w2);
        executor.execute(w1);
        executor.execute(boss);

        executor.shutdown();
    }
}

框架

JDBC

Jdbc的连接过程:

  • 注册驱动,将driver注册到driverManager中
  • 获取链接,driverManager根据url获取数据库连接
  • 得到执行sql的statement(提供了executeQuery,excuteUpdate,execute)
  • 执行sql语句
  • 处理结果集
  • 关闭连接

Mybatis

Mybatis主要是封装了jdbc,对外提供了api接口,对内提供了接口的实现, 不同的厂商都基于这个接口规范,比如mysql,oracle,db2等驱动。

整体流程是这样的,xmlConfigBuilder将mybatis-config.xml中的配置信息封装成一个environment, xmlMapperBuilder将mapper.xml中的sql封装成mappedStatement这两个共同组成了configuration, sqlSessionFactionBuilder基于configuration创建sqlSessionFactory然后创建sqlSession

sqlSession中引用了executor,引用了statementHandler, paramterHandler, resultSetHandler, 执行语句注入参数处理结果集

Java面试总纲_第11张图片

Java面试总纲_第12张图片

Servlet

servlet是一个java程序,面向请求和响应,生成动态的web

Servlet的框架是由两个Java包组成的:javax.servlet与javax.servlet.http

  • Web Client 客户端向Servlet容器(Tomcat)发出Http请求;
  • 接收到之后,找到项目名称和servlet名称,找到class文件的完整路径,基于反射创建HttpServlet对象
  • 同时创建一个HttpRequest对象,将Web Client请求的信息封装到这个对象中; 创建一个HttpResponse对象;
  • Servlet容器调用HttpServlet对象的service方法,把HttpRequest对象与HttpResponse对象作为参数传给 HttpServlet对象;
  • HttpServlet调用HttpRequest对象的有关方法(doGet/doPost),获取Http请求信息;HttpServlet调用HttpResponse对象的有关方法,生成响应数据;
  • Servlet容器把HttpServlet的响应结果传给Web Client;

SpringMVC

springMVC是以请求为驱动,围绕servlet设计

其核心是dispatcherServlet,它是一个Servlet

  • (1) 客户端(浏览器)发送请求url,直接请求到DispatcherServlet。
  • (2) DispatcherServlet根据请求信息调用HandlerMapping,解析请求对应的Handler, 并返回Controller的名字
  • (3) 解析到对应的Handler名字后,开始由HandlerAdapter适配器处理。
  • (4) HandlerAdapter会根据名字来调用真正的处理器ControllerHandler开处理请求,并处理相应的业务逻辑。
  • (5) 处理器处理完业务后,会返回一个ModelAndView对象,Model是返回的数据对象,View是个逻辑上的View(View的名字)。
  • (6) ViewResolver会根据逻辑View查找实际的View。
  • (7) DispaterServlet把返回的Model传给View,进行渲染。
  • (8) 通过View返回给请求者(浏览器)

Java面试总纲_第13张图片

Spring

Spring是个轻量级的框架集合, 模块包括IOC,AOP,DAO,ORM,WEB,MVC

其中最主要的是IOC和AOP

IOC实现了bean的周期管理,缓存等

Bean的生命周期

Java面试总纲_第14张图片

  1. spring 启动是会加载spring管理的bean,并对bean进行实列化(默认实列化 单例模式scope=singleton)
  2. 对实列化bean进行属性设置
  3. 检查aware相关方法以及设置相关依赖
    • 实现beanNameAware接口,将bean的id设置到setBeanName()方法
    • 实现beanFactoryAware接口,spring将调用setBeanFactory方法,将beanFactory实列传进来
    • 实现beanClassLoadAware接口,setBeanClassLoader方法被调用,将类加载器传入到bean
  4. 调用beanPostProcess前置处理
    • bean实现BeanPostProcessor接口,它的postProcessBeforeInitialization方法将被调用
  5. 检查是否是InitializingBean以决定是否调用afterProperties方法
    • bean实现了InitializingBean接口,spring将调用它的afterPropertiesSet接口
  6. 检查是否配置自定义init-method方法
    • 配置了init-method方法,会被调用
  7. 调用beanPostProcess后置处理
    • bean实现了BeanPostProcessor接口,它的postProcessAfterInitialization接口方法被调用
  8. 注册必要的destruction回调接口
  9. bean的准备已经完成,可以在应用上下文进行调用
  10. 是否实现disposableBean接口
    • bean实现了disposableBean接口,spring就会调用destroy()接口
  11. 是否配置自定义的destroy方法
    • bean上配置了destroy-method属性就会调用destroy接口

IOC

ApplicationContext为对象提供一个存储和应用的环境

有两种实现, (Xml)ClassPathXmlApplicationContext和(注解)AnnotationConfigApplicationContext

每个类被包扫描之后,会将它的配置信息存放在beanDefinition容器中

创建好的bean对象被放入beanPool池中

getBean的时候先去beanPool池中取,没有beanFactory再基于beanDefinition中的配置信息创建

Java面试总纲_第15张图片

Spring依赖注入的四种方式:1.构造器注入,2.setter方法注入,3.静态工厂注入,4.实例工厂注入

循环依赖

即A的属性中依赖B,B的属性中也依赖A

创建bean对象的时候分为3步, 1实例化对象,2注入属性,3初始化

解决依赖注入的条件

  • 1.必须是单例
  • 2.至少有一方是setter方式注入

A先实例化,然后创建一个工厂,将工厂放到三级缓存,然后进行属性注入的时候需要依赖B

B再实例化, 属性注入的时候需要依赖A, 这时候去三级缓存中取工厂,通过工厂获取A的实例对象或者代理对象,然后放入二级缓存并清除三级缓存中的工厂, 然后初始化B并走完后续流程

然后A对象进行初始化,并将bean放入一级缓存并清除二级缓存,结束

Java面试总纲_第16张图片

AOP

Aop是基于动态代理来实现的, 在动态代理中通过invoke织入功能

动态代理有两种 1.jdk动态代理。2.cglib动态代理

区别是 jdk动态代理与目标类的关系是同级且组合关系, cglib与目标类的关系是子与父的继承关系

  • @Aspect:声明是一个切面类
  • @Component: 将该类注入到spring中管理
  • @PointCut:配置一个切点
  • @PostConstruct:服务器加载servlet的时候执行一次,可以简单理解为spring容器加载时执行(被注解方法必须是void方法、非、static、不能抛出异常等)

Java面试总纲_第17张图片


数据库

数据库引擎 Innodb 和 myisam 区别

Myisam使用的是表锁,不支持高并发。

InnoDB使用的是行锁,支持高并发。

Myisam不支持外键。

InnoDB支持外键。

Myizam支持全文索引。

InnoDB不支持全文索引。不过可以通过中间件实现,比如Solr,ElasticSearch.

Myisam不支持事务,innoDB支持事务

Myisam使用的是非聚集索引,也就是树节点存储的是数据的指针(地址),当查找数据的时候,首先找到树节点相对应的指针,再根据指针去数据实际存储的位置查找真正的数据。(索引文件和数据文件是分离的)。

InnoDB使用的是聚集索引,也就是树节点实际存储的就是真实的数据,当查找数据的时候,查找到相对应的叶子结点对应的数据就结束了。(叶节点包含了完整的数据记录)

InnoDB的B+树结构(mysq数据库中的索引结构)

csdn

二叉树:

  • 二叉树每个节点最多支持两个分叉,相比于单向链表,多了一个分支
  • 二叉查找树是在二叉树上增加了一个规则:左子树所有子节点都要小于它的根结点,右子树所有子节点都要大于它的根结点

二叉查找树:

  • 二叉树有可能出现斜树的问题,导致时间复杂度升高
  • 因此引入了平衡二叉树的机制,具有二叉查找树所有特点,同时增加了一个规则:左右两个子树高度差的绝对值不超过1
  • 引入了左旋和右旋的机制实现树的平衡

B树:

  • B树是一种多路平衡查找树
  • B树满足平衡二叉树的特点,同时有多个子树
  • 子树数量等于 关键字(根结点)数量+1, 所以基于这个特征,存储同样数据量情况下平衡二叉树的高度一定高于B树

B+树在B树基础上做了增强:

  • B树数据存在每个节点上,而B+树的数据存储在叶子结点上
  • 并且通过双向链表的方式将叶子结点的所有数据进行链接
  • B+树子树数量等于关键字(根结点)的数量

B树和B+树一般是应用于文件系统或者数据库系统中,用于减少磁盘IO带来的磁盘损耗机制

以Mysql中innoDB为例,当我们通过select语句查询一条数据的时候,innoDB需要从磁盘上读取数据

而这个过程涉及到磁盘IO和磁盘随机IO,性能比较低

系统会把数据的逻辑地址传给磁盘,磁盘控制电路按照寻址的逻辑把逻辑地址翻译成物理地址,也就是确定数据在哪个磁道,哪个扇区

为了读取这个扇区的数据需要把磁头放到这个扇区上面,磁盘会不断的旋转,把目标的扇区旋转到磁头下面,使得磁头能够找到对应的磁道

涉及到寻道的时间和旋转时间的损耗,所以在查询数量比较多的情况下,磁盘IO的性能损耗是非常大的

所以在InnoDB里干脆对存储在磁盘上的数据建立一个索引,然后把索引数据以及索引列对应的磁盘地址以B+树的方式进行存储

由于B+树的分支比较多,只需要较少次数的磁盘IO就能查到目标数据

AVL树高度比B树或者B+树高度更高,而高度就意味着磁盘IO的数量,所以文件系统或数据库才会选择B树或者B+树

深入了解MySQL数据库的索引及底层结构

导致SQL执行慢的原因?

  1. 硬件问题(网路慢,内存不足,io吞吐量小,磁盘满)
  2. SQL没有使用索引或者索引失效
  3. 数据过多(分库分表)
  4. 服务器调优

慢sql问题解决入口:

  1. 开启慢查询日志,设置阈值,在生产上执行一天,在看SQL比较慢的
  2. 使用explain和慢SQL分析(system,const,eq_ref,ref,range,index,all)
  3. show profile比explain更进一步的执行细节,可以查询到执行每一个SQL都干的事情,花费多长时间
  4. 找DBA或者运维对mysql进行服务器的参数调优

什么是索引?

索引是帮助MySQL高效获取数据的数据结构(快速查找排好序的数据结构)

MySQL索引的二种结构:b+Tree索引和hash索引

索引其实也是一张表,该表保存了主键和索引的字段,并指向实体表的记录

索引的缺点:索引提高了查询速度,但是降低了更新表的速度,更新表的时候,MySQL会保存数据还需要保存一个索引文件,建立索引会占用磁盘空间。

索引类型:

FullText、hash、BTREE、RTREE

  • FULLTEXT:
    • 全文索引,目前只有myisam引擎支持,其可以在create table ,alter table 、create index使用,目前只有char varchar text 上创建全局索引。
    • 全文索引是为了解决where name like “%word%” 针对文本的模糊查询效率低的问题。
  • hash索引:
    • 哈希索引是基于哈希表实现的,只有精确的匹配索引的所有列的查询才有效,对于每一行的数据,存储引擎都会对所有的索引列计算一个哈希码,哈希码是较小的值,不同健值得行计算出来的哈希码是不一样的,哈希索引将所有的哈希码都存储的索引中,同时在哈希表中保存指向每一个数据行的指针。
    • 在mysql中,只有memory引擎支持哈希索引,这就是memory引擎表的默认类型,memory引擎同时致辞b-tree索引,memory支持非唯一哈希索引的
    • hash冲突:避免哈希 冲突,必须在where条件中代入哈希值和对应列值。不带人就直接使用CRC32()的哈希值查询
  • BTREE索引:
    • btree又叫做多路平衡查找树
    • 根结点的索引也带数据
  • b+tree
    • b+tree的每一个叶节点增加一个指向相邻叶子节点的指针,就形成带有顺序访问指针的b+tree,提高区间访问性能。
    • 比如要查询key为18到49的所有数据记录,当找到18的时候,只需要顺着节点和指针顺序遍历就可以一次性访问到所有的数据节点,极大的提高区间查询的效率(无需返回到上层父节点重复遍历查找减少IO操作)
    • 只有叶子结点的索引带数据
  • RTREE索引:
    • RTREE在MySQL很少使用,只支持geometry数据类型,支持该类型的存储引擎只有myisam、bdb、innnodb、ndb、archive
    • 相对于BTREE,RTREE的优势在于范围查找;

索引的种类:

普通索引,唯一索引,组合索引,全文索引

MySQL聚集索引和非聚集索引的区别?

根本区别:聚集索引和非聚集索引区别在 表记录排列的顺序和索引的排列顺序是否一致

优缺点:

  • 聚集索引:

    • 优点:聚集索引表记录的排序和索引的排列顺序一致,所以查询的效率快,因为只要找第一个索引值的记录,其余的连续性的记录在物理表中也会连续的存放,一起就可以查询出来(也就是说,主键和数据文件存在一起)
    • 缺点:新增比较慢,因为为了保证表中的记录的物理顺序和索引顺序一致,在记录插入的时候,会对数据页重新排序
  • 非聚集索引:

    • 索引的逻辑顺序与磁盘上行的物理存储顺序不同,非聚集索引在叶子节点存储的是主键和索引列;
    • 因此当我们使用非聚集索引查询数据的时候,需要拿到叶子节点上的主键在去表中查到想要查到的数据,这个过程叫做回表

聚集索引和非聚集索引的存储区别?

  • 聚集索引在叶子节点存储的是数据文件
  • 非聚集索引在叶子节点存储的是主键和索引列

哈希索引:

哈希索引就是采用一定的哈希算法,只需一次哈希算法即可立刻定位到相应的位置,速度比较快(实质:利用哈希值进行快速的定位)

哈希索引的缺点:

  1. 哈希索引没办法利用索引进行排序
  2. 不能进行多字段的查询
  3. 存在大量重复的健值的情况下,哈希索引的效率就低(因为存在重复的键值就会出现哈希碰撞)
  4. 不支持范围查

B+TREE索引:

MySQL的常用的innodb引擎中使用的是b+tree的索引

  • 非叶子节点的子树指针与关键字个数相同
  • 所有的叶子节点都增加一个链指针
  • 所有的关键字都会在叶子节点上出现
  • 所有的关键字都出现在叶子节点的链表。

比如要查询key为18到49的所有数据记录,当找到18的时候,只需要顺着节点和指针顺序遍历就可以一次性访问到所有的数据节点,极大的提高区间查询的效率(无需返回到上层父节点重复遍历查找减少IO操作)

联合索引以及联合索引的失效问题

当多条件联合查询时,优化器会评估哪一个条件的索引效率高,会选择最佳的索引去使用。

因此:多个单列索引在条件查询时候优化器会选择最优索引策略,可能只用一个索引,也可能将多个索引都用上。

多个索引的底层都是建立在多个B+TREE,比较占用空间,也就是会浪费搜索效率,所以多条件联合查询最好建联合索引,

联合索引:遵循左匹配原则,最左优先,以最左边的为起点任何连续的索引都会匹配上,如果出现不连续,就会出现不匹配

联合索引的失效情况?

  1. 违法最左匹配原则(连续性),列如:建立索引(a,b,c),这是时候能建立索引的有(a),(a,b),(a,b,c)三种查询
  2. 在索引列上使用(计算,函数,类型转换等操作,)都会导致索引失效而进行全表扫描
  3. 使用不等于 (!=)
  4. like中以通配符开头("%abc")
  5. 字符串不加单引号索引失效
  6. or连接索引失效
  7. order by (正常索引参与排序,没有违反左匹配原则)一旦违反左匹配原则,就会导致额外的文件排序
  8. group by 违反左匹配原则,会导致产生临时表

数据库连接池只有100个连接,有3000个连接请求

  1. 如果是长期请求都比较多,根据实际情况与经验适当增加连接池中连接的数量
  2. 使用资源调度,建立合适的队列,设置优先级,让优先级高的先执行,优先级低的排队执行
  3. 使用二八分配原则,使用缓存技术,实际上百分之八十的访问只集中在百分之二十的数据上,所以把经常访问的数据存放到缓存中,这个可以减轻IO,减少数据库访问,直接从缓存中读取数据,没有命中,再去访问数据库,缓存里面的数据要定时更新
  4. 增加数据库服务器的数量,使用读写分离技术,让主数据库服务器负责增、删、改操作,多个从服务器负责读操作,读的请求通过负载均衡,根据从服务器的访问压力来进行分配服务请求。

数据库锁

MySQL锁概述

相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。

比如,MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);

BDB存储引擎采用的是页面锁(page-level locking),但也支持表级锁;

InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。

数据库的三种锁特性

开销 加锁速度 死锁 粒度 并发性能
表级锁 开销小 加锁快 不会出现死锁 锁定粒度大 发生锁冲突的概率最高,并发度最低。
行级锁 开销大 加锁慢 会出现死锁 锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
页面锁 开销界于表锁和行锁之间 加锁时间界于表锁和行锁之间 会出现死锁 锁定粒度界于表锁和行锁之间 并发度一般。

什么是锁?

锁(lock)在多人处理同一个数据的时候,保证每次只有一个人可以操作。

MySQL提供了页锁(全局锁)、行锁、表锁。其中innodb采用的是行锁和表锁,myisam只支持表锁。

什么是死锁

是指二个或者二个以上的进程在执行时候,因为争夺资源造成相互等待的现象,进程一直处于等待中,无法得到释放,这种状态就叫做死锁,

数据库死锁出现的案列

批量入库,存在则更新,不存在则插入,insert into tab(xx,xx) on duplicate key update xx=‘xx’

如何处理死锁?

  1. 通过innodblockwait_timeout来设置超时时间,一直等待直到超时
  2. 发起死锁检测,发现死锁之后,主动回滚死锁中的事务,不需要其他事务继续

如何避免死锁?

为了在单个innodb表上执行多个并发写入操作时避免死锁,可以在事务开始时,通过为预期要修改行,使用select …for update语句来获取必要的锁,即使这些行的更改语句是在之后才执行的

在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时在申请排他锁。因为这时候当用户在申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,

如果事务需要修改或锁定多个表,则应在每个事务中以相同的顺序使用加锁语句。在应用中,如果不同的程序会并发获取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。

通过 select …lock in share mode获取行的读锁后,如果当前事务在需要对该记录进行更新操作,则很有可能造成死锁;

改变事务隔离级别

innodb默认是如何对待死锁的?

innodb默认是使用设置死锁时间来让死锁超时的策略,默认innodblockwait_timeout设置的时长是50s

如何开启死锁检测?

设置innodbdeadlockdetect设置为on可以主动检测死锁,在innodb中这个值默认就是on开启的状态

什么是全局锁?它的应用场景有哪些?

全局锁就是对整个数据库实例加锁,它的典型使用场景就是做全库逻辑备份,这个命令可以使用整个库处于只读状态,使用该命令之后,数据更新语句,数据定义语句,更新类事务的提交语句等操作都会被阻塞。

使用全局锁会导致的问题?

如果在主库备份,在备份期间不能更新,业务停止,所以更新业务会处于等待状态

如果在从库备份,在备份期间不能执行主库同步的binlog,导致主从延迟

如何处理逻辑备份时,整个数据库不能插入的情况?

如果使用全局锁进行逻辑备份都会让整个库成为只读状态,解决办法需要使用MySQLdump时,使用参数-single-transaction就会在导入数据之前启动一个事务来保证数据的一致性,并且这个过程是支持数据更细操作的。

innodb如何实现行锁?

行锁是MySQL中粒度最小的一种锁,innodb的行锁有共享锁和排他锁两种

共享锁允许事务读一行记录,不允许任何线程对行记录进行修改操作

排他锁允许当前事务删除或者更新一行记录,其他线程不能操作该记录。

什么是共享锁?

共享锁是读取操作创建的锁。其他用户可以并发读取数据,但是任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。当如果事务对读锁进行修改操作,很可能会操作死锁。

什么是排他锁?

排他锁又称写锁

事务对某一行加上排他锁,只能这个事务对其进行读写操作,在事务结束以前,其他事务不能对其进行加任何锁,其他进程可以读取,但是不能进行写操作,需要等待释放以后。排他锁是悲观锁的一种实现。

事务1对数据对象A加上排他锁,事务1可以读写A,其他事务就不能在对A进行任何加锁操作,直到事务1释放A上的锁,这保证其他事务在事务1释放A上的锁之前不能在读取和修改A,排他锁会阻塞所有的排他锁和共享锁。

悲观锁和乐观锁的区别?

悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据都会block直到它拿到锁。因此,悲观锁需要耗时比较的多,跟乐观锁比较,悲观锁是有数据库自己实现的,用的时候我们直接调用数据的相关语句就可以

悲观锁涉及到的另两个锁,他们是共享锁和排他锁,共享锁和排他锁时悲观锁的不同的实现,属于悲观锁的范畴。

乐观锁是用数据版本记录机制实现,这是乐观锁最常用的方式

所谓的数据版本,为数据增加一个版本号的字段,一般是通过为数据表增加一个数据类型的version字段实现,当读取数据时,将把二十年字段的值一同读取出来,数据每次更新都需要对version值加一,在我们提交更新的时候,判断数据表对应记录的当前版本信息与第一次取出来的version值进行对比,如果数据库的表当前版本号鱼取出来的version值相等,则给与更新否则认为过期数据不给与更新。

乐观锁有什么优点和缺点?

乐观锁虽然叫锁其实在使用的时候是没有加锁,所以执行性能高。

缺点:会产生ABA的问题,ABA问题指的是有一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到他的值还是为A值,会误认为没有被修改做为正常的执行修改操作,实际上这段时间他的值可能被修改为其他值,之后又被修改为A值,

innodb存储引擎有几种算法?

1.record lock—单个行记录上的锁
2.gap lock — 间隙锁,锁定一个氛围,不包括记录本身
3.next-key lock–锁定一个范围,包含记录本身

优化锁方面的意见?

  • 使用较低的隔离级别
  • 设计索引,尽量使用索引去访问数据,加锁更加精确,从而减少锁冲突
  • 选择合理的事务大小,
  • 给记录显示加锁时,最好一次性请求足够级别的锁。列如,修改数据的话,最好申请排他锁,而不是先申请共享锁,修改时在申请排他锁,这样会导致死锁
  • 不同的程序访问一组表的时候,应尽量约定一个相同的顺序访问各表,对于一个表而言,尽可能的固定顺序的获取表中的行。这样大大的减少死锁的机会。
  • 尽量使用相等条件访问数据,这样可以避免间隙锁对并发插入的影响
  • 不要申请超过实际需要的锁级别
  • 数据查询的时候不是必要,不要使用加锁。MySQL的MVCC可以实现事务中的查询不用加锁,优化事务性能:MVCC只在committed read(读提交)和 repeatable read (可重复读)两种隔离级别
  • 对于特定的事务,可以使用表锁来提高处理速度活着减少死锁的可能。

mysql数据库读写分离原理

Java面试总纲_第18张图片

  • 什么是读写分离(读写分离原理)

读写分离,基本的原理是让主数据库处理事务性增、改、删操作(INSERT、UPDATE、DELETE),而从数据库处理SELECT查询操作。

数据库复制被用来把事务性操作导致的变更同步到集群中的从数据库。

读写分离就是在主服务器上修改,数据会同步到从服务器,从服务器只能提供读取数据,不能写入,实现备份的同时也实现了数据库性能的优化,以及提升了服务器安全。

  • 为什么读写分离

因为数据库的“写”(写10000条数据到oracle可能要3分钟)操作是比较耗时的。

但是数据库的“读”(从oracle读10000条数据可能只要5秒钟)。

所以读写分离,解决的是,数据库的写入,影响了查询的效率。

  • 什么时候用读写分离

数据库不一定要读写分离,如果程序使用数据库较多时,而更新少,查询多的情况下会考虑使用,利用数据库 主从同步

可以减少数据库压力,提高性能。当然,数据库也有其它优化方案。memcache 或是 表折分,或是搜索引擎。都是解决方法。

mysql主从复制机制

Java面试总纲_第19张图片

  • mysq支持的复制类型
  1. 基于语句的复制。在服务器上执行sql语句,在从服务器上执行同样的语句,mysql默认采用基于语句的复制,执行效率高。
  2. 基于行的复制。把改变的内容复制过去,而不是把命令在从服务器上执行一遍。
  3. 混合类型的复制。默认采用基于语句的复制,一旦发现基于语句无法精确复制时,就会采用基于行的复制。
  • 复制的工作过程
  1. 在每个事务更新数据完成之前,master在二进制日志记录这些改变。写入二进制日志完成后,master通知存储引擎提交事务。

  2. Slave将master的binary log复制到其中继日志

    • 首先slave开始一个工作线程(I/O)
    • I/O线程在master上打开一个普通的连接,然后开始binlog dump process
    • binlog dump process从master的二进制日志中读取事件
    • 如果已经跟上master,它会睡眠并等待master产生新的事件,I/O线程将这些事件写入中继日志。
  3. Sql slave thread(sql从线程)处理该过程的最后一步,

    • sql线程从中继日志读取事件,并重放其中的事件而更新slave数据,使其与master中的数据一致,
    • 只要该线程与I/O线程保持一致,中继日志通常会位于os缓存中,所以中继日志的开销很小。
  • 前较为常见的Mysql读写分离分为以下两种:
  1. 基于程序代码内部实现

在代码中根据select 、insert进行路由分类,这类方法也是目前生产环境下应用最广泛的。优点是性能较好,因为程序在代码中实现,不需要增加额外的硬件开支,缺点是需要开发人员来实现,运维人员无从下手。

  1. 基于中间代理层实现

代理一般介于应用服务器和数据库服务器之间,代理数据库服务器接收到应用服务器的请求后根据判断后转发到,后端数据库,有以下代表性的程序。

(1)mysql_proxy。mysql_proxy是Mysql的一个开源项目,通过其自带的lua脚本进行sql判断。

MySQL官方提供的数据库代理层产品MySQLProxy搭建读写分离。

MySQLProxy实际上是在客户端请求与MySQLServer之间建立了一个连接池。所有客户端请求都是发向MySQLProxy,然后经由MySQLProxy进行相应的分析,判断出是读操作还是写操作,分发至对应的MySQLServer上。对于多节点Slave集群,也可以起做到负载均衡的效果

Java面试总纲_第20张图片

(2)Atlas。是由 Qihoo 360, Web平台部基础架构团队开发维护的一个基于MySQL协议的数据中间层项目。它是在mysql-proxy 0.8.2版本的基础上,对其进行了优化,增加了一些新的功能特性。360内部使用Atlas运行的mysql业务,每天承载的读写请求数达几十亿条。支持事物以及存储过程。

(3)Amoeba。由阿里巴巴集团在职员工陈思儒使用序java语言进行开发,阿里巴巴集团将其用户生产环境下,但是他并不支持事物以及存储过程。

Java面试总纲_第21张图片

redis中的数据类型和数据结构

Java面试总纲_第22张图片

redis主从复制机制

常用3招

  • 一主二仆

一个Master,两个Slave,Slave只能读不能写;

当Slave与Master断开后需要重新slave of连接才可建立之前的主从关系;Master挂掉后,Master关系依然存在,Master重启即可恢复。

  • 薪火相传

上一个Slave可以是下一个Slave的Master,Slave同样可以接收其他slaves的连接和同步请求,那么该slave作为了链条中下一个slave的Master,如此可以有效减轻Master的写压力。如果slave中途变更转向,会清除之前的数据,重新建立最新的。

  • 反客为主

当Master挂掉后,Slave可键入命令 slaveof no one使当前redis停止与其他Master redis数据同步,转成Master redis。

复制原理

  1. Slave启动成功连接到master后会发送一个sync命令;
  2. Master接到命令启动后的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步;
  3. 全量复制:而slave服务在数据库文件数据后,将其存盘并加载到内存中;
  4. 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步;
  5. 但是只要是重新连接master,一次完全同步(全量复制)将被自动执行。

哨兵模式(sentinel)

反客为主的自动版,能够后台监控Master库是否故障,如果故障了根据投票数自动将slave库转换为主库。一组sentinel能同时监控多个Master。

使用步骤:

  1. 在Master对应redis.conf同目录下新建sentinel.conf文件,名字绝对不能错;
  2. 配置哨兵,在sentinel.conf文件中填入内容:
    sentinel monitor 被监控数据库名字(自己起名字) ip port 1

并发

redis和数据库双写一致性问题

从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。因此,接下来讨论的思路不依赖于给缓存设置过期时间这个方案。

三种策略可供参考

  • 先更新数据库,再更新缓存
  • 先删除缓存,再更新数据库
  • 先更新数据库,再删除缓存

1.先更新数据库,再更新缓存(有脏数据,不采用)

举例:同时有请求A和请求B进行更新操作,那么会出现

  • 线程A更新了数据库
  • 线程B更新了数据库
  • 线程B更新了缓存
  • 线程A更新了缓存

这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。

2.先删缓存,再更新数据库(有脏数据,不采用,可用延时双删策略解决)

该方案会导致不一致的原因是

同时有一个请求A进行更新操作,另一个请求B进行查询操作

那么会出现如下情形:

  • 请求A进行写操作,删除缓存
  • 请求B查询发现缓存不存在
  • 请求B去数据库查询得到旧值
  • 请求B将旧值写入缓存
  • 请求A将新值写入数据库

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

那么,如何解决上面这种情况呢?采用延时双删策略

3.延时双删策略(可以采用)

  • 先淘汰删除缓存
  • 再写数据库(这两步和原来一样)
  • 休眠1秒,再次淘汰删除缓存

这么做,可以将1秒内所造成的缓存脏数据,再次删除。

那么,这个1秒怎么确定的,具体该休眠多久呢?

针对上面的情形,应该自行评估自己的项目的读数据业务逻辑的耗时。

然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

4.先更新数据库,再删缓存(可以采用)

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从cache中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

杠精:这种策略也有极端情况

并发情况,假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生

  • 缓存刚好失效
  • 请求A查询数据库,得一个旧值
  • 请求B将新值写入数据库
  • 请求B删除缓存
  • 请求A将查到的旧值写入缓存

但是这种情况概率很低,因为先天条件是数据库 写 操作 比 读 操作 慢,这一点很难发生

高并发情况下如何保证数据安全

数据库行锁

kafka异步执行数据库操作,redis控制库存数量,利用redis的incr和decr的原子性

分布式锁(效率差)

高并发情况下如何保证系统安全

高并发情况下如何解决redis雪崩和穿透

解决雪崩: 限流、降级、熔断、缓存备份

解决穿透:业务层面查询到null,也在redis中存null,以保证请求不再向后面服务发送


事务

ACID

1、原子性:

一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。
事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

2、一致性:

在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

3、隔离性:

数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执 行时由 于 交 叉 执 行而导致数据 的不一致 。 事务隔离分为不同级别,包括读未 提 交 ( Readuncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。

4、持久性:

事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

Spring 和 数据库 事务的隔离级别

脏读是指一个事务在处理数据的过程中,读取到另一个为提交事务的数据。

不可重复读是指对于数据库中的某个数据,一个事务范围内的多次查询却返回了不同的结果,这是由于在查询过程中,数据被另外一个事务修改并提交了

幻读是事务非独立执行时发生的一种现象。

例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。

幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)

  1. 未提交读: 会导致 读脏
  2. 提交读: 解决脏读
  3. 可重复读: 解决脏读,不可重复读
  4. 序列化(串行化): 解决脏读,不可重复读,和幻读;

    这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
  5. Spring还有一个默认, 按照数据库事务级别来

传播行为

有七大传播行为,也是在TransactionDefinition接口中定义。

  • PROPAGATION_REQUIRED:支持当前事务,如当前没有事务,则新建一个。
  • PROPAGATION_SUPPORTS:支持当前事务,如当前没有事务,则已非事务性执行(源码中提示有个注意点,看不太明白,留待后面考究)。
  • PROPAGATION_MANDATORY:支持当前事务,如当前没有事务,则抛出异常(强制一定要在一个已经存在的事务中执行,业务方法不可独自发起自己的事务)。
  • PROPAGATION_REQUIRES_NEW:始终新建一个事务,如当前原来有事务,则把原事务挂起。
  • PROPAGATION_NOT_SUPPORTED:不支持当前事务,始终已非事务性方式执行,如当前事务存在,挂起该事务。
  • PROPAGATION_NEVER:不支持当前事务;如果当前事务存在,则引发异常。
  • PROPAGATION_NESTED:如果当前事务存在,则在嵌套事务中执行,如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作(注意:当应用到JDBC时,只适用JDBC 3.0以上驱动)。

Spring实现编程式事务,依赖于2大类,PlatformTransactionManager,与模版类TransactionTemplate(推荐使用)
声明式事务实现方式, Spring的tx:advice定义事务通知与AOP相关配置实现,另为一种通过@Transactional

Spring事务什么时候会失效?

  1. 如果数据库不支持事务,则失效

因为事务是作用于数据库。例如使用MySQL且引擎是MyISAM,则事务会不起作用,因为MyISAM引擎本身不支持事务;如果改成InnoDB,则可以。

  1. Service类没有被Spring管理

因为Spring的事务是基于AOP,所以如果Service类没有被Spring管理,变成一个Spring Bean,即使添加了@Transactional注解,事务也是无效的。

  1. 内部调用

不带事务的方法调用该类中带事务的方法,不会回滚。因为Spring的回滚是用过代理模式生成的,如果是一个不带事务的方法调用该类的带事务的方法,直接通过this.xxx()调用,而不生成代理事务,所以事务不起作用。常见解决方法“拆类”

  1. 发生的异常不是RuntimeException, 比如IOException

spring的事务默认是对RuntimeException进行回滚,而不继承RuntimeException的不回滚

因为在java的设计中,它认为不继承RuntimeException的异常是CheckException或普通异常,如IOException,这些异常在java语法中是要求强制处理的。

对于这些普通异常,Spring默认它们都已经处理,所以默认不回滚。可以添加rollbackfor=Exception.class来表示所有的Exception都回滚

  1. 异常被捕捉处理没有抛出
  2. 事务只能应用于 public 方法

@Transactional注解只能应用于public方法,如果你在protected、private或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。

分布式事务控制

TPC

一般的实现形式:所有的事务一阶段执行sql不提交 ,都成功之后TC通知所有事务进行二阶段主动提交,如果有一个失败TC通知所有事务进行二阶段回滚

一阶段:协调器问“你们几个子事务参与者对应的活能不能干成?" 子事务参与者们一一回复“能干/干不成”

二阶段:协调器问根据子事务参与者们的反馈如果都能干则告诉所有人都去干吧,如果有人说干不了,特通知大家别干了

Java面试总纲_第23张图片

TCC

try阶段所有参与者进行尝试提交业务(eg:创建订单的订单状态是CREATING,减库存虽然进行了100-2=98,但是会记录本次有2个冻结中的库存,等类似try操作);

Confirm阶段 如果try阶段的执行都成功了则TM通知所有参与者执行真正的提交(eg:创建订单的订单状态改为CREATED,减库存 被冻结的2个库存直接删掉,等类似Confirm操作【因为用网络超时等原因可能会有重复的调用所有要求支持幂等性】);

cancel阶段 如果try中有一个执行失败则TM通知所有参与者进行补偿操作(eg:创建订单的订单状态改为CANCELED,减库存中被冻结的2重新加回到数据库中,等类似cancel操作

Java面试总纲_第24张图片

幂等性

幂等性:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等。

  • 1.token机制解决幂等性

使用uuid生成一个防重令牌token,并把token放到redis里,然后把这个token,封装到出参,给到前端的订单确定页面

  • 2.防重表解决幂等性
  • 3.业务层分布式锁

工具

Nginx

反向代理

Java面试总纲_第25张图片

正向代理

Java面试总纲_第26张图片

server{
listen 80;
server_name manage.jt.com;
location / {
proxy_pass http://jtWindows;
}
}
#定义windows集群
upstream jtWindows {
server localhost:8081;
server localhost:8082;
server localhost:8083;
}

rabbitMQ

Java面试总纲_第27张图片

rabbitmq生产

Java面试总纲_第28张图片

rabbitmq消费

Java面试总纲_第29张图片

kafka

Java面试总纲_第30张图片

Java面试总纲_第31张图片

kafka生产

Java面试总纲_第32张图片

kafka消费

Java面试总纲_第33张图片

kafka对消息保存时根据Topic进行归类,发送消息者就是Producer,消息接受者就是Consumer,每个kafka实例称为broker。然后三者都通过Zookeeper进行协调

kafka中的broker 是消息的代理,Producers往Brokers里面的指定Topic中写消息,Consumers从Brokers里面pull拉取指定Topic的消息,然后进行业务处理,broker在中间起到一个代理保存消息的中转站。

每个Topic被分成多个partition(区)。每条消息在partition中的位置称为offset(偏移量),类型为long型数字。消息即使被消费了,也不会被立即删除,而是根据broker里的设置,保存一定时间后再清除,比如log文件设置存储两天,则两天后,不管消息是否被消费,都清除。

每个consumer属于一个consumer group。在kafka中,一个partition的消息只会被group中的一个consumer消费

kafka如何确保数据不丢失

  • 生产时

kafka的ack机制:在kafka发送数据的时候,每次发送消息都会有一个确认反馈机制,确保消息正常的能够被收到。

  • 消费时

通过offset commit 来保证数据的不丢失,kafka自己记录了每次消费的offset数值,下次继续消费的时候,接着上次的offset进行消费即可

kafka怎么保证消息的消费顺序?

kafka只保证单partition有序,如果Kafka要保证多个partition有序,不仅broker保存的数据要保持顺序,消费时也要按序消费。

假设partition1堵了,为了有序,那partition2以及后续的分区也不能被消费,这种情况下,Kafka 就退化成了单一队列,毫无并发性可言,极大降低系统性能。因此Kafka使用多partition的概念,并且只保证单partition有序。这样不同partiiton之间不会干扰对方。

kafka保证消息顺序有2种方法。

  • 第1种:全局消费顺序

实现方式:1个Topic(主题)只创建1个Partition(分区),这样生产者的所有数据都发送到了一个Partition(分区),保证了消息的消费顺序。

比如3个直播间同时发消息,全局顺序就是保证直播间1先发的消息那么一定先到

  • 第2种:局部消费顺序

实现方式:生产者在发送消息的时候指定要发送到哪个Partition(分区)(1个)。

比如3个直播间同时发消息,局部顺序就是直播间1先发,直播间2后发,但是可能直播间2的消息先到,这个顺序是不保证的。但是直播间1先发了“消息1”,再发了“消息2”,这个顺序是能保证的,也就是在直播间内是有序的,但是直播间之间的消息顺序不能保证。

消费者以组的名义订阅topic,topic下有多个partition,消费者组中有多个消费者实例。
同一时刻,一条消息只能被组中的一个消费者实例消费。
如果按照从属关系来说的话就是,主题下的每个分区只从属于组中的一个消费者,不可能出现组中的两个消费者负责同一个分区。消息就是存储在partition中。

RabbitMQ和Kafka的区别

  • rabbitmq:
  1. producer,broker遵循AMQP(exchange,bind,queue),consumer;
  2. broker为中心,exchange分topic,direct,fanout和header,路由模式适合多种场景;
  3. consumer消费位置由broker通过确认机制保存;
  • kafka:
  1. producer,broker,consumer,未遵循AMQP;
  2. consumer为中心,获取消息模式由consumer自己决定;
  3. offset保存在消费者这边,broker无状态;
  4. 消息是名义上的永久存储,每个parttition按segment保存自己的消息为文件(可配置清理周期);
  5. consumer可以通过重置offset消费历史消息;
  6. 需要绑定zk;
  • AMQP是什么

AMQP(Advanced Message Queuing Protocol,高级消息队列协议)是一个进程间传递异步消息的网络协议。

Java面试总纲_第34张图片

发布者(Publisher)发布消息(Message),经由交换机(Exchange)。

交换机根据路由规则将收到的消息分发给与该交换机绑定的队列(Queue)。

最后 AMQP 代理会将消息投递给订阅了此队列的消费者,或者消费者按照需求自行获取。

kafka的优势

  1. 高吞吐量、低延迟:kafka每秒可以处理几十万条的消息,它的延迟最低只有几毫秒
  2. 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失
  3. 容错性:允许集群中节点故障(若副本数量为N,则允许N-1个节点故障)
  4. 高并发:支持数千个客户端同时读写(多个生产者和多个消费者同时读写)
  5. 可扩展性:kafka集群支持热扩展

Zookeeper

什么是ZooKeeper

zookeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 google chubby 的开源实现,是 hadoop 和 hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

zookeeper 都有哪些功能?

  • 集群管理:监控节点存活状态、运行请求等。
  • 主节点选举:主节点挂掉了之后可以从备用的节点开始新一轮选主,主节点选举说的就是这个选举的过程,使用 zookeeper 可以协助完成这个过程。
  • 分布式锁:zookeeper 提供两种锁:独占锁、共享锁。独占锁即一次只能有一个线程使用资源,共享锁是读锁共享,读写互斥,即可以有多线线程同时读同一个资源,如果要使用写锁也只能有一个线程使用。zookeeper可以对分布式锁进行控制。
  • 命名服务:在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。

zookeeper 有三种部署模式:

  • 单机部署:一台集群上运行;
  • 集群部署:多台集群运行;
  • 伪集群部署:一台集群启动多个 zookeeper 实例运行。

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

zookeeper的核心是原子广播,这个机制保证了各个server 之间的同步。

实现这个机制的协议叫做 zab 协议。

zab 协议有两种模式,分别是恢复模式(选主)和广播模式(同步)。

当服务启动或者在领导者崩溃后,zab 就进入了恢复模式,当领导者被选举出来,且大多数 server 完成了和 leader 的状态同步以后,恢复模式就结束了。

状态同步保证了 leader 和 server 具有相同的系统状态。


设计模式

http://c.biancheng.net/design_pattern/


网络

HTTP和HTTPS的区别

HTTP 明文传输,数据都是未加密的,安全性较差,HTTPS(SSL+HTTP) 数据传输过程是加密的,安全性较好。

HTTPS比HTTP更加安全,对搜索引擎更友好,利于SEO,谷歌、百度优先索引HTTPS网页;

使用 HTTPS 协议需要到 CA(数字证书认证机构) 申请SSL证书。

HTTP 页面响应速度比 HTTPS 快,主要是因为 HTTP 使用 TCP 三次握手建立连接,客户端和服务器需要交换 3 个包,而 HTTPS除了 TCP 的三个包,还要加上SSL 握手需要的 9 个包,所以一共是 12 个包。

HTTP 和 HTTPS 用的端口也不一样,前者是 80,后者是 443。

在 OSI 网络模型中,HTTP 工作于应用层,而 HTTPS 工作在传输层。

HTTP和TCP的区别和联系

TCP连接

手机能够使用联网功能是因为手机底层实现了TCP/IP协议,可以使手机终端通过无线网络建立TCP连接。TCP协议可以对上层网络提供接口,使上层网络数据的传输建立在“无差别”的网络之上。

建立起一个TCP连接需要经过“三次握手”:

第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连 接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次握手”(过程就不细写 了,就是服务器和客户端交互,最终确定断开)

HTTP连接

HTTP协议即超文本传送协议(Hypertext Transfer Protocol ),是Web联网的基础,也是手机联网常用的协议之一,HTTP协议是建立在TCP协议之上的一种应用。

HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。

1)在HTTP 1.0中,客户端的每次请求都要求建立一次单独的连接,在处理完本次请求后,就自动释放连接。

2)在HTTP 1.1中则可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后再发送下一个请求。

由于HTTP在每次请求结束后都会主动释放连接,因此HTTP连接是一种“短连接”,要保持客户端程序的在线状态,需要不断地向服务器发起连接请求。通常的 做法是即时不需要获得任何数据,客户端也保持每隔一段固定的时间向服务器发送一次“保持连接”的请求,服务器在收到该请求后对客户端进行回复,表明知道客 户端“在线”。若服务器长时间无法收到客户端的请求,则认为客户端“下线”,若客户端长时间无法收到服务器的回复,则认为网络已经断开。

相互联系和区别

TCP是传输层,而http是应用层

http是要基于TCP连接基础上的: 简单的说,TCP就是单纯建立连接,不涉及任何我们需要请求的实际数据,简单的传输。http是用来收发数据,即实际应用上来的。

TCP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。

TCP/IP和HTTP协议的关系,从本质上来说,二者没有可比性,我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如果没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET等,也可以自己定义应用层协议。WEB使用HTTP协议作应用层协议,以封装HTTP 文本信息,然后使用TCP/IP做传输层协议将它发到网络上。

Http协议是建立在TCP协议基础之上的,当浏览器需要从服务器获取网页数据的时候,会发出一次Http请求。Http会通过TCP建立起一个到服务器的连接通道,当本次请求需要的数据完毕后,Http会立即将TCP连接断开,这个过程是很短的,所以Http连接是一种短连接,是一种无状态的连接。所谓的无状态,是指浏览器每次向服务器发起请求的时候,不是通过一个连接,而是每次都建立一个新的连接。如果是一个连接的话,服务器进程中就能保持住这个连接并且在内存中记住一些信息状态。而每次请求结束后,连接就关闭,相关的内容就释放了,所以记不住任何状态,称为无状态连接。而我们直接通过Socket编程使用TCP协议的时候,因为我们自己可以通过代码区控制什么时候打开连接什么时候关闭连接,只要我们不通过代码把连接关闭,这个连接就会在客户端和服务端的进程中一直存在,相关状态数据会一直保存着。

形象的描述:HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。对于从C#编程的角度来讲,为了方便,你可以直接选择已经制造好的轿车Http来与服务器交互。但是有时候往往因为环境因素或者其他的一些定制的请求,必须要使用TCP协议,这时就需要使用Socket编程,然后自己去处理获取的数据。就像是你用已有的发动机,自己造了一辆卡车,去从服务器交互。

HTTP(超文本传输协议)是利用TCP在两台电脑(通常是Web服务器和客户端)之间传输信息的协议。客户端使用Web浏览器发起HTTP请求给Web服务器,Web服务器发送被请求的信息给客户端。

你可能感兴趣的:(Java,java,面试,程序人生)