Java 相关

JDK 1.8对hash算法和寻址算法如何优化的

image

有一个32位的key的hash值,将此二进制值右移16位,低16位的值变为高16位的值,然后在进行异或运算

image

[16个元素]->hash值对数组长度取模,定位到数组的一个位置,塞进去

寻址算法优化

(n-1)& hash ->数组里的一个位置

1111 1111 1111 1111 1111 1010 0111 1101(没有经过优化的 hash 值)

0000 0000 0000 0000 0000 0000 0000 1111(n-1)

取模运算性能比较差,为了优化寻址效率,所以(n-1)& hash -> 效果是和hash对n取模是一样的,但是与运算的性能要比hash对n取模要高很多。数学问题,只要保持数组长度是2的n次方,hash 对 n 取模的效果就和 hash&(n-1)是一样的,后者性能更高。

(n-1)一般很小,高16位一般都为0,没有经过优化的hash值的高16位和n-1的高16位之间的与运算都为0,是可以忽略的,核心点在于低16位的与运算。hash值的高16位没有参与到运算

1111 1111 1111 1111 0000 0101 1000 0011(经过优化的hash值),优化后的hash值的低16位包含了原来未优化的hash值的高16位和低16位的特性,在有些hash值的低16位很相似,优化后的hash值融合了原来高低16位的特征,让hash值尽量不一样,避免hash冲突的发生。
image

HashMap如何解决hash碰撞 ,链表+红黑树( O(n) ,O(logn))

当有两个key或多个key,他们算出来的hash值与 n-1 进行与运算之后,发现定位出来的位置是一样的。

会在这个位置挂一个链表,在这个链表里放入多个元素,让多个 key-value 放在数组的一个位置。

get时,如果发现定位的位置挂了一个链表,遍历链表取出自己需要的那个 key-value就可以。

但是假设链表很长的话可能会导致遍历链表,性能比较差 O(n)。

优化:如果链表长度到达一定长度,就会把链表转为红黑树,遍历一颗红黑树查找元素,此时为 O(logn),性能会比链表更高。

HashMap是如何扩容的?

2倍扩容。

[数组长度16]

image

数组长度扩容之后=32,数组中元素的位置可能发生变化,需要 rehash 对每个hash值进行寻址,也就是用每个 hash 值跟新数组长度的 length-1 进行与操作。

image

判断二进制结果中是否多出来一个bit为1的位,如果没多,那么就是原来的 index ,如果多了,那么就是 index+oldCap (index+原数组长度),通过这种方式避免了对数组.length取模。

synchronized关键字的底层原理是什么?

如果用到了synchronized关键字,在底层编译后的 jvm 指令中会有 monitorenter 和 monitorexit 两个指令。

加锁会执行 monitorenter 指令,释放锁会执行 monitorexit 指令。

monitorenter 执行时,每个对象都有一个关联的 monitor ,一个类的class 对象也有一个 monitor ,如果要对这个对象加锁,那么必须获取这个对象的 monitor。

它的原理和思路大概如下:monitor 里面有一个计数器,是从0开始的,如果一个线程要获取 monitor 的锁,就看看它的计数器是不是0 如果是0,说明没人获取锁,他就可以获取锁,然后计数器加1.

monitor 支持重入加锁的
image
image

当synchronized代码执行完毕后,会执行monitorexit指令来对计数器进行减操作,释放锁。

CAS的理解及其底层实现原理

当出现多线程并发安全问题时,可能要用到并发包下面的很多技术,如synchronized:

Map map=new HashMap();

synchronized(map){
 //对map里的数据进行复杂的读写处理
}

并发包下的其他一些技术:

CAS

一段代码:
image

class MyObject{
package com.basic.sync;

public class MyObject extends Thread{
    private int i=0;
    //对 MyObject 对象

    @Override
    public synchronized void run() {
        super.run();
        i++;
        System.out.println(currentThread().getName()+":"+i);
    }

    public static void main(String[] args) {
        Thread myObject = new MyObject();
        Thread t1 = new Thread(myObject, "A");
        Thread t2 = new Thread(myObject, "B");
        Thread t3 = new Thread(myObject, "C");
        Thread t4 = new Thread(myObject, "D");
        Thread t5 = new Thread(myObject, "E");
        Thread t6 = new Thread(myObject, "F");
        Thread t7 = new Thread(myObject, "G");
        Thread t8 = new Thread(myObject, "H");

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();
        t7.start();
        t8.start();

    }
}

}

只有一个线程可以对 MyObject 加锁,可以对他关联的 monitor 计数器去加一,加锁,一旦多个线程并发的去进行 synchronized加锁,串行化,效率不是太高,很多线程都要去排队去执行。

image

CAS: compare and set,CAS 操作时原子的(底层硬件级别保证一定是原子的,同一时间只有一个线程可以执行 CAS),先比较,再设置,其他线程同时间去执行 CAS 会失败。
image

ConcurrentHashMap 实现线程安全的底层原理

HashMap 底层是一个大的数组,有很多元素。

假若:Map map = new HashMap()

//多个线程过来,线程 1 要 put 的位置是数组 [5],线程 2 要put 的位置是 [21]
synchronized(map){
map.put(xxx,xxx);
}

明显不妥:数组里很多元素,除非是对同一个元素执行 put 操作,此时是需要多线程是同步的。

JDK 并发包里提供了 ConcurrentHashMap,默认实现了 线程安全。

在 JDK 1.7 以前,分段加锁:将一个数组分成多个数组,每个数组都对应一个锁,分段加锁。

image

在 JDK 1.8及以后,做了优化,锁粒度的细化

[一个大的数组],数组里的每个元素进行 put 操作时,都会有一个不同的锁,刚开始 put 的时候,如果都是在 [5]的这个位置进行 put,采取的是 CAS 策略。

同一时间,只能有一个线程成功执行 CAS ,开始会先获取数组[5]这个位置的元素:null,再执行 CAS ,比较一下和原来的值是不是一样,是一样就执行 put 操作,同时间其他现车鞥执行 CAS 就会失败。

分段加锁,通过对数组里的每个元素执行 CAS 的策略,如果很多线程对数组里的不同元素执行 put操作,大家是没有关系的

如果是对数组里同一个元素执行操作,才会加锁串行化处理;如果是对数组不同位置元素进行操作,大家是可以并发执行的。

AQS 的实现原理是什么

抽象队列同步器(Abstract Queue Synchorizer)

image

可以 new ReentrankLock(true) 变为公平锁;

image

线程池底层工作原理

系统不可能让其无限制的创建很多线程,会构建一个线程池,有一定数量的线程,让他们执行各种各样的任务,执行完之后不要销毁线程,继续去等待执行下一个任务。

避免频繁的创建线程,销毁线程。

image

提交任务:先看一下线程池里的线程数量是否小于corePoolSize,也就是10,如果小于,直接创建一个新线程出来执行任务。

任务执行完之后,这个线程不会销毁,他会尝试去从一个无界的LinkedBlockingQueue里获取新的任务,如果没有新任务,就会阻塞住,等待新任务的到来。

image

线程池的核心配置参数都是干什么的?应该怎么用?

newFixedThreadPool(3)

image

代表线程池的类是 ThreadPoolExecutor

image

image

如果说把queue做成有界队列,比如说 new ArrayBlockingQueue< Runnable > (200),那么假设 corePoolSize 每个线程都在工作,大量任务进入队列,队列满了怎么办?

这个时候假如 maximunPoolSize 大于 corePoolSize,此时会创建额外的线程放入线程池里,来处理这些任务,然后超过 corePoolSize 数量的线程执行完一个任务之后也会尝试从队列里去获取任务来执行。

当任务队列为空时,额外创建的那些线程会保留一段时间(keepAliveTime),然后销毁。

如果额外线程都处理完了,队列也满了,此时还有新的任务来怎么办?只能 reject掉,他有几种reject策略:

image

如果线上机器突然宕机,线程池中的阻塞队列中的请求怎么办?

必然导致线程池中积压的任务的丢失。

如果要提交一个任务到线程池中去,在提交之前,先在数据库里插入这个任务的信息,更新它的状态,未提交,已提交,已完成。提交成功以后,更新它的状态是已提交。

系统重启以后,后台线程去扫描数据库里的未提交和已提交状态的任务,把任务的信息读取出来,再提交到线程池里,继续执行。

Java 内存模型理解

image

对每个线程而言,都有一个自己的工作内存,对应的是CPU 级别的缓存,反映到 JAVA 内存模型里就是工作内存。

多个线程对共享变量进行++操作时,会先把 data 变量 read 出来,然后 load 加载到工作内存中,然后通过 use操作把data变量提取出来,执行++操作,assign 操作更新工作内存中的 data 值,执行store操作尝试写入主内存,最后执行 write 操作写入主内存。多线程并发执行更新内存中的共享变量。

Java 内存模型中的原子性,,有序性,可见性是什么?

并发编程中,可能会产生的三类问题:

1。可见性

没有可见性:线程1执行更新操作,将更新后的data值写入主内存,而线程2去读取data的值,但线程2在一段时间内看到的data值任有可能是之前读到的在工作内存中的原来的data值。

有可见性:线程1对data更新后,强制线程2读取data的时候从主内存中重新读取重新加载,看到线程1对data的修改。一个线程对data修改后,其他线程能立马看到它的变化。

2.原子性

在同一时间只能有一个线程对 data进行操作(read,load,use,assign,store,write),当线程1操作执行完毕后线程2才能对 data 执行操作。

3.有序性

对于代码,还有个问题就是指令重排,编译器和指令器,有时为了提高代码的执行效率,会将指令重排序,比如:

image

具备有序性:不会发生指令重排导致代码异常;

不具备有序性:可能会发生指令重排,导致代码异常;

Java 底层 volatile关键字原理

内存模型 -> 原子性,可见性,有序性 -> volatile

volatile主要是用来解决可见性和有序性的,主要不是用来保证原子性的。

可见性:

线程1和线程2将 data=0 读取到自己的工作内存中,线程1对data 执行++操作后,将data=1写入主内存,当data用 volatile关键字修饰后,会将线程2工作内存中的 data值失效,当线程2尝试从工作内存中读取data 值的时候,会发现data值已经失效,会强制从住内存中再次读取(read)data的值加载到工作内存中。

有序性:

指令重排以及 happens-before原则是什么?

image

happens-before原则:如果符合这个原则,就不能胡乱重排,不符合就可以自己重排

1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先于发生书写在后面的操作;

2.一个 unlock操作先于发生于后面对同一个锁的lock操作

3.volatile变量规则:对一个volatile变量的写操作先于发生于对这个volatile变量的写操作。

4.传递规则

5.线程启动规则:Thread对象的start()先行发生于此线程的每一个动作;

6.线程中断规则

7.线程终结规则

8.对象终结规则

程序中的代码如果满足这个规则,就一定会按照这个规则来保证指令执行顺序;

volatile 底层是如何基于内存屏障保证可见性可有序性的?

volatile + 原子性:不能够保证原子性,但在一些极端特殊情况下有保证原子性的效果:oracle下对64位的long类型的数字进行操作时,volatile保证原子性。

保证原子性:synchronize,lock,加锁;

1.lock指令:保证可见性
对volatile修饰的变量执行写操作时,JVM会发送一条 lock前缀指令给CPU,CPU在计算完之后就会立即将这个值写入到主内存,其他线程会嗅探缓存里的某个变量被其他线程修改,就会将这个变量失效,再读取的时候直接去主内存读。

2.内存屏障:volatile禁止指令重排
对volatile修饰的变量,前后会加入各种各样的内存屏障(Load,Store,StoreStore,LoadStore),可以避免前后代码指令重排。

Spring 的Ioc机制

没有spring Ioc时候,各个类的实例都要new 出来,代码的耦合度很高,当要修改相关实现类是,需要改动大量的代码。

image

Spring Ioc,Spring 容器根据xml配置或者注解,去实例化你的一些bean对象,然后根据xml配置或注解,去对bean对象之间的引用关系进行依赖注入。系统的类和类之间彻底解耦合了

底层核心技术就是反射,会通过反射技术直接根据你的类去构建对应的对象出来。

Spring AOP机制

在spring运行时,会对哪些需要进行代码增强的类去生成动态代理类,即跟他们实现一样接口的代理类,在代理类的对象里会注入自己本来的那个类(接口,代理的是接口!),去实现一样的方法,代理类的对象就会注入到像 Controller里面去,然后在调用对象时会直接调用到代理类对象的方法。

详见 文章中的《Spring Aophttps://www.cnblogs.com/huang580256/articles/14577196.html,
《静态/动态代理》
https://www.cnblogs.com/huang580256/articles/14574153.html。

cglib动态代理?jdk动态代理?

其实就是动态的创建一个代理类出来,创建这个代理类的实例对象,在这个里面引用你真正自己写的类,所有方法的调用都是先走代理类的对象,他负责做一些代码的增强,再去调用你写的的那个类。

spring里使用AOP,比如对一批类和他们的方法做了一个切面,定义好了要在这些类的方法里增强的代码,spring必然对哪些类生成动态代理类,在动态代理类中去执行定义的一些增强的代码。

如果你的类是实现了某个接口,spring Aop 会使用 jdk动态代理,生成一个跟你实现了同样一个接口的代理类,构造一个实例对象出来。

如果某个类没有实现接口的,spring Aop会改用 cglib的动态代理,它会生成你的类的一个子类,动态生成字节码,覆盖你的一些方法,在方法里加入增强的代码。

详见 《静态/动态代理》

Spring 中的Bean是线程安全的吗?

Spring 容器中的Bean作用域可分为五个范围:

1.singleton:默认,每个容器只有一个bean 实例。

2.prototype:为每一个bean请求创建一个Bean实例。

3.request:为每一个网络请求创建一个Bean实例,请求完成后Bean会失效被垃圾回收。

3.session

4.global-session

Bean 不是线程安全的,spring 容器中Bean是单例的,只会创建一个bean实例,tomcat中多个线程并发调用执行对data进行操作。java web系统。一般来说很少在spring bean里放一些实例变量,一般最终都是去访问数据库的。

image

Spring 的事务实现原理是什么?事务传播机制的理解?

原理:

@Transactional注解,此时spring会使用AOP思想,对你的这个方法在实行之前,去开启事务,执行完毕之后,会根据方法是否报错,来决定是回滚还是提交

事务传播机制:
image

image

Spring Boot核心架构

自动配置原理

pom.xml

  • spring-boot-dependencies : 核心依赖在父工程中

启动器

  • 启动器:就是springboot的启动场景
  • 比如spring-boot-starter-web,就会导入web环境的所有依赖

主程序
image

  • 注解
    1.@SpringBoot里的组合注解
@SpringBootConfiguration  springboot的 配置
	@Configuration: spring配置类
	@Component:说明这是一个spring的组件

@EnableAutoConfiguration :自动配置
	@AutoConfigurationPackage :自动配置包
		@Import(AutoConfigurationPackages.Registrar.class) :自动配置包注册
	@Import(AutoConfigurationImportSelector.class) :自动配置导入选择

//获取所有的配置
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
		List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
				getBeanClassLoader());
		Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
				+ "are using a custom packaging, make sure that file is correct.");
		return configurations;
	}

META-INF/spring.factories :自动配置的核心文件
image

Properties properties = PropertiesLoaderUtils.loadProperties(resource);
所有的资源加载到配置类中!

详见 《springBoot 自动配置原理》

spring 中都使用了哪些设计模式?

工厂,单例,代理。详见 《设计模式》

工厂模式:把对一个对象的创建封装在工厂里,要使用这个对象的时候通过工厂拿出来。spring ioc的核心设计模式的思想体现,他自己就是一个大的工厂,把所有的 bean实例都放在 spring 容器里(大工厂),如果要使用bean,就找spring容器,自己就不用创建对象了。

spring 默认对每个bean都是单例模式,确保一个类在运行期间只有一个实例,只有一个bean。

代理模式:AOP,如果要对一些类的方法切入一些增强的方法,会创建一些动态代理对象,让你对那些目标对象的访问先经过代理对象,动态代理对象先做一些增强的代码,再调用你的目标对象。

spring mvc的架构

1.整体流程

image

具体步骤:

  • 首先用户发送请求到前端控制器,前端控制器根据请求信息(如 URL)来决定选择哪一个页面控制器进行处理并把请求委托给它,即以前的控制器的控制逻辑部分;图中的 1、2 步骤;

  • 页面控制器接收到请求后,进行功能处理,首先需要收集和绑定请求参数到一个对象,这个对象在 Spring Web MVC 中叫命令对象,并进行验证,然后将命令对象委托给业务对象进行处理;处理完毕后返回一个 ModelAndView(模型数据和逻辑视图名);图中的 3、4、5 步骤;

  • 前端控制器收回控制权,然后根据返回的逻辑视图名,选择相应的视图进行渲染,并把模型数据传入以便视图渲染;图中的步骤 6、7;

  • 前端控制器再次收回控制权,将响应返回给用户,图中的步骤 8

2.核心流程

image

具体步骤:

  • 第一步:发起请求到前端控制器(DispatcherServlet)

  • 第二步:前端控制器请求HandlerMapping查找 Handler (可以根据xml配置、注解进行查找)

  • 第三步:处理器映射器HandlerMapping向前端控制器返回Handler,HandlerMapping会把请求映射为HandlerExecutionChain对象(包含一个Handler处理器(页面控制器)对象,多个HandlerInterceptor拦截器对象),通过这种策略模式,很容易添加新的映射策略

  • 第四步:前端控制器调用处理器适配器去执行Handler

  • 第五步:处理器适配器HandlerAdapter将会根据适配的结果去执行Handler

  • 第六步:Handler执行完成给适配器返回ModelAndView

  • 第七步:处理器适配器向前端控制器返回ModelAndView (ModelAndView是springmvc框架的一个底层对象,包括 Model和view)

  • 第八步:前端控制器请求视图解析器去进行视图解析 (根据逻辑视图名解析成真正的视图(jsp)),通过这种策略很容易更换其他视图技术,只需要更改视图解析器即可

  • 第九步:视图解析器向前端控制器返回View

  • 第十步:前端控制器进行视图渲染 (视图渲染将模型数据(在ModelAndView对象中)填充到request域)

  • 第十一步:前端控制器向用户响应结果

JVM是如何运行起来的?对象时如何分配的?

image

JVM在哪些情况下回触发垃圾回收?

image
在JVM里有一个内存分代模型,年轻代和年老代。

当 eden区域满了,此时就会触发垃圾回收,young gc,ygc,回收的垃圾对象就是哪些没有人引用的对象。

JVM的年轻代垃圾回收算法?对象什么时候转移到老年代?

image

如果让代码一边运行,一边有变动,一边判断哪些对象时可以回收的,这是不现实的,垃圾回收的时候有一个概念,叫stop the word,停止你的jvm里的工作线程的运行,然后扫描所有对象,判断哪些可以回收,哪些不能回收。回收完成以后恢复线程的运行,周而复始。

大部分年轻代中对象的生存周期是很短的,可能在很短时间内,线程执行了多个方法,创建多个对象,很短时间内方法执行结束,此时那些对象就会变成垃圾,可以回收。

复制算法:将那些存活的对象把它对象复制到survivor区,然后清空Eden区,完成一次 young gc。

对象什么时候转移到老年代?

1.有的对象在年轻代里熬过了很多次垃圾回收,此时就会认为这个是要长期存活的对象。如:在spring容器里,每个bean实例对象就一个,被spring容器引用,长期存活。

2.survivor区存放不下的对象。

3.大对象会放入老年区。

老年代的垃圾回收算法?常用的垃圾回收器?

老年代里的对象很多都是被长期引用的,例如spring容器管理的各种bean,对老年代而言,它里面的垃圾对象可能没有那么多。标记-清除,找出那些垃圾对象,然后直接在老年代里清除掉。会出现内存碎片问题。

标记-整理:把老年代里存活的对象标记出来,移动到一起,存活对象压缩到一片内存空间里去,剩余的空间就是连续的可用的空间,解决了内存碎片问题。

parnew+cms,g1直接分代回收。

你们生产环境中的Tomcat是如何设置JVM参数的?如何检查JVM运行情况?

对自己的系统的JVM参数有一定了解,内存区域大小的分配,每个线程的栈的大小,metaspace大小,堆内存大小,年轻代和年老代的大小,eden和survivor区大小分别是多少,若没有设置,会有一些默认值。

对系统运行时对对象的数量的预估,对内存压力进行预估,对整个jvm情况进行预估,预估完毕后,根据预估的情况去设置一些jvm参数。

进行压测,其实就去观察jvm运行的情况,jstat工具去分析jvm运行的情况,它的年轻代里的Eden区对象的增长情况,ygc的频率,每次ygc过后有多少对象存活,s能否放得下,老年代对象的增长情况,老年代多久会触发一次fgc。

TCP/IP四层网络模型?OSI七层模型?

1.物理层:在物质层面怎么让电脑联网,网线,海底光缆,将各个电脑连接起来。形成一个网络,这就是物理层的含义,物理层负责传输0和1的电路信号。

2.数据链路层:物理层将各个电脑连接起来,传输最底层的0和1的电信号,但还得定义清楚哪些0和1分为一组,这些信号啥意思,才能进行通信,所以数据链路层就是将电信号分组。

很早以前,每个公司都有自己的电路信号分组方式,但后来出现了以太网协议,一组电信号就是一个数据包,叫一个帧(frame),每个帧分为两个部分,标头(head)和数据(data),标头包含一些说明性的东西,如发送者,接受者,数据类型之类的。以太网协议规定接入网络的所有设备里,都得有个网卡,每个网卡必须包含一个mac地址,是网卡的唯一表示

在以太网里传输数据包时,必须知道接收者的mac地址才只能传输数据。

但以太网的数据包是怎么从一个mac地址发送到另一个mac地址?这个不是精准推送的,以太网里一个电脑发送数据包,会广播给局域网里的所有电脑设备的网卡,然后每台电脑从数据包里获取接受者的mac地址,然后对比自己的mac地址,如果相同,则接受数据。

3.网络层:上面说到,子网内的电脑发送数据包,对局域网内的电脑是广播出去的,那怎么知道哪些电脑是在一个子网内呢?这就得靠网络层了。根据ip地址区分哪些电脑是同一个子网。

网络层里有ip协议,单单从IP地址是看不出来哪些电脑是一个子网的,需要通过IP地址的二进制来判断,根据ip地址的子网掩码,如果要判断两个ip地址是不是同一个子网,就分别把两个IP地址和自己的子网掩码来进行二进制的与运算,之后比较一下代表网络的那部分。

但是如果发现接收数据包的计算机不在一个子网内,就不能通过广播来发送数据了,需要通过路由来发送数据包,路由器就负责将多个子网进行连接,

交换机:一个子网内通过交换机走以太网协议进行广播。

image

4.传输层:当一台机器上很多个程序共用一个网卡进行通信时,如浏览器,qq等,这些软件都用一个网卡往外面发送数据,从网卡接收数据。

所以还需要一个端口的概念,就是发送数据包到某个机器的一个网卡上面去,然后那个机器上监听那个那个端口的程序就可以提取发送到这个端口的数据了。

udp和tcp都是传输层的协议,作用就是在数据包里加入端口号,可以通过端口号进行点对点的通信,udp协议是不可靠的,发送出去别人收没收到不知道,tcp是可靠地,要求三次握手,而且要求别人接收到数据必须回复你。

传输层的tcp协议,仅仅只是规定了一套基于端口的点对点的通信协议,包括如何建立连接,如何发送和读取消息,,但是实际上如果说基于tcp协议来开发,一般是是用socket,java scoket网络编程。

5.应用层:通过传输层的tcp协议可以传输数据,但是收到数据后怎么处理?比如收到邮件怎么处理,收到网页怎么处理?这个应用层,我们假设综合了会话层,表示层,和应用层。

比如最常见的,应用层的协议就是http协议,进行网络通信

DNS地址是啥?我们一般是通过IP地址+mac地址+端口号来定位一个通信目标的,但如果在浏览器上输入www.baidu.com,这个时候是先把www.baidu.com 发送给 DNS 服务器,,然后 DNS 服务器会告诉你 www.baidu,com 对应的IP地址。

浏览器请求 www.baidu.com 的全过程是怎么样的?

image

首先会找DNS服务器,解析域名后返回一个IP地址,接着会判断两个IP地址是不是在一个子网内,用子网掩码和IP地址进行与运算,判断是不是一个子网内。

如果不是那就发送一个数据包到网关,也可认为是路由器,而且可以拿到网关的IP地址和mac地址。现在从应用层出发,通过浏览器访问一个网站,是走应用层的http协议

浏览器请求一个地址,先按照应用层的http协议,封装一个应用层数据包,仅仅是数据包的数据部分,此时数据包是没有头的,数据包里就放了http请求报文。

接着就到了传输层了,这个层是tcp协议,这个tcp协议会让你设置端口,这个时候会把应用层的数据包封装到tcp数据包里,而且会加一个tcp头,这个tcp头里就放了端口信息。

接着走到了网络层,走ip协议,这个时候会把tcp头和tcp数据包,放到ip数据包里面去,然后在搞一个ip头,里面包含了发送者IP地址和接受者IP地址。

接着就是数据链路层,会走以太网协议,会把ip头和ip数据包封装到以太网数据包里面,在加一个以太网数据包的头,头里包含了本机的网卡mac地址,接受者网卡mac地址。

以太网数据包限定1500个字节,但是假设ip数据包有5000个字节,那么需要将ip数据包切割一下,这时一个以太网数据包要切割为四个数据包,1500,1500,1500,500,ip头和切割后的ip数据包。

image

这四个以太网数据包都会通过交换机发送到你的路由器上,你的路由器是可以连通别的子网的,这个时候你的路由会转发到别的子网也可能是某个路由器里面去,以此类推,N多个路由(网关)转发之后,就会跑到百度的某个服务器,接收到4个以太网数据包。

百度服务器接收到4个以太网数据包后,会根据ip头的序号,把4个以太网数据包里的ip数据包给拼起来,还原成一个完整的ip数据包,接着从ip数据包里拿出tcp数据包,再从tcp数据包里取出http数据包,读取http请求报文,接着做一些处理,再把相应结果封装为http相应报文,封装在http数据包里,再一样的过程,封装tcp数据包,封装ip数据包,封装以太网数据包,接着通过网关给发回去。

TCP三次握手流程?四次挥手?为什么不是两次或者四次?

1.tcp三次握手过程

image

传输层的tcp协议建立网络连接时,其实走的就是三次握手过程

建立三次握手时,TCP报头用到了下面几个东西:ACK,SYN,FIN。

第一次握手,客户端发送连接请求报文,此时SYN=1,ACK=0,这就是说这是个连接请求,seq=x,接着客户端处于SYN_SENT状态,等待服务器响应。

第二次握手,服务端收到SYN=1的请求报文,需要返回一个确认报文,ack=x+1,SYN=1,ACK=1,seq=y,发送给客户端,自己处于SYN_RECV状态。

第三次握手,客户端收到报文,将ack=y+1,ACK=1,seq=x+1。

为啥不是二次握手或者四次握手呢?

image

假定客户端向服务端发送一个请求,但这个请求在一个网络节点逗留了较长时间,连接超时之后客户端再向服务端发送一个连接请求,服务端收到了之后建立了连接,请求完成之后释放连接,若此时客户端发送的第一次请求到达了服务端,服务端会以为这个连接已经建立了,开辟了资源准备通信,但客户端之前已经建立了一个连接,第二次握手不是自己想要的,客户端不会理第二次握手,如果是两次握手,服务端开辟的资源会浪费掉,但如果是三次握手,第三次握手的时候会要求服务端释放开辟的资源(复位连接)。

三次握手就已经够了,四次或五次握手浪费资源了。

tcp四次挥手断开连接

第一次挥手,客户端发送报文FIN=1,seq=u,进入FIN-WAIT-1状态

第二次挥手,服务端收到报文,这时候进入CLOSE-WAIT状态,返回一个报文,ACK=1,ack=u+1,seq=v,客户端收到报文后,进入FIN-WAIT-2状态,此时客户端到服务器的链接就释放了。

第三次挥手,服务端发送连接释放报文,FIN=1,ack=u+1,seq=w,服务端进入LAST-ACK状态。

第四次挥手,客户端收到连接释放报文后,发应答报文,ACK=1,ack=w+1,seq=u+1,进入TIME-WAIT状态,等待一会客户端进入CLOSE状态,服务端收到报文后也进入CLOSE状态。

image

HTTP协议的工作原理?

http的工作原理,底层都是tcp,ip,以太网那快,一层一层包裹数据包,所以http的关键就是http请求和http响应的规范。

http1.0 要指定keep-alive来开启持久连接,默认是短连接,就是浏览器每次请求都要重新建立一次连接,完事释放tcp连接。

http1.1 默认支持持久连接,就是浏览器打开一个网页之后,底层的tcp连接就保持着。

http2.0 支持多路复用,基于一个tcp连接并行发送多个请求以及接受响应,解决了http1.1对同一时间同一个域名的请求有限制的问题,二进制分帧,将传输数据拆分为更小的帧,提高了性能,实现低延迟高吞吐。

https的工作原理,为啥用https就可以加密通信?

image

1.浏览器把自己的加密规则发送给网站

2.网站从这套加密算法里选出来一套加密算法和hash算法,网站把自己的身份信息用证书的方式返回浏览器,证书里有网站地址,加密公钥,颁发机构。

3.浏览器验证证书合法性,然后浏览器地址栏上会出现一把小锁,接着浏览器生成一串随机密码,然后用证书里的公钥进行加密,非对称加密,用约定好的hash算法生成握手的hash值,然后用密码进行加密,把所有的东西发送给网站。

4.网站用本地的私钥对消息解密取出来密码,然后用密码解密浏览器发来的消息,计算消息的hash值,并验证与浏览器发来的hash值是否一样,最后用密码加密一段握手消息,发送给浏览器。

5.浏览器解密握手消息,然后计算消息的hash值,如果和网站发来的hash值一样,握手就结束了。之后所有的数据都会由之前浏览器生成的随机密码然后用堆成加密进行加密。

MySql的innodb,myisam存储引擎区别?

myisam,不支持事务,不支持外键约束,索引文件和数据文件分开,这样可以在内存里缓存更多的索引,,对查询的性能更好,适用于少量插入,大量查询的场景。

最经典的就是基于myisam的报表系统,这种报表是最适合myisam存储引擎的,不需要事务,就是一次性批量导入,接下来一天之内就是纯查询。
image

innodb,支持事务,强制要求主键,支持外键约束,高并发,大数据量(分库分表),高可用(读写分离)

mysql的索引实现原理?各种索引平时都是怎么使用的?

索引的数据结构
索引说白了就是用一个数据结构来组织某一列的数据,然后如果要根据那一列的数据去查询的时候,就可以不用全表扫描了,只需要根据那个特定的数据结构去找到那一列的值,然后找到对应行的物理地址就行。

b+树是啥?

先说说b-树:b-树要满足以下几个条件:
image
image

假如有一张表:

{
  id int
  name varchar
  age int
}

对id建个索引:15,56,77,20,49

select * from table where age=20
查找的时候就是从根节点开始二分查找。

image
每个节点左边的值小于右边,处于左右两个值中间范围的数会有一个指针指向他们,指向的数同样左边小于右边,

b+树:

b+数和b-数的不同之处在于:
1.每个节点的指针上限为2d,而不是2d+1
2.节点内不存储data,只存储key,叶子节点不存储指针。
要查找数据一般都是定位到最后的叶子节点才能拿到对应的data。
image

但一般数据库的索引对b+数进行了优化,加了顺序访问的指针,这样在查找范围的时候就很方便,比如要查18-49 之间的数据,先找到18对应的数据,再往右找到49,这个范围内的数据就可以直接找到。
image

myisam存储引擎的索引实现
myisam的最大特点是数据文件和索引文件是分开的,如:

索引文件:
image

image

而innodb的数据文件本身就是一个索引文件,key就是主键,然后叶子节点的data就是那个数据行。

索引的使用规则
最左前缀匹配原则是跟联合索引(复合索引)相关联的,在java系统里写的SQL,都必须符合最左前缀匹配原则,确保所有的sql都可以使用上这个联合索引,通过索引来查询。

如果说要对一个商品表按照店铺,商品,创建时间三个维度来查询,那么就可以创建一个联合索引:shop_id,product_id,gmt_time.

image
image
image

索引的缺点及其使用注意
常见的就是增加磁盘消耗,同时高并发的时候频繁插入和修改索引,会导致性能的损耗,要尽量创建少的索引。

在创建索引的时候,要注意一个选择性的问题,就是要一个字段的值几乎都不太一样,此时使用索引效果才是最好的。

事务的几个特性是啥?有哪几种隔离级别?

1.事物的ACID
1.Atomic:原子性,一堆sql要么一起成功,要么一起失败。

2.Consitency:一致性,数据的一致性,一组sql执行之前,数据必须是正确的,执行之后数据必须也是正确的。

3.Isolation:隔离性,多个事务在跑时互相不干扰。

4.Durability:持久性,事务成功了,就必须永久对数据的修改是有效的。

2.事务隔离级别

1.读未提交:事务A读到了未提交的事务B的数据。
image

2.读已提交(不可重复读):
image

3.可重复读:事务A多次读取到的数据是一样的
image

4.幻读(不是隔离级别)
image

5.串行化(为了解决幻读):事务A先执行,执行完之后再执行事务B,事务A执行期间事务B不执行
image

mysql的默认事务隔离级别就是可重复读

MYSQL是如何实现可重复读的?是通过MVCC (多版本并发控制)机制来实现的,innodb存储引擎,会在每行数据的最后加两个列,一个是行的创建时间,一个是行的删除时间,但这存放的不是时间,而是事务id,事务id是mysql自己维护的自增的,全局唯一。

在一个事务内查询时,mysql只会查询创建时间的事务的id小于或等于当前事务id的行,这样可以确保这个行是在当前事务中创建的,或者之前创建的;同时一个行的删除的事务id要么没有定义(没有删除),要么是比当前事务id大(事务开启之后删除),满足这两个条件的数据会被查出来。

image
image

mysql数据库锁的实现原理?死锁了怎么办?

mysql锁的类型:行锁,表锁,页锁。

一般myisam会加表锁,执行查询的时候,会默认加个表共享锁,也就是表读锁,这个时候别人只能查,不能写数据的,myisam写的时候,会加个表写锁,别人不能读也不能写。

innodb一般用行锁,行锁有共享锁和排他锁,共享锁就是多个事务都可以加共享锁读同一行数据,但是别的事务不能写这行数据,排他锁就是一个事务可以写这行数据,别的事务只能读,不能写。

innodb的表锁,分为意向共享锁,就是加共享行锁的时候,必须先加共享表锁,还有一个意向排他锁,就是给某行加排他锁的时候,必须先给表加排他锁。

insert,update,delete的时候会自动加行级排他锁。

select时啥锁都不加,默认实现了可重复读,MVCC机制。

但是innodb从不会自己主动加共享锁,除非用语句自己加锁:
image

悲观锁和乐观锁是什么?
mysql里的悲观锁是走 select * from table where id=1 for update,在这个期间当前事务没有结束,其他事务不能更新这一行,容易造成死锁。

乐观锁,每次修改时,会比较这条数据的当前版本号和之前查出来的版本号是不是一样,如果是一样就修改然后把本本号加1,否则就不更新任何一条数据。

死锁
image
比如事务A拿到了数据1的锁,事务B拿到了数据2的锁,然后事务A要获取数据2的锁就会等待,事务B要获取数据1的锁就会等待,此时就在成死锁,互相等待。

发生死锁的情况太多了,怎么排查呢?
找dba看下死锁日志,根据对应的sql,找下对应的代码。

MySQL的sql调优一般有哪些手段?

一般都是简单的单表查询,复杂的逻辑都是放在java里去写。

如果某个sql跑的慢,十有八九是没有用索引,这个时候就去看看mysql的执行计划,看看那个sql有没有用到索引,如果没用到,改写一下sql让他用上索引。

MYSQL执行计划:
explain select * from table,就可以了。
image

Socket的工作原理?跟Tcp/IP之间是啥关系?

socket就是在传输层里把tcp协议封装了一下,大体来说就是在服务端搞一个ServerSocket等待别人来连接你,然后在客户端传建一个socket去连接服务端,建立连接之后,在服务器上,ServerSocket也会创建出来一个Socket,通过客户端的socket和服务端的socket来进行通信。

这个底层,比如建立连接和释放连接都是基于tcp的三次握手和四次挥手,包括基于tcp协议传输数据,其实就是之前说的对数据包一层一层的封装。

进程间是如何通信的?线程之间是如何切换的?

进程之间的通信方式有很多种,比如:管道(pipe),命名管道(fifo),消息队列,共享内存。

管道:linux里用来缓存要在进程间通信的数据,输一个固定大小的缓冲区,4kb,管道中的数据一旦被读取出来,就不在管道里了,一个进程写数据,一个进程读数据,管道数据只能流向一个方向,就是架设一个管道,只能一个进程写,一个进程读。

命名管道:管道通信,要求必须是父子关系的进程间进行通信,之前的管道是没有名字的,所以可以用命名管道来解决这个问题。

消息队列:linux内核里的一种链表数据结构,一样是写入数据和消费数据,

共享内存:一块物理内存被映射到两个进程的进程地址空间,所以进程间可以立即看到对方在共享内存里作出的修改,因为是共享的,所以需要用锁来保证同步。

线程间如何切换?

多线程间的切换时候涉及到了上下文切换,就是有一个时间片算法,cpu给每个线程一个时间片来执行,时间片结束之后就保存这个线程的状态,然后切换到下一个线程去执行,这就是多线程并发执行的原理。

BIO,NIO,AIO分别都是啥?有什么区别?

BIO网络通信原理
image
基于socket来进行通信的,一个客户端对一个服务端建立连接,服务端就会创建一个socket线程去和客户端进行通信,这种方式弊端在于每一个客户端接入,都是要在服务端创建一个线程,这会导致大量的客户端连接时,线程适量过多导致服务端崩溃。

会搞一个线程池,固定线程的数量来处理请求,但是高并发还是会导致延迟和排队。

NIO通信原理
image客户端请求会在服务端创建一个对应的channle,NIO中间就是通过channle来读写数据的,这些channle会注册到selector中,若有客户端对channle发起请求,selector就会创建一个线程就去读channle中的请求参数,然后将请求结果发送回去,通信完然后线程被销毁。

AIO通信原理
每个链接发来的请求,都会绑定一个buffer,然后通知操作系统去异步的完成读,此时你的程序是可以去干别的事,等操作系统完成数据的读之后,就会回调你的接口,让你操作系统异步完成读取的数据。

线上服务器cpu 100%了,怎么排查,定位和解决?

核心就是找到这台服务器上,是哪个进程的哪个线程的哪段代码,导致cpu 100%了,是否熟悉常见的一些线上的命令

1.定位耗费cpu的进程
image

2.定位耗费cpu的线程
image
image

3.定位哪段代码导致的cpu过高
image

线上进程kill不掉怎么办?

kill一个进程杀不死,那个进程就是一个僵尸进程,就是zombie进程,是因为这个进程释放了资源,但是没有得到父进程的确认。

ps aus,看看stat那一栏,如果是z,就是zombie状态的僵尸进程。

ps -ef | grep 僵尸进程id,可以找到父进程id

先kill父进程即可。

再谈原子性:Java规范规定所有变量的写操作都是原子性的

java并发技术底层原理,volatile,synchronized对可见性,有序性的保障的语义,底层都是基于内存屏障来实现的,硬件底层原理(高速缓存,写缓冲器,无效队列)

apps=loadApps;//原子的,不需要AtomicReference来处理

java语言规范里面,int i=0,resource=loadResource,flag=false,各种变量的简单赋值,规定都是原子的。

赋值的时候,要保证没有人先赋值过,没有人修改过,才能赋值,通过AtomicReference的CAS操作来实现的。

但是一些复杂的操作,i++,先读取i的值,再更新i的值,这种复杂的操作不是简单的赋值写,有计算的过程在里面,此时java默认是不保证原子性的。

特例:32位Java虚拟机中的long和double变量写操作为何不是原子的?

因为long和double是64位的,如果多个线程并发的执行 long i=30,long是64位的,就会导致有的线程在修改i的高32位,有的线程在修改低32位,多线程并发给long类型的变量进行复制操作是,在32位的虚拟机下,是有问题的。

volatile原来还可以保证long和double变量写操作的原子性

volatile对原子性保障的语义,在java里是很有限的,几乎可以忽略不计,32位的java虚拟机里,对long/double变量的赋值操作是不保证原子性的,此时如果加上了volatile修饰,就可以保证在32位java虚拟机里,对long/double变量的写是原子性的。

volatile long i;
多线程赋值时,volatile保证了操作的原子性。

到底哪些操作在java规范中不保证原子性?

所有变量的简单赋值操作,java规范是保证原子性的,32位java虚拟机里的long/double是不保证赋值写的原子性的,volatile可以解决这个问题。

不保证原子性的一些操作,例如:
i++;
i=y+1;
i=x*y:==>先把x和y从主内存读取到工作内存里面来,再从工作内存里加载出来执行计算(处理器),计算的结果再协会到工作内存里去,最后把 i 的值从工作内存刷回主内存。

可见性涉及的硬件级别

posted @ 2021-07-20 19:36  Chcode  阅读(115)  评论(0)    收藏  举报