前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试常用知识笔记

面试常用知识笔记

原创
作者头像
深雾
修改2021-07-27 14:31:44
4300
修改2021-07-27 14:31:44
举报
文章被收录于专栏:工具类工具类

# 前言

常用的知识点记录下来

# Volatile

Volatile是Java虚拟机提供的轻量级的同步机制(三大特性)

1、保证可见性

2、不保证原子性

3、禁止指令重排

## JMM内存模型

数据传输速率:硬盘 < 内存 < < cache < CPU

线程解锁前,必须把共享变量的值刷新回主内存

线程解锁前,必须读取主内存的最新值,到自己的工作内存

加锁和解锁是同一把锁

主内存:就是计算机的内存,也就是经常提到的8G内存,16G内存

工作内存:但我们实例化new student,那么 age = 25 也是存储在主内存中

当同时有三个线程同时访问 student中的age变量时,那么每个线程都会拷贝一份,到各自的工作内存,从而实现了变量的拷贝

![image.png](http://liujun11.cn/upload/2020/09/image-493e9a0abd7b449190b4270c18802064.png)

即:JMM内存模型的可见性,指的是当主内存区域中的值被某个线程写入更改后,其它线程会马上知晓更改后的值,重新得到更改后的值,多线程共享主内存变量。

### 缓存一致性

需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,这类协议主要有MSI、MESI等等。

MESI当CPU写数据时,如果发现操作的变量是共享变量,即在其它CPU中也存在该变量的副本,会发出信号通知其它CPU将该内存变量的缓存行设置为无效,因此当其它CPU读取这个变量的时,发现自己缓存该变量的缓存行是无效的,那么它就会从内存中重新读取。

### 总线嗅探

发现数据是否失效用到了总线嗅探技术,就是每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期了,当处理器发现自己的缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从内存中把数据读取到处理器缓存中。

总线风暴:

Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和CAS循环,无效的交互会导致总线带宽达到峰值。因此不要大量使用volatile关键字

## 不保证原子性

原子性

不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要具体完成,要么同时成功,要么同时失败。

各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后在写回到主内存中的。

这就可能存在一个线程AAA修改了共享变量X的值,但是还未写入主内存时,另外一个线程BBB又对主内存中同一共享变量X进行操作,但此时A线程工作内存中共享变量X对线程B来说是不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题。即主内存值共享,工作内存不可见。

![image.png](http://liujun11.cn/upload/2020/09/image-bebdc7e31ba3413f9500acfe79a8fc64.png)

### 加锁和原子类

解绝方法

1、在方法上加入 synchronized

```java

public synchronized void addPlusPlus() {

number ++;

}

```

2、JUC下面的原子包装类

```java

/**

* 创建一个原子Integer包装类,默认为0

*/

AtomicInteger atomicInteger = new AtomicInteger();

public void addAtomic() {

// 相当于 atomicInter ++

atomicInteger.getAndIncrement();

}

```

### 禁止指令重排

保证特定操作的顺序

保证某些变量的内存可见性

线程安全获得保证

# CAS底层原理

CAS的全称是Compare-And-Swap,旋转并比较的自旋锁。功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的,乐观锁思想。

体现在Java语言中就是sun.misc.Unsafe类的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,也就是说CAS是线程安全的。

## 修改实例

```java

public class CASDemo {

public static void main(String[] args) {

// 创建一个原子类

AtomicInteger atomicInteger = new AtomicInteger(5);

/**

* 一个是期望值,一个是更新值,但期望值和原来的值相同时,才能够更改

* 假设三秒前,我拿的是5,也就是expect为5,然后我需要更新成 2019

*/

System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data: " + atomicInteger.get());

System.out.println(atomicInteger.compareAndSet(5, 1024) + "\t current data: " + atomicInteger.get());

}

}

```

执行结果

![image.png](http://liujun11.cn/upload/2020/09/image-af27f65add2143b9b3cfe49b666b2c7e.png)

这是因为我们执行第一个的时候,期望值和原本值是满足的,因此修改成功,但是第二次后,主内存的值已经修改成了2019,不满足期望值,因此返回了false,本次写入失败。

![image.png](http://liujun11.cn/upload/2020/09/image-89d0dbaa618d4425bd880e0d0ac4bcf0.png)

类似于SVN或者Git的版本号,如果没有人更改过,就能够正常提交,否者需要先将代码pull下来,合并代码后,然后提交。

## 底层实现

valueOffset表示变量在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的,通过valueOffset,直接通过内存地址,获取到值进行加1的操作。

![image.png](http://liujun11.cn/upload/2020/09/image-a9c60b19271d426986fdb288fd388cbb.png)

底层又调用了一个unsafe类的getAndAddInt方法,操作的int值为volite修饰多线程之间的内存可见性

![image.png](6)

Unsafe是CAS的核心类,Java方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe相当于一个后门,该类可以直接操作特定的内存数据。Unsafe类存在sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中的CAS操作的执行依赖于Unsafe类的方法。

Unsafe类的所有方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务

Atomic修饰的包装类,能够保证原子性,依靠的就是底层的unsafe类

![image.png](http://liujun11.cn/upload/2020/09/image-e27ff4292d0d4ffd827f29a03978577a.png)

var5:就是我们从主内存中拷贝到工作内存中的值

那么操作的时候,需要比较工作内存中的值,和主内存中的值进行比较

假设执行 compareAndSwapInt返回false,那么就一直执行 while方法,直到期望的值和真实值一样

val1:AtomicInteger对象本身

var2:该对象值得引用地址

var4:需要变动的数量

var5:用var1和var2找到的内存中的真实值

用该对象当前的值与var5比较

如果相同,更新var5 + var4 并返回true

如果不同,继续取值然后再比较,直到更新完成

没有用synchronized,而用CAS,这样提高了并发性,也能够实现一致性,是因为每个线程进来后,进入的do while循环,然后不断的获取内存中的值,判断是否为最新,然后在进行更新操作。

## CAS缺点

CAS不加锁,保证一次性,但是需要多次比较

循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)

只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以通过循环CAS的方式来保证原子操作

但是对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性

引出来ABA问题

### ABA问题

当线程one从内存位置V取出A,另一个线程two也从内存取出A,并且进行操作将值变成B,然后将V位置数据变成A,此时线程one进行CAS操作发现内存仍然啥A,线程one操作成功,数据产生误差。

解决方法:添加修改版本号

## 总结

CAS是compareAndSwap,比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否者继续比较直到主内存和工作内存的值一致为止

# 集合并发安全问题

## Arraylist

初始长度默认10,add元素时如果超出当前长度,调用grow方法扩容1.5倍。

多线程下操作会产生ConcurrentModificationException。

解决方法:

1、Vector是在add方法加锁解决,但并发下降

2、Collections工具类,Collections.synchronizedList(new ArrayList());

3、new CopyOnWriteArrayList();

## CopyOnWriteArrayList

实现List接口

private transient volatile object[] array;高并发volatile修饰

add方法源码,写时复制,读写分离,复制完后,引用类型指向新列表

```java

public boolean add(E e) {

final ReentrantLock lock = this.lock;

lock.lock();

try {

Object[] elements = getArray();

int len = elements.length;

Object[] newElements = Arrays.copyOf(elements, len + 1);

newElements[len] = e;

setArray(newElements);

return true;

} finally {

lock.unlock();

}

}

```

## CopyOnWriteArraySet

底层创建的时候创建一个CopyOnWriteArrayList,只有key,value都为null。

hashset底层是hashmap,add方法源码,保存key

```java

public boolean add(E e) {

return map.put(e,PRESEND) == null;

```

value为恒定常量final PERSNT = new Object()

## ConcurrentHashMap

jdk1.7:采用Segment + HashEntry的方式进行实现

![image.png](http://liujun11.cn/upload/2020/09/image-12c5dc8efaaf47c3b63ae519ec645465.png)

jdk1.8:放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,结构如下:

![image.png](http://liujun11.cn/upload/2020/09/image-9e741c9be98d4d32a11205f6534f2fe8.png)

底层啥table类型是Node数组;继承自Map.Entry<K, V>的链表,而当这个链表结构中的数据大于8,则将数据结构升级为TreeBin类型的红黑树结构

# 多线程锁

## 锁的升级

JVM优化synchronized的运行机制,当JVM检测到不同的竞争状态时,就会根据需要自动切换到合适的锁,这种切换就是锁的升级。升级是不可逆的,也就是说只能从低到高,也就是偏向-->轻量级-->重量级,不能够降级

锁级别:无锁->偏向锁->轻量级锁->重量级锁

当一个线程访问同步块并获取锁时,会在对象头和栈帧的锁记录里存储偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需测试Mark Word里线程ID是否为当前线程。

偏向锁应用的场景是一个同步代码块只有一个线程频繁访问,使用偏向锁,就不需要频繁使用CAS获取锁和释放锁,只需要简单判断对象头中记录的偏向锁的线程ID是否是当期线程的就可以了,所以偏向锁在这种场景下可以大大提升效率

当线程存在竞争时,偏向锁的效率就会降低,因为当多条线程竞争同一个偏向锁时,会频繁产生偏向锁的撤销,所以此时应该升级为轻量级锁,轻量级锁当线程竞争锁失败时,线程不会阻塞进入自旋,继续获取锁,当竞争非常激烈时,持续自旋而获取不到锁会消耗大量CPU资源,此时就会升级为重量级锁,重量级锁当获取锁失败线程会阻塞,重量级锁的缺点是线程上下文会频繁的切换

## 公平锁

公平锁:指多个线程按照申请锁的顺序来获取锁,先来后到

非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,有可能先申请的线程比后申请的线程优先获取锁,在高并发情况下,有可能会造成优先级反转或者饥饿现象

ReentrantLock和Synchronized都是可重入锁,加锁方式都是阻塞式的同步

Synchronized是关键字,通过JVM实现加锁解锁,锁的范围是整个方法或synchronized块部分,不可中断,除非抛出异常

ReentrantLock实现Lock接口,api层面的加锁解锁,需要手动释放锁,lock()和unlock()方法配合try/finally语句块来完成,可以设置公平锁

当同步非常激烈的时候,synchronized锁升级性能下降

## 可重入锁(递归锁)

外层函数获得锁,内层递归函数仍然能获取该锁,线程可以进入任何一个它已经拥有的锁所同步着的代码块

可以避免死锁

## 自旋锁实现

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处啥减少线程上下文切换的消耗,缺点啥会消耗CPU

```java

/**

* 手写一个自旋锁

*

* 循环比较获取直到成功为止,没有类似于wait的阻塞

*

* 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到

*/

public class SpinLockDemo {

// 现在的泛型装的是Thread,原子引用线程

AtomicReference<Thread> atomicReference = new AtomicReference<>();

public void myLock() {

// 获取当前进来的线程

Thread thread = Thread.currentThread();

System.out.println(Thread.currentThread().getName() + "\t come in ");

// 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋

while(!atomicReference.compareAndSet(null, thread)) {

}

}

/**

* 解锁

*/

public void myUnLock() {

// 获取当前进来的线程

Thread thread = Thread.currentThread();

// 自己用完了后,把atomicReference变成null

atomicReference.compareAndSet(thread, null);

System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");

}

public static void main(String[] args) {

SpinLockDemo spinLockDemo = new SpinLockDemo();

// 启动t1线程,开始操作

new Thread(() -> {

// 开始占有锁

spinLockDemo.myLock();

try {

TimeUnit.SECONDS.sleep(5);

} catch (InterruptedException e) {

e.printStackTrace();

}

// 开始释放锁

spinLockDemo.myUnLock();

}, "t1").start();

// 让main线程暂停1秒,使得t1线程,先执行

try {

TimeUnit.SECONDS.sleep(5);

} catch (InterruptedException e) {

e.printStackTrace();

}

// 1秒后,启动t2线程,开始占用这个锁

new Thread(() -> {

// 开始占有锁

spinLockDemo.myLock();

// 开始释放锁

spinLockDemo.myUnLock();

}, "t2").start();

}

}

```

## 读写锁(独占/共享)互斥锁

独占锁:该锁一次只能被一个线程所持有

共享锁:该锁可以被多个线程所持有

ReentrantReadWriteLock读锁是共享锁。

写锁是独占锁,与其它锁互斥,写操作原子和独占性,整个过程必须是一个完整的统一体,中间不许被分割打断。

先进行写入操作

# 线程

## 阻塞队列

队列,线程运行时先进先出

当阻塞队列是空时,从队列中获取元素的操作将会被阻塞

当阻塞队列是满时,往队列中添加元素的操作将会被阻塞

在多线程领域:阻塞就是在某些情况下挂起线程,一旦条件满足,被挂起的线程又会自动被唤醒

BlockingQueue好处:

不需要观星什么时候需要阻塞线程,什么时候需要唤醒线程,自动控制效率和安全

生产者消费者模式,队列取出是消费,队列进入是生产者

## Synchronized和lock区别

1、Synchronized关键字是JVM层面,Lock是应用方法

2、Synchronized不需要手动释放锁,执行完成自动让线程释放对锁的占用

Lock需要lock()和unlock()方法配合try/finally语句块完成

3、Synchronized不可中断

ReentrantLock可中断,设置超时方法trylock(long timeout,timeunit unit)

lockInterruptibly()放代码块中,调用interrypt()方法可中断

4、Synchronized非公平锁

ReentrantLock两者都可,默认非公平锁

5、ReentrantLock锁绑定多个条件Condition,用来实现分组唤醒线程

## 线程池

线程池做的工作主要啥控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行

主要特点:线程复用;控制最大并发数;管理线程

1、降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

2、提高响应速度。但任务到达时,任务可以不需要的等到县城创建就能立即执行

3、提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

线程池啥通过Executor框架实现啊的,该框架用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类

### 线程池参数

1、corePoolSize:常驻核心线程数

2、maximumPoolSize:同时执行的最大线程数,最小为1

3、keepAliveTime:多余的空闲线程存活时间,超过corePoolSize的线程空闲时间达到keepAliveTime会被销毁,直到线程数为corePoolSize

4、unit:keepAliveTime的单位

5、workQueue:任务队列,被提交但尚未被执行的任务

6、threadFactory:生成线程池中工作线程的线程工厂,用默认即可

7、hanler:拒绝策略,但队列满了并且工作线程超过最大线程数如何拒绝

### 线程池工作原理

1、在创建了线程池后,等待提交过来的任务请求

2、当调用execute()方法添加一个请求任务时,线程池会做如下判断:

2.1:如果真在运行的线程数量小于corePoolSize,那么马上创建线程执行任务

2.2:如果真在运行的线程数量大于或等于corePoolSize,任务放进队列

2.3:如果队列已满且正在运行的线程数量小于maximumPoolSize,那么创建非核心线程立刻执行这个任务

2.4:如果队列已满且正在运行的线程数量大于等于maximumPoolSize,线程池会启动饱和拒绝策略执行

3、当一个线程无事可做超出一定的时间(keepAliveTime)时,线程池会判断:

如果当前运行线程大于corePoolSize,那么这个线程就被停掉

所以线程池所有任务完成后它最终会收缩到corePoolSize的大小

### 拒绝策略

1、AbortPolicy:默认直接跑超出RejectedExeccutionException异常阻止系统正常运行

2、CallerRunsPolicy:调用者运行,调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务会退给调用者

3、DiscardOldestPolicy:抛弃队列中等待最久的任务,然后当前任务加入队列

4、DiscardPolicy:直接丢弃任务,不予任务处理也不抛出异常,如果允许任务丢失,这是最好的方案

### 工作中使用线程池

```java

public ScheduledFuture<?> scheduleAtFixedRate(Runnable run, int initialDelay, int period, TimeUnit unit) {

return executor.scheduleAtFixedRate(run, initialDelay, period, unit);

}

/** 线程执行器 */

private NamedScheduleExecutor executor;

@PostConstruct

private void initialize() {

executor = new NamedScheduleExecutor(Runtime.getRuntime().availableProcessors() + 1, "battle-room");

executor.scheduleAtFixedRate(new Runnable() {

@Override

public void run() {

try {

// 获取所有战场房间循环执行

Collection<BattleRoom> roomList = roomFacade.getRooms();

for (BattleRoom room : roomList) {

// 正在运行

if (room.getRunningState().get()) {

continue;

}

boolean result = isDestoryBattleRoom(room);

if (result) {

continue;

}

if (room.getRunningState().compareAndSet(false, true)) {

int roundLimit = globalConfigService.findGlobalConfig(GlobalConfigKey.BATTLE_ROUND_LIMIT).findInt();

// 开始处理

executor.submit(new BattleRoomRunnable(room, roundLimit));

}

}

} catch (Exception e) {

LOGGER.error("battleroom queue thread error:{}", e);

}

}

}, 1000, 100, TimeUnit.MILLISECONDS);

LOGGER.info("scene execute thread is running!");

}

public ThreadPoolExecutor(int corePoolSize,

int maximumPoolSize,

long keepAliveTime,

TimeUnit unit,

BlockingQueue<Runnable> workQueue,

ThreadFactory threadFactory,

RejectedExecutionHandler handler) {

if (corePoolSize < 0 ||

maximumPoolSize <= 0 ||

maximumPoolSize < corePoolSize ||

keepAliveTime < 0)

throw new IllegalArgumentException();

if (workQueue == null || threadFactory == null || handler == null)

throw new NullPointerException();

this.acc = System.getSecurityManager() == null ?

null :

AccessController.getContext();

this.corePoolSize = corePoolSize;

this.maximumPoolSize = maximumPoolSize;

this.workQueue = workQueue;

this.keepAliveTime = unit.toNanos(keepAliveTime);

this.threadFactory = threadFactory;

this.handler = handler;

}

```

### 线程池配置

CPU密集型:该任务需要大量的计算,而没有阻塞,CPU一直全速运行

CPU密集任务只再真正的多核CPU才能得到加速(多线程)

公式 CPU核数+1个线程的线程池

IO密集型:该任务需要大量的IO,即大量的阻塞

再单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费等待

所以再IO密集型任务中使用多线程可以加速程序运行,即使再单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间,大部分线程都阻塞,需要配置多线程数

参考公式:CPU核数/1-阻塞系数

例如8核CPU:8/(1 - 0.9) = 80线程数

### 死锁

产生原因:

1、系统资源不足

2、进行运行推进的顺序不合适

3、资源分配不当

死锁是指多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁的可能性就很低,否则就会因为争夺有限的资源而陷入死锁

线程A持有锁A试图获取锁B,此时线程B持有锁B试图获取锁A

解决:

jps命令定位进程号

jstack找到死锁查看

# Spring的IOC和AOP

Spring 的核心是控制反转(IOC)和面向切面(AOP)

## IOC和DI

控制反转(IOC)与依赖注入(DI)是同一个概念,控制反转是种设计思想一种设计思想,将原本程序中手动创建对象的控制权,交由 Spring框架来原理。

依赖注入是指Spring创建对象的过程中,将对象依赖的属性(常量,对象,集合)通过配置设值给该对象。

Spring支持的依赖注入方式:xml配置注入和注解注入

引入IOC的目的:

1、降低类之间的耦合

2、倡导面向接口编程、实施依赖倒换原则

3、提高系统可插入、可测试、可修改等特性

## IOC容器

BeanFactory:生产 bean对象的工厂,Spring最底层的接口,只提供了IoC容器的功能:负责配置,创建和管理bean,被 Spring IOC容器管理的对象称为bean。Spring IOC容器通过读取配置文件中的配置元数据,通过元数据对应用中的各个对象进行实例化和装配,需要等到获取某个bean的时候才会才会创建该bean,即延迟初始化

ApplicationContext:在应用中,一般不使用BeanFactory,而推荐使用 ApplicationContext(应用上下文)

接口继承了BeanFactory接口,还提供了AOP集成、国际化处理等功能

区别,在启动Spring容器时就会创建所有的bean(Web应用中推荐),在xml中也可以配置延迟lazy-init

### 注入方式

1、setter方法注入,通过成员变量的setter方法来注入被依赖对象,使用 property标签

2、构造器注入,即通过构造函数完成依赖关系的设定,使用constructor-arg标签

3、注解注入,使用注解注入依赖对象不用再在代码中写依赖对象的setter方法或者该类的构造方法,并且不用再配置文件中配置大量的依赖对象,使代码更加简洁,清晰,易于维护

### 常用注解

Component最初spring框架设计的,后来为了标识不同代码层,衍生出Controller,Service,Repository三个注解 作用相当于配置文件的bean标签,被注解的类,spring始化时,就会创建该对象。它们的功能都是相同的,只是用于标注不同类型的组件,只能添加在类上

@Scope(scopeName="singleton")用于指定scope作用域的(控制类生成的时候采用单例还是多例)

定义在类的属性字段上:

@Value(value="112")给简单类型属性赋值,可以用在方法上或属性上

@Resource(name="user")给对象引用类型赋值,默认按名称装配,当找不到与名称匹配的bean才会按类型装配。该值user类必须已经声明(在配置文件中已配置或在类中已经注解)

@Autowired自动装配,默认按类型装配,给对象引用类型赋值,其作用是为了消除代码Java代码里面的getter/setter与bean属性中的property。

@Configuration把一个类作为一个IOC容器,它的某个方法头上如果注册了@Bean,就会作为这个Spring容器中的Bean

## AOP

AOP(Aspect Oriented Programming)称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等待,Struts2的拦截器设计就是基于AOP的思想,是个比较经典的例子。

AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。

### AOP概念

AOP利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点

Aspect(切面):通常是一个类,里面可以定义切入点和通知

Advice(通知):AOP在特定的切入点上执行的增强处理,有before(前置),after(后置),afterReturning(最终),afterThrowing(异常),around(环绕)

AOP代理(AOP Proxy):AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类

(9)目标对象(Target Object): 包含连接点的对象。也被称作被通知或被代理对象。POJO

### 代理对象

AOP像OOP一样,只是一种编程范式。可以采用代理模式,就是我再生成一个代理类,去代理UserController的saveUser()方法,代码如下:

```java

class UserControllerProxy {

private UserController userController;

public void saveUser() {

checkAuth();

userController.saveUser();

}

}

```

这样在实际调用saveUser()时,我调用的是代理对象的saveUser()方法,从而实现了鉴权。

代理分为静态代理和动态代理,静态代理,顾名思义,就是你自己写代理对象,动态代理,则是在运行期,生成一个代理对象。

Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用JDK Proxy去进行代理了

# Java基础

## String

String:字符串常量,值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间

StringBuffer:可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量。StringBuffer中的方法大都采用了synchronized关键字进行修饰,因此是线程安全的

StringBuilder:可变类,速度更快,线程不安全的

### 原理

实现java.io.Serializable接口: 序列化,string可以写到io流中,并可保存整个对象以及用于网络传输

Comparable:比较,若返回值>0则,当前字符串小于另一个字符串

CharSequence:CharSequence与String都能用于定义字符串,但CharSequence的值是可读可写序列,而String的值是只读序列

String被final修饰,说明String类绝不可能被继承了,也就是说任何对 String的操作方法,都不会被继承覆写,保存数据的是一个char的数组 value

扩容:使用append()方法在字符串后面追加东西的时候,如果长度超过了该字符串存储空间大小了就需要进行扩容:构建新的存储空间更大的字符串,将久的复制过去;

StringBuilder的append方法实际上就是调用父类AbstractStringBuilder的append方法

```java

public AbstractStringBuilder append(String str) {

if (str == null)

return appendNull();

int len = str.length();

// 确保容量够,不够则扩容

// 扩容机制是2*capcity+2

ensureCapacityInternal(count + len);

// 将要append的string拷贝到当前数组,0表示从要拼接的字符串第一位开始拷贝

str.getChars(0, len, value, count);

count += len;

return this;

}

```

为什么缓冲字符串拼接效率高

1、String是final的,一个数组只能给一个字符串使用,所以每拼接一次都要新创建然后拷贝一次数组

2、StringBuilder虽然底层也是字符数组,但是他不final,即允许在容量充足的情况下,一个数组可以被拼接多次;

相应的StringBuilder也要有扩容机制

## 集合

JAVA的util包中有两个所有集合的父接口Collection和Map

Collection接口extends自java.lang.Iterable接口,实现它的有List、Queue、Set接口,单列结构每个位置只有一个元素

Map接口,实现它的有HashMap、Hashtable、TreeMap类,key-value键值对双列结构,像个小型数据库。

### 数组

首先不能不先说一下数组(Array)

存储及随机访问一连串对象”的做法,array是最有效率的一种。

效率高,但容量固定且无法动态改变。

缺点是无法判断其中实际存有多少元素,length只是告诉我们array的容量

Arrays类,专门用来操作array

equals():比较两个array是否相等。array拥有相同元素个数,且所有对应元素两两相等

sort():用来对array进行排序

### List特点

ArrayList:

1、底层数据结构是数组,查询快、增删慢

2、线程不安全,效率高

Vector:

1、底层数据结构是数组,查询快,增删慢

2、线程安全,效率底

LinkedList:

1、底层数据结构是链表,查询慢,增删快

2、线程不安全,效率高

### Set集合

无序(存储和取出顺序不一致,有可能会一致),但是元素唯一,不能重复

HashSet:

1、底层数据是哈希表

2、通过两个方法hashCode()和equals()保证元素的唯一性,方法自动生成

3、子类LinkedHashSet底层数据结构是链表和哈希表,由链表保证元素有序,

由哈希表保证元素唯一

TreeSet:

1、底层数据是红黑二叉树

2、排序方式:自然排序、比较器排序

3、通过比较返回值是否为0来保证元素的唯一性

### Hashtable

和HashMap最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全,Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合

### HashSet

HashSet虽然实现Set接口,内部是使用HashMap实现,只不过HashSet里面的HashMap所有的value都是同一个Object而已,因此HashSet也是非线程安全的

## HashMap

hashMap术语介绍:

桶: 就是hashmap的table数组

bin: 就是挂在数组上的链表

TreeNode: 红黑树

capacity: table总容量

底层是数组+链表结构,默认容量为16的Entry数组,默认加载因子为0.75,扩容翻倍

桶中链表元素个数大于等于8时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表

1.8优化概述:

1、resize 扩容优化

2、引入了红黑树,目的是避免单条链表过长而影响查询效率

3、解决了resize时多线程死循环问题,但仍是非线程安全的

## ConcurrentHashMap

### 1.8更新

线程安全的,在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,如下图所示:

![image.png](http://liujun11.cn/upload/2020/09/image-7b9bbfd2206c4d69aa3ff8403650fb88.png)

Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样

JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本

![image.png](http://liujun11.cn/upload/2020/09/image-87d49c165ed041338a60d6d46167ed98.png)

### Node

Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据

```java

static class Node<K,V> implements Map.Entry<K,V> {

//链表的数据结构

final int hash;

final K key;

//val和next都会在扩容时发生变化,所以加上volatile来保持可见性和禁止重排序

volatile V val;

volatile Node<K,V> next;

```

Node数据结构很简单,从上可知,就是一个链表,但是只允许对数据进行查找,不允许进行修改

### TreeNode

TreeNode继承与Node,但是数据结构换成了二叉树结构,它是红黑树的数据的存储结构,用于红黑树中存储数据,当链表的节点数大于8时会转换成红黑树的结构,他就是通过TreeNode作为存储结构代替Node来转换成黑红树源代码如下

```java

static final class TreeNode<K,V> extends Node<K,V> {

//树形结构的属性定义

TreeNode<K,V> parent; // red-black tree links

TreeNode<K,V> left;

TreeNode<K,V> right;

TreeNode<K,V> prev; // needed to unlink next upon deletion

boolean red; //标志红黑树的红节点

```

### TreeBin

TreeBin从字面含义中可以理解为存储树形结构的容器,而树形结构就是指TreeNode,所以TreeBin就是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制,部分源码结构如下

```java

static final class TreeBin<K,V> extends Node<K,V> {

//指向TreeNode列表和根节点

TreeNode<K,V> root;

volatile TreeNode<K,V> first;

volatile Thread waiter;

volatile int lockState;

// 读写锁状态

static final int WRITER = 1 ; // 获取写锁的状态

static final int WAITER = 2 ; // 等待写锁的状态

static final int READER = 4 ; // 增加数据时读锁的状态

```

### 扩容transfe

```java

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {

int n = tab.length, stride;

// 每核处理的量小于16,则强制赋值16

if ((stride = (NCPU > 1 ) ? (n >>> 3 ) / NCPU : n) < MIN_TRANSFER_STRIDE)

stride = MIN_TRANSFER_STRIDE; // subdivide range

if (nextTab == null ) { // initiating

try {

@SuppressWarnings ( "unchecked" )

Node<K,V>[] nt = (Node<K,V>[]) new Node<?,?>[n << 1 ]; //构建一个nextTable对象,其容量为原来容量的两倍

nextTab = nt;

} catch (Throwable ex) { // try to cope with OOME

sizeCtl = Integer.MAX_VALUE;

return ;

}

nextTable = nextTab;

transferIndex = n;

}

int nextn = nextTab.length;

// 连接点指针,用于标志位(fwd的hash值为-1,fwd.nextTable=nextTab)

ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);

// 当advance == true时,表明该节点已经处理过了

boolean advance = true ;

boolean finishing = false ; // to ensure sweep before committing nextTab

for ( int i = 0 , bound = 0 ;;) {

Node<K,V> f; int fh;

// 控制 --i ,遍历原hash表中的节点

while (advance) {

int nextIndex, nextBound;

if (--i >= bound || finishing)

advance = false ;

else if ((nextIndex = transferIndex) <= 0 ) {

i = - 1 ;

advance = false ;

}

// 用CAS计算得到的transferIndex

else if (U.compareAndSwapInt

( this , TRANSFERINDEX, nextIndex,

nextBound = (nextIndex > stride ?

nextIndex - stride : 0 ))) {

bound = nextBound;

i = nextIndex - 1 ;

advance = false ;

}

}

if (i < 0 || i >= n || i + n >= nextn) {

int sc;

// 已经完成所有节点复制了

if (finishing) {

nextTable = null ;

table = nextTab; // table 指向nextTable

sizeCtl = (n << 1 ) - (n >>> 1 ); // sizeCtl阈值为原来的1.5倍

return ; // 跳出死循环,

}

// CAS 更扩容阈值,在这里面sizectl值减一,说明新加入一个线程参与到扩容操作

if (U.compareAndSwapInt( this , SIZECTL, sc = sizeCtl, sc - 1 )) {

if ((sc - 2 ) != resizeStamp(n) << RESIZE_STAMP_SHIFT)

return ;

finishing = advance = true ;

i = n; // recheck before commit

}

}

// 遍历的节点为null,则放入到ForwardingNode 指针节点

else if ((f = tabAt(tab, i)) == null )

advance = casTabAt(tab, i, null , fwd);

// f.hash == -1 表示遍历到了ForwardingNode节点,意味着该节点已经处理过了

// 这里是控制并发扩容的核心

else if ((fh = f.hash) == MOVED)

advance = true ; // already processed

else {

// 节点加锁

synchronized (f) {

// 节点复制工作

if (tabAt(tab, i) == f) {

Node<K,V> ln, hn;

// fh >= 0 ,表示为链表节点

if (fh >= 0 ) {

// 构造两个链表 一个是原链表 另一个是原链表的反序排列

int runBit = fh & n;

Node<K,V> lastRun = f;

for (Node<K,V> p = f.next; p != null ; p = p.next) {

int b = p.hash & n;

if (b != runBit) {

runBit = b;

lastRun = p;

}

}

if (runBit == 0 ) {

ln = lastRun;

hn = null ;

}

else {

hn = lastRun;

ln = null ;

}

for (Node<K,V> p = f; p != lastRun; p = p.next) {

int ph = p.hash; K pk = p.key; V pv = p.val;

if ((ph & n) == 0 )

ln = new Node<K,V>(ph, pk, pv, ln);

else

hn = new Node<K,V>(ph, pk, pv, hn);

}

// 在nextTable i 位置处插上链表

setTabAt(nextTab, i, ln);

// 在nextTable i + n 位置处插上链表

setTabAt(nextTab, i + n, hn);

// 在table i 位置处插上ForwardingNode 表示该节点已经处理过了

setTabAt(tab, i, fwd);

// advance = true 可以执行--i动作,遍历节点

advance = true ;

}

// 如果是TreeBin,则按照红黑树进行处理,处理逻辑与上面一致

else if (f instanceof TreeBin) {

TreeBin<K,V> t = (TreeBin<K,V>)f;

TreeNode<K,V> lo = null , loTail = null ;

TreeNode<K,V> hi = null , hiTail = null ;

int lc = 0 , hc = 0 ;

for (Node<K,V> e = t.first; e != null ; e = e.next) {

int h = e.hash;

TreeNode<K,V> p = new TreeNode<K,V>

(h, e.key, e.val, null , null );

if ((h & n) == 0 ) {

if ((p.prev = loTail) == null )

lo = p;

else

loTail.next = p;

loTail = p;

++lc;

}

else {

if ((p.prev = hiTail) == null )

hi = p;

else

hiTail.next = p;

hiTail = p;

++hc;

}

}

// 扩容后树节点个数若<=6,将树转链表

ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :

(hc != 0 ) ? new TreeBin<K,V>(lo) : t;

hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :

(lc != 0 ) ? new TreeBin<K,V>(hi) : t;

setTabAt(nextTab, i, ln);

setTabAt(nextTab, i + n, hn);

setTabAt(tab, i, fwd);

advance = true ;

}

}

}

}

}

}

```

主要涉及到多线程并发扩容,ForwardingNode的作用就是支持扩容操作,将已处理的节点和空节点置为ForwardingNode,并发处理时多个线程经过ForwardingNode就表示已经遍历了,就往后遍历,下图是多线程合作扩容的过程:

![image.png](http://liujun11.cn/upload/2020/09/image-b2eec62f8d924315a7c42140043c16b2.png)

### 总结

看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,总结如下:

1、JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)

2、数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,不需要分段锁的概念和Segment这种数据结构,由于粒度的降低,实现的复杂度增加

3、使用红黑树来优化链表,基于长度很长的链表的遍历是漫长的过程,红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档

4、使用内置锁synchronized来代替重入锁ReentrantLock,因为粒度降低,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了

JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然,大数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存

## IO流

流的定义:流是指一连串流动的字符,是以先进先出方式发送信息的通道。

按流向分:

输出流:OutputStream和Writer为基类

输入流:InputStream和Reader为基类

按处理数据单元划分:字节流:

字节输入流:InputStream基类

字节输出流:OutputStream基类

字符流:字符输入流:Reader基类

字节输出流:Writer基类

(字节流是8位通用字节流,字符流是16位Unicode字符流)

字节(Byte )是计算机信息技术用于bai计量存储容du量的一种计量单位

字符是指计算机中使用的文字和符号

### BIO

BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

在while循环中服务端会调用accept方法等待接收客户端的连接请求,一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成。

如果BIO要能够同时处理多个客户端请求,就必须使用多线程,即每次accept阻塞等待来自客户端请求,一旦受到连接请求就建立通信套接字同时开启一个新的线程来处理这个套接字的数据读写请求,然后立刻又继续accept等待其他客户端连接请求,即为每一个客户端连接请求都创建一个线程来单独处理,大概原理图就像这样:

![image.png](http://liujun11.cn/upload/2020/09/image-ae198f54971f4d4bb3439a210b9a480b.png)

虽然此时服务器具备了高并发能力,即能够同时处理多个客户端请求了,但是却带来了一个问题,随着开启的线程数目增多,将会消耗过多的内存资源,导致服务器变慢甚至崩溃,NIO可以一定程度解决这个问题

### NIO

NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

监听事件:

A. 若服务端监听到客户端连接请求,便为其建立通信套接字(java中就是通道),然后返回继续监听,若同时有多个客户端连接请求到来也可以全部收到,依次为它们都建立通信套接字。

B. 若服务端监听到来自已经创建了通信套接字的客户端发送来的数据,就会调用对应接口处理接收到的数据,若同时有多个客户端发来数据也可以依次进行处理。

C. 监听多个客户端的连接请求和接收数据请求同时还能监听自己时候有数据要发送。

![image.png](http://liujun11.cn/upload/2020/09/image-d20eeb6436ba4ac28871be71be370cae.png)

总之就是在一个线程中就可以调用多路复用接口(java中是select)阻塞同时监听来自多个客户端的IO请求,一旦有收到IO请求就调用对应函数处理。

### AIO

AIO(NIO.2):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

### 应用场景

1、NIO适合处理连接数目特别多,但是连接比较短(轻操作)的场景,Jetty,Mina,ZooKeeper等都是基于java nio实现。

2、BIO方式适用于连接数目比较小且固定的场景,这种方式对服务器资源要求比较高,并发局限于应用中。

3、AIO新的IO2.0,即NIO2.0,jdk1.7开始应用,叫做异步不阻塞的IO。AIO引入异常通道的概念,采用了Proactor模式,简化了程序编写,一个有效的请求才启动一个线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间长的应用。

## 通信协议

设备之间的通信,经常需要自己设计一套通信协议。当然此处的通信协议一般都是建立在TCP/IP协议等协议基础之上的协议,也就是在已有协议的基础之上,在定义一套协议

协议的设计:是为了保证双方能够正常的通信,由于上位设备和下位设备一般是不同的设备,处理能力有很大差异,这些都是设计协议必须考虑的问题

### 协议的构成部分

一套完整的协议,通常包含很多命令,完整的命令(也就是一个完整的数据帧)包含哪些部分,一般包含以下几部分:

1、数据帧的组成的形式: 一般形式有字节流(有的地方也叫做二进制协议)和字符流。字节流一般会规定每一个字节表示的含义,而字符流由于都是可见的字符,一般会规定,字符的含义。

字节流协议难度显然比字符流大很多,解析过程也更复杂,如果没有协议文档,几乎不能解析,因为收到的就是一串毫无意思的数字,一个字节处理错误就完全可能导致整条数据解析错误,此外字节流还需要规定大端模式和小端模式等。当然好处就是,传输同样的信息,数据量明显少很多。

字符流就简单很多,解析的过程可能就是一个正则表达式。

2、数据帧头和尾:数据帧头和尾其实是为了解析数据而设计的,主要是为了获取一个完整的帧。由于网络的不确定性,无法保证一条完整的数据帧的一次性就发送给对方。一般选择用很少出现的字节或者字符作为数据帧头和尾。

3、数据帧的验证部分:对于字节流的协议,一般会规定验证数据帧的验证部分,例如MODE 04 PROTOCOL就规定了数据帧最后两个字节是crc验证部分。对于字符流的也可以加验证字段,但因为每一部分都都是可视的字符,也可以不加。

4、转义字符: 再少出现的字符由于数据帧内容的不确定性,也有可能在数据帧内部出现,例如:MODE 04 PROTOCOL协议当数据帧内部出现了0x03,0x14,也就是数据帧规定的开始部分,就必须转义,否则就会解析出错。导致一个数据帧变成两部分不完整无法理解的数据帧。

5、命令字及其他: 命令字就是标示此数据帧,需要完成的命令,例如:读取时间命令数据帧,就有一部分标示此数据帧是读取时间,设置时间命令数据帧就有一部分标示此数据帧是用来设置时间的,当然会有命令内容例如把时间调整到多少。

6、心跳包: 心跳包作为一条很特殊的数据帧,作用其实和人的心跳类似。每隔一段时间,就会发送一条很特殊的数据帧心跳包。作用就是表明此设备还在工作。

### 协议设计

当需要进行网络通讯时,要想让双方识别对方,就涉及对协议的设计。那么在具体项目中,如何设计协议呢,一般来说,一个基本的数据包协议需要以下部分:

1、协议的标识 2、协议版本号 3、协议包的序号 4、协议包的发出时间

5、协议包的类型 6、协议包的数据长度 7、数据 8、校验码 9、结束符

### 通信层

下面的图表试图显示不同的TCP/IP和其他的协议在最初OSI模型中的位置:

7、应用层:例如HTTP、SMTP、SNMP、FTP、Telnet、SIP、SSH、NFS、RTSP、XMPP、Whois、ENRP

6、表示层:例如XDR、ASN.1、SMB、AFP、NCP

5、会话层:例如ASAP、TLS、SSH、ISO 8327 / CCITT X.225、RPC、NetBIOS、ASP、Winsock、BSD sockets

4、传输层:例如TCP、UDP、RTP、SCTP、SPX、ATP、IL

3、网络层:例如IP、ICMP、IGMP、IPX、BGP、OSPF、RIP、IGRP、EIGRP、ARP、RARP、 X.25

2、数据链路层:例如以太网、令牌环、HDLC、帧中继、ISDN、ATM、IEEE 802.11、FDDI、PPP

1、物理层:例如线路、无线电、光纤、信鸽

### TCP/IP连接

TCP三次握手:为了对每次发送的数据量进行跟踪与协商,确保数据段的发送和接收同步,根据所接收到的数据量而确认数据发送、接收完毕后何时撤消联系,并建立虚连接。

第一次握手:建立连接时,客户端发送 syn(Synchronize Sequence Numbers:同步序列编号)包(seq=j)到服务器,并进入SYN_SEND(请求连接)状态,等待服务器确认。

第二次握手:服务器接收到 syn包,必须确认客户的 SYN(ack=j+1)(ack:确认字符,表示发来的数据已确认接收无误),同时自己也发送一个 syn包(seq=k),既 SYN+ACK 包,此时服务器进入SYN_RECV(发送了ACK)状态。

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

TCP四次挥手(连接终止协议,性质为终止协议):

第一次挥手:TCP客户端发送一个FIN+ACK+SEQ,用来传输关闭客户端到服务端的数据。进入FIN_WAIT1状态。

第二次挥手:服务端收到FIN,被动发送一个ACK(SEQ+1),进入CLOSE_WAIT状态,客户端收到服务端发送的ACK,进入FIN_WAIT2状态。

第三次挥手:服务器关闭客户端连接,发送一个 FIN+ACK+SEQ 给客户端。进入 LAST_ACK 状态。

第四次挥手:客户端发送 ACK(ACK=SQE序号+1)报文确认,客户端进入 TIME_WAIT 状态,服务端收到 ACK 进入 CLOSE状态

由于TCP连接是双向的,因此每个方向都需要单独进行关闭。原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个FIN只意味着这一个方向上没有数据流动,一个 TCP连接到一个 FIN后仍能发送数据。首次执行FIN的一方主动关闭,另一方则执行被动关闭。当只握手两次时,就只会关闭主动发起的一端,另一个仍能发送数据。

### HTTP连接

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

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

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

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

由于HTTP在每次请求结束后都会主动释放连接,因此HTTP连接是一种“短连接”,要保持客户端程序的在线状态,需要不断地向服务器发起连接

请求。通常的做法是即时不需要获得任何数据,客户端也保持每隔一段固定的时间向服务器发送一次“保持连接”的请求,服务器在收到该请求后对客户端进行回

复,表明知道客户端“在线”。若服务器长时间无法收到客户端的请求,则认为客户端“下线”,若客户端长时间无法收到服务器的回复,则认为网络已经断开。

### SOCKET原理

套接字(socket)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。

应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个

TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应

用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

### SOCKET连接

建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket ,另一个运行于服务器端,称为ServerSocket 。

套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。

1、服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。

2、客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。

3、连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描

述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

#### SOCKET连接与TCP/IP连接

创建Socket连接时,可以指定使用的传输层协议,Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。

socket则是对TCP/IP协议的封装和应用(程序员层面上)。也可以说,TPC/IP协议是传输层协议,主要解决数据 如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。关于TCP/IP和HTTP协议的关系,网络有一段比较容易理解的介绍:

传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如

果没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET等,也

可以自己定义应用层协议。WEB使用HTTP协议作应用层协议,以封装HTTP文本信息,然后使用TCP/IP做传输层协议将它发到网络上。”

我们平时说的最多的socket是什么呢,实际上socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。 实际上,Socket跟TCP/IP协议没有必然的联系。Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以说,Socket的出现

只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口,比如create、

listen、connect、accept、send、read和write等等。网络有一段关于socket和TCP/IP协议关系的说法比较容易理解:

“TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。这个就像操作系统会提供标准的编程接口,比如win32编程接口一样,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。”

实际上,传输层的TCP是基于网络层的IP协议的,而应用层的HTTP协议又是基于传输层的TCP协议的,而Socket本身不算是协议,就像上面所说,它只是提供了一个针对TCP或者UDP编程的接口。socket是对端口通信开发的工具,它要更底层一些.

#### Socket连接与HTTP连接

由于通常情况下Socket连接就是TCP连接,因此Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。但在实际网络应用中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket 连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。

而HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。

很多情况下,需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。此时若双方建立的是Socket连接,服务器就可以直接

将数据传送给客户端;若双方建立的是HTTP连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端,因此,客户端定时向服务器端发送连接请

求,不仅可以保持在线,同时也是在“询问”服务器是否有新的数据,如果有就将数据传给客户端。

http协议是应用层的协议

有个比较形象的描述:HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。

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

### 常见知识点

**HTTP响应码:**

【1】200 OK:表示客户端请求成功。

【2】400 Bad Request 语义有误:不能被当前服务器理解。

【3】401 Unauthorized: 当前请求需要用户验证。

【4】403 Forbidden: 服务器收到消息,但是拒绝提供服务。

【5】404 Not Found :请求资源不存在。

【6】408 Request Timeout: 请求超时,客户端没有在服务器预备等待的时间内完成发送。

【7】500 Internal Server Error: 服务器发生不可预期的错误。

【8】503 Server Unavailable :由于临时的服务器维护或过载,服务器当前不能处理请求,此状况知识临时的,可恢复。

**TCP/IP如何保证可靠性:**

1)、三次握手。

2)、将数据截断为合理的长度。应用数据被分割成 TCP 认为最合适发送的数据块。

3)、超时重发。

4)、对于收到的请求,给予确认响应。

5)、如果校验出数据包有错,则丢弃报文段,不响应。

6)、对失序数据进行重新排序,发送于客户端。

7)、能够丢弃重复数据。

8)、流量控制。TCP连接的两端都有缓存大小控制,接收端只允许发送端发送自己缓存剩余大小的数据。有效防止缓存溢出。

9)、拥塞控制。当网络阻塞时,减少数据的发送。

**TCP头的结构:**

1)、源端口(source port)16 位的字段,定义了发送这个报文段的主机中的应用程序的端口号。

2)、目的端口(destination port)16 位的字段,定义了接收这个报文段的主机中的应用程序的端口号。

3)、序列号(sequence number)32 位的字段,定义了指派给本报文段第一个数据字节的编号。为了保证连接性,要发送的每一个字节都要编上号。序号可以告诉终点,报文段中的第一个字节是这个序列中的哪一个字节。在建立连接时,双方使用各自的随机数生成器生产一个初始序号(inital squence number,ISN),通常两个方向上的 ISN 是不同的。

4)、确认号(acknowledgment nimber)32 位字段定义了报文段的接收方期望从对方接收的字节编码。如果报文段的接收方成功地接收了对方发来的编号为x的字节,那么它就返回x+1作为确认号,确认可以和数据捎带在一起发送。

5)、头部长度(Hlen)(header length)这个4字节字段指出TCP段的头部长度,以32位字段来衡量,头部长度并不规定并可以根据选项字段中设置的参数面改变。

6)、保留(reserved)这个保留字段占用6位,它被保留以提供将来使用。

7)、URG 紧急数据(urgent data)---这是一条紧急信息。

8)、ACK 确认已收到段

9)、PSH 请求在缓冲区尚未填满时发送消息,注意TCP可以等待缓冲区填满之后再发送段,如果需要立即传送,应用程序必须利用push参数来通知协议。

10)、RST 申请重置连接。

11)、SYN 此消息用于在建立连接时同步传输数据的计时器。

12)、FIN 该属性申明发送端已经发送出被传输数据的最后一个字节。

13)、窗口大小(window)16位字段,这个字段定义的是发送TCP的窗口大小,以字节为单位。窗口最大长度是65535字节,这个值通常被称为接收窗口(rwnd),并由接收方来决定。这种情况下,发送方必须服从接收方的指示。

14)、校验和(checksum)16位字段包含的是检验和,检验和是差错控制的手段之一。

15)、紧急指针(urgent point)该字段占用2字节,与URG代码位一起使用并且申明及时使存在着缓冲区溢出也必须紧急接收的数据末端。因此,如果有些数据需要不按照顺序被送往目的应用程序,那么发送端的应用程序必须利用紧急数据参数通知TCP。

16)、选项(option)该字段为变长且可以忽略。他的最大长度为3字节,用于解决一些辅助任务----比如,选择最大段长。选项可以位于TCP头部的末端,其长度必须是8的倍数。

17)、填充(padding)该字段长度不固定,这是个用于补充头部字段使得它的长度为32位字的整数倍的一个伪字段9

**HTTP协议的无状态性:**

HTTP协议是无状态的,指的是HTTP协议对于事务处理没有记忆功能,服务器不知道客户端是什么状态。相当于,打开一个服务器上的网页与上一次打开这个服务器上的网页之间没有任何联系。HTTP是一个无状态的面向连接的协议,无状态不代表HTTP不能保持TCP连接,更不代表HTTP使用的是UDP协议(无连接)。

Http请求get和post的区别以及数据包格式:

【1】GET请求可被缓存,POST请求不能被缓存。

【2】GET请求被保留着浏览器历史记录中,POST请求不会被保留。

【3】GET请求能被收藏至书签中,POST请求不能被收藏至书签。

【4】GET请求不应在处理敏感数据时使用,POST可以用户处理敏感数据。

【5】 GET请求有长度限制,POST请求没有长度限制。

【6】POST不限制提交的数据类型,所以POST可以提交文件到服务器。

数据包格式 == TCP头结构

**HTTP 请求的报文格式:**

客户端与服务端通信时传输的内容我们称之为报文。

客户端发送给服务器的称为”请求报文“,服务器发送给客户端的称为”响应报文“。

![image.png](http://liujun11.cn/upload/2020/09/image-2da8017905d0446d9af8a0f247865f88.png)

![image.png](http://liujun11.cn/upload/2020/09/image-b64350064e4f4d929bfa796fec14ce5d.png)

**HTTP的长连接是什么意思:**

长连接是指客户端与服务端建立连接后,不会因完成了一次请求后,它们之间的连接主动关闭。后续的读写操作会继续使用这个链接。 如果一个连接两小时内都没有任何动作,服务器会向客户端发送一个探测报文段、根据客户端主机相应探测4个客户端状态,①、客户端正常时,且服务器可达。此时客户端TCP响应正常,服务器将定时器复位。②、客户端已经崩溃,并且关闭或正在重启,客户端不能响应TCP,服务器将无法收到客户端对探测器的响应。服务器总共发送10个这样的探测,每间隔75秒。如服务器没有收到任何响应,他就认为客户端已经关闭并终止连接。③、客户端崩溃,但已重启。服务器将对其保持探测响应,这个响应是一个复位,使得服务器终止这个连接。④、客户机正常运行,但是服务器不可达。这种与②类似。

由上可以看出,长连接可以省去较多的TCP建立和关闭操作,减少浪费,节约时间。对于频繁请求资源的客户端适合使用长连接。在长连接的应用场景下,client 端一般不会主动关闭连接,当 client 与 server 之间的连接一直不关闭,随着客户端连接越来越多,server 会保持过多连接。这时候 server 端需要采取一些策略,如关闭一些长时间没有请求发生的连接,这样可以避免一些恶意连接导致 server 端服务受损;如果条件允许则可以限制每个客户端的最大长连接数,这样可以完全避免恶意的客户端拖垮整体后端服务,例如:数据库的连接用长连接。

**HTTPS的加密方式是什么**

加密方式:【1】对称密码算法:指加密和解密使用相同的密钥,速度高,可加密内容较大,用来加密会话过程中的消息。典型算法 DES、AES、RC5、IDEA(分组加密)RC4。

【2】非对称密码算法:又称为公钥加密算法,是指加密和解密使用不同的密钥,加密速度较慢,但能提供更好的身份认证技术,用来加密对称加密的密钥(公开的密钥用于加密,私有的密钥用于解密)典型的算法RSA、DSA、DH。

【3】散列算法:将文件内容通过此算法加密变成固定长度的值(散列值),这个过程可以使用密钥也可以不使用。这种散列变化是不可逆的,也就是说不能从散列值编程原文,因此散列变化通道常用语验证原文是否被篡改。典型的算法MD5、SHA、BASE64、CRC等。

注意:SSL协议在建立链路时,SSL首先对对称加密的密钥进行对公加密,链路建立好之后,SSL对传输内容使用对称加密。SSL加密过程:参考15问题图7 双向认证:单向认证作为了解加密过程简化流程。

![image.png](http://liujun11.cn/upload/2020/09/image-cae34fe0b53148debe18a282d51a1de5.png)

第三步:客户端使用服务端返回的信息验证服务器的合法性,包括:

1)、证书是否过期。

2)、发布服务器证书的CA是否可靠。

3)、返回的公钥是否能正确解开返回证书中的数字签名。

4)、服务器证书上的域名是否和服务器实际域名相匹配。

**DNS使用的协议:**

【1】首先了解一下 TCP与 UDP传送字节的长度限制:UDP报文的最大长度为 512字节,而 TCP则允许报文长度超过 512字节。当 DNS查询超过 512字节时,协议的 TC标志出现删除标志,这时则使用 TCP发送。通常传统的 UDP报文一般不会大于512字节。

【2】区域传送时使用TCP,主要有一下两点考虑:辅域名服务器会定时(一般3小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,则会执行一次区域传送,进行数据同步。区域传送将使用 TCP而不是 UDP,因为数据同步传送的数据量比一个请求和应答的数据量要多得多。TCP是一种可靠的连接,保证了数据的准确性。

【3】域名解析时使用 UDP 协议:客户端向 DNS 服务器查询域名,一般返回的内容都不超过 512 字节,用 UDP 传输即可。不用经过 TCP 三次握手,这样 DNS 服务器负载更低,响应更快。虽然从理论上说,客户端也可以指定向 DNS 服务器查询的时候使用 TCP,但事实上,很多 DNS 服务器进行配置的时候,仅支持 UDP 查询包。

**Forward和Redirected的区别:**

1】间接请求转发(Redirect):实际是两次 HTTP请求,服务器端在响应第一次请求的时候,让浏览器再向另外一个 URL发出请求,从而达到转发的目的。一般用于避免用户的非正常访问。例如:用户在没有登录的情况下访问后台资源,Servlet可以将该HTTP请求重定向到登录页面,让用户登录以后再访问。在Servlet中,通过调用response对象的SendRedirect()方法,告诉浏览器重定向访问指定的URL,示例代码如下: 

```java

//Servlet中处理get请求的方法

public void doGet(HttpServletRequest request,HttpServletResponse response){

//请求重定向到另外的资源

response.sendRedirect("资源的URL");

}

```

2】直接请求转发(Forward):客户端和浏览器只发出一次请求,Servlet、HTML、JSP或其它信息资源,由第二个信息资源响应该请求,在请求对象 request中,保存的对象对于每个信息资源是共享的。

## Linux查看系统参数

top命令查看cpu占用

free查看内存占用 free g m单位

ps -ef|grep java 查看java进程

pidstart -u 1 -p 进程编号 查看进行占用cpu

pidstart -p 进程编号 -r 采样间隔参数

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档