知识点汇总

Java基础

快速失败(fail-fast)和安全失败(fail-safe)

快速失败就是在迭代器迭代时,若集合被改变了,就抛异常。
原因是迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。
每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedModCount 值,是的话就返回遍历;否则抛出异常,终止遍历。

安全失败,因为遍历的时候是复制集合再遍历,所以没事。

HashMap

   深入版参考    简易版参考

有什么办法能解决HashMap线程不安全的问题呢?

Java 中有 HashTable、Collections.synchronizedMap、以及 ConcurrentHashMap 可以实现线程安全的 Map。

HashTable 是直接在操作方法上加 synchronized 关键字,锁住整个table数组,粒度比较大;
Collections.synchronizedMap 是使用 Collections 集合工具的内部类,通过传入 Map 封装出一个 SynchronizedMap 对象,内部定义了一个对象锁,方法内通过对象锁实现;
ConcurrentHashMap 在jdk1.7中使用分段锁,在jdk1.8中使用CAS+synchronized。

put过程

在这里插入图片描述

扰动函数

在计算key值得hashcode时,hashMap自己实现了哈希算法

	static final int hash(Object key) {
        int h;
        // key的hashCode和key的hashCode右移16位做异或运算
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

本身的key实现的hashcode的值 h,再与 h>>>16 做异或,为的是将高16位的信息也融入进来,减少哈希碰撞。
在这里插入图片描述
HashMap容量长度为何都是2次幂

indexFor
在得到 hashcode 之后就是调用 indexFor() 的时候了(根据hashcode得到要插入的位置)。

	bucketIndex = indexFor(hash, table.length);

	static int indexFor(int h, int length) {
     return h & (length-1);
	}

首先,在length为2次幂时,h & (length - 1) 与 h%length 的结果是相同的,但位运算比取模要快,所以保证 length 为2次幂就可以使用位运算取代取模。

HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。

若在构造函数中,传入奇数作为数组长度会怎样

HashMap 会向上找离此奇数最近的2的整数幂,作为数组长度。

如何做到的

熟悉的代码

static final int tableSizeFor(int cap) {
 int n = cap - 1;
 n |= n >>> 1;
 n |= n >>> 2;
 n |= n >>> 4;
 n |= n >>> 8;
 n |= n >>> 16;
 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }

现将传进来的值减一,奇数得到1xxxxx,偶数得到111111,以奇数为例。
先右移一位,在与移动之前的n |(有一得一,双零才得零) ,其实只要看最开头的一位 1 就行,移动之后开头的下一位也变成了 1 ,在 | 上之前的 1 相当于保存了上一个 1 ,现在有两个 1 了,之后移动两位,| 完之后得到4个 1 。以此类推得到你所能得到的最多的11111111,结果加上1,也就是离此奇数最近的2的整数幂。偶数则中间操作无意义,直接加上1。

扩容再插入

取得插入位置时,是利用 h & (length - 1) 得到的,若扩容了,也就是容量变为之前的两倍,那么相当于length - 1 增加了一位,而此Node所处的位置就取决于,与新增加的一位对应的自身位置上的值,若为0是放在原位置,若为1,则是放在原位置加原容量。

在这里插入图片描述

1.8做了哪些优化
在这里插入图片描述

ConcurrentHashMap

1.7
1.7中ConcurrentHashMap大致组成是,Segment数组结构和HashEntry数组组成。

1.7 put流程:
在这里插入图片描述
1.8 put流程:
在这里插入图片描述

HashMap与HashTable的区别

1.底层数据结构不同:jdk1.7底层都是数组+链表,但jdk1.8 HashMap加入了红黑树

2.Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null。
添加key-value的hash值算法不同:HashMap添加元素时,是使用自定义的哈希算法,而HashTable是直接采用key的hashCode()

3.实现方式不同:Hashtable 继承的是 Dictionary类,而 HashMap 继承的是 AbstractMap 类。都实现了Map接口。

4.初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。

5.扩容机制不同:当已用容量>总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 +1。

6.支持的遍历种类不同:HashMap只支持Iterator遍历,而HashTable支持Iterator和Enumeration两种方式遍历

7.迭代器不同:HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。而Hashtable 则不会。

8.是否包含contains方法。 hashMap把contains方法去掉了, 改成了 containsKey() 和 containsValue()方法。 hashTable有 contains() 、containsKey()、containsValue()方法 , 其中 contains()和containsValue()功能相同。HashMap没有重写toString方法,HashTable重写了。

9.同步性不同: Hashtable是同步(synchronized)的,适用于多线程环境,
而hashmap不是同步的,适用于单线程环境。多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。

Set

Set集合是无序、不可重复的。
不可重复是指,不存在重复的值,插入时会比较hashCode,相同再使用 equals 比较值。
这里所说的无序是指,元素无插入的顺序。HashSet则是按照元素的HashCode进行排序的,其他的实现类则有自己实现的排序方式。

String、StringBuilder、StringBuffer

String不能被继承和修改
不能被继承是因为此类被final修饰了,不能被修改是因为String底层是被final修饰了的char[]数组。
String为什么是线程不安全的
因为底层使用了StringBuilder。

Cookies、Session、Token

Cookies与Session

基于HTTP是一个无状态的协议,所以服务端每次从客户端接收到的请求都是新的请求,服务端并不知道客户端的历史请求记录。Session与Cookies就是为了弥补 HTTP 这一无状态的特性。

Session是什么

当客户端向服务端发送请求,服务器便为这次请求在内存中开辟的一块空间,数据结构为ConcurrentHashMap,这个对象就是session对象。服务端可以利用session记录客户端在一个会话内的操作记录。

Session如何判断是否同一会话
服务器第一次接收到请求时,开辟了一块 Session 空间(创建了Session对象),同时生成一个 sessionId ,并通过响应头的 **Set-Cookie:JSESSIONID=XXXXXXX **命令,向客户端发送要求设置 Cookie 的响应; 客户端收到响应后,在本机客户端设置了一个 **JSESSIONID=XXXXXXX **的 Cookie 信息,该 Cookie 的过期时间为浏览器会话结束;
在这里插入图片描述
接下来客户端每次向同一个网站发送请求时,请求头都会带上该 Cookie信息(包含 sessionId ), 然后,服务器通过读取请求头中的 Cookie 信息,获取名称为 JSESSIONID 的值,得到此次请求的 sessionId。

Session 的缺点

Session 机制有个缺点,比如 A 服务器存储了 Session,就是做了负载均衡后,假如一段时间内 A 的访问量激增,会转发到 B 进行访问,但是 B 服务器并没有存储 A 的 Session,会导致 Session 的失效。

Cookies是什么
在这里插入图片描述

它是服务器发送到 Web 浏览器的一小块数据。服务器发送到浏览器的 Cookie,浏览器会进行存储,并与下一个请求一起发送到服务器。通常,它用于判断两个请求是否来自于同一个浏览器,例如用户保持登录状态。

创建Cookies

当接收到客户端发出的 HTTP 请求时,服务器可以发送带有响应的 Set-Cookie 标头,Cookie 通常由浏览器存储,然后将 Cookie 与 HTTP 标头一同向服务器发出请求。

JWT(Json Web Token)

Token意思是“令牌”,用户身份的验证方式,有点类似于Cookie,相对来说更安全。
比如你授权(登录)一个程序时,他就是个依据,判断你是否已经授权该软件;Cookie就是写在客户端的一个txt文件,里面包括你登录信息之类的,这样你下次在登录某个网站,就会自动调用Cookie自动登录用户名;Session和Cookie差不多,只是Session是写在服务器端的文件,也需要在客户端写入Cookie文件,但是文件里是你的浏览器编号。Session的状态是存储在服务器端,客户端只有session id;而Token的状态是存储在客户端。

使用 JWT 主要用来下面两点

认证(Authorization):这是使用 JWT 最常见的一种情况,一旦用户登录,后面每个请求都会包含 JWT,从而允许用户访问该令牌所允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小。

信息交换(Information Exchange):JWT 是能够安全传输信息的一种方式。通过使用公钥/私钥对 JWT 进行签名认证。此外,由于签名是使用 head 和 payload 计算的,因此你还可以验证内容是否遭到篡改。

Token的组成

由三部分组成:
Header:它通常由两部分组成:令牌的类型(即 JWT)和使用的 签名算法。
Payload:主要由三种声明组成:注册声明、公共声明、私有声明。包含了时间信息,以及可选的签发者信息、接受者信息。
Sign:签名,将Header+PayLoad加密而成。

三者的不同

cookies存储在客户端,每次发送请求时带上数据。
session储存在服务端,利用cookies让将sessionid设置在请求头中,带给服务器,服务器用此sessionid校验与本地存储的是否一致。
token,最大的不同就是token有进行加密,更加安全。存在前端。

final修饰符

final可以修饰类、方法、属性
修饰类,类不能被继承:String。
修饰方法,方法不能被重写。
修饰属性,属性不能被修改。

IO

Socket
Socket(套接字)放在操作系统内核中,套接字内部有两个重要的缓冲结构,读缓冲(read buffer)以及 写缓冲(write buffer),他们都是有限大小的数组结构。

同步、异步、阻塞、非阻塞

异步与同步描述事物的行为,阻塞非阻塞描述事物的状态。

阻塞IO 和 非阻塞IO
这两个概念是程序(进程线程)级别的。主要描述的是程序请求操作系统IO操作后,如果IO资源没有准备好,那么程序该如何处理的问题: 前者等待;后者继续执行(并且使用线程一直轮询,直到有IO资源准备好了)

同步IO 和 非同步IO
这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求IO操作后,如果IO资源没有准备好,该如何响应程序的问题: 前者不响应,直到IO资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给程序。

同步

一个任务的执行需要另一个任务的某种方法的执行。只有另一方法返回了,才能执行。

异步

就是步调各异。既然是各异,那就是都不相同。所以结果就是:多个事物可以你进行你的、我进行我的,谁都不用管谁,所有的事物都在同时进行中。
注:一定要去体会“多个事物”,多个线程是多个事物,多个方法是多个事物,多个语句是多个事物,多个CPU指令是多个事物。

阻塞

阻塞就是发起一个请求,调用之一直等待请求结果的返回,返回之前不能干其他事情。

非阻塞

非阻塞则是,不用等待请求结果的返回。

BIO

BIO

BIO 俗称同步阻塞 IO,一种非常传统的 IO 模型。同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到在内核把数据拷贝到用户空间。
有一个ACCEPT线程专门监听请求,有请求进来就创建一个线程用来处理请求。
简单来说,在服务器中BIO是一个连接由一个专门的线程来服务的工作模式。就像餐厅里来一个客人就给这个客人安排一个专用服务员,这个服务员就只服务这一个客人直到他离开为止。

在这里插入图片描述

存在的问题

1.只要没有客户端连接上服务器,accept方法就一直不能返回,这就是阻塞;对应的读写操作道理也一样,想要读取数据,必须等到有数据到达才能返回,这就是阻塞。
2.服务端对于每个请求都要建立一个独立的线程。
3.并发数大时,要创建大量的线程,系统资源占用大。
4.建立连接后,如果当前线程没有数据可读,则线程就阻塞在read上,造成线程资源的浪费。

NIO

一种同步非阻塞的I/O模型,由Channel、Buffer、Selector组成。
数据都是从缓冲区来到通道后获取,或者从通道去到缓冲区,是面向缓冲的。
流只可以读或者写,而channel可读可写,channel分为几种:FileChannel(文件通道)、SocketCannel、ServerSocketCannel(基于TCP套接字通道)、DatagramCannel(基于UDP套接字通道)。
现在已有Channel注册事件进入selector里了,执行selector.select(),会一直堵塞,当有事件被触发时程序往下走。使用selectedKeys()返回selectedKey集合,每一个key都是对应的通道与选择器的关联关系,可以用此key返回对应的channel或者

Buffer(缓冲区)介绍

  1. **容量(Capacity):**作为一个内存块,Buffer具有一定的固定大小,也称为“容量”,缓冲区容量不能为负,并且创建后不能更改。

  2. **限制(limit):**表示缓冲区中可以操作数据的大小(limit后数据不能进行读写)。缓冲区的限制不能为负,并且不能大于其容量。写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量。

  3. **位置(position):**下一个要读取或者写入数据的索引。缓冲区的位置不能为负,并且不能大于其限制

  4. **标记(Mark)与重置(reset):**标记就是一个索引,通过Buffer中的mark()方法指定Buffer中的一个特定的position,之后可以通过调用reset()方法恢复到这个position。

  5. 标记、位置、限制、容量遵循以下不变式: 0<=mark <= position <= limit <= capacity

Selector(选择器)介绍

Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。
使用Selector的好处在于: 使用更少的 线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销。

拷贝

拷贝主要分两种:引用拷贝以及对象拷贝

1、引用拷贝

创建一个指向对象的引用变量的拷贝。

结果分析:由输出结果可以看出,它们的地址值是相同的,那么它们肯定是同一个对象。teacher和otherTeacher的只是引用而已,他们都指向了一个相同的对象Teacher(“riemann”,28)。 这就叫做引用拷贝。
在这里插入图片描述
2、对象拷贝
要进行拷贝的对象都需要实现 Cloneable接口,实现clone方法

**2.1 浅拷贝:**浅拷贝会创建一个新对象,这个对象有着与原始对象属性值的一份精准拷贝。若属性是基本类型,拷贝的就是基本类型的值;若属性是内存地址(引用类型),拷贝的就是内存地址。

public Object clone() throws CloneNotSupportedException {
        Object object = super.clone();
        return object;
    }

在这里插入图片描述

**2.2 深拷贝:**深拷贝会把拷贝对象中的引用对象也拷贝一份。引用的对象也需要实现Cloneable接口。

class Teacher implements Cloneable {
    private String name;
    private int age;
	...
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public Object clone() throws CloneNotSupportedException {
        // 浅复制时:
        // Object object = super.clone();
        // return object;

        // 改为深复制:
        Student student = (Student) super.clone();
        // 本来是浅复制,现在将Teacher对象复制一份并重新set进来
        student.setTeacher((Teacher) student.getTeacher().clone());
        return student;

    }

在这里插入图片描述

Stream流

用于对集合进行一系列操作。
比如:

list.stream().distinct() 去重操作。
.sorted(Compare.comparing(list中的实体类)::getxxx).collect(Collector.toList()) 以xxx进行升序排序(默认,加.reserve()为降序)后以List形式返回。
list.stream().collect(Collectors.groupingBy(实体类 :: getxxx)) 以xxx为标识进行分组,然后返回Map<xxx,list<实体类>>,如:Map<String, List> collect。

过滤:
->之后的式子为真时,当前这个元素s留下来。

collect = warningNameVOS.stream().filter(s -> !warningNos.contains(s.getWarningNo())).collect(Collectors.toList());

多线程

ThreadLocal

ThreadLocal的作用?用在什么地方?

ThreadLocal可以将线程作为key取出专属于本线程的value。ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。
经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,
不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。

ThreadLocalMap

wait() 与 sleep()

1. wait(): wait()方法执行后会让当前线程暂时退让出同步锁,让其他在等待该资源的线程继续往下执行,只有调用了notify()方法,之前调用wait()方法的线程才会解除wait状态,可以去参与竞争同步锁,进而得到执行(注意:notify的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度)。

2. sleep(): 执行sleep()方法的线程会主动让出CPU,CPU就可以去执行其他任务,在sleep了指定时间之后,CPU再回到此线程继续执行(注意:sleep方法只是让出了CPU,而并不会让出同步资源锁!!)

3. 两者的不同:

  1. sleep()方法可以再任何地方使用,wait()方法则只能在同步方法或者同步块中使用。
  2. sleep()是线程类(Thread)的方法,调用会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自然恢复;wait()方法是Object的方法,调用会放弃对象锁,进入等待队列,待调用 notify() / notifyAll() 唤醒指定的线程或者所有线程。.

原子操作类 AtomicInteger

用AtomicInteger可以保证在多线程环境下的数据安全。底层使用了CAS操作,例如incrementAndGet() 方法

	public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

incrementAndGet()方法在一个无限循环体内,不断尝试将一个比当前值大1的新值赋给自己,如果失败则说明在执行"获取-设置"操作的时已经被其它线程修改过了,于是便再次进入循环下一次操作,直到成功为止。这个便是AtomicInteger原子性的"诀窍"了,继续进源码看它的compareAndSet方法:

/**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return true if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

可以看到,compareAndSet()调用的就是Unsafe.compareAndSwapInt()方法,即Unsafe类的CAS操作。

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
  1. var1:要修改的对象起始地址 如:0x00000111
  2. var2:需要修改的具体内存地址 如100 。0x0000011+100 = 0x0000111就是要修改的值的地址
  3. 注意没有var3
  4. var4:期望内存中的值,拿这个值和0x0000111内存中的中值比较,如果为true,则修改,返回ture,否则返回false,等待下次修改。
  5. var5:如果上一步比较为ture,则把var5更新到0x0000111其实的内存中。
    原子操作,直接操作内存。

线程池

为何使用线程池

线程创建与销毁的开销是巨大的,使用线程池则可以实现线程使用完之后放回线程池,免去了创建销毁的时间,实现了线程的复用。

创建线程池的核心参数

1.corePoolSize:核心线程最大数
2.maximumPoolSize:最大线程数
3.keepLiveTime:非核心线程数的最大空闲存活时间
(核心线程是会一直存在的,非核心线程才有存活时间)
4.unit:时间单位
5.TreadFactory:线程工厂
6.workQueue:工作队列
7.Handler:拒绝策略

四种拒绝策略

1.直接抛出异常(默认)
2.直接丢弃任务,但是不抛出异常。
3.将等待队列中等待时间最长的任务丢弃,之后重新提交新任务。
4.使用提交任务的主线程执行任务。

四种常见线程池

CachedThreadPool: 可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。

SecudleThreadPool: 周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。

SingleThreadPool: 只有一条线程来执行任务,适用于有顺序的任务的应用场景。

FixedThreadPool: 定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程

线程池创建线程流程
在这里插入图片描述

乐观锁与悲观锁

悲观锁
比较悲观,认为每一次都会发生并发问题,所以在每一次操作都加上锁。

乐观锁
比较乐观,每次都不认为会发生问题,在操作完准备提交时会检查,是否在自己读取之后,有事务进行了更新。一般有两种方式实现:

  1. 版本号 2. 时间戳,都是读取数据时将它一并读出来,更新数据同时将版本号更新。提交更新时用之前的版本号与现在数据库对应的版本号进行对比,相同提交,不同则认为数据已经过期了。

CAS
CAS(Compare And Swap 比较与交换)是一项乐观锁的技术,CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。 ”这其实和乐观锁的冲突检查+数据更新的原理是一样的。

存在的问题

ABA问题

volatile关键字

参考
Java内存模型JMM(Java Memory Model )

首先,需要了解JMM。它是一种Java虚拟机所定义的抽象规范,用来屏蔽不同操作系统、不同硬件的内存访问差异。让Java程序在各种平台上的内存访问效果一致。每个线程都有自己的工作内存,工作内存中放着主内存中的共享变量的副本,线程操作变量必须在工作内存,主内存作为各个工作内存的中转站。

存在的问题
所以会导致工作内存中的变量的改变不能及时地更新到主内存中,其他线程也不能及时地获取到最新的变量。也就是可见性问题。

解决方案

  1. 可以使用synchronized关键字进行加锁
    加锁之后为什么就可以保证可见性了?因为当一个线程进入synchronized代码块之后,会先清空自己的工作内存,然后从主内存中拷贝共享变量的最新值到本地内存中作为副本,执行代码,又将修改后的副本刷新到主内存中,最后线程释放锁。除了synchronized 外,其它锁也能保证变量的内存可见性。
  2. 使用volatile关键字
    使用volatile关键字可以保证属性的可见性,在线程将共享变量修改并写回主内存之后,会通过 CPU 总线嗅探机制告知其他线程该变量副本已经失效,需要重新从主内存中读取。
    总线嗅探机制工作原理:每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。
    volatile不能保证原子性:在多线程环境下,volatile关键字可以保证共享数据的可见性,但是不能保证数据操作的原子性,也就是说,在多线程环境下,使用volatile修饰的变量是线程不安全的。
    volatile可以净禁止指令重排序:即保证了被volatile修饰过的变量编译后的顺序与程序执行的顺序一致。

分布式、微服务

应用架构的演变

在这里插入图片描述
单体架构:
优点:开发简单,直接将整体打成一个jar/war包部署到服务器上即可。
缺点:显而易见,不管多小的改动,都得重新打包,不容易维护,代码耦合。

垂直应用架构:
将单体分成若干个子项目,独立运行在容器当中,若单一的子项目的访问众多,只需要将此项目进行横向的拓展,形成集群即可。

优点:

  1. 可以解决高并发问题
  2. 可以针对不同的模块进行优化
  3. 方便水平拓展
  4. 一定的容错,其中一个子模块出现问题,其他模块可以进行访问,不像单体架构一点小问题就全盘崩溃。

缺点:系统间相互独立,没有联系,过多的重复性开发工作,代码冗余。

分布式架构:
将整体分为展示层以及服务层,展示层包括不同的系统,服务层则是可以给展示层提供服务的子模块。服务层的模块是可复用的,一个用户服务可以用在很多不同的系统(解决了垂直应用架构中代码冗余的问题)。

CAP

C(一致性):在同一时间点,让所有节点的数据都相同。一致性的问题在并发系统中不可避免,对于客户端来说,一致性指的是并发访问时更新过的数据如何获取的问题。从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。
A(可用性):即服务一直可用,而且是正常响应时间。好的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。
P(分区容错性):即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性或可用性的服务。
分区容错性要求能够使应用虽然是一个分布式系统,而看上去却好像是在一个可以运转正常的整体。比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,对于用户而言并没有什么体验上的影响。

幂等性

幂等性是指,不论多少次请求,返回的结果都相同。
实现幂等性的操作

幂等操作实现方式有:
1、操作之前在业务方法进行判断如果执行过了就不再执行。
2、缓存所有请求和处理的结果,已经处理的请求则直接返回结果。
3、在数据库表中加一个状态字段(未处理,已处理),数据操作时判断未处理时再处理。

分布式事务

在分布式系统中一次操作由多个系统协同完成,这种一次事务操作由多个系统通过网络协同完成的成为分布式事务

MySQL

binlog redolog undolog

事务

同时开启多个事务会发生为什么问题

1.丢失修改
2.脏读:读到还未提交的内容
3.不可重复读:使用同一语句,查出来不同的数据
4.幻读:使用同一语句,查出来的内容多了或者少了
5.第一类丢失更新:A在更新期间,B更新完了,但A撤销了事务,数据回滚,B更新的丢失。
6.第二类丢失更新:A更新期间,B更新完了,A提交事务,把B提交的覆盖了,B更新丢失。

参考

隔离级别

1.读提交:一个事务提交之后,做出的改变才能被看到。
2.可重复读:在事务执行过程中读到的数据,总是和事务启动时读到的数据一致。在这个级别下的事务未提交,做出的更改也不会被其他事务看见。
3.串行读:对同一行记录进行读写会分别加读写锁,当发生读写锁冲突时,后面的事务必须等之前的事务完成才能继续执行。

索引

1.简介

索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。
举例说明索引:如果把数据库中的某一张看成一本书,那么索引就像是书的目录,可以通过目录快速查找书中指定内容的位置,对于数据库表来说,可以通过索引快速查找表中的数据。

2.索引的本质

MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。提取句子主干,就可以得到索引的本质:索引是数据结构。

     我们知道,数据库查询是数据库的最主要功能之一。我们都希望查询数据的速度能尽可能的快,因此数据库系统的设计者会从查询算法的角度进行优化。最基本的查询算法当然是顺序查找(linear search),这种复杂度为O(n)的算法在数据量很大时显然是糟糕的,好在计算机科学的发展提供了很多更优秀的查找算法,例如二分查找(binary search)、二叉树查找(binary tree search)等。如果稍微分析一下会发现,每种查找算法都只能应用于特定的数据结构之上,例如二分查找要求被检索数据有序,而二叉树查找只能应用于二叉查找树上,但是数据本身的组织结构不可能完全满足各种数据结构(例如,理论上不可能同时将两列都按顺序进行组织),所以,在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引

3.索引的原理

索引一般以文件形式存在磁盘中(也可以存于内存中),存储的索引的原理大致概括为以空间换时间,数据库在未添加索引的时候进行查询默认的是进行全量搜索,也就是进行全局扫描,有多少条数据就要进行多少次查询,然后找到相匹配的数据就把他放到结果集中,直到全表扫描完。而建立索引之后,会将建立索引的KEY值放在一个n叉树上(BTree)。因为B树的特点就是适合在磁盘等直接存储设备上组织动态查找表,每次以索引进行条件查询时,会去树上根据key值直接进行搜索。

聚簇索引与非聚簇索引

MyISAM与InnoDB,都采用B+Tree作为索引结构
在MyISAM引擎中,B+Tree叶子结点data域储存的是数据记录的地址,取出地址后再根据地址取出数据记录,这被称为“非聚簇索引”。

在InnoDB引擎中,数据文件本身就是一个索引文件,主键作为B+Tree的key,树的叶子结点的data域是完整的数据记录,这被称为“聚簇索引”。

其余索引都作为辅助索引,data域中保存着主键值(非聚簇索引)而不是地址(和MyISAM不同)。根据辅助索引取数据时,先取出主键值,再走一遍主索引。

InnoDB的普通索引叶子节点存储的是主键(聚簇索引)的值,而MyISAM的普通索引存储的是记录指针。

因此设计表时,不建议使用过长的字段作为主键,因为每个B+Tree的节点大小固定的,字段太大,一个节点内所能放的节点就会变少了,树的高度也就会变高,查询磁盘的次数就会增加。非单调的字段也不行,插入时为了维护树的特性会导致频繁分裂。
//建立索引的字段值,查询到的行数,不能小于全表条数的百分之20,小于百分之20,会被优化成全表搜索,
所以不建议在值比较少的情况下建立索引。

覆盖索引

指查询条件包括索引,查询结果也是索引的一部分。覆盖索引就是从索引中直接获取查询结果,要使用覆盖索引需要注意select查询列中包含在索引列中;where条件包含索引列或者复合索引的前导列;

索引失效

1.or前后不是都为索引
2.like语句 以%开头
3.复合索引,没有遵循最左匹配原则(左、左右都行,单独一个右不行)
4.使用数学运算
添加链接描述

B+树

磁盘IO次数与节点的关系:
参考
系统从磁盘读取数据到内存是以磁盘块(block)为单位的,位于同一个磁盘块中的数据会被一并取出,而不是需要什么取什么。
InnoDB存储引擎中有页(page)的概念,页是其磁盘管理的最小单位。InnoDB存储引擎中默认每个页的大小是16KB,可以通过参数innodb_page_size将页的大小设置为4K、8K、16K。
在MySQL中可以通过如下命令查看页的大小

mysql> show variables like ‘innodb_page_size’;

而系统一个磁盘块的存储空间往往没有那么大,因此InnoDB每次申请磁盘空间时都会是若干个地址连续磁盘块(成为大磁盘块即页)来达到页的大小16KB。
InnoDB在把磁盘数据读取到内存时会以页为基本单位。

每次读取数据都会读取一个页大小的数据进入内存,然后根据SQL语句进行筛选。

B树与B+树的区别
因为B树的非叶子节点除了键值(表中的主键)还包含了数据,所以在一个页大小的节点中,能够存放的key就相对少了,那么要找到数据就需要更多次的磁盘IO,而B+树的非叶子节点只包含键值,所以一次读进内存的一页就包含更多的键值。B+树整体的高度就更矮,所以IO次数就更少。

sql优化

  1. 使用show [session | global] status 命令可以提供服务器状态信息,可以查看session(当前连接)或者global(自数据库上次启动至今) 的统计结果在这里插入图片描述
    在这里插入图片描述
  2. 定位执行效率较低的SQL语句

可以通过以下两种方式定位执行效率较低的SQL语句。 通过慢查询日志定位那些执行效率较低的SQL语句,用-log-slow-queries[=file_name]选 项启动时,mysqld写一个包含所有执行时间超过long_query_time秒的SQL语句的日志 文件。
慢查询日志在查询结束以后才纪录,所以在应用反映执行效率出现问题的时候查询慢 询日志并不能定位问题,可以使用show processlist命令查看当前MySQL在进行的线程, 包括线程的状态、是否锁表等,可以实时地查看SQL的执行情况,同时对一些锁表操 作进行优化。

  1. 通过EXPLAIN分析低效的SQL执行计划
    在这里插入图片描述
    在这里插入图片描述
    每个列的简单解释如下:

select_type:表示 SELECT 的类型,常见的取值有 SIMPLE(简单表,即不使用表连接
或者子查询)、PRIMARY(主查询,即外层的查询)、UNION(UNION 中的第二个或
者后面的查询语句)、SUBQUERY(子查询中的第一个 SELECT)等。

table:输出结果集的表。

type:表示表的连接类型,性能由好到差的连接类型为 system(表中仅有一行,即
常量表)、const(单表中最多有一个匹配行,例如 primary key 或者 unique index)、 eq_ref(对于前面的每一行,在此表中只查询一条记录,简单来说,就是多表连接 中使用primarykey或者uniqueindex)、re(f 与eq_ref类似,区别在于不是使用primary key 或者 unique index,而是使用普通的索引)、ref_or_null(与 ref 类似,区别在于 条件中包含对 NULL 的查询)、index_merge(索引合并优化)、unique_subquery(in 的后面是一个查询主键字段的子查询)、index_subquery(与 unique_subquery 类似, 区别在于 in 的后面是查询非唯一索引字段的子查询)、range(单表中的范围查询)、 index(对于前面的每一行,都通过查询索引来得到数据)、all(对于前面的每一行,都通过全表扫描来得到数据)。

possible_keys:表示查询时,可能使用的索引。

key:表示实际使用的索引。

key_len:索引字段的长度。

rows:扫描行的数量。

Extra:执行情况的说明和描述。
在这里插入图片描述
在这里插入图片描述
t5

sql 基础

联合查询
使用关键字 UNION 将结果集 列 在一起,所以需要查询出来的结果要拥有相同的列数,以及列要是相同的类型,查出来的结果集的列名是第一个查询出结果的列名。
UNION默认=UNION DISTINCT,结果去重,UNION ALL 不去重。

左连接
左连接
会显示左边表的全部以及右边表的符合条件的项。
添加链接描述

修改字段类型

ALTER TABLE table_name MODIFY COLUMN column_name int(4) ZEROFILL 

创建表

CREATE TABLE table_name(
	//这里int后面跟的4,代表的是
	id int(4)
)

Redis

Redis的五大数据类型

list(集合)、set(无序集合)、hash(哈希)、string(字符串)、zset(有序集合)

list:在Redis3.2版本之前使用的是 ziplist 加 linkedlist
满足下列两个条件时,使用ziplist。不满足其中一个就会进行转型,转为linkedlist。

 列表对象保存的所有字符串元素的长度都小于64字节
 列表对象保存的元素数量小于512个

Redis3.2之后,引入了新的结构,quicklist:是由ziplist与linkedlist结合在一起的,linkedlist双向链表,每一个节点的结构为ziplist

ziplist的结构

由表头和N个entry节点和压缩列表尾部标识符zlend组成的一个连续的内存块。然后通过一系列的编码规则,提高内存的利用率,主要用于存储整数和比较短的字符串。可以看出在插入和删除元素的时候,都需要对内存进行一次扩展或缩减,还要进行部分数据的移动操作,这样会造成更新效率低下的情况。

这篇文章对ziplist的结构讲的还是比较详细的:

https://blog.csdn.net/yellowriver007/article/details/79021049

linkedlist的结构

意思为一个双向链表,和普通的链表定义相同,每个entry包含向前向后的指针,当插入或删除元素的时候,只需要对此元素前后指针操作即可。所以插入和删除效率很高。但查询的效率却是O(n)[n为元素的个数]。

了解了上面的这两种数据结构,我们再来看看上面说的“ziplist组成的双向链表”是什么意思?实际上,它整体宏观上就是一个链表结构,只不过每个节点都是以压缩列表ziplist的结构保存着数据,而每个ziplist又可以包含多个entry。也可以说一个quicklist节点保存的是一片数据,而不是一个数据。总结:

整体上quicklist就是一个双向链表结构,和普通的链表操作一样,插入删除效率很高,但查询的效率却是O(n)。不过,这样的链表访问两端的元素的时间复杂度却是O(1)。所以,对list的操作多数都是poll和push。
每个quicklist节点就是一个ziplist,具备压缩列表的特性。

Redis的持久化机制

RDB
RDB持久化是通过创建快照的方式进行持久化的,保存某个时间点的全量数据。RDB持久化是Redis默认的持久方式,RDB持久化触发包括手动触发和自动触发两种方式。
手动触发可以通过 save 或者 flushall进行触发,在目录下会多出rdb文件。也可以通过bgsave进行触发,与save不同的是,Redis服务一般不会阻塞。Redis进程会执行fork操作创建子进程,RDB持久化由子进程负责,不会阻塞Redis服务进程。Redis服务的阻塞只发生在fork阶段,一般情况时间很短。

bgsave执行流程:
1.执行bgsave命令,Redis进程先判断当前是否存在正在执行的RDB或AOF子线程,如果存在就是直接结束。
2.Redis进程执行fork操作创建子线程,在fork操作的过程中Redis进程会被阻塞。
3.Redis进程fork完成后,bgsave命令就结束了,自此Redis进程不会被阻塞,可以响应其他命令。
4.子进程根据Redis进程的内存生成快照文件,并替换原有的RDB文件。
5.子进程通过信号量通知Redis进程已完成。

自动触发则是根据配置中的来,多少秒内发生了key值得变化,就会触发,默认有900/1 , 300/10 , 60/10000。采用的也是bgsave后台保存的形式。

AOF
AOF持久化的方式是记录所有变更数据库状态的指令,当需要恢复数据时重新执行AOF文件中的命令就可以了。AOF解决了数据持久化的实时性,也是目前主流的Redis持久化方式。

AOF持久化流程
在这里插入图片描述

  1. 命令追加(append):所有的命令会被追加到AOF缓存区(aof_buf)中。
  2. 文件同步(sync):根据不同的策略,将AOF缓存同步到AOF文件当中。
  3. 文件重写(rewrite):定期对AOF文件进行重写,以达到压缩的目的。
  4. 数据加载(load):当需要恢复数据的时候,重新执行AOF文件中的命令。

文件同步策略
AOF持久化流程中的文件同步有以下几个策略:

  1. always :每次写入缓存区都进行同步,硬盘操作进行较慢,限制了Redis的高并发,不建议使用。
  2. no:每次写入缓存区都不进行同步,进行同步的时间交由给操作系统负责,缓存区同步到AOF文件的周期不可控,而且也增加了每次同步的硬盘的数据量。
  3. eversec:每次写入缓存区之后,由专门的线程每秒同步一次,做到了兼顾性能和数据安全。是建议的同步策略,也是默认的策略。

触发文件的重写
AOF持久化流程中的文件重写可以被手动触发,也可以自动触发。
1.手动触发:使用 bgrewriteaof 命令。
2.自动触发:根据
auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage
配置确定自动触发的时机。auto-aof-rewrite-min-size表示运行AOF重写时文件大小的最小值,默认为64MB;auto-aof-rewrite-percentage表示当前AOF文件大小和上一次重写后AOF文件大小的比值的最小值,默认为100。只用前两者同时超过时才会自动触发文件重写。

AOF持久化配置
对AOF持久化的具体流程有了了解后,我们来看一下如何配置AOF。AOF持久化默认是不开启的,需要修改配置文件,如:

# appendonly改为yes,开启AOF
appendonly yes
# AOF文件的名字
appendfilename "appendonly.aof"
# AOF文件的写入方式
# everysec 每个一秒将缓存区内容写入文件 默认开启的写入方式
appendfsync everysec
# 运行AOF重写时AOF文件大小的增长率的最小值
auto-aof-rewrite-percentage 100
# 运行AOF重写时文件大小的最小值
auto-aof-rewrite-min-size 64mb

Mybatis

动态sql

动态sql标签有:if ; foreach ; where ; sql ; choose,when,otherwise ; set
前四个比较常用。关于前四个 前四个详细链接
if 和 where 的用法:

<!--where标签-->
    <select id="selectByWhere" resultType="com.itjuzi.entity.Student">
        select * from student
        <where>
            <if test="name != null and name!='' ">
                or name like "%" #{name} "%"
            </if>
            <if test="age > 0">
                or age &lt;= #{age}
            </if>
        </where>
    </select>

foreach用法:
foreach标签中有open以及close属性,一般都分别赋予 ‘(’ 与 ‘)’,赋值给这两个属性的话,一般是在批量查询之中,将多个属性作为查询条件。不加的话,则一般是插入多个(同类型的,因为是在一个集合内)值。详细查看这儿

<foreach collection="集合类型" open="开始的字符" close="结束的字符"
    	 item="集合中的成员" separator="集合成员之间的分割符">
    #{item的值}
</foreach>
标签属性:
collection:表示循环的对象是数组还是list集合。如果dao方法的形参是数组,collection="array";
			如果dao方法形参是list,collection="list";
open:循环开始的字符。sql.append("(");
close:循环结束的字符。sql.append(")");
item:集合成员,自定义的变量。Integer item = idList.get(i);
separator:集合成员之间的分隔符。sql.append(",");
#{item的值}:获取集合成员的值;


<!--foreach第一种方式,循环简单类型的List-->
    <select id="selectForeachOne" resultType="com.itjuzi.entity.Student">
        select * from student
        <if test="list != null and list.size>0">
            where id in
            <foreach collection="list" open="(" close=")" separator="," item="myId">
                #{myId}
            </foreach>
        </if>
    </select>


<!--foreach第二种方式,循环对象List<Student>-->
    <select id="selectForeachTwo" resultType="com.itjuzi.entity.Student">
        select * from student
        <if test="list != null and list.size>0">
            where id in
            <foreach collection="list" open="(" close=")" separator="," item="student">
                #{student.id}
            </foreach>
        </if>
    </select>

sql用法:

1)在mapper文件中定义sql代码片段。<sql id="唯一字符串">部分sql语句</sql>
2)在其它位置,使用include标签引用某个代码片段。

<!--定义代码p片段-->
    <sql id="studentSelect">
        select * from student
    </sql>
    
 <select id="selectByIf" resultType="com.itjuzi.entity.Student">
 
        <include refid="studentSelect"/>
        
        where id=-1
        <if test="name != null and name!='' ">
            or name like "%" #{name} "%"
        </if>
        <if test="age > 0">
            or age >= #{age}
        </if>
    </select>

关于 where 1=1

简单来说 where 1=1 是用来在 后面条件都不成立 的情况下,让语句前面的语句能够执行。
但使用标签的话就不用考虑这个问题,后续条件成立会自动插入where,同时后续第一条语句成立且带有 and/or 则会自动删除 and/or ,不成立则不会插入。
在这里插入图片描述

#{}和${}

 #{}表示一个占位符号,通过#{}可以实现preparedStatement向占位符中设置值,自动进行java类型和jdbc类型转换,#{}可以有效防止sql注入。 #{}可以接收简单类型值或pojo属性值。 如果parameterType传输单个简单类型值,#{}括号中可以是value或其它名称。

 ${}表示拼接sql串,通过${}可以将parameterType 传入的内容拼接在sql中且不进行jdbc类型转换, ${}可以接收简单类型值或pojo属性值,如果parameterType传输单个简单类型值,${}括号中只能是value。

ParameterType

 编写xml时,用来说明传入参数的类型。
 但其实没什么必要使用,单独一个参数时,是会自动识别的,多参数的时候又直接在方法参数位置使用@Parma(name=" ");
参考文章链接

计算机网络

OSI网络模型

物理层主要设备:中继器、集线器。
数据链路层主要设备:二层交换机、网桥
网络层主要设备:路由器

HTTP

长连接

HTTP状态码

在这里插入图片描述

HTTPS加密过程

HTTPS=HTTP+SSL/TSL
整体思路:

首先由网站生成一对公私钥,利用CA认证中心将公钥送给浏览器,CA会将此公钥利用自己的私钥加密,浏览器利用CA的公钥进行解密,得到网站的公钥。浏览器得到网站的公钥之后,用此公钥将生成的之后要使用的对称加密秘钥 加密,发送给网站,网站用自己的私密就可以解密得到对称秘钥,自此两端可以进行对称秘钥进行通信。参考

TCP 与 UDP

TCP与UDP的区别

  1. TCP面向连接,UDP无连接。
  2. TCP基于流传输,UDP基于数据报。
  3. TCP保证数据正确性,UDP可能丢包。
  4. TCP保证数据的顺序,UDP不能。
  5. UDP程序结构相对简单。

TCP与UDP的特点,分别用在什么场景

TCP是面向字节流、面向连接、可靠的传输协议。有着握手机制、ACK、重传机制所以可靠。多用于文件传输、邮件传输。
UDP适用于一些要求传输延迟小,能够容忍一些数据丢失的实时程序,多用于视频聊天、网络语音通话。
参考

UDP

UDP好文章
全称 User Datagram Protocol,UDP为应用提供了一种无需建立连接就可以发送封装的IP数据包的方法。如果应用程序开发人员选择的是 UDP 而不是 TCP 的话,那么该应用程序相当于就是和 IP 直接打交道的。

想了解UDP,我们先了解他的包头结构
在这里插入图片描述
从上图可以看出UDP除了两个端口号,基本就不包括其他东西了。一个UDP套接字是由一个二元组标识的,这个二元组指的是目的IP地址和目的端口号,也就是说服务器上的进程,不在乎你是哪个客户端来的(只要目的端口一致,不论源IP相不相同),我都放进一个套接字处理,处理完再根据源IP以及端口回复信息。
TCP套接字是由四元组标识的,即源IP和源端口、目的IP以及目的端口,所以即使是同一个IP发来的请求,若源端口不同,也会将请求分解为不同的请求,分别处理。

从应用层传递过来的数据,会附加上多路复用/多路分解的源和目的端口号字段,以及其他字段,然后将形成的报文传递给网络层,网络层将运输层报文段封装到IP数据报中,然后尽力而为地交付给目标主机。最关键的一点是,使用UDP协议在将数据包传递给目标主机时,发送方和接收方的运输层实体之间是没有握手的。正因如此,UDP被称为是无连接的协议。

UDP的特点

  1. 速度快,采用 UDP 协议时,只要应用进程将数据传给 UDP,UDP 就会将此数据打包进 UDP 报文段并立刻传递给网络层,然后 TCP 有拥塞控制的功能,它会在发送前判断互联网的拥堵情况,如果互联网极度阻塞,那么就会抑制 TCP 的发送方。使用 UDP 的目的就是希望实时性。
  2. 无须建立连接,TCP 在数据传输之前需要经过三次握手的操作,而 UDP 则无须任何准备即可进行数据传输。因此 UDP 没有建立连接的时延。如果使用 TCP 和 UDP 来比喻开发人员:TCP 就是那种凡事都要设计好,没设计不会进行开发的工程师,需要把一切因素考虑在内后再开干!所以非常靠谱;而 UDP 就是那种上来直接干干干,接到项目需求马上就开干,也不管设计,也不管技术选型,就是干,这种开发人员非常不靠谱,但是适合快速迭代开发,因为可以马上上手!
  3. 无连接状态,TCP 需要在端系统中维护连接状态,连接状态包括接收和发送缓存、拥塞控制参数以及序号和确认号的参数,在 UDP 中没有这些参数,也没有发送缓存和接受缓存。因此,某些专门用于某种特定应用的服务器当应用程序运行在 UDP 上,一般能支持更多的活跃用户
  4. 分组首部开销小,每个 TCP 报文段都有 20 字节的首部开销,而 UDP 仅仅只有 8 字节的开销。

但也不是所有使用UDP的应用层都是不可靠的,应用层序可以通过自己实现可靠的数据传输,通过增加确认和重传机制,所以UDP最大的特点就是快!

TCP

TCP最大的特点就是可靠,他通过哪些方法去实现可靠呢?为了解决数据丢失、数据重复、数据顺序、数据破坏等问题,TCP有相应的方法去解决避免这些问题:序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现可靠性传输的。

关于重传、窗口、流量控制、拥塞控制

重传机制

超时重传

顾名思义就是给发出去的包设置一个定时器,RTO(设定的时间)一般略大于两倍的RTT(一次数据往返的时间),若在时间结束前还未收到ACK那么就会启动重传,若还是未收到,会采取设置加倍时间。

快速重传

超时重传存在着需要等待时间这么一个问题,我们可以使用快速重传来加快这一进程。和以时间为驱动的超时重传不一样,快速重传是以 数据为驱动的,即返回三个一样的ACK则表示此序列号的数据未接收到。
这时发送端收到三个一样的ACK,那么他就会在定时器触发之前重发数据。

SACK(带确认的重传)

快速重传虽然好用,但也存在着一些问题,也就是在重传数据的时候,不知道应该传多少个包,SACk简单来说就是在快速重传的基础上,增加了返回最近收到的报文段的序列号范围,这样客户端就知道,哪些数据包已经到达服务器了。会在TCP首部的选项中添加。

DSACK

即重复 SACK,这个机制是在 SACK 的基础上,额外携带信息,告知发送方有哪些数据包自己重复接收了。DSACK 的目的是帮助发送方判断,是否发生了包失序、ACK 丢失、包重复或伪重传。让 TCP 可以更好的做网络流控。

滑动窗口

TCP每次发送一个数据,等到收到应答ACk之后,才发送下一个数据,这样子效率很低,受限于数据包的往返时间。为了解决这个问题,TCP引入了滑动窗口的概念,有了窗口,就可以指定窗口的大小,滑动窗口的大小就是指无需等待确认应答,而可以继续发送数据的最大值。
窗口其实就是在操作系统开辟的一个缓存空间,发送方主机在等待确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区中清除。

窗口大小由哪一方决定?

TCP头里有一个字段叫window , 也就是窗口大小。

这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个处理能力来发送数据,而不会导致接收端处理不过来。

所以窗口大小是由接收方决定的。

发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。

发送方的滑动窗口
在这里插入图片描述
剩余未发送全部发送出去之后:
在这里插入图片描述
一部分数据ACK已经接收之后,窗口后移。
在这里插入图片描述

如何确定这四部分呢?

在这里插入图片描述
SND.WND、SND.UN、SND.NXT

SND.WND:表示发送窗口的大小(大小是由接收方指定的);

SND.UNA:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。

SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。

指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量,就可以指向 #4 的第一个字节了。

那么可用窗口大小的计算就可以是:
可用窗口大 = SND.WND -(SND.NXT - SND.UNA)

接收方窗口大小

在这里插入图片描述
接收窗口
其中三个接收部分,使用两个指针进行划分:

RCV.WND:表示接收窗口的大小,它会通告给发送方。

RCV.NXT:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。

指向 #4 的第一个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移量,就可以指向 #4 的第一个字节了。

接收窗口和发送窗口的大小是相等的吗?

并不完全相等,接收窗口大小是约等于发送窗口大小的。

因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据速度非常快的话,这样的话接收窗口可以很快的就空缺出来。 那么新的接收窗口大小,是通过TCP报文中的Windows字段来告诉发送方,那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。

流量控制

发送方不能无脑地一直发数据给接收方,要考虑接收方的处理能力。
如果一直无脑发送,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端浪费。
为了解决这种现象的发生,TCP提供了一种机制可以让【发送方】根据【接收方】的实际接收能力控制发送的数据量,这就是所谓的流量控制。

简单说就是每次回ACK时,带上window,那么发送方就可以根据这个窗口大小,来调整自己将要发送的数据的大小了。
在这里插入图片描述

操作系统缓冲区与滑动窗口的关系

前面的流量控制例子,我们假定了发送窗口和接收窗口是不变的,但是实际上,发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的,而操作系统的缓冲区,会被操作系统调整。

当应用进程没办法及时读取缓冲区的内容时,也会对我们的缓冲区造成影响。

为了不发生下图这种情况,TCP规定是不允许同时减少缓存又收缩窗口的,而是先采取收缩窗口,过段时间再减少缓存,这样就避免了丢包的情况。
在这里插入图片描述

窗口关闭问题

接收方发送窗口大小为0,会阻止发送方继续发送数据,直到自身处理了数据腾出了空间接收数据,之后会发送一个窗口非0的ACK报文,但如果这个ACK在网络中丢失,那问题就出现了。
在这里插入图片描述
窗口关闭潜在的危险
这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不不采取措施,这种相互等待的过程,会造成了死锁的现象。

如何解决

为了解决这个问题,TCP为每一个连接设定一个持续定时器,只要TCP连接一方收到对方的 0 窗口通知时,启动持续计时器。计时器超时之后,就会发送窗口探测报文,而对方在确认此探测报文之后,给出自己现在的接收窗口大小。
在这里插入图片描述

窗口探测

如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器;

如果接收窗口不是 0,那么死锁的局面就可以被打破了。

窗口探查探测的次数一般为 3 此次,每次次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接。

糊涂窗口综合征

在这里插入图片描述

糊涂窗口可能发生在发送端,也可能在接收端

  1. 接收方通告一个小的窗口。
  2. 发送方发送小数据
    解决这两个问题就可以解决糊涂窗口了。

解决办法

接收方通常的策略如下:

MSS:MSS是每一个TCP报文段中数据字段的最大长度,注意:只是数据部分的字段,不包括TCP的头部。

当「窗口大小」小于 min(MSS,缓存空间 / 2) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。
等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。

发送方通常的策略:
使用 Nagle 算法,该算法的思路是延时处理,它满足以下两个条件中的一条才可以发送数据:

要等到窗口大小 >= MSS 或是 数据大小 >= MSS
收到之前发送数据的 ack 回包

只要没满足上面条件中的一条,发送方一直在囤积数据,直到满足上面的发送条件。
另外,Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnet 或 ssh 这样的交互性比较强的程序,则需要关闭 Nagle 算法。
可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应用自己的特点来关闭)

拥塞控制

前面的流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。

一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。

在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大….

所以,TCP 不能忽略网络上发生的事,它被设计成一个无私的协议,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。

于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。
为了在「发送方」调节所要发送数据的量,定义了一个叫做「拥塞窗口」的概念。

拥塞窗口

拥塞窗口 cwnd 是发送方维护的一个 的状态变量,它会根据网络的拥塞程度动态变化的。
我们在前面提到过发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于入了拥塞窗口的概念后,此时发送窗口的值是 swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。
拥塞窗口 cwnd 变化的规则:

只要网络中没有出现拥塞,cwnd 就会增大;
但网络中出现了拥塞,cwnd 就减少;

三次握手

1.开始C/S双方都属于close状态
2.C端发送并带着ISN©(初始化序号seq)过去,数据包有SYN标识表示是要建立连接,需要有ACK确认,需要消耗一个序列号,说明接下来的数据序列号会从c+1开始。初始序号由ISN函数随机生成。
3.S端收到SYN包之后,返回ACK包

关于三次握手、四次挥手、为什么需要等待2MSL可以看这篇
写地很好的三次握手文章

应用层协议有哪些

  1. HTTP协议
    超文本传输协议(Hypertext Transfer Protocol,HTTP)是一个简单的请求-响应协议,是用于从万维网服务器传输超文本到本地浏览器的传送协议。它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以ASCII形式给出;
  2. FTP 协议
    FTP工作在TCP/IP模型的应用层,基于传输协议TCP,FTP客户端和服务器之间的连接是可靠的,面向连接的,为数据的传输提供了可靠的保证。
  3. SMTP协议
    SMTP(Simple Mail Transfer Protocol)协议,即简单邮件传输协议,尽管邮件服务器可以用SMTP发送、接收邮件,但是邮件客户端只能用SMTP发送邮件,接收邮件一般用IMAP 或者 POP3 。邮件客户端使用TCP的25号端口与服务器通信。

JVM

类的加载过程

  从类的生命周期而言,一个类包括如下阶段:
在这里插入图片描述
  加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序进行,而解析阶段则不一定,它在某些情况下可能在初始化阶段后在开始,因为java支持运行时绑定。

数据结构

关于char与varchar

char表示定长,长度固定。varchar表示变长,长度可变。char如果插入的长度小于定义长度时,则用空格填充;varchar小于定义长度时,还是按实际长度存储,插入多长就存多长。

因为其长度固定,char的存取速度还是要比varchar要快得多,方便程序的存储与查找;但是char也为此付出的是空间的代价,因为其长度固定,所以会占据多余的空间,可谓是以空间换取时间效率。varchar则刚好相反,以时间换空间。

区别之二,存储的容量不同
对 char 来说,最多能存放的字符个数 255,和编码无关。
而 varchar 呢,最多能存放 65532 个字符。varchar的最大有效长度由最大行大小和使用的字符集确定。整体最大长度是 65,532字节。

中缀表达式转为后缀表达式(栈)

添加链接描述

循环队列

在这里插入图片描述

Spring

bean的生命周期

在这里插入图片描述

普通的Java对象的生命周期: 实例化 -> GC回收

bean的生命周期: 普通Java类 -> beanDefinition -> Spring Bean

为何不可以将普通Java类直接变为Bean: 因为Java类不足以描述一个Bean,所以需要封装多一层,成为beanDefinition。beanDefinition和普通Java类相比多了很多东西:作用域(多例还是单例)、是否懒加载
懒加载即在首次用到此bean时,才进行初始化。而普通bean则是在初始化阶段就已经初始化了,而被lazy-init修饰的bean则是从容器第一次进行context.getbean(“”)进行触发。

spring bean的懒加载原理
1 普通的bean的 初始化是在初始化阶段开始执行的,而被lazy-init修饰的bean则是从容器第一次进行context.getbean(“”)进行触发

2 接下来对每个BeanDefinition进行处理,如果是懒加载的则在容器初始化阶段不处理,其他的则在容器初始化阶段进行初始化并依赖注入

懒加载:对象使用的时候才去创建。节省资源,但是不利于提前发现错误;
提前加载:容器启动时立马创建。消耗资源,但有利于提前发现错误

)等等。

一、构造器推断:

此阶段中将会选出一个合适的构造器进行对象的实例化。
两种情况:有使用@Autowired进行修饰的构造器、没有使用@Autowired进行修饰的构造器。

先说没有的情况:有且仅有一个有参构造,使用有参;有且仅有一个无参,使用无参;多个有参也有无参,无参;多个有参没有无参,报错。

有的情况:

  1. 获取beanClass的所有构造器进行遍历,将有@Autowired修饰的放入候选构造器集合中。
  2. 判断集合中的注解中的required属性是否为true(为true即表示此构造器为指定的构造器),便不可以再有有注解的构造器,有则报错。
  3. required值为false则表示,可以继续添加值也为false的构造器。
  4. 若集合不为空,也不含@Autowired(required = true)的构造器,并且beanClass中有个无参构造器,放入集合中。
  5. 若集合为空,且beanClass中有且仅有一个有参构造,放入集合。

在required=false有多个时,且每个的参数都有不是bean的,那么就需要用无参构造,这就是在required=true时加入无参构造的理由(如果有的话),也是多个有参构造时需要有一个无参构造的原因。

在required=false有多个时,如何选择合适的构造器:

1、public修饰的构造器 > private修饰的构造器

2、修饰符相同的情况下参数数量更多的优先

二、beanDefinition处理:

选择出合适的构造器后,构造出bean,现在要做的就是处理bean中的属性。Spring将处理属性分为两个步骤:拿出被注释的属性、给属性赋值。

负责角色:

  1. AutowiredAnnotationBeanPostProcessor:处理@Autowired 、@Value 、@Inject注解
  2. CommonAnnotationBeanPostProcessor:处理@PostConstruct,@PreDestroy,@Resource

以AutowiredAnnotationBeanProcessor为例:
1.遍历beanClass中的所有Field、Method(Java中成为Member)。
2.判断Member是否被@Autowired等修饰。
3.将有被修饰的Member封装进InjectionMetadata类中。
4.判断Member是否已经被解析过,若一个Member同时标识了@Autowired和@Resource注解,那么这个Member会被处理两次,造成重复保存。
5.没有解析过就放入 checkedElements(已检查集合)中,待后续进行属性填充时使用。

三、属性填充:

角色:
1.AutowiredAnnotationBeanPostProcessor
2.CommonAnnotationBeanPostProcessor

还是以 AutowiredAnnotationBeanPostProcessor为例:

  1. 以beanName为key,将对应的InjectionMetadata从InjectionMetadataCache中取出来
  2. 遍历InjectionMetadata中的checkedElements。
  3. 取出Element中的Member,根据Member的类型在Spring中获取Bean
  4. 利用反射机制将获取到的Bean设置到属性中

四、初始化:

在Spring中,Bean填充属性之后还可以做一些初始化的逻辑,比如Spring的线程池TheadPoolTaskExecutor在填充属性之后的创建线程池逻辑、RedisTemplate的设置默认值。

Spring的初始化逻辑分为四个部分:

  1. invokeAwareMethods:若实现了xxxAware接口,就调用实现了的方法
  2. applyBeanPostProcessorsBeforeInitialization:初始化前的处理
  3. invokeInitMethods:调用初始化方法
  4. applyBeanPostProcessorsAfrerInitialization:初始化后的处理

IOC

深入理解Spring两大特性:IoC和AOP

IOC控制反转,创建对象的控制权,被反转到了Spring框架上。

通常,我们实例化一个对象时,都是使用类的构造方法来new一个对象,这个过程是由我们自己来控制的,而控制反转就把new对象的工交给了Spring容器。

单例模式
Spring的Bean默认是单例的,Bean的作用于可以通过scope属性进行设置,包括:默认情况下scope=“singleton”,那么该Bean是单例,任何人获取该Bean实例的都为同一个实例;
scope=“prototype”,任何一个实例都是新的实例;
scope=“request”,在WEB应用程序中,每一个实例的作用域都为request范围;
scope=“session”,在WEB应用程序中,每一个实例的作用域都为session范围;

在Spring中,bean可以被定义为两种模式:prototype(多例)和singleton(单例)

singleton(单例):只有一个共享的实例存在,所有对这个bean的请求都会返回这个唯一的实例。

单例模式又被分为饿汉模式以及懒汉模式:
饿汉模式: Spring singleton的缺省(默认)是饿汉模式:即在启动容器时,为所有Spring配置文件中定义的Bean都生成一个实例。

**懒汉模式:**在第一个请求时才生成第一个实例,以后的请求都调用这个实例
spring singleton设置为懒汉模式:

AOP

**切面(Aspect):**共有功能的实现。如日志切面、权限切面、验签切面。在实际开发中通常是一个存放共有功能实现的标准Java类。当Java类使用了@Aspect注解修饰时,就能被AOP容器识别为切面。

**通知(Advice):**切面的具体实现。就是要给目标对象织入的事情。以目标方法为参照点,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)、环绕通知(Around)5种。在实际开发中通常是切面类中的一个方法,具体属于哪些类通知,通过方法上的注解区分。

**连接点(JoinPoint):**程序运行过程中,能够插入切面的点。例如,方法的调用、异常抛出等。Spring只支持方法级的连接点。一个类的所有方法前、后、抛出异常时等都是连接点。

切入点(Pointcut):用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的。

目标对象(Target): 哪些即将切入切面的对象,也是哪些被通知的对象。这些对象专注业务本身的逻辑,所有的共有功能等待AOP容器的切入。

**代理对象(Proxy):**将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象的功能等于目标对象本身业务逻辑加上共有功能。代理对象对于使用者来说是透明的,是程序运行过程中的产物。目标对象被织入共有功能后产生的对象。

**织入(Weaving):**将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译时、类加载时、运行时。Spring是在运行时完成织入,运行时织入通过Java语言的反射机制与动态代理机制来动态实现。

常用注解

@Resource 与 @Autowired 的区别

@Resource以名字优先查找并注入,@Autowired则以类型优先。

参考,写的很清楚

若有两个实现类如何处理:

  1. 可以加@Qualifier(" "),来进行名字定位。可以按照实现类的类名,也可以另外使用@Service(value = " ")来指定。
  2. 也可以通过@Resource(name = " ")指定。

@RestController
 @RestController = @ResponseBody + @Controller
 @ResponseBody: 后端返回给前端的数据以 json 方式返回
 @RequestBody: 前端返回给后端的数据若是以 json 方式返回的,转换为java对象的形式。

Linux

僵尸进程与孤儿进程

正常情况下的子进程回收:
wait():父进程先注册某个信号处理函数(SIGCHILD),在子进程结束之前会向父进程发送一个信号,发送之后退出。父进程在受到信号之前处理自己的事情,收到信号之后执行先前注册的信号处理函数,在函数内调用wait()函数进行子进程的回收,防止出现僵尸进程。
waitpid():查阅资料得知waitpid函数的用法:

pid_t waitpid(pid_t pid,int *status,int options)

该方法有三个参数,相较于wait函数只有status多了两个参数,一个pid,一个options。
pid主要四个选择:

pid大于0时候,等待进程id为pid的进程退出;
等于0时,等待同一个进程组中的任何子进程;
等于-1时,等待任意一个子进程退出;
小于-1,等待进程组标识符与 pid 绝对值相等的所有子进程。

options可以按位或操作,有三个标志:

使用WNOHANG时,如果指定的子进程状态不变,函数不会等待阻塞,直接返回;
使用WCONTINUED,返回那些因收到 SIGCONT 信号而恢复执行的已停止子进程的状态信息。
使用WUNTRACED,除了返回终止子进程的信息外,还返回因信号而停止的子进程;

status用于传入指针并使得用户获取进程状态。

Redis进行RDB持久化,使用bgsave命令会fork出来一个子进程,在此子进程持久化结束后,Redis的回收方式是:waitpid()中的标志位为WNOHANG,也就是使用非阻塞轮询的方式,不断询问子进程是否结束。

在这里插入图片描述

孤儿进程:
孤儿进程,顾名思义就是没父进程的进程。父进程先于子进程结束,那么就没有父进程进行回收,这时子进程的父进程会变为pid为1的init进程,它来进行回收。

僵尸进程:
僵尸进程,指结束后父进程却没有进行回收,子进程结束后仍然会为其保留一定的信息(包括进程号、退出状态、运行时间等),会占用有限的进程号。
处理:
1.干掉父进程
干掉父进程后,让剩下的子进程成为孤儿进程,成为孤儿进程后就和我们上面说的一样了,由init进程来领养这些进程,并且来处理这些进程的资源释放等工作。

2.父进程调用wait或waitpid
等函数等待子进程结束,这会导致父进程挂起。
执行wait()或 waitpid()系统调用,则子进程在终止后会立即把它在进程表中的数据返回给父进程,此时系统会立即删除该进入点。在这种情形下就不会产生defunct进程。

3.fork两次
第一次 fork : 父进程fork一个子进程
第二次 fork : 子进程fork一个孙进程后退出
那么孙进程被init接管,当孙进程结束后,init会回收。
但子进程的回收还要自己做。

4.signal函数
父进程来处理:用signal函数为SIGCHLD安装handler,在子进程结束后,父进程会收到该信号,可以在handler中调用wait回收。
内核来处理:
如果父进程不关心子进程什么时候结束,可以通过以下两个函数通知内核自己不感兴趣子进程的结束,此时,子进程结束后,内核会回收并不再给你父进程发信号。

命令

Fopen:
Open:
Write:
Create:

排序算法

参考
内部排序与外部排序:
内部排序:指将需要排序的数据全部加载进内存,进行排序。
外部排序:值要排序的数据量过大,只能采用分而治之的方法,一次性只排序一部分,排序期间需要对外存进行访问。

衡量效率的方式:
内部排序:时间复杂度。
外部排序:磁盘IO次数,读写外存的次数。

单词记录

  1. Dynamic 动态
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值