互联网java工程师面试突击第三季知识点总结

目录

Java集合包

01. HashMap的底层数据结构是什么?

在这里插入图片描述
哈希表底层数据结构实际上就是数组。它利用数组支持按照下标随机访问的时候,时间复杂度是o(1)的特性。我们通过哈希函数把元素的键值映射为下标,然后将数据存储在数组中对应下标的位置。当我们按照键值查询元素时,我们使用相同的哈希函数,将键值转化为数组下标,从对应的数组下标的位置取出数据。

02. JDK1.8中对hash算法和寻址算法是如何优化的?

//JDK1.8以后的HashMap部分源码
static final int hash(Object key){
	int h;
	return (key == null)?0(h=key.hashCode())^(h>>>16);
	}

hash算法的优化:
对每个hash值,将他的高低十六位进行异或操作,让低十六位同时保持了高低十六位的特征。同时也可以避免一些hash值后续出现冲突。

寻址算法的优化:
寻址算法就是对长度为n的数组取模,得到在数组中的位置。根据数学规律,对n取模,就是和n-1进行与运算。与运算的效率远远高于求模运算,所以采用与运算。而数组的长度通常没有很大,所以高位与出来都是0,如果不进行hash算法优化,那么高位的信息就会丢失。
综上就是JDK8的hash算法的优化。

03.HashMap是如何解决hash碰撞问题的?

hash冲突问题, 链表 + 红黑树 ,o(n)和o(logn)
当发生hash冲突时,会在数组中重复的位置放置一个链表,然后将value的值加入链表中。但是由于链表的查询时间复杂度是o(n),所以当链表的变的很长的时候,我们获取值会变的很慢。为了提升性能,当链表的长度到达一定值时,我们将链表转换成红黑树,红黑树的查询时间复杂度是o(logn),提升性能。

04.说说HashMap是如何进行扩容的?

hashMap底层默认是一个数组,当这个数组满了以后,就会自动扩容,变成一个更大的数组,可以在里面放更多的元素。
hashMap的默认大小是16位的,当16存满以后就会进行***2倍扩容***,变成长度为32的数组。这个时候就要对原先数组中存储的元素进行rehash,即将他们的哈希值和(32-1)进行与运算,原本在长度为16的处于相同位置的几个元素,可能就要变换位置,不在同样的位置了。
为什么进行两倍扩容?
两倍扩容就是二进制位的上一位变成1,比如
0000 0000 0000 1111
变成
0000 0000 0001 1111
在进行rehash操作时,判断二进制结果是否多了一个bit的1,如果没多,那么就是原来的index,如果多了,那么就是index + oldcap,通过这个方式,避免rehash的时候,进行取模运算,位运算的性能更高。
注意,我们最好在使用hashMap的时候能够指定合适的hashMap的大小,来避免扩容,这样就能避免rehash操作,影响性能。

05.ArrayList,LinkedList,TreeMap,LinkedHashMap,HashSet等底层的数据结构和各自的优势和劣势?

List: List是用来存储数据的集合,也就是容器。它的特点是支持存储重复的元素,是有序的。(这里的有序指的是取出的顺序和存储的顺序是相同的。)

  • ArrayList:底层数据结构是数组。特点是增删慢,但是查询很快。因为每次增删的时候都会创建一个新的符合大小的数组,然后将原来的数据拷贝到新数组中,然后删除原来的数组。
  • LinkedList: 底层数据结构是链表。(java中创建队列和双端队列可以使用它)特点是增删很快,但是查询相对ArrayList来说比较慢。其实就是链表和数组的结构造成的不同。

Set:set也是用来存储数据的集合,也是一种容器。它的特点是不支持重复元素,实际上java中的set在底层都是通过map来实现的,就是取map中key的那一列,所以只需要理解下面的map就能理解set。

Map

  • HashMap:这个容器要非常熟悉,上面有详细的讲述,这里就不赘述了
  • LinkedHashMap: 用链表实现的HashMap,是有序的(存入和取出的顺序是一样的),它的底层是哈希表和链表的结合,首先使用散列函数找到要存储的位置,然后用链表将他们连接起来。简单理解就是定义了一个双向链表,将entry按照顺序连接起来,这样就保证了有序性。
  • TreeMap:底层是红黑树。通过一个比较器,可以自己定义,然后结果按照红黑树的原则找到合适的位置,具体关于红黑树的理解参见我的另一篇博客:平衡二叉搜索树、二三树与红黑树

06.equals和hashcode之间的关系?

  • equals 相同hashcode一定要相同
  • hashcode相同 equals不一定相同

Java并发编程

00.BAT面试官为什么都喜欢问并发编程的问题?

synchronized实现原理、CAS无锁化的原理、AQS是什么、Lock锁、ConcurrentHashMap的分段加锁的原理、线程池的原理、java内存模型、volatile说一下吗、对java并发包有什么了解?一连串的问题

写一些java web系统,运用一些框架和一些第三方技术,写一些类似于crud的业务逻辑,把各种技术整合一下,写一些crud而已,没什么技术含量。很多人可能写好几年的代码,都不会用到多少java并发包下面的东西

如果说你要面试一些稍微好一点的公司,技术稍微好一点,你只要去做一个技术含量稍微高一点的系统,并发包下面的东西还是很容易会用到的。尤其是BAT,中大厂,有一定规模的公司,做出来的系统还是有一定的技术含量的

01.JAVASE多线程基础回顾

多线程概述

进程:正在进行中的程序(例如QQ ,游戏客户端,360),就是分配给一个应用软件的一块内存空间。进程是不负责直接执行的。

线程:就是进程中一个负责程序执行的控制单元(执行路径)

一个进程中可以有多个执行路径就叫做多线程

每一个线程都有自己运行的内容。这个内容可以称为线程要执行的任务。(例如一个班级中,老师喊两个同学,一个同学点名,一个同学发成绩单,这就是两个线程同时执行两个任务。如果没有任务,线程也就没有开启的必要了)

多线程的好处和弊端

通过上面的描述我们发现多线程似乎是一个很美好的东西,它可以让多个程序同时执行,那我们是否就可以无限使用多线程技术呢?
答案明显是否定的,其实我们看到的程序同时执行,实际上并不是同时执行。我们的操作系统就是一个多线程系统,我们可以同时使用QQ,360,微信等程序。假如我们只有一个CPU,那么CPU实际上是在这些线程之间不断切换的,而且是随机的,并不是同时执行,切换到哪个就执行哪个,cpu的切换速度是非常非常快的,所以给我们的感觉是很多个程序同时执行。但cpu的切换是随机性的,如果线程数非常非常多,那么切换到一个线程的概率就会变小给我们的感受就是程序变卡了。这就是多线程的问题所在。

要解决这个问题就是要配置多个cpu,就是cpu变成多核,实现真正意义上的同时执行。同时内存也要相应的变大,才能够不良费cpu的资源,否则内存中防止的线程的数量还不够一个cpu处理的那么就很浪费资源。

所以我们发现多线程虽然可以让多个程序同时运行,但是也有会让执行效率变低的问题,为了提高执行效率就要更多的cpu和更大的内存。

多线程创建的方式

我们计算机上的多线程是操作系统自动创建的,那么在java中我们要如何创建线程来让多个任务同时执行呢?

方法一
1.定义一个类继承Thread类
2.覆盖Thread类中的run方法
3.创建子类对象就是创建了一个线程
4.调用start方法,开启一个线程 (start 做两件事:开启线程、调用run方法)
解释:Thread 类是用于描述线程的类,run方法是Thread类中描述线程任务的方法。

用线程调用run方法和start方法有什么区别?

现在假设我们有一个类Demo 继承了Thread 类并且复写了它的run方法。现在如果我们创建了demo1对象。那么demo1.run实际上是没有开启线程的,仅仅是main线程中的一个对象调用了它自己方法而已。只有使用demo1.start方法,才会开启一个线程,同时执行run方法,多个线程同时执行。

方法二
1.定义类实现Runnable接口
2.覆盖接口中的run方法,将线程的任务代码封装到run方法中
3.通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递
4.调用线程对象的start方法开启线程

第二种方式有什么好处

  1. 避免单继承的局限性
  2. 第二种方式仅仅是将任务封装成了对象,不用创建一个子类去具有父类的所有方法,更加明确。(将任务单独封装成了对象)

线程状态

在这里插入图片描述
注意要会描述下面这张图

在这里插入图片描述

多线程安全问题

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

/*
循环的嵌套就会产生同步死锁
注意:一般run方法里都会有循环结构 
分析:
定义线程的第二种方法 
首先定义一个子类实现Runnable接口 复写线程的任务就是覆盖Runnable接口中的run方法
创建任务对象 
创建线程对象 并将任务对象作为参数传入线程对象 
让线程对象得到自己的任务 即自己的run函数
*/

//一段死锁的代码示例

class DeadLock implements Runnable 
{
	private boolean flag;
	DeadLock(boolean flag)
	{
		this.flag = flag;
		
	}
	public void run()
	{
		if (flag)
		{
			while (true)//while 循环开始一段完整的线程 放在if里面
			{
				synchronized (MyLock.locka)
				{
					System.out.println(Thread.currentThread().getName()+"if locka");
					synchronized(MyLock.lockb)
					{
						System.out.println(Thread.currentThread().getName()+"if lockb");
					}
				}
			}
		}
		else
			{
				while (true)
				{
					synchronized (MyLock.lockb)
					{
						System.out.println(Thread.currentThread().getName()+"else lockb");
						synchronized(MyLock.locka)
						{
							System.out.println(Thread.currentThread().getName()+"else locka");
						}
					}
					
				}
					
			}
	}
}
class MyLock
{
	public static final Object locka = new Object();
	public static final Object lockb = new Object();
}
class DeadLockTest 
{
	public static void main(String[] args) 
	{
		DeadLock d1 = new DeadLock(true);
		DeadLock d2 = new DeadLock(false);
		
		Thread t1 = new Thread(d1);
		//d.flag = false;
		Thread t2 = new Thread(d2);
		t1.start();//调用对象的成员函数记得加上括号
		t2.start();
	}
}

线程间通信

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一个lock可以挂多个Condition对象,即多个监视器。
Condition c1 = lock.newCondition();//通过已有的锁产生监视器对象
lock接口的出现替代了同步代码块或者同步函数。将同步的隐士锁变成了显示锁。同时更加灵活,可以添加多个监视器。

class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition();//创建多个监视器,每次唤醒一个,提高效率
    final Condition notEmpty = lock.newCondition();
    final Object[] items = new Object[100];
    int putptr, takeptr, count;

    public void put(Object x) throws InterruptedException {
        lock.lock();//获取锁
        try {
            while (count == items.length)
                notFull.await();
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();//释放锁
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

02.synchronized关键字的底层原理(基础)以及跟lock锁之间的区别?

之前有一些同学去一线互联网大厂里去面试,聊并发编程这块的内容,问的比较深一点,就说synchronized的底层原理是什么呢?他当时就答不出来了

如果我要是对synchronized往深了讲,他是可以很深很深的,内存屏障的一些东西,cpu之类的硬件级别的原理,原子性、可见性、有序性,指令重排,JDK对他实现了一些优化,偏向锁,几个小时

面试突击第三季,快速过一下常见的高频面试题而已

其实synchronized底层的原理,是跟jvm指令和monitor有关系的

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

monitorenter

// 代码对应的指令

monitorexit

那么monitorenter指令执行的时候会干什么呢?

每个对象都有一个关联的monitor,比如一个对象实例就有一个monitor,一个类的Class对象也有一个monitor,如果要对这个对象加锁,那么必须获取这个对象关联的monitor的lock锁

他里面的原理和思路大概是这样的,monitor里面有一个计数器,从0开始的。如果一个线程要获取monitor的锁,就看看他的计数器是不是0,如果是0的话,那么说明没人获取锁,他就可以获取锁了,然后对计数器加1

这个monitor的锁是支持重入加锁的,什么意思呢,好比下面的代码片段

// 线程1

synchronized(myObject) {  -> 类的class对象来走的

	// 一大堆的代码

	synchronized(myObject) {

		// 一大堆的代码

}

}

加锁,一般来说都是必须对一个对象进行加锁

如果一个线程第一次synchronized那里,获取到了myObject对象的monitor的锁,计数器加1,然后第二次synchronized那里,会再次获取myObject对象的monitor的锁,这个就是重入加锁了,然后计数器会再次加1,变成2

这个时候,其他的线程在第一次synchronized那里,会发现说myObject对象的monitor锁的计数器是大于0的,意味着被别人加锁了,然后此时线程就会进入block阻塞状态,什么都干不了,就是等着获取锁

接着如果出了synchronized修饰的代码片段的范围,就会有一个monitorexit的指令,在底层。此时获取锁的线程就会对那个对象的monitor的计数器减1,如果有多次重入加锁就会对应多次减1,直到最后,计数器是0

然后后面block住阻塞的线程,会再次尝试获取锁,但是只有一个线程可以获取到锁
synchronized和lock的区别
在这里插入图片描述

03.聊聊你对CAS的理解以及底层实现原理?

上一个讲解的非常好的CAS博客地址:什么是CAS机制

下面是我对CAS的理解和总结:Conmpare And Swap(比较和交换)
首先说一说CAS能解决的问题。我们都知道当多个线程对同一个数据进行操作的时候,如果没有同步就会产生线程安全问题。为了解决线程线程安全问题,我们需要加上同步代码块,操作,如加上synchronized。但是某些情况下这并不是最优选择。

synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。这个过程是一个串行的过程,效率很低。
尽管JAVA 1.6为synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过过度,但是在最终转变为重量级锁之后,性能仍然比较低。所以面对这种情况,我们就可以使用java中的“原子操作类”。

而原子操作类的底层正是用到了“CAS机制”。

CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。(具体实现详细的见上面的博客中介绍)

从思想上来说,synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。

说了这么多,CAS是否是完美的呢,答案也是否定的。下面是说一说CAS的缺点:

1) CPU开销过大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

2) 不能保证代码块的原子性

CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

3) ABA问题

这是CAS机制最大的问题所在。

04.ConcurrentHashMap实现线程安全的底层原理是什么?

这里有可能会问到HashMap为什么线程不安全,推荐一篇文章:HashMap会产生的线程安全问题
首先聊一聊ConcurrentHashMap存在的必要性,即它能解决的问题。在编程中我们常常要对一个hashMap进行多个线程的操作,这个时候为了避免线程安全问题,我们就要给她加上同步。
但是这个时候又会有新的问题产生。

我们知道hashMap的底层实现实际上是数组
多个线程过来,线程1要put的位置是数组[5],线程二要put的位置是[21]
synchronized(map({
	map.put(xxx,xxx)
	}

我们可以看到向两个不同的位置添加元素,也被锁管理了,这明显是没有必要的,会造成效率低下。我们需要解决这个问题。JDK并发包里推出了ConcurrentHashMap,默认实现了线程的安全性。

下面聊一聊,它是如何实现的。

在JDK 1.7 版本,它的实现方式是分段加锁,将HashMap在底层的数组分段成几个小数组,然后给每个数组分别加锁。

JDK1.8以及之后,做了一些优化和改进,锁粒度的细化。

这里仍然是一个大的数组,数组中每个元素进行put都是有一个不同的锁,刚开始进行put的时候,如果两个线程都是在数组[5]这个位置进行put,这个时候,对数组[5]这个位置进行put的时候,采取的是CAS策略。
同一时间,只有一个线程能成功执行CAS,其他线程都会失败。
这就实现了分段加锁的第一步,如果很多个线程对数组中不同位置的元素进行操作,大家是互相不会影响的。

如果多个线程对同一个位置进行操作,CAS失败的线程,就会在这个位置基于链表+红黑树来进行处理,synchronized([5]),进行加锁。

综上所述,JDK1.8之后,只有对相同位置的元素操作,才会加锁实行串行化操作,对不同位置进行操作是并发执行的。

05.对JDK中的AQS了解吗?AQS的实现原理是什么?

我们上面三节分别学习了三种多个线程访问一个共享数据实现线程安全的三种解决方案,synchronized,CAS和ConcurrentHashMap(并发安全的数据结构)。下面介绍最后一种,Lock。它的底层就使用到AQS技术。Abstract Queue Synchronizer,抽象队列同步器

在创建锁时候 可以创建公平锁和非公平锁
创建非公平锁
ReentrantLock lock = new ReentrantLock();//非公平锁
创建公平锁
ReentrantLock lock = new ReentrantLock(true);//公平锁
lock.lock();

lock.unlock();
非公平锁,就是当线程1结束运行释放锁以后,它去唤醒等待队列中的线程2,但是还没等线程2CAS成功,这时候冒出来一个线程3插队,优先实现加锁,线程2CAS失败,继续等待,这就是非公平锁。

公平锁,就是线程3,在想插队时,会进行判断,等待队列中是否还有线程,如果有它就不能插队,会进入等待队列中排队。这就是公平锁。

在这里插入图片描述

06.说说线程池的底层工作原理?

在这里插入图片描述

首先说一说为什么要有

系统是不可能频繁的创建线程有销毁线程的,这样会非常影响性能,所以我们需要线程池。

 ExecutorService threadPool = Executors.newFixedThreadPool(3);//corePoolSize
threadPool.submit(new Callable<>() {

            @Override
            public Object call() throws Exception {
                return null;
            }
            
        });

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

如果执行完你的任务之后,这个线程是不会死掉的,他会尝试从一个无界的LinkedBlockingQueue里获取新的任务,如果没有新的任务,此时就会阻塞住,等待新的任务到来

你持续提交任务,上述流程反复执行,只要线程池的线程数量小于corePoolSize,都会直接创建新线程来执行这个任务,执行完了就尝试从无界队列里获取任务,直到线程池里有corePoolSize个线程

接着再次提交任务,会发现线程数量已经跟corePoolSize一样大了,此时就直接把任务放入队列中就可以了,线程会争抢获取任务执行的,如果所有的线程此时都在执行任务,那么无界队列里的任务就可能会越来越多

fixed,队列,LinkedBlockingQueue,无界阻塞队列

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

当我们调用上一节的函数生成fixed线程池的时候
ExecutorService threadPool = Executors.newFixedThreadPool(3);
它的底层执行的代码如下
return new ThreadPoolExecutor(
			nThreads,//corePoolSize
            nThreads,//maximumPoolSize
            0l,//表示等待的时间
            TimeUint.MiLLISECONDS,//代表等待时间是毫秒级别的
            new LinkedBlockingQueue<Runnable>());//线程池放任务的队列

上面几个的参数分别是,corePoolSize,maximumPoolSize,keepAliveTime,queue这几个东西,如果你不用fixed之类的线程池,完全可以使用这个构造函数创造自己的线程池。`

corePoolSize:3
maximumPoolSize:200
keepAliveTime:60s
new ArrayBlockQueue<Runnable>(200) //这是一个有界队列

如果我们把queue创建成有界队列,假设corePoolSize所有线程都在繁忙的工作,这个时候仍然有大量的任务进入队列,队列满了,此时怎么办?

这个时候,如果你的maximumPoolSize是比corePoolSize大的,此时线程池就会继续创建额外的线程放入线程池中,来处理这些任务。这些额外创建的线程如果处理完了一个任务也会尝试从队列中获取任务来执行。线程池总共可以创建的线程的数量就是maximumPoolSize

线程池的队列满了会发生什么?

但是还有一种情况,如果任务非常多,额外线程全部创建完了,队列还是满的,此时还是有新的任务来,又该怎么办?
此时只能reject掉,有几种不同的reject策略,可以传入RejectedExecutionHandler
(1)AbortPolicy (2)DiscardPolicy (3)DiscardOldestPolicy (4) CallerRunsPolicy (5) 自定义

如果后续慢慢没有任务了,额外创建的线程出去空闲状态,那么线程会等待最大存活时间,如果在这个时间内没有获取新的任务,它就会销毁。实际上maximumPoolSize就是起到一个缓冲的作用。

综上所述,如果定制自己的线程池,要考虑到corePoolSize的数量,队列类型,最大线程数量,拒绝策略,还有线程释放的时间。

特别补充:我们常用的fixedThreadPool是无界队列,maximumPoolSize 和 corePoolSize是一样的。队列永远不会满。或者我们采取有界对列,可以将maximumPoolSize设置的很大,来缓冲。

08.如果在线程中使用无界阻塞队列会发生什么问题?等同于问,在远程服务异常的情况下,使用无界阻塞队列,是否会导致内存异常飙升?

在这里插入图片描述
调用超时,队列变的越来越大,此时会导致内存飙升起来,而且还可能会导致内存溢出。

09.线程池的队列满了之后,会发生什么?

通过上一个问题分析我们发现使用无界队列可能会因为线程处理任务速度比较慢,但是有很多任务堆积导致堆内存溢出。

有界队列不存在堆内存溢出的问题,但同样会有它的问题。如果我们将maximumPoolSize设置的非常大,那么当任务很多时,就会创建很多的额外线程,一台机器上,有几千个,甚至是几万个线程,每个线程都有自己的栈内存,占用一定的内存资源,创建太多的线程会导致栈内存耗尽,可能会产生栈内存溢出。

那么如果我们将maximumPoolSize设置的很小,又会导致额外线程也满了但是任务还是多出队列的限制,此时就会拒绝策略,导致某些任务不能够顺利执行。

这里提供一种自定义策略的思路作为参考,我们可以把这个任务信息持久化写入到磁盘中去,后台专门启动一个线程,后续等待线程池的工作负载降低了,可以慢慢的从磁盘中读取之前持久化的任务,重新提交到线程池中去执行。

综上所述,我们在选用线程池时要综合考虑。

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

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

如何解决这个问题呢?

我们可以在提交任务之前,在数据库中插入这个任务的信息,更新任务的状态:未提交、已提交、已完成。提交成功后,更新它的状态是已提交状态。

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

11.谈谈对JAVA内存模型的理解?

read、load、use、assign、store、write

public class HelloWord(){
	private int data = 0;
	public void increment(){
		data++;
	}
}

HelloWorld helloWorld = new HelloWorld();

//线程1
new Thread(){
	public void run(){
		helloWorld.increment();
		}
	}.start();

//线程2
new Thread(){
	public void run(){
		helloWorld.increment();
		}
	}.start();

上面这段代码在内存中的过程如下图所示。
在这里插入图片描述

12.说说JAVA内存模型中的原子性、有序性、可见性?

连环炮:Java内存模型 > 原子性、可见性、有序性 > volatile > happens-before/内存屏障 有可见性 就是如果有多个线程对一个数据进行操作时,如果一个线程成功修改了数据,那么其他线程能够立即更新工作内存中的该数据,即随时保持最新数据状态。这就叫有可见性,反之没有可见性。

原子性,就是当有一个线程在对内存中的某个数据进行操作的时候,必须要等这个线程完全操作结束后,其他线程才能够操作,这就是原子性。反之就是没有原子性,多线程默认是没有原子性的,需要我们通过各种方式来实现原子性,如同步等等。

有序性,就是代码的顺序应该和指令的顺序相同。在执行过程中不会发生指令重排,这就是有序性,反之就是没有有序性。

13.能从JAVA底层角度聊聊volatile关键字的原理么?

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

讲清楚volatile关键字,直接问你volatile关键字的理解,对前面的一些问题,这个时候你就应该自己去主动从内存模型开始讲起,原子性、可见性、有序性的理解,volatile关键字的原理

volatile关键字是用来解决可见性和有序性,在有些罕见的条件之下,可以有限的保证原子性,他主要不是用来保证原子性的

image.png

可见性,概念进行了加强和深化,volatile在可见性上的作用和原理,有一个很清晰的了解

在很多的开源中间件系统的源码里,大量的使用了volatile,每一个开源中间件系统,或者是大数据系统,都多线程并发,volatile

image.png

volatile保证可见性的原理,如果多个线程操作一个被volatile修饰的变量,当其中一个线程成功对组内存中的数据完成修改以后,它会将其他线程工作内存中的该变量的数据设为失效状态,迫使其它线程重新从主内存中读取变量数据,从而实现有可见性。

在很多的开源中间件系统的源码里,大量的使用了volatile。常常使用的一个场景是对于一个变量,有的线程要更新它有的线程要读取它来进行判断操作,这个时候就需要使用volatile关键字,来保证读取到最新的数据。

14. 你知道指令重排、内存栅栏以及happens-before这些是什么么?

volatile关键字和有序性的关系,volatile是如何保证有序性,从而避免发生指令重排的。

boolean volatile flag = false;

//线程1:
prepare(); // 准备资源
flag = true;

//线程2:
while(!flag){
	Thread.sleep(10000);
} 
execute(); //基于准备好的资源执行操作

Java有一个happens-before原则:

编译器、指令器可能对代码重排序,乱排,要遵守一定的规则,happens-before原则,只要符合happens-before原则,那么就不能胡乱重排,如果不符合这些规则的话,那就可以自己排序。

1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
2、锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。

3、volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个volatile变量的读操作。必须保证先写再读。

4、传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,那么可以得出操作A
先行发生于操作C。
5、线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
6、线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生。
7、线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测现场是否已经终止执行
8、对象终结规则:一个对象的初始完成先行发生于他的finalize()方法的开始。

上面这8条原则的意思即是,如果程序中的代码满足上述条件,就一定会按照这个规则来保证指令的顺序。

规则制定了一些特殊情况下,不允许编译器、指令器对我们写的代码进行指令重排,必须保证代码的有序性。

除了上述功能外,volatile还要其他的能够预防指令重排的规定,例如volatile前面的代码一定不能指令重排到volatile变量操作的后面,它后面的代码不能指令重排得到volatile前面。

重点
指令重排的概念>happens-before>volatile起到避免指令重排

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

volatile+原子性:不能够保证原子性,只有在一些极端情况下能保证原子性。

保证原子性:synchronized,lock,加锁。

(1)lock指令:volatile保证可见性

对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被别人修改

如果发现别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期掉,然后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据了

lock前缀指令 + MESI缓存一致性协议

(2)内存屏障:volatile禁止指令重排序

volatille是如何保证有序性的?加了volatile的变量,可以保证前后的一些代码不会被指令重排,这个是如何做到的呢?指令重排是怎么回事,volatile就不会指令重排,简单介绍一下,内存屏障机制是非常非常复杂的,如果要讲解的很深入

Load1:

int localVar = this.variable

Load2:

int localVar = this.variable2

LoadLoad屏障:Load1;LoadLoad;Load2,确保Load1数据的装载先于Load2后所有装载指令,他的意思,Load1对应的代码和Load2对应的代码,是不能指令重排的

Store1:

this.variable = 1

StoreStore屏障

Store2:

this.variable2 = 2

StoreStore屏障:Store1;StoreStore;Store2,确保Store1的数据一定刷回主存,对其他cpu可见,先于Store2以及后续指令

LoadStore屏障:Load1;LoadStore;Store2,确保Load1指令的数据装载,先于Store2以及后续指令

StoreLoad屏障:Store1;StoreLoad;Load2,确保Store1指令的数据一定刷回主存,对其他cpu可见,先于Load2以及后续指令的数据装载

volatile的作用是什么呢?

volatile variable = 1

this.variable = 2 => store操作

int localVariable = this.variable => load操作

对于volatile修改变量的读写操作,都会加入内存屏障

每个volatile写操作前面,加StoreStore屏障,禁止上面的普通写和他重排;每个volatile写操作后面,加StoreLoad屏障,禁止跟下面的volatile读/写重排

每个volatile读操作后面,加LoadLoad屏障,禁止下面的普通读和voaltile读重排;每个volatile读操作后面,加LoadStore屏障,禁止下面的普通写和volatile读重排

并发这块,往深了讲,synchronized、volatile,底层都对应着一套复杂的cpu级别的硬件原理,大量的内存屏障的原理;lock API,concurrenthashmap,都是各种复杂的jdk级别的源码,技术深度是很深入的

16.能说说ThreadLocal的底层实现原理么?

附上两个讲的很好的链接,自己暂时还没有总结:
ThreadLocal
面试官:知道ThreadLocal嘛?谈谈你对它的理解?(基于jdk1.8)

Spring

01.说说你对Spring的IOC机制的理解可以嘛?

在这里插入图片描述
程序的耦合和解耦 IOC叫控制翻转其目的就是为了降低程序的耦合性

package com.iteima.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

/**
 * 程序的耦合
 *      耦合:程序间的依赖关系
 *          包括:
 *              类之间的依赖
 *              方法间的依赖
 *      解耦:
 *          降低程序间的依赖关系
 *      实际开发中:
 *          应该做到:编译期不依赖,运行时才依赖。
 *      解耦的思路:
 *          第一步:使用反射来创建对象,而避免使用new关键字。
 *          第二步:通过读取配置文件来获取要创建的对象全限定类名
 *
 */
public class JdbcDemo1 {
    public static void main(String[] args) throws  Exception{
        //1.注册驱动
//        DriverManager.registerDriver(new com.mysql.jdbc.Driver());
        Class.forName("com.mysql.jdbc.Driver");

        //2.获取连接
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/eesy","root","1234");
        //3.获取操作数据库的预处理对象
        PreparedStatement pstm = conn.prepareStatement("select * from account");
        //4.执行SQL,得到结果集
        ResultSet rs = pstm.executeQuery();
        //5.遍历结果集
        while(rs.next()){
            System.out.println(rs.getString("name"));
        }
        //6.释放资源
        rs.close();
        pstm.close();
        conn.close();
    }
}

为什么我们在进行Jdbc注册驱动的时候要使用class.forname()目的就是将编译时期的错误转换成运行时的异常。解耦的解决方案就是将new操作用反射来实现,但是反射就要用到全限定类名来生成对象,我们通过读取配置文件的方式来获取全限定类名,这样就可以避免在全限定类名产生变化时要修改源码。

package com.itheima.factory;

import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * 一个创建Bean对象的工厂
 *
 * Bean:在计算机英语中,有可重用组件的含义。
 * JavaBean:用java语言编写的可重用组件。
 *      javabean >  实体类
 *
 *   它就是创建我们的service和dao对象的。
 *
 *   第一个:需要一个配置文件来配置我们的service和dao
 *           配置的内容:唯一标识=全限定类名(key=value)
 *   第二个:通过读取配置文件中配置的内容,反射创建对象
 *
 *   我的配置文件可以是xml也可以是properties
 */
public class BeanFactory {
    //定义一个Properties对象
    private static Properties props;

    //定义一个Map,用于存放我们要创建的对象。我们把它称之为容器
    private static Map<String,Object> beans;

    //使用静态代码块为Properties对象赋值
    static {
        try {
            //实例化对象
            props = new Properties();
            //获取properties文件的流对象
            InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
            props.load(in);
            实例化容器是因为如果不将创建的对象放入到容器中,那么由于垃圾回收机制就会被回收了
            //实例化容器
            beans = new HashMap<String,Object>();
            //取出配置文件中所有的Key
            Enumeration keys = props.keys();
            //遍历枚举
            while (keys.hasMoreElements()){
                //取出每个Key
                String key = keys.nextElement().toString();
                //根据key获取value
                String beanPath = props.getProperty(key);
                //反射创建对象 反射创建对象调用的是默认构造函数
                Object value = Class.forName(beanPath).newInstance();
                //把key和value存入容器中
                beans.put(key,value);
            }
        }catch(Exception e){
            throw new ExceptionInInitializerError("初始化properties失败!");
        }
    }

    /**
     * 根据bean的名称获取对象
     * @param beanName
     * @return
     */
    public static Object getBean(String beanName){
        return beans.get(beanName);
    }

    /**
     * 根据Bean的名称获取bean对象
     * @param beanName
     * @return
	此种方法创建对象是多例的 所以要读取配置文件的时候
    public static Object getBean(String beanName){
        Object bean = null;
        try {
            String beanPath = props.getProperty(beanName);
//            System.out.println(beanPath);
            bean = Class.forName(beanPath).newInstance();//每次都会调用默认构造函数创建对象
        }catch (Exception e){
            e.printStackTrace();
        }
        return bean;
    }*/
}

需要一个配置文件来配置我们的bean对象,配置文件里面的信息是唯一标识和全限定类名的一一对应的关系,获得全限定类名以后通过反射的方式创建bean对象,这就是工厂模式。

单例可能造成成员变量的线程安全问题,但是dao,service中一般都没有成员变量,所以不用考虑线程安全问题可以使用单例设计模式。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

还有set方法注入,实际开发中通常使用set方法注入。

02.说说你对spring的AOP机制的理解?

Spring 核心框架里面,最关键的两个机制,就是ioc和aop。根据xml配置或者注解,去实例化我们所有的bean,管理bean之间的依赖注入,让类与类之间的耦合性降低,维护代码的时候更加方便轻松。

AOP是面向切面编程,简单的说就是把我们重复的代码抽取出来,在需要执行的时候,使用动态代理技术,在不修改源码的基础上,对我们已有的方法进行增强。

AOP的作用和优势
作用:在程序运行期间,不修改源码对已有方法进行增强。
优势:减少重复代码,提高开发效率,维护方便

AOP的实现方式:使用动态代理技术

03.了解过cglib动态代理吗?他跟jdk动态代理的区别是什么?

动态的代理的特点

在不改变源码的基础上,对一个类中的方法进行增强。其特点是:字节码随用随创建,随用随加载。它与静态代理的区别也在于此。因为静态代理是字节码一上来就创建好,并完成加载。装饰者模式就是静态代理的一种体现。

动态代理的两种实现方式

package com.itheima.cglib;

/**
 * 一个生产者
 */
public class Producer implements IProducer{

    /**
     * 销售
     * @param money
     */
     @override
    public void saleProduct(float money){
        System.out.println("销售产品,并拿到钱:"+money);
    }

    /**
     * 售后
     * @param money
     */
     @override
    public void afterService(float money){
        System.out.println("提供售后服务,并拿到钱:"+money);
    }
}

基于接口的动态代理
package com.itheima.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 模拟一个消费者
 */
public class Client {

    public static void main(String[] args) {
        final Producer producer = new Producer();

        /**
         * 动态代理:
         *  特点:字节码随用随创建,随用随加载
         *  作用:不修改源码的基础上对方法增强
         *  分类:
         *      基于接口的动态代理
         *      基于子类的动态代理
         *  基于接口的动态代理:
         *      涉及的类:Proxy
         *      提供者:JDK官方
         *  如何创建代理对象:
         *      使用Proxy类中的newProxyInstance方法
         *  创建代理对象的要求:
         *      被代理类最少实现一个接口,如果没有则不能使用
         *  newProxyInstance方法的参数:
         *      ClassLoader:类加载器
         *          它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。
         *      Class[]:字节码数组
         *          它是用于让代理对象和被代理对象有相同方法。固定写法。
         *      InvocationHandler:用于提供增强的代码
         *          它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
         *          此接口的实现类都是谁用谁写。
         */
       IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
                producer.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 作用:执行被代理对象的任何接口方法都会经过该方法
                     * 方法参数的含义
                     * @param proxy   代理对象的引用
                     * @param method  当前执行的方法
                     * @param args    当前执行方法所需的参数
                     * @return        和被代理对象方法有相同的返回值
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //提供增强的代码
                        Object returnValue = null;

                        //1.获取方法执行的参数
                        Float money = (Float)args[0];
                        //2.判断当前方法是不是销售
                        if("saleProduct".equals(method.getName())) {
                            returnValue = method.invoke(producer, money*0.8f);
                        }
                        return returnValue;
                    }
                });
        proxyProducer.saleProduct(10000f);
    }
}

基于子类的动态代理
package com.itheima.cglib;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * 模拟一个消费者
 */
public class Client {

    public static void main(String[] args) {
        final Producer producer = new Producer();

        /**
         * 动态代理:
         *  特点:字节码随用随创建,随用随加载
         *  作用:不修改源码的基础上对方法增强
         *  分类:
         *      基于接口的动态代理
         *      基于子类的动态代理
         *  基于子类的动态代理:
         *      涉及的类:Enhancer
         *      提供者:第三方cglib库
         *  如何创建代理对象:
         *      使用Enhancer类中的create方法
         *  创建代理对象的要求:
         *      被代理类不能是最终类
         *  create方法的参数:
         *      Class:字节码
         *          它是用于指定被代理对象的字节码。
         *
         *      Callback:用于提供增强的代码
         *          它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
         *          此接口的实现类都是谁用谁写。
         *          我们一般写的都是该接口的子接口实现类:MethodInterceptor
         */
        Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
            /**
             * 执行北地阿里对象的任何方法都会经过该方法
             * @param proxy
             * @param method
             * @param args
             *    以上三个参数和基于接口的动态代理中invoke方法的参数是一样的
             * @param methodProxy :当前执行方法的代理对象
             * @return
             * @throws Throwable
             */
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                //提供增强的代码
                Object returnValue = null;

                //1.获取方法执行的参数
                Float money = (Float)args[0];
                //2.判断当前方法是不是销售
                if("saleProduct".equals(method.getName())) {
                    returnValue = method.invoke(producer, money*0.8f);
                }
                return returnValue;
            }
        });
        cglibProducer.saleProduct(12000f);
    }
}

两者之间的区别

jdk动态代理是面向接口的动态代理。如果你的类是实现了某个接口,spring aop会使用jdk动态代理,生成一个和你实现同样接口的一个代理类,构造一个实例对象出来 。
cglib动态代理是通过子类实现的动态代理。如果我们的类没有实现接口,spring aop会改用cglib 来实现动态代理,他是生成你的类的一个子类,可以动态生成字节码,覆盖你的方法,在方法里面加入增强的方法 。

04.能说说Spring中的Bean是线程安全的吗?

bean的作用域如下图所示:
在这里插入图片描述
在这里插入图片描述
是线程不安全的。spring bean默认来说是单例的,是线程不安全的。但是java web系统中,一般来说很少在spring bean中放一些实例变量,通常都是多个组件互相调用,最终去访问数据库的,所以一般结果就是多个线程并发的访问数据库。

05.Spring的事务实现原理是什么?能聊聊你对事务传播机制的理解么?

spring ioc 和 aop ,动态代理技术,bean的线程安全问题,事务机制

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

事务传播机制的理解:事务传播机制的理解

事务传播机制

比如说,我们现在有一段业务逻辑,方法A调用方法B,我希望的是如果说方法A出错了,此时仅仅回滚方法A,不能回滚方法B,必须得用REQUIRES-NEW,传播机制,让他们两的事务是不同的。

方法A调用方法B,如果出错,方法B只能回滚他自己,方法A可以带着方法B一起回滚,NESTED嵌套事务。

06.能说说Spring中使用了哪些设计模式吗?

工厂模式,单例模式,代理模式
spring ioc 核心的设计模式的思想体现。

07.能画一张图说一说springMvc的核心架构么?

在这里插入图片描述
注意上图是前后端没有分离的架构,如果是前后端分离的架构,那么在第十步应该是返回一个json串,然后前端获取json串以后对页面进行渲染,然后返回html页面给浏览器。而不是放在后端中由后端来完成。

JVM

01.JVM中有哪几块内存区域?Java 8 之后对内存分代做了什么改进?

tomcat部署,tomcat自己就是基于java来开发的,我们启动的不是自己的系统,是一个tomcat是一个jvm进程,我们写得系统只不过是一些代码,放在tomcat的目录里,tomcat会加载我们的代码到jvm中去。

JVM最常用的内存区域有三块 栈内存,堆内存和永久代。栈内存是线程每个线程独享的,堆内存是共享的,永久代中存储的是类的信息。

java8 之后将永久代中的常量池放到了堆内存中,永久代变成了metaspace(元区域)。

02.你知道JVM是如何运行起来的吗?我们的对象是如何分配的?

03.说说JVM在哪些情况下会触发垃圾回收可以吗?

在这里插入图片描述
我们的jvm的内存其实是有限制的,不可能是无限的,昂贵的资源,2核4G的机器,堆内存也就2GB左右,4核8G的机器堆内存也就4G左右,栈内存也需要空间,metaspace区域放类信息也需要空间。

jvm中有一个内存分代模型,年轻代和老年代,加在一起是堆内存,其中年轻代又分为三部分。年轻代和老年代的比例是我们可以设置的。

比如说年轻代一共是2GB内存,给老年代是2GB内存,默认情况下eden和2个s的比例是:8:1:1,eden是1.6GB, s是0.2GB

如果eden区域满了,此时必然触发垃圾回收,young gc ,ygc。谁是可以回收的垃圾对象?没有被引用的对象就是可以被回收的对象。

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

垃圾回收有一个概念,叫做stop the world,停止你的jvm里的工作线程的运行,然后扫描所有的对象,判断哪些可以回收,哪些不可以回收。

年轻代,大部分情况下,对象生存周期是很短的,可能0.01ms之内,线程执行了3个方法,创建了几个对象,0.01ms之后方法就都执行结束了,此时那几个对象就会在0,01ms之内变成垃圾,可以回收了。

复制算法,一次young gc,年轻代的垃圾回收。将eden中存活的对象复制到s1中,清空eden:
将s1和eden中存活的复制到s2中,清空eden和s1;将s2和eden存活的复制到s1中,清空eden和s2.

三种场景,第一种场景,有的对象在年轻代里面熬过了很多次的垃圾回收,例如15次垃圾回收,此时会认为这个对象是要长期存活的对象。例如Spring容器中的bean对象。

第二种情况就是s区放不下存活的对象。

第三种情况就是特别大的对象。反复移动大对象消耗性能。

05.说说老年代的垃圾回收算法?常用的垃圾回收器都有什么?

老年代对象越来越多,老年代内存空间也会满,是不是可以使用类似的年轻代的复制算法?
答案是否定的,因为在老年代的对象中,很多都是长期引用的,spring容器管理的各种bean。

长期存活的对象是比较多的,可能甚至都有几百mb

对老年代而言,他里面的垃圾对象可能是没有那么多的。一开始采用标记-清理的方式,找出那些垃圾对象,让后直接把垃圾对象在老年代里清理掉,这样就会产生内存 碎片的问题。
后来采用标记-整理的方法,把老年代里的存活对象标记出来,移动到一起,存活对象压缩到一片内存空间去

剩余的空间都是垃圾整个给清理掉,剩余的都是连续的可用的内存空间,解决了内存碎片的问题。

垃圾回收器
parnew (新生代)+ cms(老年代)的组合, g1 直接分代回收 。g1可以实现新生代和老年代的垃圾一起回收。新版本,慢慢的就是主推g1垃圾回收器了,以后会淘汰掉parnews+cms组合,但是现在使用jdk8~jdk9居多,所以还是parnew+cms组合居多。

cms分成好几个阶段,初始标记,并发标记,并发清理等等,老年代垃圾回收是比较慢的,一般起码比年轻代垃圾回收慢个10倍以上。所以将它拆分成几个阶段,尽可能得让其和运行的其它线程并发执行。

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

07.你在实际项目中是否做过JVM GC优化,怎么做的?

08.谈谈你对java跨平台性的理解?为什么java可以一次编译到处运行?

不是说java语言可以跨平台 ,而是各个不同的平台都可以有让java语言运行的环境而已。

程序从源码到运行可以分为以下几个阶段,编码-编译-运行-调试。java在编译阶段体现了跨平台的特性。编译的过程大概是这样的:首先将java源码转化成class字节码文件,这是第一次编译,class字节码文件就是可以到处运行的文件。然后java字节码会被转化为目标机器的机器代码,这是由JVM来执行的,就是java的第二次编译。

“到处运行”的关键和前提就是JVM。因为在第二次编译的过程中JVM起着至关重要的作用。在可以运行java虚拟机的地方都内含着一个JVM操作系统。从而实现到到处运行的效果。需要强调的是,java不是编译机制,而是解释机制。java字节码的设计充分考虑了JIT这一及时编译方式,可以将字节码直接转化成高性能的本地机器码,这同样是虚拟机的一个构成部分。

网络

01.你能聊一聊TCPIP四层网络模型嘛?OSI七层网络模型也要说一下!

设想一下,各个电脑厂商,比如IBM、苹果啥的,都弄自己的协议,结果就苹果电脑和苹果电脑自己可以通信,和IBM电脑就不可以通信,这不是尴尬么。所以搞一个国际通行的协议,大家都按照这个来,所有电脑都可以通信,不是很好么。

此时就必须搞一个标准的网络模型出来,大家都按照这个来走,大家都要遵守统一的规范。这就是所谓OSI七层模型,他们分别是:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。那么在这个基础上,又简化出了TCP/IP四层模型,数据链路层、网络层、传输层、应用层。

物理层

网线,海底电缆等都属于网络的物理层,在物质层面将两台电脑连接起来,然后传递0/1的电路信号。
物理层

数据链路层

数据链路层,物理层给各个电脑连接起来了,还传输最底层的0和1电路信号,关键不行啊,你得定义清楚哪些0和1分为一组,这些信号啥意思?这才能进行通信。所以数据链路层就干这事儿,定义一下电路信号咋分组。

00000011(从电脑1出发,要到电脑2去)

00101(从电脑1出发,要到电脑3去)

0101(从电脑2触发,要到电脑4去)

01(从电脑3出发,要到电脑5去)

很多年前,每个公司都定义自己的电路信号分组方式,但是后来出来了以太网协议,以太网。一组电信号是一个数据包,叫一个帧(frame),每个帧分成两个部分,标头(head)和数(data),标头包含一些说明性的东西,比如说发送者、接收者和数据类型之类的。

每台电脑要往另外一台电脑发送数据,一堆0/1电路信号,封装成数据包,包含头和数据,头里包含了从哪儿来到哪儿去,必须从一台电脑的一个网卡,发送到另外一个电脑的一个网卡,所以以太网发送的数据包必须得指定,目标电脑的网卡的mac地址。

以太网规定了,每个网卡必须得包含一个mac地址,mac地址就是这个网卡的唯一标识,

以太网协议规定了,接入网络里的所有设备,都得有个网卡,以太网协议里的那个数据包,在数据链路层传输的数据包,必须从一个电脑的网卡传输到另外一个电脑的网卡,而这个网卡地址就叫做所谓的mac地址。每块网卡出厂的时候,就有一个唯一的mac地址,48位的二进制,但是一般用12个16进制数字表示,前6个16进制是厂商编号,后6个16进制是网卡流水号。

windows上,ipconfig /all,看看物理地址,就是mac地址,7C-67-A2-20-AB-5C

所以在以太网里传输数据包的时候,必须指定接收者的mac地址才能传输数据。

但是以太网的数据包怎么从一个mac地址发送到另一个mac地址?这个不是精准推送的,以太网里面,如果一个电脑发个数据包出去,会广播给局域网内的所有电脑设备的网卡 ,然后每台电脑都从数据包里获取接收者的mac地址,跟自己的mac地址对比一下,如果一样,就说明这是发给自己的数据包。

但是上面这种广播的方式,仅仅针对一个子网(局域网)内的电脑,会广播,否则一个电脑不能广播数据包给全世界所有的其他电脑吧,是仅仅广播给一个子网里面的电脑的。

如下图:
在这里插入图片描述

网络层

上面说到,子网内的电脑,通过以太网发个数据包,对局域网内的电脑,是广播出去的。那么怎么知道哪些电脑在一个子网内呢?这就得靠网络层了,这里就有一套IP地址,IP地址就可以让我们区分哪些电脑是一个子网的。

网络层里有IP协议,IP协议定义的地址就叫做IP地址。IP地址有IPv4和IPv6两个版本,目前广泛使用的是IPv4,是32个二进制数字组成的,但是一般用4个十进制数字表示,范围从0.0.0.0到255.255.255.255之间。

每台计算机,都会分配一个ip地址,ip地址的前24位(就是前面3个十进制数字),代表了网络,后8位(就是最后1个十进制数字),代表了主机。

如果几台电脑是一个子网的,那么前面的3个十进制数字一定是一样的。举个例子,大家平时做实验,玩儿虚拟机吧,自己win上开几个linux虚拟机,你会发现,win上的ip地址可能是192.168.0.103,然后几个虚拟机的ip地址是192.168.0.182,192.168.0.125,192.168.0.106,类似这样的。

这个win机器和几个虚拟机,前面3个十进制数字都是192.168.0,就代表大家是一个子网内的,最后那个数字是这个子网的不同主机的编号。

但是实际上上面就是举个例子,其实单单从ip地址是看不出来哪些机器是一个子网的,因为从10进制是判断不出来的。需要通过ip地址的二进制来判断,结合一个概念来判断,叫做子网掩码。

比如说ip地址是192.168.56.1,子网掩码是255.255.255.0。知道了子网掩码之后,如果要判断两个ip地址是不是一个子网的,就分别把两个ip地址和自己的子网掩码进行二进制的与运算,与运算之后,比较一下代表网络的那部分。

192.168.56.1和192.168.32.7,判断是不是一个子网的,拿子网掩码255.255.255.0,跟两个ip地址的二进制做与运算

11000000.10101000.00111000.00000001

11111111.11111111.11111111.00000000

子网掩码的二进制是:11111111.11111111.11111111.00000000,然后就跟ip地址的二进制做与好了,通过二进制来比较网络部分的地址是不是一模一样的。

有了网络层的ip地址之后,两台在子网内的电脑终于可以通过广播+mac地址判断来传输数据包进行通信了。

但是如果发现要接受数据包的计算机不在子网内,那么就不能通过广播来发送数据包,需要通过路由来发送数据包。

看到路由,就想到了路由器了,对了,路由器大家都熟悉吧,自己平时也会去买对吧,比如小米的路由器啥的,家里上网一般都会弄个路由器对吧,ok。路由器负责将多个子网进行连接,因为比如你在自己家里,其实你就只是你自己的一个子网,你要是访问网站啥的,是跟那个网站机器所在的子网进行通信。

每个电脑都可以搞多个网卡的,不是只有一个网卡,一般笔记本电脑都有以太网网卡和wifi网卡,发送数据包的时候要决定走哪个网卡。路由器,其实就是配置了多个网卡的一个专用设备,可以通过不同的网卡接入不同的网络。

家里的路由器是包含了交换机和路由的两个功能的,如果是连接到局域网内的设备就把线插LAN那儿;如果是连接到英特网,就把线插在WAN那儿。

这儿给大家举个例子,就是两个局域网之间,如果要是通过一个路由器进行通信的话,是怎么弄的。

大概过程就是,路由器配置了两块网卡,每个网卡可以连到一个局域网内。

局域网1内的电脑,要发送数据包到局域网2内的电脑,在数据包里写上自己的ip地址和对方的ip地址。但是他们俩不在一个局域网内,于是局域网1内的电脑,先通过交换机将数据包发送给路由器,这个过程需要将路由器的一块网卡的ip地址对应的mac地址写到数据包的头部,然后才能通过交换机广播出去,路由器接收到之后比较自己一块网卡的mac地址,就知道是来找自己的。

接着路由器接收到数据包之后,就会在局域网2内,将目标机器的ip地址对应的mac地址写入头部,接着再次通过交换机发送广播通知,发送给局域网2内的电脑。

一个局域网内的每台机器都有自己的ARP cache,这个ARP就是用来在一个局域网内让各个设备都知道每个设备的ip地址和mac地址的对应关系的,一般就是某个机器发送广播通知自己的ip地址和mac地址的对应关系,然后每个机器给他一个回应。以此类推,大家都互相这样广播一把,ip地址和mac地址的对应关系,大家不就都知道了吗?

所以大家在上面可以看到,一个子网内的机器之间通信,就是在数据包里写上对方的mac地址,然后交换机广播出去ok了;但是如果是跨子网的通信,就是写上对方的ip地址和路由器的mac地址,然后先通过mac地址广播到路由器,让路由器再根据另外一个子网的ip地址转换为mac地址,通过另外一个子网的交换机广播过去。就这个意思。

如图:
在这里插入图片描述
在这里插入图片描述
下面介绍几个概念:

  • 网关其实就是路由器的一种,运作在网络层。可以把路由器上的ip地址认为就是网关,路由器上每个网卡都有mac地址和对应的ip地址。路由器虽然有mac地址,但是不能通过mac地址寻址的,必须通过ip地址寻址,所以路由器其实是工作在网络层的设备。(电脑上有默认网关)

  • 网络交换机是通过mac地址来寻址和传输数据包的;但是路由器是通过ip地址寻址和传输数据包的。网络交换机主要用在局域网的通信,一般你架设一个局域网,里面的电脑通信是通过数据链路层发送数据包,通过mac地址来广播的,广播的时候就是通过网络交换机这个设备来把数据广播到局域网内的其他机器上去的;路由器一般用来让你连入英特网。。

  • LAN,就是local area network,就是局域网;WAN,就是wide area network,就是广域网。WLAN是wireless local area network,就是无线局域网,也就是wifi,在局域网内,直接通过wifi无线联网。

假设你访问百度网站,先通过mac地址和交换机广播到默认网关,然后进行一层一层网关在寻址,一直到找到百度所在的那个服务器的ip地址和对应的mac地址(这里也用到交换机在子网的广播),然后传输数据。

传输层

上面我们大概明白了通过网络层的ip地址怎么划分出来一个一个的子网,然后在子网内部怎么通过mac地址广播通信;跨子网的时候,怎么通过ip地址 -> mac地址 -> 交换机 -> 路由器 -> ip地址 -> mac地址 -> 交换机的方式来通过路由器进行通信。

但是这里还有一个问题,就是一台机器上,是很多个程序用一个网卡进行网络通信的,比如说浏览器、QQ、视频直播,这些软件都用了一个网卡往外面发送数据,然后从网卡接收数据,对吧。

所以还需要一个端口号的概念,就是你得发送数据包到某个机器的一个网卡的某个端口上去,然后那个机器上监听那个端口的程序,就可以提取发送到这个端口的数据,知道是自己的数据。端口号是065536的范围内,01023被系统占用了,别的应用程序就用1024以上的端口就ok了。

电脑1,是在端口48362监听的,通过网卡发送了一条数据 -> 电脑2的ip地址的20386这个端口 -> 电脑2的上面的某个QQ,监听着20386的端口 -> 电脑2的网卡接收到一条数据之后,发现人家找的是20386这个端口,就去找谁哪个哥儿们在监听20386端口,QQ在监听,我就把这个网卡过来的数据,传递给QQ,通过端口知道,哪条数据是给你的

所以其实大家会发现一点,网络层,是基于ip协议,进行主机和主机间的寻址和通信的,然后传输层,其实是建立某个主机的某个端口,到另外一个主机的某个端口的连接和通信的。

这个通信,就是通过socket来实现的,通过socket就可以基于tcp/ip协议完成刚才上面说的一系列的比如基于ip地址和mac地址转换和寻址啊,通过路由器通信啊之类的,而且会建立一个端口到另外一个端口的连接。

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

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

应用层

通过传输层的tcp协议可以传输数据,但是人家收到数据之后,怎么来解释?比如说收到个邮件你怎么处理?收到个网页你怎么处理?类似这个意思,所以针对各种不同的应用,邮件、网页之类的,都是定义不同的应用层协议的。这个应用层,我们就假设综合了会话层、表示层和应用层了,3层合成1层。

电脑1走tcp协议发送了一段东西过来,发送到电脑2的20386端口

GET http://localhost:8080/ http/1.1

key:valuel

key:value

电脑2走tcp协议读取到了属于自己这个20386端口 的一段数据

GET http://localhost:8080/ http/1.1

key:valuel

key:value

发送了一段响应

200

key;value

key:value

又通过底层的tcp发了出去,电脑1的30987端口,ip

电脑1,网卡,走以太网协议收到一个数据包

200

key;value

key:value

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

然后我们看下自己的网络设置,一般包含了ip地址、子网掩码、网关地址、DNS地址。前面3个我们其实都知道啥意思了。ip地址和子网掩码用来划分子网的,判断哪些ip地址在一个子网内。同时你的ip地址和mac地址关联起来的,唯一定位了你的网卡。网关地址,你就认为是路由器上的那个网卡的ip地址吧,路由器的网卡也有mac地址,mac地址对应了一个ip地址。

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

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

宏观

1、客户端浏览器通过DNS解析到www.baidu.com的IP地址202.108.22.5,通过这个IP地址找到客户端到服务器的路径。客户端浏览器发起一个HTTP会话到202.108.22.5,然后通过TCP进行封装数据包,输入到网络层

2、在客户端的传输层,把HTTP会话请求分成报文段,添加源和目的端口,如服务器使用80端口监听客户端的请求,客户端由系统随机选择一个端口如5000,与服务器进行交换,服务器把相应的请求返回给客户端的5000端口。然后使用IP层的IP地址查找目的端。 (建立tcp连接)

3、客户端的网络层不用关心应用层或者传输层的东西,主要做的是通过查找路由表确定如何到达服务器,期间可能经过多个路由器,这些都是由路由器来完成的工作,我不作过多的描述,无非就是通过查找路由表决定通过那个路径到达服务器。 (IP的重要功能是寻址和路由

4、客户端的链路层,包通过链路层发送到路由器,通过邻居协议查找给定IP地址的MAC地址,然后发送ARP请求查找目的地址,如果得到回应后就可以使用ARP的请求应答交换的IP数据包现在就可以传输了,然后发送IP数据包到达服务器的地址。(IP地址转为MAC)

事件顺序

  1. 由域名→IP 地址
    寻找 IP 地址的过程依次经过了浏览器缓存、系统缓存、hosts文件、路由器缓存、 递归搜索根域名服务器。

  2. 建立 TCP/IP 连接(三次握手具体过程)

  3. 由浏览器发送一个 HTTP 请求

  4. 经过路由器的转发,通过服务器的防火墙,该 HTTP 请求到达了服务器(下面的过程其实就是这一步)

  5. 服务器处理该 HTTP 请求,返回一个 HTML 文件

  6. 浏览器解析该 HTML 文件,并且显示在浏览器端

  7. 这里需要注意:

o HTTP 协议是一种基于 TCP/IP 的应用层协议,进行 HTTP 数据请求必须先建立 TCP/IP 连接

o 可以这样理解:HTTP 是轿车,提供了封装或者显示数据的具体形式;Socket 是发动机,提供了网络通信的能力。

o 两个计算机之间的交流无非是两个端口之间的数据通信 , 具体的数据会以什么样的形式展现是以不同的应用层协议来定义的

参考链接:浏览器输入一个网址(www.baidu.com)后执行的全过程


微观

首先应该知道网络七层模型大概都是怎么回事了,然后四层模型其实就是会话层、表示层和应用层,合并为了一个应用层,同时没把物理层算在内。

并且我们也大概知道每一层的协议和作用,网络通信的时候都是怎么回事了,现在我们来看看假设通过浏览器发送一个请求,你访问到那个网站对应的机器,然后人家再给你一个响应的全过程。

现在我们先假设,我们给电脑设置了几个东西:

ip地址:192.168.31.37

子网掩码:255.255.255.0

网关地址:192.168.31.1

DNS地址:8.8.8.8

这时,我们打开一个浏览器,请求www.baidu.com地址,这个时候找DNS服务器,DNS服务器解析域名之后,返回一个ip地址,比如172.194.26.108。

接着会判断两个ip地址是不是一个子网的,用子网掩码255.255.255.0,对两个ip地址做与运算,拿到192.168.31.0和172.194.26.0,明显不是一个子网的。

如图:
在这里插入图片描述
那就得发送一个数据包给网关,其实你就认为是我们的路由器吧,就是192.168.31.1,而且我们是可以拿到网关ip地址的mac地址的,现在我们从应用层出发,通过浏览器访问一个网站,是走应用层的http协议的,并且要把浏览器发出的请求打包成数据包,要把哪些东西给放到数据包中去呢?

先介绍一下http协议 :
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这就构成了一个http请求报文浏览器请求一个地址,先按照应用层的http协议,封装一个应用层数据包,数据包里就放了http请求报文,这个时候会将这个http请求报文打包成一个数据包,仅仅只是数据包的数据部分,此时是数据包是没有头的。上面根据http协议搞一个http请求报文,然后搞一个数据包出来,就是网络模型中到的应用层干的事儿了。

接着就是跑传输层来了,这个层是tcp协议,这个tcp协议会让你设置端口,发送方的端口随机选一个,接收方的端口一般是默认的80端口。

这个时候,会把应用层数据包给封装到tcp数据包中去,而且会加一个tcp头,这个tcp数据包是对应一个tcp头的,这个tcp头里就放了端口号信息。如图:
在这里插入图片描述
接着跑到网络层来了,走ip协议,这个时候会把tcp头和tcp数据包,放到ip数据包里去,然后再搞一个ip头,ip头里本机和目标机器的ip地址。

这里本机ip地址是192.168.31.37,

目标机器是172.194.26.108。

因为,通过ip协议,可以判断说,两个ip地址不是在一个子网内的,所以此时只能将数据包先通过以太网协议广播到网关上去,通过网关再给他发送出去,如图:
在这里插入图片描述
接着是数据链路层,这块走以太网协议,这里是把ip头和ip数据包封到以太网数据包里去,然后再加一个以太网数据包的头,头里放了本机网卡mac地址,和网关的mac地址。但是以太网数据包的限制是1500个字节,但是假设这个时候ip数据包都5000个字节了,那么需要将ip数据包切割一下。

这个时候一个以太网数据包要切割为4个数据包,每个数据包包含了以太网头、ip头和切割后的ip数据包,4个数据包的大小分别是1500,1500,1500,560。ip头里包含了每个数据包的序号。

如图:
在这里插入图片描述
这4个以太网数据包都会通过交换机发到你的网关上,然后你的路由器是可以联通别的子网的,这个是时候你的路由器就会转发到别的子网的可能也是某个路由器里去,然后以此类推吧,N多个路由器或者你叫网关也行,N多个网关转发之后,就会跑到百度的某台服务器,接收到4个以太网数据包。

百度服务器接收到4个以太网数据包以后,根据ip头的序号,把4个以太网数据包里的ip数据包给拼起来,就还原成一个完整的ip数据包了。接着就从ip数据包里面拿出来tcp数据包,再从tcp数据包里取出来http数据包,读取出来http数据包里的各种协议内容,接着就是做一些处理,然后再把响应结果封装成http响应报文,封装在http数据包里,再一样的过程,封装tcp数据包,封装ip数据包,封装以太网数据包,接着通过网关给发回去。

如下图:
在这里插入图片描述

03.TCP三次握手和四次挥手的流程图?为啥不是两次或者五次呢?

附上一个讲的很好的链接两张动图-彻底明白TCP的三次握手与四次挥手

首先我们来回顾一下 TCP 的数据传输单元,TCP 传送的数据单元称为报文段。一个 TCP 报文段分为 TCP 首部和 TCP 数据两部分,整个 TCP 报文段都封装在 IP 数据报中的数据部分,TCP 首部长度是4的整数倍,其中有固定的20个字节,剩余的可变动的就是选项和填充「最常见的可选字段是最长报文大小,又称为MSS(Maximum Segment Size),每个连接方通常都在通信的第一个报文段(为建立连接而设置SYN标志为1的那个段)中指明这个选项,它表示本端所能接受的最大报文段的长度。」,20个固定的字节包括了源端口号(2 字节)、目的端(2字节)、seq序列号(4字节)、确认号ack(4字节)、以及确认位ACK 等等。

  1. 确认ACK,仅当ACK=1时,确认号字段才有效。TCP规定,在连接建立后所有报文的传输都必须把ACK置1;
  2. 同步SYN,在连接建立时用来同步序号。当SYN=1,ACK=0,表明是连接请求报文,若同意连接,则响应报文中应该使SYN=1,ACK=1;
  3. 终止FIN,用来释放连接。当FIN=1,表明此报文的发送方的数据已经发送完毕,并且要求释放;

其次,我们来详细讲解一下三次握手、四次挥手的过程:

  • 三次握手

img
首先,在三次握手建立连接的阶段,是不会传输 TCP 报文段的,传输的是 传输控制块(TCB),传输控制块 TCB(Transmission Control Block)存储了每一个连接中的一些重要信息,如:TCP 连接表,指向发送和接收缓存的指针,指向重传队列的指针,当前的发送和接收序号等等。

收到报文,发送报文,同时修改自身的状态,如果不发送报文,自身状态也不会改变

最开始的时候客户端和服务器都是处于CLOSED状态。主动打开连接的为客户端,被动打开连接的是服务器。

  1. TCP服务器进程先创建传输控制块TCB时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;
  2. TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
  3. TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。
  4. TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
  5. 当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。

1.为何不直接在第一次握手就带上报文段消息,非要第三次才可以带?

因为 TCP 是要保证数据的不丢失且可靠,如果在第一次就带上报文段消息,此次建立连接很有可能就会失败,那么就不能保证数据的不丢失了,在不可靠的机制上进行这种操作,换来的代价太大,每次发送报文段的资源也会增大,得不偿失;

而第三次握手的时候,客户端已经知道服务器端准备好了,所以只要告诉服务器端自己准备好了就okay了,所以此时带上报文段信息没有任何问题。

​ 2.可不可以只握手两次?

一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。

肯定是不可以的,三次握手主要是解决这样一个常见的问题,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。

如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。

  • 四次挥手
    四次挥手

数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,然后客户端主动关闭,服务器被动关闭。

  1. 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
  2. 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。**TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,**也就是整个CLOSE-WAIT状态持续的时间。
  3. 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文**(在这之前还需要接受服务器发送的最后的数据)**。
  4. 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了**LAST-ACK(最后确认)**状态,等待客户端的确认。
  5. 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2*MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
  6. 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些

一个很常见的问题,为何不能三次挥手呢?

  • 首先如果去掉最后一次挥手,那么服务器端就不知道自己要关闭的报文有没有传输成功,可能半路上就失败了,但是此时客户端不知道,导致客户端一直在等待服务器关闭,但是此时服务器端直接就关闭了;
  • 如果中间的两次挥手合并,那是肯定不行的,因为此时服务器端可能还有很多报文未处理完,此时直接关闭肯定会对传输有很大影响。

为什么客户端在收到 服务器端发来的 FIN 包后要等 2 个最长报文段传输时间?

防止最后自己发去的 ack 没传送到服务器,如果服务器没收到客户端的 ack,肯定会选择重发一次 FIN 包,那么此时如果客户端已经关闭了,客户端就不能再发 ack 确认收到了,至于为何是 2 个报文段传输时间,因为刚好一去一回嘛… 2 个最长报文传输时间没有 FIN 包发来,就说明服务器已经关闭了,客户端也就可以安心关闭了。

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

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

04.说一下http的工作流程?http1.0,http1.1,http2.0具体有哪些区别?

http协议,是每个搞java必须会的基础
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
http 1.0要指定keep-alive来开启持久连接,默认是短连接,就是浏览器每次请求都要重新建立一次tcp连接,完事儿了就释放tcp连接。早期的网页都很low,没啥东西,就一点文字,就用这个没问题。但是现在,一个网页打开之后,还要加载大量的图片、css、js,这就坑爹了,发送多次请求。

早期,2000年之前,那个时候网页,都很low,当时你打开一个网页,就是说现场底层tcp三次握手,跟网站建立一个tcp连接,然后通过这个tcp连接,发送一次http请求,网站返回一个http响应(网页的html,里面有一大段文字),浏览器收到html渲染成网页,浏览器就走tcp四次挥手,跟网站断开连接了

到了后面,发现2000之后,2010之后更不用说了,网页发展很迅猛,一个网页包含着大量的css、js、图片等资源。比如你请求一个网页,这个网页的html先过来,过来之后,浏览器再次发起大量的请求去加载css、js、图片,打开一个网页可能浏览器要对网站服务器发送几十次请求。

http 1.0,疯了,刚开始请求网页的html,tcp三次握手建立连接 -> 请求/响应 -> tcp四次挥手断开连接,接着再次要加载css、js、图片,要发送30个请求,上面的过程来30次,30次频繁的建立tcp连接以及释放tcp连接。很慢很慢。

其实最慢的不是发送请求和获取响应,打开和释放连接,这都是很重的过程

**http 1.1默认支持长连接,就是说,浏览器打开一个网页之后,底层的tcp连接就保持着,不会立马断开,之后加载css、js之类的请求,都会基于这个tcp连接来走。**http 1.1还支持host头,也就可以支持虚拟主机;而且对断点续传有支持。

浏览器,第一次请求去一个网站的一个页面的时候,就会打开一个tcp连接,接着就在一段时间内都不关闭了,然后接下来这个网页加载css、js、图片大量的请求全部走同一个tcp连接,频繁的发送请求获取响应,最后过了一段时间,这些事儿都完了,然后才会去释放那一个tcp连接。大幅度的提升复杂网页的打开的速度,性能。

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

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

在这里插入图片描述
附上两个解析链接:HTTP原理详解 深入理解HTTPS工作原理

http协议都是明文的,是没有加密的,所以其实现在一般大部分应用都是用https协议的。之前是基于SSL协议对http进行加密,后来又升级到了TSL协议来加密,现在称之为SSL / TSL吧。

HTTP 的不足之处

  • 通信内容使用明文——内容可能被窃听
  • 不验证通信方的身份——可能遭遇伪装
  • 无法验证报文的完整性——报文有可能已遭篡改

HTTPS = HTTP + 加密 + 认证 + 数字签名 解决上述的三个问题

注意:秘钥如果没用在路径上传递,只是保存在服务器和客户端上是没有安全问题的,因为黑客都是从通信的路径上截获消息,然后破解的。如果不在路径上发送,就会是安全的。

https的工作原理大概是这样的:

(1)浏览器把自己支持的加密规则发送给网站

(2)网站从这套加密规则里选出来一套加密算法和hash算法,然后把自己的身份信息用证书的方式发回给浏览器,证书里有网站地址、加密公钥、证书颁发机构(hash算法用来保证验证消息是否没有被篡改,专业术语叫做 数字签名

(3)浏览器验证证书的合法性,然后浏览器地址栏上会出现一把小锁;浏览器接着生成一串随机数密码,然后用证书里的公钥进行加密,这块走的非对称加密;用约定好的hash算法生成握手消息的hash值,然后用密码对消息进行加密,然后把所有东西都发给网站,这块走的是对称加密。这里的总的消息分成三部分,用公钥加密的随机密码,消息的hash值,和用随机密码加密的消息

(4)网站,从消息里面可以取出来公钥加密后的随机密码,用本地的私钥对消息解密取出来密码,然后用密码解密浏览器发来的握手消息,计算消息的hash值,并验证与浏览器发送过来的hash值是否一致,最后用密码加密一段握手消息,发给浏览器

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

常用的非对称加密是RSA算法,对称加密是AES、RC4等,hash算法就是MD5

非对称加密,有个人说我加密的时候是用了一个公钥去加密,然后你解密的时候是用私钥去解密;我加密的时候用的算法,跟解密的时候用的算法,是一样的,对称加密。

06.什么是长连接?http长连接是什么?

http本身没什么所谓的长连接短连接之说,其实说白了都是http下层的tcp连接是长连接还是短连接,tcp连接保持长连接,那么多个http请求和响应都可以通过一个链接来走。其实http 1.1之后,默认都是走长连接了,就是底层都是一个网页一个tcp连接,一个网页的所有图片、css、js的资源加载,都走底层一个tcp连接,来多次http请求即可。

http 1.0的时候,底层的tcp是短连接,一个网页发起的请求,每个请求都是先tcp三次握手,然后发送请求,获取响应,然后tcp四次挥手断开连接;每个请求,都会先连接再断开。短连接,建立连接之后,发送个请求,直接连接就给断开了

http 1.1,tcp长连接,tcp三次握手,建立了连接,无论有多少次请求都是走一个tcp连接的,走了n多次请求之后,然后tcp连接被释放掉了

07.聊聊Socket的工作原理?Socket跟TCP IP之间是啥关系?

1、面试题

说说socket通信的原理?

2、面试官心里分析

其实不知道大家发现没有,网络相关的问题,都是围绕着所谓的七层模型,或者是四层模型去走的。聊完四层模型,接着就是一次请求的全过程,紧接着就是聊传输层的tcp的连接,然后就是传输层的tcp协议之上的socket编程,接下来还会聊聊应用层的http协议。

所以说,来吧,这都是最最基础的网络知识。

3、面试题剖析

其实说白了,socket就是在传输层里把tcp/ip协议给封装了一下,我们程序员一般都是面向socket来编程的,比如java原生就支持socket网络编程的。

大体来说这个步骤,就是我们搞一个ServerSocket无限等待别人来连接你,然后某个机器要跟你连接,就在本地创建一个socket去连接你,然后建立连接之后,在服务器上,ServerSocket也会创建出来一个socket的。通过客户端的socket跟服务端的socket进行通信,我给你写数据,你读数据,你给我写数据,我读数据,就这个过程。

当然这个底层,比如建立连接和释放连接,都是基于tcp三次握手和四次挥手的规范来搞的,包括基于tcp协议传输数据,其实就跟我们之前说的一样,都是封装个tcp数据包,里面有tcp报头,整了端口号啥的,然后封装在ip数据包里,最后封在以太网数据包里传递。
在这里插入图片描述

08.进程间通信?线程间切换?

1、面试题

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

2、面试官心里分析

这个问题不是高频基础问题,但是确实可能有人会问,因为怎么说呢,计算机基础,就这点儿东西,网络、cpu、磁盘、内存、进程,所以可能有人会看看你的基础知识咋样,所以问问你这个问题。

3、面试题剖析

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

(1)管道(pipe)

unix操作系统里面,有一个fork操作,可以创建进程的子进程,或者说是复制一个进程完全一样的子进程,共享代码空间,但是各自有独立的数据空间,不过子进程的数据空间是拷贝父进程的数据空间的。

管道机制要求的是两个进程之间是有血缘关系的,就比如fork出来的父子进程。

linux操作系统里,管道用来缓存要在进程间传输的数据,管道是一个固定大小的缓冲区,是4kb。管道中的数据一旦被读取出来,就不在管道里了。

但是如果管道满了,那么写管道的操作就阻塞了,直到别人读了管道的数据;反之如果管道是空的,那么读操作就阻塞了。就这个意思。管道一边连着一个进程的输出,一边连着一个进程的输入,然后就一个进程写数据,另外一个进程读数据,两个进程都没了,管道也就没了。管道是半双工的,就是数据只能流向一个方向,比如说你架设一个管道,只能一个进程写,另外一个进程读。

linux里面对管道的实现,是用了两个文件,指向了一个VFS(虚拟文件系统)的索引节点inode,然后VFS索引节点指向一个物理页面,接着一个进程通过自己关联的那个文件写数据,另外一个进程通过自己关联的那个文件读数据。

(2)命名管道(fifo)

管道的通信,要求必须是父子关系的进程间通信,就受到了限制,所以可以用命名管理来解决这个问题。

之前的管道,是没有名字的,所以必须是有父子关系的进程才能使用。但是这个命名管道是有名字的。这个命名管道,相当于是一个有名字的文件,是有路径的,所以没有血缘关系的进程多可以通过这个命名管道来通信,名字在文件系统上,数据在内存里。其他的跟管道一样,一个进程写,一个进程读,也是半双工的,数据只能单向流动。

(3)消息队列

linux的消息队列可以认为是个链表结构,linux内核有一个msgque链表,这个链表里每个指针指向一个msgid_ds结构,这个结构就描述了一个消息队列。然后进程之间就通过这个消息队列通信就可以,一样是写入数据和消费数据。消息队列的好处就是对每个消息可以指定类型,消费的时候就消费指定类型的消息就行了,功能更多一些。这种方式其实用的不多的。

(4)共享内存

一块物理内存被映射到两个进程的进程地址空间,所以进程之间互相都可以立即看到对方在共享内存里做出的修改,但是因为是共享内存,所以需要锁来保证同步。这个说对了很复杂,我在这里就不多说了,我觉得如果被人问到这个问题,短期内突击的话,回答到这个程度就行了,就是知道有哪些方式。如果你要深入理解各种机制,那是要好好学习linux的各种东西了。

(5)线程间如何切换

一个进程的多个线程间切换的时候就涉及到了上下文切换,这个东西说复杂了就很复杂,但是简单来说,就是有一个时间片算法,cpu给每个线程一个时间片来执行,时间片结束之后,就保存这个线程的状态,然后切换到下一个线程去执行,这就是所谓多线程并发执行的原理,就是多个线程来回来去切换,每个线程就一个时间片里执行。太复杂的我也不讲了,大家就记住一个线程上下文切换指的是什么就行了。

数据库

01.MySQL MylSAM 和 InnoDB存储引擎的区别是啥?

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

innodb是现在最常用的存储引擎,是mysql5.5之后的默认存储引擎。主要特点就是支持事务,走聚簇索引,强制要求有主键,支持外键约束,高并发、大数据量、高可用等相关成熟的数据库架构,分库分表、读写分离、主备切换,全部都可以基于innodb存储引擎来实现。到这里就可以展开来说一说是怎么支撑大数据的,怎么进行读写分离的。
在这里插入图片描述
主从同步的实现:首先主服务器将数据的变化记录到BinaryLog中,接下来从服务器会开启一个线程将主服务器中的数据操作同步到自己的RelayLog中,然后在开启一个线程执行该操作,更新从服务器中的数据。

02.MySQL索引的原理和数据结构能介绍一下么?b+树和b-树有什么区别?MySQL聚簇索引和非聚簇索引的区别是什么?他们分别是如何存储的?使用MySQL索引都有哪些原则?MySQL复合索引如何使用?

关于B+树的数据结构这里推荐一篇文章B+树
这里给一个简单的总结,至于为什么要这样实现,m的取值要多大,B+树是如何衍生出来的参照上面的文章:

  • 每个节点中子节点的个数不能超过 m,也不能小于 m/2;
  • 根节点的子节点个数可以不超过 m/2,这是一个例外;
  • m 叉树只存储索引,并不真正存储数据,这个有点儿类似跳表;
  • 通过链表将叶子节点串联在一起,这样可以方便按区间查找;
  • 一般情况,根节点会被存储在内存中,其他节点存储在磁盘中。

2.1 索引的数据结构是什么

其实就是让你聊聊mysql的索引底层是什么数据结构实现的,弄不好现场还会让你画一画索引的数据结构,然后会问问你mysql索引的常见使用原则,弄不好还会拿个SQL来问你,就这SQL建个索引一般咋建?

至于索引是啥?这个问题太基础了,大家都知道,mysql的索引说白了就是用一个数据结构组织某一列的数据,然后如果你要根据那一列的数据查询的时候,就可以不用全表扫描,只要根据那个特定的数据结构去找到那一列的值,然后找到对应的行的物理地址即可。

那么回答面试官的一个问题,mysql的索引是怎么实现的?

答案是,不是二叉树,也不是一颗乱七八糟的树,而是一颗b+树。这个很多人都会这么回答,然后面试官一定会追问,那么你能聊聊b+树吗?

但是说b+树之前,咱们还是先来聊聊b-树是啥,从数据结构的角度来看,b-树要满足下面的条件:

(1)d为大于1的一个正整数,称为B-Tree的度。

(2)h为一个正整数,称为B-Tree的高度。

(3)每个非叶子节点由n-1个key和n个指针组成,其中d<=n<=2d。

(4)每个叶子节点最少包含一个key和两个指针,最多包含2d-1个key和2d个指针,叶节点的指针均为null 。

(5)所有叶节点具有相同的深度,等于树高h。

(6)key和指针互相间隔,节点两端是指针。

(7)一个节点中的key从左到右非递减排列。

(8)所有节点组成树结构。

(9)每个指针要么为null,要么指向另外一个节点。

(10)如果某个指针在节点node最左边且不为null,则其指向节点的所有key小于v(key1),其中v(key1)为node的第一个key的值。

(11)如果某个指针在节点node最右边且不为null,则其指向节点的所有key大于v(keym),其中v(keym)为node的最后一个key的值。

(12)如果某个指针在节点node的左右相邻key分别是keyi和keyi+1且不为null,则其指向节点的所有key小于v(keyi+1)且大于v(keyi)。

上面那段规则,我也是从网上找的,说实话,没几个java程序员能耐心去看明白或者是背下来,大概知道是个树就好了。就拿个网上的图给大家示范一下吧:

比如说我们现在有一张表:

(

id int

name varchar

age int

)

我们现在对id建个索引:15、56、77、20、49

select * from table where id = 49

select * from table where id = 15

img

反正大概就长上面那个样子,查找的时候,就是从根节点开始二分查找。大概就知道这个是事儿就好了,深讲里面的数学问题和算法问题,时间根本不够,面试官也没指望你去讲里面的数学和算法问题,因为我估计他自己也不一定能记住。

好了,b-树就说到这里,直接看下一个,b+树。b+树是b-树的变种,啥叫变种?就是说一些原则上不太一样了,稍微有点变化,同样的一套数据,放b-树和b+树看着排列不太一样的。而mysql里面一般就是b+树来实现索引,所以b+树很重要。

b+树跟b-树不太一样的地方在于:

  1. 每个节点的指针上限为2d而不是2d+1。

  2. 内节点不存储data,只存储key;

    叶子节点不存储指针。

这图我就不自己画了,网上弄个图给大家瞅一眼:

img

select * from table where id = 15

select * from table where id>=18 and id<=49

但是一般数据库的索引都对b+树进行了优化,加了顺序访问的指针,如网上弄的一个图,这样在查找范围的时候,就很方便,比如查找18~49之间的数据:

img

其实到这里,你就差不多了,你自己仔细看看上面两个图,b-树和b+树都现场画一下,然后给说说区别,和通过b+树查找的原理即可。

接着来聊点稍微高级点的,因为上面说的只不过都是最基础和通用的b-树和b+树罢了,但是mysql里不同的存储引擎对索引的实现是不同的。

2.2 myism存储引擎的索引实现

先来看看myisam存储引擎的索引实现。就拿上面那个图,咱们来现场手画一下这个myisam存储的索引实现,在myisam存储引擎的索引中,每个叶子节点的data存放的是数据行的物理地址,比如0x07之类的东西,然后我们可以画一个数据表出来,一行一行的,每行对应一个物理地址。

索引文件

img

id=15,data:0x07,0a89,数据行的物理地址

数据文件单独放一个文件

img

select * from table where id = 15 -> 0x07物理地址 -> 15,张三,22

myisam最大的特点是数据文件和索引文件是分开的,大家看到了么,先是索引文件里搜索,然后到数据文件里定位一个行的。

2.3 innodb存储引擎的索引

好了,再来看看innodb存储引擎的索引实现,跟myisam最大的区别在于说,innodb的数据文件本身就是个索引文件,就是主键key,然后叶子节点的data就是那个数据的所在行。我们还是用上面那个索引起来现场手画一下这个索引好了,给大家来感受一下。

img

innodb存储引擎,要求必须有主键,会根据主键建立一个默认索引,叫做聚簇索引,innodb的数据文件本身同时也是个索引文件,索引存储结构大致如下:

15,data:完整的一行数据,(15,张三,22)

22,data:完整的一行数据,(22,李四,30)

就是因为这个原因,innodb表是要求必须有主键的,但是myisam表不要求必须有主键。另外一个是,innodb存储引擎下,如果对某个非主键的字段创建个索引,那么最后那个叶子节点的值就是主键的值,因为可以用主键的值到聚簇索引里根据主键值再次查找到数据,即所谓的回表,例如:

select * from table where name = ‘张三’

先到name的索引里去找,找到张三对应的叶子节点,叶子节点的data就是那一行的主键,id=15,然后再根据id=15,到数据文件里面的聚簇索引(根据主键组织的索引)根据id=15去定位出来id=15这一行的完整的数据

所以这里就明白了一个道理,为啥innodb下不要用UUID生成的超长字符串作为主键?因为这么玩儿会导致所有的索引的data都是那个主键值,最终导致索引会变得过大,浪费很多磁盘空间。

还有一个道理,一般innodb表里,建议统一用auto_increment自增值作为主键值,因为这样可以保持聚簇索引直接加记录就可以,如果用那种不是单调递增的主键值,可能会导致b+树分裂后重新组织,会浪费时间。

2.4 索引的使用规则

一般来说跳槽时候,索引这块必问,b+树索引的结构,一般是怎么存放的,出个题,针对这个SQL,索引应该怎么来建立

select * from table where a=1 and b=2 and c=3,你知道不知道,你要怎么建立索引,才可以确保这个SQL使用索引来查询

好了,各位同学,聊到这里,你应该知道具体的myisam和innodb索引的区别了,同时也知道什么是聚簇索引了,现场手画画,应该都ok了。然后我们再来说几个最最基本的使用索引的基本规则。

其实最基本的,作为一个java码农,你得知道最左前缀匹配原则,这个东西是跟联合索引(复合索引)相关联的,就是说,你很多时候不是对一个一个的字段分别搞一个一个的索引,而是针对几个索引建立一个联合索引的。

给大家举个例子,你如果要对一个商品表按照店铺、商品、创建时间三个维度来查询,那么就可以创建一个联合索引:shop_id、product_id、gmt_create

一般来说,你有一个表(product):shop_id、product_id、gmt_create,你的SQL语句要根据这3个字段来查询,所以你一般来说不是就建立3个索引,一般来说会针对平时要查询的几个字段,建立一个联合索引

后面在java系统里写的SQL,都必须符合最左前缀匹配原则,确保你所有的sql都可以使用上这个联合索引,通过索引来查询

create index (shop_id,product_id,gmt_create)

(1)全列匹配

这个就是说,你的一个sql里,正好where条件里就用了这3个字段,那么就一定可以用到这个联合索引的:

select * from product where shop_id=1 and product_id=1 and gmt_create=’2018-01-01 10:00:00’

(2)最左前缀匹配

这个就是说,如果你的sql里,正好就用到了联合索引最左边的一个或者几个列表,那么也可以用上这个索引,在索引里查找的时候就用最左边的几个列就行了:

select * from product where shop_id=1 and product_id=1,这个是没问题的,可以用上这个索引的

(3)最左前缀匹配了,但是中间某个值没匹配

这个是说,如果你的sql里,就用了联合索引的第一个列和第三个列,那么会按照第一个列值在索引里找,找完以后对结果集扫描一遍根据第三个列来过滤,第三个列是不走索引去搜索的,就是有一个额外的过滤的工作,但是还能用到索引,所以也还好,例如:

select * from product where shop_id=1 and gmt_create=’2018-01-01 10:00:00’

就是先根据shop_id=1在索引里找,找到比如100行记录,然后对这100行记录再次扫描一遍,过滤出来gmt_create=’2018-01-01 10:00:00’的行

这个我们在线上系统经常遇到这种情况,就是根据联合索引的前一两个列按索引查,然后后面跟一堆复杂的条件,还有函数啥的,但是只要对索引查找结果过滤就好了,根据线上实践,单表几百万数据量的时候,性能也还不错的,简单SQL也就几ms,复杂SQL也就几百ms。可以接受的。

(4)没有最左前缀匹配

那就不行了,那就在搞笑了,一定不会用索引,所以这个错误千万别犯

select * from product where product_id=1,这个肯定不行

(5)前缀匹配

这个就是说,如果你不是等值的,比如=,>=,<=的操作,而是like操作,那么必须要是like ‘XX%’这种才可以用上索引,比如说

select * from product where shop_id=1 and product_id=1 and gmt_create like ‘2018%’

(6)范围列匹配

如果你是范围查询,比如>=,<=,between操作,你只能是符合最左前缀的规则才可以范围,范围之后的列就不用索引了

select * from product where shop_id>=1 and product_id=1

这里就在联合索引中根据shop_id来查询了

(7)包含函数

如果你对某个列用了函数,比如substring之类的东西,那么那一列不用索引

select * from product where shop_id=1 and 函数(product_id) = 2

上面就根据shop_id在联合索引中查询

2.5 索引的缺点以及使用注意

索引是有缺点的,比如常见的就是会增加磁盘消耗,因为要占用磁盘文件,同时高并发的时候频繁插入和修改索引,会导致性能损耗的。

我们给的建议,尽量创建少的索引,比如说一个表一两个索引,两三个索引,十来个,20个索引,高并发场景下还可以。

字段,status,100行,status就2个值,0和1

你觉得你建立索引还有意义吗?几乎跟全表扫描都差不多了

select * from table where status=1,相当于是把100行里的50行都扫一遍

你有个id字段,每个id都不太一样,建立个索引,这个时候其实用索引效果就很好,你比如为了定位到某个id的行,其实通过索引二分查找,可以大大减少要扫描的数据量,性能是非常好的

在创建索引的时候,要注意一个选择性的问题,select count(discount(col)) / count(*)就可以看看选择性,就是这个列的唯一值在总行数的占比,如果过低,就代表这个字段的值其实都差不多,或者很多行的这个值都类似的,那创建索引几乎没什么意义,你搜一个值定位到一大坨行,还得重新扫描。

就是要一个字段的值几乎都不太一样,此时用索引的效果才是最好的

还有一种特殊的索引叫做前缀索引,就是说,某个字段是字符串,很长,如果你要建立索引,最好就对这个字符串的前缀来创建,比如前10个字符这样子,要用前多少位的字符串创建前缀索引,就对不同长度的前缀看看选择性就好了,一般前缀长度越长选择性的值越高。

好了,各位同学,索引这块能聊到这个程度,或者掌握到这个程度,其实普通的互联网系统中,80%的活儿都可以干了,因为在互联网系统中,一般就是尽量降低SQL的复杂度,让SQL非常简单就可以了,然后搭配上非常简单的一个主键索引(聚簇索引)+ 少数几个联合索引,就可以覆盖一个表的所有SQL查询需求了。更加复杂的业务逻辑,让java代码里来实现就ok了。

大家要明白,SQL达到95%都是单表增删改查,如果你有一些join等逻辑,就放在java代码里来做。SQL越简单,后续迁移分库分表、读写分离的时候,成本越低,几乎都不用怎么改造SQL。

我这里给大家说下,互联网公司而言,用MySQL当最牛的在线即时的存储,存数据,简单的取出来;不要用MySQL来计算,不要写join、子查询、函数放MySQL里来计算,高并发场景下;计算放java内存里,通过写java代码来做;可以合理利用mysql的事务支持

03.说说事务的几个特性是什么?有哪几种隔离级别?

面试题

  • 事务的几个特点是什么?
  • 数据库事务有哪些隔离级别?
  • MySQL的默认隔离级别?

面试官心里分析

用mysql开发的三个基本面:存储引擎、索引,然后就是事务,你必须得用事务。

因为一个业务系统里,肯定要加事务保证一堆关联操作,要么一起成功要么一起失败,对不对?所以这是聊数据库必问的一个问题

最最最基本的用mysql来开发,就3点:存储引擎(了解),索引(能建索引,写的SQL都用上索引),事务(了解事务的隔离级别,基于spring的事务支持在代码里加事务)

存储引擎 -> innodb,索引,基本按照你的SQL的需求都建了索引(可能漏了部分索引忘了建),事务(@Transactional注解,对service层统一加了事务)

面试题剖析

3.1 事务的ACID

这个先说一下ACID,必须得知道:

(1)Atomic:原子性,就是一堆SQL,要么一起成功,要么都别执行,不允许某个SQL成功了,某个SQL失败了,这就是扯淡,不是原子性。

(2)Consistency:一致性,这个是针对数据一致性来说的,就是一组SQL执行之前,数据必须是准确的,执行之后,数据也必须是准确的。别搞了半天,执行完了SQL,结果SQL对应的数据修改没给你执行,那不是坑爹么。

(3)Isolation:隔离性,这个就是说多个事务在跑的时候不能互相干扰,别事务A操作个数据,弄到一半儿还没弄好呢,结果事务B来改了这个数据,导致事务A的操作出错了,那不就搞笑了。

(4)Durability:持久性,事务成功了,就必须永久对数据的修改是有效的,别过了一会儿数据自己没了,不见了,那就好玩儿了。

3.2 事务隔离级别

总之,面试问你事务,先聊一下ACID,然后聊聊隔离级别

(1)读未提交,Read Uncommitted:这个很坑爹,就是说某个事务还没提交的时候,修改的数据,就让别的事务给读到了,这就恶心了,很容易导致出错的。这个也叫做脏读。

(2)读已提交,Read Committed(不可重复读):这个比上面那个稍微好一点,但是一样比较尴尬

就是说事务A在跑的时候, 先查询了一个数据是值1,然后过了段时间,事务B把那个数据给修改了一下还提交了,此时事务A再次查询这个数据就成了值2了,这是读了人家事务提交的数据啊,所以是读已提交。

这个也叫做不可重复读,就是所谓的一个事务内对一个数据两次读,可能会读到不一样的值。如图:
在这里插入图片描述

(3)可重复读,Read Repeatable:这个比上面那个再好点儿,就是说事务A在执行过程中,对某个数据的值,无论读多少次都是值1;哪怕这个过程中事务B修改了数据的值还提交了,但是事务A读到的还是自己事务开始时这个数据的值。如图:
在这里插入图片描述

(4)幻读:不可重复读和可重复读都是针对两个事务同时对某条数据在修改,但是幻读针对的是插入

比如某个事务把所有行的某个字段都修改为了2,结果另外一个事务插入了一条数据,那个字段的值是1,然后就尴尬了。第一个事务会突然发现多出来一条数据,那个数据的字段是1。

那么幻读会带来啥问题呢?因为在此隔离级别下,例如:事务1要插入一条数据,我先查询一下有没有相同的数据,但是这时事务2添加了这条数据,这就会导致事务1插入失败,并且它就算再一次查询,也无法查询到与其插入相冲突的数据,同时自身死活都插入不了,这就不是尴尬,而是囧了。

(5)串行化:如果要解决幻读,就需要使用串行化级别的隔离级别,所有事务都串行起来,不允许多个事务并行操作。如图:
在这里插入图片描述

(6)MySQL的默认隔离级别是Read Repeatable,就是可重复读,就是说每个事务都会开启一个自己要操作的某个数据的快照,事务期间,读到的都是这个数据的快照罢了,对一个数据的多次读都是一样的。

接下来我们聊下MySQL是如何实现Read Repeatable的吧,因为一般我们都不修改这个隔离级别,但是你得清楚是怎么回事儿,MySQL是通过MVCC机制来实现的,就是多版本并发控制,multi-version concurrency control。

当我们使用innodb存储引擎,会在每行数据的最后加两个隐藏列,一个保存行的创建时间,一个保存行的删除时间,但是这儿存放的不是时间,而是事务id,事务id是mysql自己维护的自增的,全局唯一。

事务id,在mysql内部是全局唯一递增的,事务id=1,事务id=2,事务id=3
在这里插入图片描述

事务id=121的事务,查询id=1的这一行的时候,一定会找到创建事务id <= 当前事务id的那一行

select * from table where id=1,就可以查到上面那一行

事务id=122的事务,将id=1的这一行给删除了,此时就会将id=1的行的删除事务id设置成122

事务id=121的事务,再次查询id=1的那一行,能查到吗?

能查到,要求创建事务id <= 当前事务id,当前事务id < 删除事务id

事务id=121的事务,查询id=2的那一行,查到name=李四

事务id=122的事务,将id=2的那一行的name修改成name=小李四

事务id=121的事务,查询id=2的那一行,答案是:李四,创建事务id <= 当前事务id,当前事务id < 删除事务id

在一个事务内查询的时候,mysql只会查询创建时间的事务id小于等于当前事务id的行,这样可以确保这个行是在当前事务中创建,或者是之前创建的;

同时一个行的删除时间的事务id要么没有定义(就是没删除),要么是必当前事务id大(在事务开启之后才被删除);满足这两个条件的数据都会被查出来。

那么如果某个事务执行期间,别的事务更新了一条数据呢?这个很关键的一个实现,其实就是在innodb中,是插入了一行记录,然后将新插入的记录的创建时间设置为新的事务的id,同时将这条记录之前的那个版本的删除时间设置为新的事务的id。

现在get到这个点了吧?这样的话,你的这个事务其实对某行记录的查询,始终都是查找的之前的那个快照,因为之前的那个快照的创建时间小于等于自己事务id,然后删除时间的事务id比自己事务id大,所以这个事务运行期间,会一直读取到这条数据的同一个版本。

记住,聊到事务隔离级别,必须把这套东西给喷出来,尤其是mvcc,说实话,市面上相当大比重的java程序员,对mvcc是不了解的

04. 你能说说MySql数据库锁的实现原理吗?如果死锁了怎么办?

锁的原理

05.MySQL的SQL调优一般都有哪些手段?你们一般怎么做?

面试题

SQL调优的常用手段

面试官心里分析

说实话,这个其实就是针对你有没有最最基础的线上SQL跑的慢的优化能力

面试题剖析

如果是应付面试,我们实在是不可能深入讲mysql的SQL优化,以后架构班里都会深入讲解,但是这里给大家说一句,互联网公司的系统,一般很少需要复杂的SQL优化

为啥呢?因为我说过很多次了,保持SQL简单,一般90%的SQL都建议是单表查询,join等逻辑放java代码里实现,不要放SQL里。

既然是单表查询了,你觉得还能有什么性能问题么?对吧

如果某个线上SQL跑的慢,十有八九就是因为那个SQL没有用索引,所以这个时候,第一步就是去看MySQL的执行计划,看看那个SQL有没有用到索引,如果没有,那么就改写一下SQL让他用上索引,或者是额外加个索引。

我的面试突击课里就讲这种互联网公司最经典和常用的SQL优化手段,其他的大家为了面试准备,可以临时去网上搜个帖子,MySQL SQL优化,随便记住一些到时候说说即可。

我这里其实主要就是讲下怎么看SQL的执行计划,这个是码农必备能力,必须能看懂执行计划,一般其实就是看SQL有没有走索引,你倒是可以在这个环节重点说下你对执行计划这块的理解就ok

explain select * from table,就ok了

table | type | possible_keys | key | key_len | ref | rows | Extra

  • table:哪个表
  • type:这个很重要,是说类型,all(全表扫描),const(读常量,最多一条记录匹配),eq_ref(走主键,一般就最多一条记录匹配),index(扫描全部索引),range(扫描部分索引)
  • possible_keys:显示可能使用的索引
  • key:实际使用的索引
  • key_len:使用索引的长度
  • ref:联合索引的哪一列被用了
  • rows:一共扫描和返回了多少行
  • extra:using filesort(需要额外进行排序),using
  • temporary(mysql构建了临时表,比如排序的时候),using
  • where(就是对索引扫出来的数据再次根据where来过滤出了结果)

06.分库分表、读写分离和主备切换分别是如何实现的?

设计模式

单例设计模式

在这里插入图片描述

/**
 * 饿汉式单例设计模式
 * 类一加载就有对象
 */
public class Single {
    //私有化构造函数
    private Single() {

    }
    //创建私有并静态的本类对象
    private static Single s = new Single();
    //定义公有并静态的方法
    public static Single getInstance() {
        return s;
    }
}
//懒汉模式 需要的时候才会加载
public class Single2 {

    private Single2() {
        
    }
    private static Single2 s = null;

    public static Single2 getInstance() {
        if (s == null) {
            s = new Single2();
        }
        return s;
    }
}

Mybatis

01.Mybatis中的延迟加载

问题:在一对多中,当我们有一个用户,他有100个账户。
在查询用户的时候,要不要把关联的账户查出来?

在查询账户的时候,要不要把关联的用户查出来?

在查询用户时,用户下的账户信息应该是什么时候用,什么时候查询
在查询账户时,账户的所属用户信息应该是随着账户查询时一起查询	  出来的

什么是延时加载

​ 在真正使用数据是时才发起查询,不用的时候不查询。按需加载

什么是立即加载

​ 只要查询就全部查询
通常一对多的情况下都会使用延时加载

02.Mybatis中的缓存

什么是缓存? 存在于内存中的临时数据
为什么使用缓存? 减少和数据库的交互次数,提高执行效率
什么样的数据适合使用缓存? 经常查询并且不经常改变的。结果没有那么重要的。
在这里插入图片描述
一级缓存

如果数据库中的信息和一级缓存中信息不一致是如何更新信息的?

一级缓存是SqlSession范围的缓存,当调用SqlSession的修改,添加,删除,commit(),close()等方法时就会清空一级缓存。

二级缓存

它指的是Mybatis中SqlSessionFactory对象的缓存。由同一个SqlSessionFactory创建的SqlSession共享其缓存。

二级缓存的使用步骤:
第一步:让Mybatis框架支持二级缓存(在SqlMapConfig.xml中配置)
第二步:让当前的映射文件支持二级缓存(在IUserDao.xml中配置)
第三步:让当前的操作支持二级缓存(在select标签中配置)
在这里插入图片描述
在这里插入图片描述

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页