五万字15张导图Java自学路线,小白零基础入门,程序员进阶,收藏这篇就够了_小白到计算机程序员学习详细图

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以点击这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

    System.out.println(Test.class.getClassLoader().getParent().getParent());
    System.out.println(Test.class.getClassLoader().getParent());
    System.out.println(Test.class.getClassLoader());
}

}


**输出**



null
null
sun.misc.Launcher E x t C l a s s L o a d e r @ 1 b 6 d 3586 s u n . m i s c . L a u n c h e r ExtClassLoader@1b6d3586 sun.misc.Launcher ExtClassLoader@1b6d3586sun.misc.LauncherAppClassLoader@14dad5dc


因为Object是jdk自带的,所以在加载的时候是走Bootstrap启动类加载器,而Bootstrap加载器是C++语言写的,所以在查的时候是null,报了NullPointException();Test类自己写的,走AppClassLoder,他的父类是扩展加载器,再父类是启动类加载器,也输出Null


**沙箱安全机制**


主要是防止恶意代码污染java源代码,比如定义了一个类名为String所在包为java.lang,因为这个类本来是属于jdk的,如果没有沙箱安全机制的话,这个类将会污染到我所有的String,但是由于沙箱安全机制,所以就委托顶层的bootstrap加载器查找这个类,如果没有的话就委托extsion,extsion没有就到appclassloader,但是由于String就是jdk的源代码,所以在bootstrap那里就加载到了,先找到先使用,所以就使用bootstrap里面的String,后面的一概不能使用,这就保证了不被恶意代码污染。


#### 垃圾回收



> 
> 垃圾回收是重点难,先理解了垃圾回收,才能理解调优的思路。
> 
> 
> 


**判断垃圾**



> 
> 判断是否是垃圾共有两种方法。引用计数法和可达性分析
> 
> 
> 


1.引用计数法


非常好理解,**引用一次标记一次**,没有被标记的就是垃圾。


在堆中存储对象时,在对象头处维护一个`counter`计数器,如果一个对象增加了一个引用与之相连,则将`counter++`。


如果一个引用关系失效则`counter--`。如果一个对象的counter变为0,则说明该对象已经被废弃,不处于存活状态,此时可以被回收。


2.引用计数的缺点


* 效率低
* 无法分析循环引用问题


3.可达性分析


类似**树**的树结构,从根结点出发,即GC root,把有关系的对象用一颗树链接起来


那么我们遍历这棵树,没遍历到的对象,就是垃圾


4.有哪些可以做GC Roots的对象?


* 虚拟机栈(栈桢中的本地变量表)中的引用的对象
* 方法区中的类静态属性引用的对象
* 方法区中的常量引用的对象
* 本地方法栈中JNI(Native方法)的引用的对象


**回收算法**



> 
> 回收算法是垃圾回收的思想,回收器是垃圾回收的实现
> 
> 
> 


1.标记-清除


两次遍历:


* 标记垃圾
* 清除垃圾


优点:


* 不需要格外空间,适合回收对象较少的区域


缺点:


* 效率低,遍历两次,时间复杂度O(n^2)
* 会有线程停顿,`stop the world (STW)`
* 空间碎片,因为垃圾可能不是连续的,大量的空间碎片会导致提前GC,这也是最主要的问题。


2.标记-复制


将空间分为相等大小的两部分,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。牺牲空间解决碎片问题。


优点:


* 高效无碎片


缺点:


* 占用大量空间


3.标记-整理


同样是为了解决空间碎片提出,区别是通过牺牲时间的方式。


和标记-清除类似,不一样的是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。


优点:


* 解决空间碎片的问题
* 不浪费空间


缺点:


* 相对比较耗时


#### 完整讲解



> 
> [JVM完整讲解](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)
> 
> 
> 


### 5.多线程



> 
> 理解多线程,才能更好的理解框架源码,进行高并发的架构设计,重中之重。
> 
> 
> 


![](https://img-blog.csdnimg.cn/img_convert/5c5cc0c509a2e440d4abdc16a3497f1b.png)


#### 并行和并发


并行:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。


并发:多个处理器或多核处理器同时处理多个任务。


举例:


并发 = 两个队列和一台咖啡机。


并行 = 两个队列和两台咖啡机。


#### 线程和进程


一个程序下至少有一个进程,一个进程下至少有一个线程,一个进程下也可以有多个线程来增加程序的执行速度。


#### 守护线程


守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。


#### 创建线程4种方式


* 继承 Thread 重新 run 方法;
* 实现 Runnable 接口;
* 实现 Callable 接口。
* 线程池


#### synchronized 底层实现


synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。


在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。


但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。


#### synchronized 和 volatile 的区别


volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。


volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。


volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。


#### synchronized 和 Lock 区别


synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。


synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁。


lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。


通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。


#### synchronized 和 ReentrantLock 区别


synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。


主要区别如下:


ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;


ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;


ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。


volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。


### 6.设计模式



> 
> 好多人觉得设计模式模式,那是因为你学的还不够深入,还没有看过源码,所以我特意将设计模式往前放了。
> 
> 
> 


![](https://img-blog.csdnimg.cn/img_convert/3a2b51c3b78752409fb55c38266fba69.png)


#### 原型模式


**定义**


官方定义



> 
> 用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。
> 
> 
> 


通俗解读


在需要创建重复的对象,为了保证性能,**本体给外部提供一个克隆体进行使用**。


类似我国的印刷术,省去`new`的过程,通过`copy`的方式创建对象。


**结构图**


![](https://img-blog.csdnimg.cn/img_convert/a3f224bda013edb2de9080a28aa09eb8.png)


**代码实现**


**目录结构**



> 
> 建议跟着一条学设计模式的小伙伴都建一个`maven`工程,并安装`lombok`依赖和插件。
> 
> 
> 并建立如下包目录,便于归纳整理。
> 
> 
> 


![](https://yitiaoit.oss-cn-beijing.aliyuncs.com/img/image-20210920205525607.png)
`pom`如下



<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.10</version>
</dependency>

**开发场景**


假设一条开发了一个替代`Mybatis`的框架,叫`YitiaoBatis`,每次操作数据库,从数据库里面查出很多记录,但是改变的部分是很少的,如果每次查数据库,查到以后把所有数据都封装一个对象,就会导致要`new`很多重复的对象,造成资源的浪费。


一条想到一个解决办法,就是把查过的数据保存起来,下来查相同的数据,直接把保存好的对象返回,也就是**缓存**的思想。


我们用代码模拟一下:


**1.创建`Yitiao`实体类**



/**
* author:一条
*/
@Data
@AllArgsConstructor
public class Yitiao {

private String name;
private Integer id;
private String wechat;

public Yitiao(){
    System.out.println("Yitiao对象创建");
}

}


**2.创建**`YitiaoBatis`类



/**
* author:一条
*/
public class YitiaoBatis {
//缓存Map
private Map<String,Yitiao> yitiaoCache = new HashMap<>();

//从缓存拿对象
public Yitiao getYitiao(String name){
    //判断缓存中是否存在
    if (yitiaoCache.containsKey(name)){
        Yitiao yitiao = yitiaoCache.get(name);
        System.out.println("从缓存查到数据:"+yitiao);
        return yitiao;
    }else {
        //模拟从数据库查数据
        Yitiao yitiao = new Yitiao();
        yitiao.setName(name);
        yitiao.setId(1);
        yitiao.setWechat("公众号:一条coding");
        System.out.println("从数据库查到数据:"+yitiao);
        //放入缓存
        yitiaoCache.put(name,yitiao);
        return yitiao;
    }
}

}


**3.编写测试类**



/**
* author:一条
*/
public class MainTest {
public static void main(String[] args) {
YitiaoBatis yitiaoBatis = new YitiaoBatis();
Yitiao yitiao1 = yitiaoBatis.getYitiao(“yitiao”);
System.out.println(“第一次查询:”+yitiao1);
Yitiao yitiao2 = yitiaoBatis.getYitiao(“yitiao”);
System.out.println(“第二次查询:”+yitiao2);
}
}


**输出结果**


![](https://img-blog.csdnimg.cn/img_convert/bdd8f792647f839a7cb48d07b06b4bf7.png)


从结果可以看出:


* 对象创建了一次,有点**单例**的感觉
* 第一次从数据库查,第二次从缓存查


好像是实现了`YitiaoBatis`框架的需求,思考🤔一下有什么问题呢?


**4.修改对象id**


在测试类继续编写



//执行后续业务,修改id
yitiao2.setId(100);

Yitiao yitiao3 = yitiaoBatis.getYitiao(“yitiao”);
System.out.println(“第三次查询:”+yitiao3);


**输出结果**


![](https://img-blog.csdnimg.cn/img_convert/f82c9c2bf6dbe193e9bd21620d5d6b87.png)


重点看第三次查询,`id=100?`


我们在内存修改的数据,导致从数据库查出来的数据也跟着改变,出现**脏数据**。


怎么解决呢?**原型模式**正式开始。


5.实现`Cloneable`接口


**本体给外部提供一个克隆体进行使用**,在缓存中拿到的对象不直接返回,而是复制一份,这样就保证了不会脏缓存。



public class Yitiao implements Cloneable{

//……

	@Override
protected Object clone() throws CloneNotSupportedException {
			return (Yitiao) super.clone();
}

}


**修改缓存**



//从缓存拿对象
public Yitiao getYitiao(String name) throws CloneNotSupportedException {
//判断缓存中是否存在
if (yitiaoCache.containsKey(name)){
Yitiao yitiao = yitiaoCache.get(name);
System.out.println(“从缓存查到数据:”+yitiao);
//修改返回
//return yitiao;
return yitiao.clone();
}else {
//模拟从数据库查数据
Yitiao yitiao = new Yitiao();
yitiao.setName(name);
yitiao.setId(1);
yitiao.setWechat(“公众号:一条coding”);
System.out.println(“从数据库查到数据:”+yitiao);
//放入缓存
yitiaoCache.put(name,yitiao);
//修改返回
//return yitiao;
return yitiao.clone();
}


6.再次测试


不用改测试类,直接看一下结果:


![](https://img-blog.csdnimg.cn/img_convert/5d09349f55d19286b69a3ff29b67837d.png)


从输出结果可以看出第三次查询`id`依然是`1`,没有脏缓存现象。


基于原型模式的克隆思想,我可以**快速**拿到和「本体」一模一样的「克隆体」,而且对象也只被`new`了一次。


不知道大家是否好奇对象是怎么被创建出来的,那我们就一起看一下「深拷贝」和「浅拷贝」是怎么回事。


**深拷贝和浅拷贝**


定义



> 
> **深拷贝**:不管拷贝对象里面是基本数据类型还是引用数据类型都是完全的复制一份到新的对象中。
> 
> 
> **浅拷贝**:当拷贝对象只包含简单的数据类型比如int、float 或者不可变的对象(字符串)时,就直接将这些字段复制到新的对象中。而引用的对象并没有复制而是将引用对象的地址复制一份给克隆对象。
> 
> 
> 


好比两个兄弟,**深拷贝**是年轻的时候关系特别好,衣服买一样的,房子住一块。**浅拷贝**是长大了都成家立业,衣服可以继续买一样的,但房子必须要分开住了。


实现



> 
> 在代码上区分深拷贝和浅拷贝的方式就是看引用类型的变量在修改后,值是否发生变化。
> 
> 
> 


**浅拷贝**


![](https://yitiaoit.oss-cn-beijing.aliyuncs.com/img/image-20210921005117224.png)
1.通过`clone()`方式的浅拷贝


新建`Age`类,作为`Yitiao`的引用属性



@Data
@AllArgsConstructor
@NoArgsConstructor
public class Age {
private int age;
}


2.测试1



public static void main(String[] args) throws CloneNotSupportedException {
Yitiao yitiao1 = new Yitiao();
Age age = new Age(1);
yitiao1.setAge(age);
yitiao1.setId(1);
Yitiao clone = yitiao1.clone();
yitiao1.setId(2);
age.setAge(2); //不能new一个age
System.out.println(“yitiao1:\n”+yitiao1+“\nclone:\n”+clone);
}


输出结果


![](https://img-blog.csdnimg.cn/img_convert/cc7635f8afc396d98566c0a005bf9bb9.png)


结论:基本类型`id`没发生改变,引用类型`Age`由于**地址指向的同一个对象**,值跟随变化。


3.通过构造方法实现浅拷贝


`Yitiao.class`增加构造方法



public Yitiao(Yitiao yitiao){
    id=yitiao.id;
    age=yitiao.age;
}

4.测试2



    Yitiao yitiao1 = new Yitiao();
    Age age = new Age(1);
    yitiao1.setAge(age);
    yitiao1.setId(1);
    Yitiao clone = new Yitiao(yitiao1);  //差别在这
    yitiao1.setId(2);
    age.setAge(2);
    System.out.println("yitiao1:\n"+yitiao1+"\nclone:\n"+clone);

输出结果


![](https://img-blog.csdnimg.cn/img_convert/760b8afde8f14b46d9aa5d4d9dacb16f.png)


与**测试1**无异


**深拷贝**


![image-20210921005209828](https://yitiaoit.oss-cn-beijing.aliyuncs.com/img/image-20210921005209828.png)
1.通过对象序列化实现深拷贝



> 
> 通过层次调用clone方法也可以实现深拷贝,但是代码量太大。特别对于属性数量比较多、层次比较深的类而言,每个类都要重写clone方法太过繁琐。一般不使用,亦不再举例。
> 
> 
> 可以通过将对象序列化为字节序列后,默认会将该对象的整个对象图进行序列化,再通过反序列即可完美地实现深拷贝。
> 
> 
> 


`Yitiao`和`Age`实现Serializable接口


2.测试



//通过对象序列化实现深拷贝
Yitiao yitiao = new Yitiao();
Age age = new Age(1);
yitiao.setAge(age);
yitiao.setId(1);
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(yitiao);
oos.flush();
ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Yitiao clone = (Yitiao) ois.readObject();
yitiao.setId(2);
age.setAge(2);
System.out.println(“yitiao:\n”+yitiao+“\nclone:\n”+clone);


输出结果


![](https://img-blog.csdnimg.cn/img_convert/f265d6157e6ab07c680dfbafea571259.png)


结论,引用对象也完全复制一个新的,值不变化。


不过要注意的是,如果某个属性被`transient`修饰,那么该属性就无法被拷贝了。


应用场景



> 
> 我们说回**原型模式**。
> 
> 
> 原型模式在我们的代码中是很常见的,但是又容易被我们所忽视的一种模式,比如我们常用的的`BeanUtils.copyProperties`就是一种对象的**浅拷贝**。
> 
> 
> 看看有哪些场景需要原型模式
> 
> 
> 


* 资源优化
* 性能和安全要求
* 一个对象多个修改者的场景。
* 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时可以考虑使用原型模式拷贝多个对象供调用者使用。


原型模式已经与 Java 融为浑然一体,可以随手拿来使用。


#### 更多设计模式



> 
> [更多设计模式](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)
> 
> 
> 


### 7.SSM框架



> 
> 这对于初学者来说,是一个坎,前几年学完这些,已经可以开始找工作了,所以恭喜你能坚持带这里,胜利就在前方。
> 
> 
> 


![](https://img-blog.csdnimg.cn/img_convert/670cfe2bd1939ea4fae5c0a094aaa433.png)


#### ORM 框架?


ORM(Object Relation Mapping)对象关系映射,是把数据库中的关系数据映射成为程序中的对象。


使用 ORM 的优点:提高了开发效率降低了开发成本、开发更简单更对象化、可移植更强。


#### MyBatis 中 #{}和 的区别


#是预编译处理,{}的区别是什么?#{}是预编译处理,的区别是什么?#是预编译处理,{}是字符替换。 在使用 #{}时,MyBatis 会将 SQL 中的 #{}替换成“?”,配合 PreparedStatement 的 set 方法赋值,这样可以有效的防止 SQL 注入,保证程序的运行安全。


#### 什么是Spring


spring 提供 ioc 技术,容器会帮你管理依赖的对象,从而不需要自己创建和管理依赖对象了,更轻松的实现了程序的解耦。


spring 提供了事务支持,使得事务操作变的更加方便。


spring 提供了面向切片编程,这样可以更方便的处理某一类的问题。


更方便的框架集成,spring 可以很方便的集成其他框架,比如 MyBatis、hibernate 等。


#### 什么是 aop


aop 是面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。


简单来说就是统一处理某一“切面”(类)的问题的编程思想,比如统一处理日志、异常等。


#### 什么是 ioc


ioc:Inversionof Control(中文:控制反转)是 spring 的核心,对于 spring 框架来说,就是由 spring 来负责控制对象的生命周期和对象间的关系。


简单来说,控制指的是当前对象对内部成员的控制权;控制反转指的是,这种控制权不由当前对象管理了,由其他(类,第三方容器)来管理。


#### spring mvc 运行流程


spring mvc 先将请求发送给 DispatcherServlet。


DispatcherServlet 查询一个或多个 HandlerMapping,找到处理请求的 Controller。


DispatcherServlet 再把请求提交到对应的 Controller。


Controller 进行业务逻辑处理后,会返回一个ModelAndView。


Dispathcher 查询一个或多个 ViewResolver 视图解析器,找到 ModelAndView 对象指定的视图对象。  
 视图对象负责渲染返回给客户端。


#### 什么是 spring boot?


spring boot 是为 spring 服务的,是用来简化新 spring 应用的初始搭建以及开发过程的。


#### 为什么要用 spring boot?


配置简单


独立运行


自动装配


无代码生成和 xml 配置


提供应用监控


易上手


提升开发效率


### 8.Redis



> 
> 随着QPS的逐渐升高,传统的mysql数据库已经无法满足。所以有了基于内存的redis缓存数据库来存储热点数据。
> 
> 
> 


![](https://img-blog.csdnimg.cn/img_convert/f4ffac7c290b3319559a423bed15d414.png)


#### 什么是Redis


Redis 是一个使用 C 语言开发的高速缓存数据库。


Redis 使用场景:


* 记录帖子点赞数、点击数、评论数;
* 缓存近期热帖;
* 缓存文章详情信息;
* 记录用户会话信息。


#### Redis 的功能


* 数据缓存功能
* 分布式锁的功能
* 支持数据持久化
* 支持事务
* 支持消息队列


#### Redis 和 memcache


存储方式不同:memcache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小;Redis 有部份存在硬盘上,这样能保证数据的持久性。


数据支持类型:memcache 对数据类型支持相对简单;Redis 有复杂的数据类型。


使用底层模型不同:它们之间底层实现方式,以及与客户端之间通信的应用协议不一样,Redis 自己构建了 vm 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。


value 值大小不同:Redis 最大可以达到 1gb;memcache 只有 1mb。


#### Redis 为什么是单线程的


因为 cpu 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现,而且 cpu 又不会成为瓶颈,那就顺理成章地采用单线程的方案了。


关于 Redis 的性能,官方网站也有,普通笔记本轻松处理每秒几十万的请求。


而且单线程并不代表就慢 nginx 和 nodejs 也都是高性能单线程的代表。


#### 缓存穿透


缓存穿透:指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。


解决方案:最简单粗暴的方法如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们就把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。


#### Redis 数据类型


Redis 支持的数据类型:string(字符串)、list(列表)、hash(字典)、set(集合)、zset(有序集合)。


### 9.Zookeeper



> 
> Zookeeper作为统一配置文件管理和集群管理框架,是后续学习其他框架的基础,在微服务中,还可以用来做注册中心。
> 
> 
> 


![](https://img-blog.csdnimg.cn/img_convert/875c9e469468b09e55033abc7045d77f.png)


#### 什么是zookeeper


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


#### zookeeper 的功能


集群管理:监控节点存活状态、运行请求等。


主节点选举:主节点挂掉了之后可以从备用的节点开始新一轮选主,主节点选举说的就是这个选举的过程,使用 zookeeper 可以协助完成这个过程。


分布式锁:zookeeper 提供两种锁:独占锁、共享锁。独占锁即一次只能有一个线程使用资源,共享锁是读锁共享,读写互斥,即可以有多线线程同时读同一个资源,如果要使用写锁也只能有一个线程使用。zookeeper可以对分布式锁进行控制。


命名服务:在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。


#### zookeeper 的部署模式


zookeeper 有三种部署模式:


单机部署:一台集群上运行;


集群部署:多台集群运行;


伪集群部署:一台集群启动多个 zookeeper 实例运行。


### 10.Kafka


![](https://img-blog.csdnimg.cn/img_convert/a72ca29ed2e10abf8cfea172ccef4862.png)


#### kafka和zookeeper的关系


kafka 不能脱离 zookeeper 单独使用,因为 kafka 使用 zookeeper 管理和协调 kafka 的节点服务器。


#### kafka的数据保留的策略?


kafka 有两种数据保存策略:按照过期时间保留和按照存储的消息大小保留。


kafka 同时设置了 7 天和 10G 清除数据,到第五天的时候消息达到了 10G,这个时候 kafka 将如何处理?  
 这个时候 kafka 会执行数据清除工作,时间和大小不论那个满足条件,都会清空数据。


#### kafka性能瓶颈


cpu 性能瓶颈


磁盘读写瓶颈


网络瓶颈


#### kafka集群


集群的数量不是越多越好,最好不要超过 7 个,因为节点越多,消息复制需要的时间就越长,整个群组的吞吐量就越低。


集群数量最好是单数,因为超过一半故障集群就不能用了,设置为单数容错率更高。


### 11.ES



> 
> elasticsearch简写es,es是一个高扩展、开源的全文检索和分析引擎,它可以准实时地快速存储、搜索、分析海量的数据。
> 
> 
> 


### 12.Dubbo



> 
> Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。简单的说,dubbo就是个服务框架
> 
> 
> 


### 13.SpringCloud



> 
> Spring Cloud是一个微服务框架。Spring Cloud提供了全套的分布式系统解决方案,不仅对微服务基础框架Netflix的多个开源组件进行了封装,同时还实现了和云端平台以及Spring Boot开发框架的集成。
> 
> 
> 


#### 什么是 spring cloud


spring cloud 是一系列框架的有序集合。它利用 spring boot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 spring boot 的开发风格做到一键启动和部署。


#### spring cloud 的核心组件


Eureka:服务注册于发现。


Feign:基于动态代理机制,根据注解和选择的机器,拼接请求 url 地址,发起请求。


Ribbon:实现负载均衡,从一个服务的多台机器中选择一台。


Hystrix:提供线程池,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题。


Zuul:网关管理,由 Zuul 网关转发请求给对应的服务。


#### 断路器的作用


在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。


### 14.Nginx



> 
> Nginx是一个高性能的HTTP和反向代理服务器。具有占内存少和并发能力强的特点。
> 
> 
> 


### 15.Netty



> 
> netty 是一个基于nio的客户、服务器端编程框架,netty提供异步的,事件驱动的网络应用程序框架和工具,可以快速开发高可用的客户端和服务器。
> 
> 
> 


### 16.架构设计



> 
> 好的架构从来不是设计出来的,而是演变出来的。所以我们在日常开发中就要不断思考性能的优化。
> 
> 
> 


下面拿如何设计一个百万人抽奖系统举例说明架构的演进。


#### V0——单体架构



> 
> 如果现在让你实现几十人的抽奖系统,简单死了吧,直接重拳出击!
> 
> 
> 


两猫一豚走江湖,中奖入库,调通知服务,查库通知,完美!


![](https://img-blog.csdnimg.cn/img_convert/210d3f27cf93bacf19f41968e961d03b.png)


相信大家学java时可能都做过这种案例,思考🤔一下存在什么问题?


* 单体服务,一着不慎满盘皆输
* 抽了再抽,一个人就是一支军队
* 恶意脚本,没有程序员中不了的奖


接下来就聊聊怎么解决这些问题?


#### V1——负载均衡



> 
> 当一台服务器的单位时间内的访问量越大时,服务器压力就越大,大到超过自身承受能力时,服务器就会崩溃。
> 
> 
> 为了避免服务器崩溃,让用户有更好的体验,我们通过负载均衡的方式来分担服务器压力。
> 
> 
> 


负载均衡就是建立很多很多服务器,组成一个服务器集群,当用户访问网站时,先访问一个中间服务器,好比管家,由他在服务器集群中选择一个压力较小的服务器,然后将该访问请求引入该服务器。


如此以来,用户的每次访问,都会保证服务器集群中的每个服务器压力趋于平衡,分担了服务器压力,避免了服务器崩溃的情况。


负载均衡是用「反向代理」的原理实现的。具体负载均衡算法及其实现方式我们下文再续。


![](https://img-blog.csdnimg.cn/img_convert/cec80f77c1ab7d567bab4cb0dffc6076.png)


负载均衡虽然解决了单体架构一着不慎满盘皆输的问题,但服务器成本依然不能保护系统周全,我们必须想好一旦服务器宕机,如何保证用户的体验。


即如何缓解开奖一瞬间时的大量请求。


#### V2——服务限流



> 
> 限流主要的作用是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量实效),造成不可用。
> 
> 
> 还可用于平滑请求。
> 
> 
> 


在上一小节我们做好了负载均衡来保证集群的可用性,但公司需要需要考虑服务器的成本,不可能无限制的增加服务器数量,一般会经过计算保证日常的使用没问题。


限流的意义就在于我们**无法预测未知流量**,比如刚提到的抽奖可能遇到的:


* 重复抽奖
* 恶意脚本


其他一些场景:


* 热点事件(微博)
* 大量爬虫


这些情况都是无法预知的,不知道什么时候会有10倍甚至20倍的流量打进来,如果真碰上这种情况,扩容是根本来不及的(弹性扩容都是虚谈,一秒钟你给我扩一下试试)


明确了限流的意义,我们再来看看如何实现限流


**防止用户重复抽奖**



> 
> 重复抽奖和恶意脚本可以归在一起,同时几十万的用户可能发出几百万的请求。
> 
> 
> 


![](https://img-blog.csdnimg.cn/img_convert/216a4f67e5a7c71be444201cf4b2de37.png)


如果同一个用户在1分钟之内多次发送请求来进行抽奖,就认为是恶意重复抽奖或者是脚本在刷奖,这种流量是不应该再继续往下请求的,在负载均衡层给直接屏蔽掉。


可以通过`nginx`配置`ip`的访问频率,或者在在网关层结合`sentinel`配置限流策略。


用户的抽奖状态可以通过redis来存储,后面会说。


**拦截无效流量**



> 
> 无论是抽奖还是秒杀,奖品和商品都是有限的,所以后面涌入的大量请求其实都是无用的。
> 
> 
> 


举个例子,假设50万人抽奖,就准备了100台手机,那么50万请求瞬间涌入,其实前500个请求就把手机抢完了,后续的几十万请求就没必要让他再执行业务逻辑,直接暴力拦截返回抽奖结束就可以了。


同时前端在按钮置灰上也可以做一些文章。


那么思考一下如何才能知道奖品抽完了呢,也就是库存和订单之前的数据同步问题。


**服务降级和服务熔断**



> 
> 有了以上措施就万无一失了吗,不可能的。所以再服务端还有降级和熔断机制。
> 
> 
> 在此简单做个补充,详细内容请持续关注作者。
> 
> 
> 


有好多人容易混淆这两个概念,通过一个小例子让大家明白:


假设现在一条粉丝数突破100万,冲上微博热搜,粉丝甲和粉丝乙都打开微博观看,但甲看到了一条新闻发布会的内容,乙却看到”系统繁忙“,过了一会,乙也能看到内容了。


(请允许一条幻想一下😎)


在上述过程中,首先是热点时间造成大量请求,发生了服务熔断,为了保证整个系统可用,牺牲了部分用户乙,乙看到的”系统繁忙“就是服务降级(fallback),过了一会有恢复访问,这也是熔断器的一个特性(hystrix)


#### V3 同步状态



> 
> 接着回到上一节的问题,如何同步抽奖状态?
> 
> 
> 


这不得不提到`redis`,被广泛用于高并发系统的缓存数据库。


我们可以基于Redis来实现这种共享抽奖状态,它非常轻量级,很适合两个层次的系统的共享访问。


当然其实用ZooKeeper也是可以的,在负载均衡层可以基于zk客户端监听某个znode节点状态。一旦抽奖结束,抽奖服务更新zk状态,负载均衡层会感知到。


![](https://img-blog.csdnimg.cn/img_convert/32fe00c8c4c45638197bfe4766028f43.png)


#### V4线程优化



> 
> 对于线上环境,工作线程数量是一个至关重要的参数,需要根据自己的情况调节。
> 
> 
> 


众所周知,对于进入Tomcat的每个请求,其实都会交给一个独立的工作线程来进行处理,那么Tomcat有多少线程,就决定了并发请求处理的能力。


但是这个线程数量是需要经过压测来进行判断的,因为每个线程都会处理一个请求,这个请求又需要访问数据库之类的外部系统,所以不是每个系统的参数都可以一样的,需要自己对系统进行压测。


但是给一个经验值的话,Tomcat的线程数量不宜过多。因为线程过多,普通服务器的CPU是扛不住的,反而会导致机器CPU负载过高,最终崩溃。


同时,Tomcat的线程数量也不宜太少,因为如果就100个线程,那么会导致无法充分利用Tomcat的线程资源和机器的CPU资源。


所以一般来说,Tomcat线程数量在200~500之间都是可以的,但是具体多少需要自己压测一下,不断的调节参数,看具体的CPU负载以及线程执行请求的一个效率。


在CPU负载尚可,以及请求执行性能正常的情况下,尽可能提高一些线程数量。


但是如果到一个临界值,发现机器负载过高,而且线程处理请求的速度开始下降,说明这台机扛不住这么多线程并发执行处理请求了,此时就不能继续上调线程数量了。


![](https://img-blog.csdnimg.cn/img_convert/ec85d18b1ef9f7c63fc5fc174da4d9ad.png)


#### V5业务逻辑



> 
> 抽奖逻辑怎么做?
> 
> 
> 


好了,现在该研究一下怎么做抽奖了


在负载均衡那个层面,已经把比如50万流量中的48万都拦截掉了,但是可能还是会有2万流量进入抽奖服务。


因为抽奖活动都是临时服务,可以阿里云租一堆机器,也不是很贵,tomcat优化完了,服务器的问题也解决了,还剩啥呢?


Mysql,是的,你的Mysql能抗住2万的并发请求吗?


答案是很难,怎么办呢?


把Mysql给替换成redis,单机抗2万并发那是很轻松的一件事情。


而且redis的一种数据结构`set`很适合做抽奖,可以随机选择一个元素并剔除。


![](https://img-blog.csdnimg.cn/img_convert/00e0cad49fb13ba0100b013b39afb717.png)


#### V6流量削峰



> 
> 由上至下,还剩中奖通知部分没有优化。
> 
> 
> 


思考这个问题:假设抽奖服务在2万请求中有1万请求抽中了奖品,那么势必会造成抽奖服务对礼品服务调用1万次。


那也要和抽奖服务同样处理吗?


其实并不用,因为发送通知不要求及时性,完全可以让一万个请求慢慢发送,这时就要用到消息中间件,进行限流削峰。


也就是说,抽奖服务把中奖信息发送到MQ,然后通知服务慢慢的从MQ中消费中奖消息,最终完成完礼品的发放,这也是我们会延迟一些收到中奖信息或者物流信息的原因。


假设两个通知服务实例每秒可以完成100个通知的发送,那么1万条消息也就是延迟100秒发放完毕罢了。


同样对MySQL的压力也会降低,那么数据库层面也是可以抗住的。


看一下最终结构图:


![](https://img-blog.csdnimg.cn/img_convert/fda84c9dbc7fc9efdb5b11e1af10d25d.png)


### 17.Linux



> 
> 作为Java程序员,不会用Linux会让人笑掉大牙的。我们不必像运维兄弟一样精通,基本的命令还是要熟练掌握。
> 
> 
> 


![](https://img-blog.csdnimg.cn/img_convert/a43edcfa329f3415e263bf92ad69cbd6.png)


* 理解一切皆文件
* 文件操作命令
* 权限管理命令
* 网络命令
* 系统磁盘命令
* ……


### 18.Git



> 
> 网上经常传出不会git被开除的新闻,所以还不学起来?
> 
> 
> 


#### 命令大全



> 
> [200条Git命令大全](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)
> 
> 
> 


### 19.数据结构和算法



> 
> 问一下各位什么是程序?——数据结构+算法。所以,学吧,刷题吧
> 
> 
> 


#### 排序算法



> 
> 《八大排序》源码](https://pan.baidu.com/s/1woTgwkVUT1xtgMB1ha36Uw),提取码:5ehp
> 
> 
> 


![](https://yitiaoit.oss-cn-beijing.aliyuncs.com/img/image-20210923184932295.png)
**准备**



> 
> 古语云:“兵马未动,粮草先行”。想跟着一条一块把「排序算法」弄明白的,建议先准备好以下代码模板。
> 
> 
> 📢 观看本教程需知道基本**循环语法**、**两数交换**、**双指针**等前置知识。
> 
> 
> 📚 建议先看完**代码**和**逐步分析**后再尝试自己写。
> 
> 
> 


* 新建一个`Java`工程,本文全篇也基于Java语言实现代码。
* 建立如下目录结构


![](https://img-blog.csdnimg.cn/img_convert/4f9e4d2eb1785154a202d4bf28f2888e.png)


* 在`MainTest`测试类中编写测试模板。



/**
* 测试类
* Author:一条
* Date:2021/09/23
*/
public class MainTest {
public static void main(String[] args) {
//待排序序列
int[] array={6,10,4,5,2,8};
//调用不同排序算法
// BubbleSort.sort(array);

    // 创建有100000个随机数据的数组
    int[] costArray=new int[100000];
    for (int i = 0; i < 100000; i++) {
        // 生成一个[0,100000) 的一个数
        costArray[i] = (int) (Math.random() \* 100000);
    }

    Date start = new Date();
    //过长,先注释掉逐步打印
			//BubbleSort.sort(costArray);
    Date end = new Date();
    System.out.println("耗时:"+(end.getTime()-start.getTime())/1000+"s");
}

}


该段代码内容主要有两个功能:


* 调用不同的排序算法进行测试
* 测试不同排序算法将`10w`个数排好序需要的时间。更加具象的理解**时间复杂度**的不同


1.冒泡排序


基本思想


通过对乱序序列从前向后遍历,依次**比较**相邻元素的值,若发现逆序则**交换**,使值较大的元素逐渐从前移向后部。


像水底下的气泡一样逐渐向上冒一样。


![](https://img-blog.csdnimg.cn/img_convert/77493b8d8371c7d97d88a47a4cc2e140.png)


动图讲解


![](https://img-blog.csdnimg.cn/img_convert/42bdb7d657e289b6c90677ce2554b5e9.gif)


代码实现



> 
> 不理解的小伙伴可以用`debug`模式逐步分析。
> 
> 
> 



/**
* 冒泡排序
* Author:一条
* Date:2021/09/23
*/
public class BubbleSort{
public static int[] sort(int[] array){
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array.length-1; j++) {
//依次比较,将最大的元素交换到最后
if (array[j]>array[j+1]){
// 用临时变量temp交换两个值
int temp=array[j];
array[j]=array[j+1];
array[j+1]=temp;
}
}
//输出每一步的排序结果
System.out.println(Arrays.toString(array));
}
return array;
}
}


**输出结果**


![](https://img-blog.csdnimg.cn/img_convert/d0bd0f950d1c68e65b8dbc225cf6a4ed.png)


**逐步分析**


1. 初始数组:`[6,10,4,5,2,8]`
2. `6`拿出来和后一个`10`比较,`6<10`,不用交换。- > `j++;`
3. `10`拿出来和后一个`4`比较,`10>4`,交换。- > `[6,4,10,5,2,8]`
4. 依次执行`j++`与后一个**比较交换**。
5. 第一层`i`循环完,打印第一行- > `[6, 4, 5, 2, 8, 10]`,此时最后一位`10`在正确位置上。 - > `i++`
6. 从`4`开始,继续**比较交换**,倒数第二位`8`回到正确位置。
7. 如上循环下去 - > ……
8. 最终结果 - > `[2, 4, 5, 6, 8, 10]`


这时再回去看动图理解。


耗时测试



> 
> 记得先注释掉排序类逐步打印代码。
> 
> 
> 


![](https://img-blog.csdnimg.cn/img_convert/4357548ea241c522dac55d13f28eb15f.png)


**时间复杂度**:`O(n^2)`


**算法优化**


**优化点一**


外层第一次遍历完,最后一位已经是正确的,`j`就不需要再比较,所以结束条件应改为`j-i-1;`。


**优化点二**


因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志`flag`判断元素是否进行过交换。从而减少不必要的比较。


![](https://img-blog.csdnimg.cn/img_convert/ab913ca7c526dafea2f91c546b47b362.png)


**优化代码**



public static int[] sortPlus(int[] array){
System.out.println(“优化冒泡排序开始----------”);
for (int i = 0; i < array.length; i++) {
boolean flag=false;
for (int j = 0; j < array.length-i-1; j++) {
if (array[j]>array[j+1]){
flag=true;
int temp=array[j];
array[j]=array[j+1];
array[j+1]=temp;
}
}
if (flag==false){
break;
}
// System.out.println(Arrays.toString(array));
}
return array;
}


**优化测试**


![](https://img-blog.csdnimg.cn/img_convert/39e7c86723ed6fcfca42e441af667f86.png)


通过基础测试看到当序列已经排好序,即不发生交换后终止循环。


![](https://img-blog.csdnimg.cn/img_convert/106715a949b139e3b15343a552eea913.png)


耗时测试由`27s`优化到`17s`。


2.选择排序


基本思想


选择排序和冒泡排序很像,是从乱序序列的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。


动图讲解


![](https://img-blog.csdnimg.cn/img_convert/89a50336d796dc1c5a1e94b6490e9b10.gif)


代码实现



public class SelectSort {
public static int[] sort(int[] array) {
System.out.println(“选择排序开始----------”);
for (int i = 0; i < array.length; i++) {
//每个值只需与他后面的值进行比较,所以从开始
for (int j = i; j < array.length; j++) {
//注意此处是哪两个值比较
if (array[i]>array[j]){
int temp=array[i];
array[i]=array[j];
array[j]=temp;
}
}
System.out.println(Arrays.toString(array));
}
return array;
}
}


**输出结果**


![](https://img-blog.csdnimg.cn/img_convert/fa10f1d9e40b40ea42b932bad41dbc96.png)


**逐步分析**


* 初始数组:`[6,10,4,5,2,8]`
* 拿出`6`与`10`比较,不交换 - > `j++`
* `6`与`2`比较,交换 - > `j++`
* 注意此时是拿`2`继续比较,都不交换,确定第一位(最小的数)为`2` - > `i++`
* 循环下去,依次找到第一小,第二小,……的数
* 最终结果 - > `[2, 4, 5, 6, 8, 10]`


这时再回去看动图理解。


耗时测试


![](https://img-blog.csdnimg.cn/img_convert/2a1554d369400622fcc8119d8e452e7b.png)


时间复杂度:`O(n^2)`


算法优化


上诉代码中使用**交换**的方式找到较小值,还可以通过**移动**的方式,即全部比较完只交换一次。


这种对空间的占有率会有些增益,但对时间的增益几乎没有,可忽略,亦不再演示。


3.插入排序


基本思想


把n个乱序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中通过不断往有序表插入元素,获取一个局部正确解,逐渐扩大有序序列的长度,直到完成排序。


动图讲解


![2021-09-25 19.20.05](https://img-blog.csdnimg.cn/img_convert/733a0824979f2928d446a864a1f8432a.gif)


代码实现



/**
* 插入排序
* Author:一条
* Date:2021/09/23
*/
public class InsertSort {
public static void sort(int[] array) {
for (int i = 1; i < array.length; i++) {
//插入有序序列,且将有序序列扩大
for (int j = i; j > 0; j–) {
if (array[j]>array[j-1]){
int temp=array[j];
array[j]=array[j-1];
array[j-1]=temp;
}
}
// System.out.println(Arrays.toString(array));
}
}
}


**输出结果**


![](https://img-blog.csdnimg.cn/img_convert/5c747b80a438c4b87bc18dd39fe96c43.png)


耗时测试


![](https://img-blog.csdnimg.cn/img_convert/feb41fa6adcfe08f1d6e2f9d6c23dcbf.png)


算法优化


见下方**希尔排序**,就是希尔对**插入排序**的优化。


4.希尔排序



> 
> 希尔排序是插入排序的一个优化,思考往`[2,3,4,5,6]`中插入`1`,需要将所有元素的位置都移动一遍,也就是说在某些极端情况下效率不高,也称该算法**不稳定**。
> 
> 
> 希尔排序是插入排序经过改进之后的一个更高效的版本,也称为**缩小增量排序**。
> 
> 
> 


基本思想


希尔排序是把记录按下标的一定增量分组,对每组使用插入排序;


随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个序列恰被分成一组,算法便终止。


和插入排序一样,从局部到全部,希尔排序是局部再局部。


动图讲解


![图片源于网络](https://img-blog.csdnimg.cn/img_convert/f35cd1d55d6fa0492cbfc69cb10125ac.gif)


代码实现



/**
* 希尔排序
* Author:一条
* Date:2021/09/23
*/
public class ShellSort {
public static void sort(int[] array) {
System.out.println(“希尔排序开始--------”);
//gap初始增量=length/2 逐渐缩小:gap/2
for (int gap = array.length/2; gap > 0 ; gap/=2) {
//插入排序 交换法
for (int i = gap; i < array.length ; i++) {
int j = i;
while(j-gap>=0 && array[j]<array[j-gap]){
//插入排序采用交换法
int temp = array[j];
array[j]=array[j-gap];
array[j-gap]=temp;
j-=gap;
}
}
System.out.println(Arrays.toString(array));
}
}
}


**输出结果**


![](https://img-blog.csdnimg.cn/img_convert/e5c0b5f9d1dd774401530ddb9fd75ba9.png)


耗时测试


![](https://img-blog.csdnimg.cn/img_convert/077e6e33fe5a02b0acfeb241ae8df8b8.png)


算法优化


无


5.快速排序



> 
> 快速排序(Quicksort)是对冒泡排序的一种改进,相比冒泡排序,每次的交换都是跳跃式的。
> 
> 
> 


基本思想


将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。


体现出**分治**的思想。


动图讲解


![](https://img-blog.csdnimg.cn/img_convert/29812b621453b7451ee23e6ad2d3ecad.gif)


代码实现



> 
> 思路如下:
> 
> 
> * 首先在这个序列中找一个数作为**基准数**,为了方便可以取第一个数。
> * 遍历数组,**将小于基准数的放置于基准数左边,大于基准数的放置于基准数右边**。此处可用双指针实现。
> * 此时基准值把数组分为了两半,**基准值算是已归位(找到排序后的位置)**。
> * 利用**递归**算法,对分治后的子数组进行排序。
> 
> 
> 



public class QuickSort {
public static void sort(int[] array) {
System.out.println(“快速排序开始---------”);
mainSort(array, 0, array.length - 1);
}

private static void mainSort(int[] array, int left, int right) {
    if(left > right) {
        return;
    }
    //双指针
    int i=left;
    int j=right;
    //base就是基准数
    int base = array[left];
    //左边小于基准,右边大于基准
    while (i<j) {
        //先看右边,依次往左递减
        while (base<=array[j]&&i<j) {
            j--;
        }
        //再看左边,依次往右递增
        while (base>=array[i]&&i<j) {
            i++;
        }
        //交换
        int temp = array[j];
        array[j] = array[i];
        array[i] = temp;
    }
    //最后将基准为与i和j相等位置的数字交换
    array[left] = array[i];
    array[i] = base;
    System.out.println(Arrays.toString(array));
    //递归调用左半数组
    mainSort(array, left, j-1);
    //递归调用右半数组
    mainSort(array, j+1, right);
}

}


**输出结果**


**![](https://img-blog.csdnimg.cn/img_convert/3cb4c82645eecb847670ddfc4092b361.png)**


**逐步分析**


* 将`6`作为基准数,利用左右指针使左边的数`<6`,右边的数`>6`。
* 对左右两边递归,即左边用`5`作为基准数继续比较。
* 直到`left > right`结束递归。


耗时测试


![](https://img-blog.csdnimg.cn/img_convert/b3a0dbb5c90dc1005dd0ba8bccce6a1b.png)


算法优化


**优化一**


三数取中(median-of-three):我们目前是拿第一个数作为基准数,对于部分有序序列,会浪费循环,可以用三数取中法优化,感性的小伙伴可自行了解。


**优化二**


快速排序对于长序列非常快,但对于短序列不如插入排序。可以综合使用。



> 
> [完整文章](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)
> 
> 
> 


#### 1eetcode刷题



> 
> 暴力穷举被一个3w+字符的测试用例教做人 [:吐血]
> 
> 
> ——leetcode此题热评
> 
> 
> 


**Question**


难度:中等



> 
> 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
> 
> 
> 示例 1:
> 
> 
> 
> ```
> 输入: s = "abcabcbb"
> 输出: 3 
> 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
> 
> ```
> 
> 示例 2:
> 
> 
> 
> ```
> 输入: s = "bbbbb"
> 输出: 1
> 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
> 
> ```
> 
> 示例 3:
> 
> 
> 
> ```
> 输入: s = "pwwkew"
> 输出: 3
> 解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
> 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
> 
> ```
> 
> 示例 4:
> 
> 
> 
> ```
> 输入: s = ""
> 输出: 0
> 
> ```
> 
> 提示:
> 
> 
> 0 <= s.length <= 5 \* 104  
>  s 由英文字母、数字、符号和空格组成
> 
> 
> 


**Solution**



> 
> 这道题用`暴力穷举法`我们不难想到
> 
> 
> 但是面试,一定会问你,`还有别的方法吗?`
> 
> 
> 有,`滑动窗口法`
> 
> 
> 


1. 假设有一个可滑动且大小可变的窗口,窗口左端(start)不动,右端(end)向后移动
2. 当end遇到重复字符,start应该放在上一个重复字符的位置的后一位,同时记录最长的长度
3. 怎样判断是否遇到重复字符,且怎么知道上一个重复字符的位置?
4. 用哈希字典的key来判断是否重复,用value来记录该字符的下一个不重复的位置。


**Code**



class Solution {
public int lengthOfLongestSubstring(String s) {

    int maxLength=0;
   int start = 0,end=0;
    HashMap<Character, Integer> map = new HashMap<>();
    char[] chars = s.toCharArray();
    while(end<chars.length){
        if (map.containsKey(chars[end])){
            start=Math.max(map.get(chars[end]),start);
        }
        maxLength=Math.max(maxLength,end-start+1);
        map.put(chars[end],end+1);
        end++;
    }
    return maxLength;
}

}


**Result**



> 
> **复杂度分析**
> 
> 
> * 时间复杂度:O(N) ,只需要end从0移动到n就可以
> 
> 
> 


![](https://img-blog.csdnimg.cn/20210715215510938.png)


### 20.计算机网络



> 
> 计算机基础课,非科班学生提升必备。
> 
> 
> 


### 21.操作系统



> 
> 计算机基础课,非科班学生提升必备。
> 
> 
> 


### 22.计算机组成原理


### 给大家的福利


**零基础入门**


对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。


![](https://img-blog.csdnimg.cn/img_convert/95608e9062782d28f4f04f821405d99a.png)


同时每个成长路线对应的板块都有配套的视频提供:


![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/a91b9e8100834e9291cfcf1695d8cd42.png#pic_center)


因篇幅有限,仅展示部分资料


网络安全面试题


![](https://img-blog.csdnimg.cn/img_convert/80674985176a4889f7bb130756893764.png)


绿盟护网行动


![](https://img-blog.csdnimg.cn/img_convert/9f3395407120bb0e1b5bf17bb6b6c743.png)


还有大家最喜欢的黑客技术


![](https://img-blog.csdnimg.cn/img_convert/5912337446dee53639406fead3d3f03c.jpeg)


**网络安全源码合集+工具包**


![](https://img-blog.csdnimg.cn/img_convert/5072ce807750c7ec721c2501c29cb7d5.png)


![](https://img-blog.csdnimg.cn/img_convert/4a5f4281817dc4613353c120c9543810.png)

**所有资料共282G**,朋友们如果有需要全套《网络安全入门+黑客进阶学习资源包》,可以扫描下方二维码领取(如遇扫码问题,可以在评论区留言领取哦)~




**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以点击这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 23
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值