既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
上面提到了重定向,那你知道什么是转发吗?
1.转发
A找B借钱,B没有钱,B去问C,C有钱,C把钱借给A的过程。
客户浏览器发送http请求,web服务器接受此请求,调用内部的一个方法在容器内部完成请求处理和转发动作,将目标资源发送给客户。
整个转发一个请求,一个响应,地址栏不会发生变化,不能跨域访问。
2.重定向
A找B借钱,B没有钱,B让A去找C,A又和C借钱,C有钱,C把钱借给A的过程。
客户浏览器发送http请求,web服务器接受后发送302状态码响应及对应新的location给客户浏览器,客户浏览器发现是302响应,则自动再发送一个新的http请求,请求url是新的location地址,服务器根据此请求寻找资源并发送给客户。
两个请求,两个响应,可以跨域。
Servlet
servlet是一个比较抽奖的概念,也是web部分的核心组件,大家回答这个问题一定要加入自己的理解,不要背定义。
servlet
其实就是一个java程序,他主要是用来解决动态页面的问题。
之前都是浏览器像服务器请求资源,服务器(tomcat)返回页面,但用户多了之后,每个用户希望带到不用的资源。这时就该servlet
上场表演了。
servlet
存在于tomcat
之中,用来网络请求与响应,但他的重心更在于业务处理,我们访问京东和淘宝的返回的商品是不一样的,就需要程序员去编写,目前MVC三层架构,我们都是在service
层处理业务,但这其实是从servlet
中抽取出来的。
看一下servlet
处理请求的过程:
Servlet的生命周期
Servlet生命周期分为三个阶段:
- 初始化阶段 调用init()方法
- 响应客户请求阶段 调用service()方法-àdoGet/doPost()
- 终止阶段 调用destroy()方法
session、cookie、token
首先我们要明白HTTP是一种无状态协议,怎么理解呢?很简单
夏洛:大爷,楼上322住的是马冬梅家吧?
大爷:马冬什么?
夏洛:马冬梅。
大爷:什么冬梅啊?
夏洛:马冬梅啊。
大爷:马什么梅?
夏洛:行,大爷你先凉快着吧。
这段对话都熟悉吧,HTTP就是那个大爷,那如果我们就直接把“大爷”放给用户,用户不用干别的了,就不停的登录就行了。
既然“大爷不靠谱”,我们找“大娘”去吧。
哈哈哈,开个玩笑,言归正传。
为了解决用户频繁登录的问题,在服务端和客户端共同维护一个状态——会话,就是所谓session
,我们根据会话id判断是否是同一用户,这样用户就开心了。
但是服务器可不开心了,因为用户越来越多,都要把session
存在服务器,这对服务器来说是一个巨大的开销,这是服务器就找来了自己的兄弟帮他分担(集群部署,负载均衡)。
但是问题依然存在,如果兄弟挂了怎么办,兄弟们之间的数据怎么同步,用户1把session
存放在机器A上,下次访问时负载均衡到了机器B,完了,找不到,用户又要骂娘。
这时有人思考,为什么一定要服务端保存呢,让客户端自己保存不就好了,所以就诞生了cookie
,下一次请求时客户段把cookie
发送给服务器,说我已经登录了。
但是空口无凭,服务器怎么知道哪个cookie
是我发过去的呢?如何验证成了新的问题。
有人想到了一个办法,用加密令牌,也就是token
,服务器发给客户端一个令牌,令牌保存加密后id和密钥,下一次请求时通过headers
传给服务端,由于密钥别人不知道,只有服务端知道,就实现了验证,且别人无法伪造。
MVC与三层架构
三层架构与MVC的目标一致:都是为了解耦和、提高代码复用。MVC是一种设计模式,而三层架构是一种软件架构。
MVC
Model 模型
模型负责各个功能的实现(如登录、增加、删除功能),用JavaBean
实现。
View 视图
用户看到的页面和与用户的交互。包含各种表单。 实现视图用到的技术有html/css/jsp/js等前端技术。
常用的web 容器和开发工具
Controller 控制器
控制器负责将视图与模型一一对应起来。相当于一个模型分发器。接收请求,并将该请求跳转(转发,重定向)到模型进行处理。模型处理完毕后,再通过控制器,返回给视图中的请求处。
三层架构
表现层(UI)(web层)、业务逻辑层(BLL)(service层)、数据访问层(DAL)(dao层) ,再加上实体类库(Model)
- 实体类库(Model),在Java中,往往将其称为Entity实体类。数据库中用于存放数据,而我们通常选择会用一个专门的类来抽象出数据表的结构,类的属性就一对一的对应这表的属性。一般来说,Model实体类库层需要被DAL层,BIL层和UI层引用。
- 数据访问层(DAL),主要是存放对数据类的访问,即对数据库的添加、删除、修改、更新等基本操作,DAL就是根据业务需求,构造SQL语句,构造参数,调用帮助类,获取结果,DAL层被BIL层调用
- 业务逻辑层(BLL),BLL层好比是桥梁,将UI表示层与DAL数据访问层之间联系起来。所要负责的,就是处理涉及业务逻辑相关的问题,比如在调用访问数据库之前,先处理数据、判断数据。
完整讲解
3.集合
工欲善其事必先利其器,集合就是我们的器。
ArrayList
底层实现
由什么组成,我说了不算,看源码。怎么看呢?
List<Object> list = new ArrayList<>();
新建一个
ArrayList
,按住ctrl
或command
用鼠标点击。
/\*\*
\* The array buffer into which the elements of the ArrayList are stored.
\* The capacity of the ArrayList is the length of this array buffer. Any
\* empty ArrayList with elementData == DEFAULTCAPACITY\_EMPTY\_ELEMENTDATA
\* will be expanded to DEFAULT\_CAPACITY when the first element is added.
\* 翻译
\* 数组缓冲区,ArrayList的元素被存储在其中。ArrayList的容量是这个数组缓冲区的长度。
\* 任何空的ArrayList,如果elementData == DEFAULTCAPACITY\_EMPTY\_ELEMENTDATA,
\* 当第一个元素被添加时,将被扩展到DEFAULT\_CAPACITY。
\*/
transient Object[] elementData;
毋庸置疑,底层由数组组成,那数组的特点就是ArrayList
的特点。
- 由于数组以一块连续的内存空间,每一个元素都有对应的下标,查询时间复杂度为
O(1)
。好比你去住酒店,每个房间都挨着,房门都写着房间号。你想找哪一间房是不是很容易。 - 相对的,一块连续的内存空间你想打破他就没那么容易,牵一发而动全身,所以新增和删除的时间复杂度为
O(n)
,想像你在做excel
表格的时候,想增加一列,后面的列是不是都要跟着移动。 - 元素有序,可重复。可用在大多数的场景,这个就不需要过多解释了。
扩容
我们知道数组是容量不可变的数据结构,随着元素不断增加,必然要扩容。
所以扩容机制也是集合中非常容易爱问的问题,在源码中都可以一探究竟。
1.初始化容量为10,也可以指定容量创建。
/\*\*
\* Default initial capacity.
\* 定义初始化容量
\*/
private static final int DEFAULT_CAPACITY = 10;
2.数组进行扩容时,是将旧数据拷贝到新的数组中,新数组容量是原容量的1.5倍。(这里用位运算是为了提高运算速度)
private void grow(int minCapacity) {
int newCapacity = oldCapacity + (oldCapacity >> 1);
}
3.扩容代价是很高得,因此再实际使用时,我们因该避免数组容量得扩张。尽可能避免数据容量得扩张。尽可能,就至指定容量,避免数组扩容的发生。
为什么扩容是1.5倍?
- 如果大于1.5,也就是每次扩容很多倍,但其实我就差一个元素的空间,造成了空间浪费。
- 如果小于1.5,扩容的意义就不大了,就会带来频繁扩容的问题。
所以,1.5
是均衡了空间占用和扩容次数考虑的。
线程安全问题
怎么看线程安全?说实话我以前都不知道,看网上说安全就安全,说不安全就不安全。
其实都在源码里。找到增加元素的方法,看看有没有加锁就知道了。
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
没有加锁,所以线程不安全
在多线程的情况下,插入数据的时可能会造成数据丢失,一个线程在遍历,另一个线程修改,会报ConcurrentModificationException(并发修改异常)
错误.
多线程下使用怎么保证线程安全?
保证线程安全的思路很简单就是加锁,但是你可没办法修改源码去加个锁,但是你想想编写
java
的大佬会想不到线程安全问题?早就给你准备了线程安全的类。
1.Vector
Vector
是一个线程安全的List
类,通过对所有操作都加上synchronized
关键字实现。
找到add
方法,可以看到被synchronized
关键字修饰,也就是加锁,但synchronized
是重度锁,并发性太低,所以实际一般不使用,随着java
版本的更新,慢慢废弃。
public void add(E e) {
int i = cursor;
synchronized (Vector.this) {
checkForComodification();
Vector.this.add(i, e);
expectedModCount = modCount;
}
cursor = i + 1;
lastRet = -1;
}
2.Collections
注意是Collections
而不是Collection
。
Collections
位于java.util
包下,是集合类的工具类,提供了很多操作集合类的方法。其中Collections.synchronizedList(list)
可以提供一个线程安全的List
。
对于Map、Set也有对应的方法
3.CopyOnWrite(写时复制)
写时复制,简称COW,是计算机程序设计领域中的一种通用优化策略。
当有多人同时访问同一资源时,他们会共同获取指向相同的资源的指针,供访问者进行读操作。
当某个调用者修改资源内容时,系统会真正复制一份副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变。修改完成后,再把新的数据写回去。
通俗易懂的讲,假设现在有一份班级名单,但有几个同学还没有填好,这时老师把文件通过微信发送过去让同学们填写(复制一份),但不需要修改的同学此时查看的还是旧的名单,直到有同学修改好发给老师,老师用新的名单替换旧的名单,全班同学才能查看新的名单。
共享读,分开写。读写分离,写时复制。
在java中,通过CopyOnWriteArrayList
、CopyOnWriteArraySet
容器实现了 COW 思想。
平时查询的时候,都不需要加锁,随便访问,只有在更新的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。
/\*\* The lock protecting all mutators \*/
final transient ReentrantLock lock = new ReentrantLock();
/\*\* The array, accessed only via getArray/setArray. \*/
private transient volatile Object[] array;
源码里用到了ReentrantLock
锁和volatile
关键字,会在《资深程序员修炼》专栏中做全面深度讲解。
LinkedList
LinkedList
和ArrayList
同属于List
集合。其共同特点可归纳为:存储单列数据的集合,存储的数据是有序并且是可以重复的。
但两者也有不同,往下看吧
底层实现
LinkedList
类的底层实现的数据结构是一个双向链表。同时还实现了Deque
接口,所以会有些队列的特性,会在下面讲。
class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
先简单说一下链表这种数据结构,与数组相反,链表是一种物理存储单元上非连续、非顺序的存储结构,一个最简单的链表(单链表)有节点Node
和数值value
组成。通俗的讲,就像串在一起的小鱼干,中间用线连着。
transient Node<E> first;
transient Node<E> last;
链表中保存着对最后一个节点的引用,这就是双端链表
在单链表的结点中增加一个指向其前驱的pre指针就是双向链表,一种牺牲空间换时间的做法。
双端链表不同于双向链表,切记!
关于链表更详细代码级讲解会放《糊涂算法》专栏更新。敬请期待!
简单了解过后分析一下链表的特点:
- 查询速度慢,因为是非连续空间,没有下标。想像你需要在一份名单上找到你的名字,没有序号,你只能从头开始一个一个的看。
- 删改速度快,因为非连续,也就没有那么多约束。想像从一根项链上扣下来一块,只需要改变引用就可以了,不会牵一发而动全身。
- 元素有序,可重复。
如何解决查询慢的问题?
如果我查找的元素在尾部,则需要遍历整个链表,所以有了双端链表。
即使不在尾部,我如果只能一个方向遍历,也很麻烦,所以有了双向队列,牺牲空间换时间。
那么空间可不可以再牺牲一点?
可以,就是跳跃链表,简称「跳表」。
通过建立多级索引来加快查询速度。
线程安全问题
老办法,看看
add()
方法。分为「头插法」和「尾插法」。
/\*\*
\* Inserts the specified element at the beginning of this list.
\*
\* @param e the element to add
\*/
public void addFirst(E e) {
linkFirst(e);
}
/\*\*
\* Appends the specified element to the end of this list.
\*
\* <p>This method is equivalent to {@link #add}.
\*
\* @param e the element to add
\*/
public void addLast(E e) {
linkLast(e);
}
都没加锁,百分之一百的不安全。
如何解决线程不安全问题
1.ConcurrentLinkedQueue
一个新的类,位于java.util.concurrent
(juc)包下。实现了Queue
接口。
class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
implements Queue<E>, java.io.Serializable{}
使用violate
关键字实现加锁。
private transient volatile Node<E> head;
private transient volatile Node<E> tail;
1.Collections
和ArrayList
一样,使用Collections.synchronizedList()
。
Map:存储双列数据的集合,通过键值对存储数据,存储 的数据是无序的,Key值不能重复,value值可以重复
和ArrayList对比一下
共同点:有序,可重复。线程不安全。
不同点:底层架构,查询和删改的速度
完整讲解
4.JVM
重点来了,Java程序员一定要深入研究的内容
JVM的体系架构
JVM体系结构如下图所示,将按照从上到下的顺序讲解
类装载器classloader
负责将class文件的字节码内容加载到内存中并将这些内容转换成方法区中的运行时数据结构,class文件在文件开头有特定标识(cafe babe:Java图标——咖啡和橡树)。
通俗来讲:classloader相当于快递员的作用,只负责加载,至于是否能运行,由Execution Engine决定
双亲委派机制
当一个类收到类加载请求,他会先把这个请求交给他的父类,只有父类无法完成这个请求时,子加载器才会尝试自己去加载。
双亲委派的好处是保护Java核心类,比如加载位于rt.jar中的java.lang.Object,不管是哪个加载器加载的,最终都会交给启动类加载器,这样就保证了不同的类加载器得到的都是同一个Object对象。
代码举例:查看类是被那个加载器加载的
/\*\*
\* @Author: 一条IT
\* @Date: 2020/12/3 21:28
\*/
public class Test {
public static void main(String[] args) {
System.out.println(Object.class.getClassLoader());
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$ExtClassLoader@1b6d3586
sun.misc.Launcher$AppClassLoader@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.标记-整理
同样是为了解决空间碎片提出,区别是通过牺牲时间的方式。
和标记-清除类似,不一样的是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
优点:
- 解决空间碎片的问题
- 不浪费空间
缺点:
- 相对比较耗时
完整讲解
5.多线程
理解多线程,才能更好的理解框架源码,进行高并发的架构设计,重中之重。
并行和并发
并行:多个任务在同一个 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.设计模式
好多人觉得设计模式模式,那是因为你学的还不够深入,还没有看过源码,所以我特意将设计模式往前放了。
原型模式
定义
官方定义
用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。
通俗解读
在需要创建重复的对象,为了保证性能,本体给外部提供一个克隆体进行使用。
类似我国的印刷术,省去new
的过程,通过copy
的方式创建对象。
结构图
代码实现
目录结构
建议跟着一条学设计模式的小伙伴都建一个
maven
工程,并安装lombok
依赖和插件。并建立如下包目录,便于归纳整理。
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);
}
}
输出结果
从结果可以看出:
- 对象创建了一次,有点单例的感觉
- 第一次从数据库查,第二次从缓存查
好像是实现了YitiaoBatis
框架的需求,思考🤔一下有什么问题呢?
4.修改对象id
在测试类继续编写
//执行后续业务,修改id
yitiao2.setId(100);
Yitiao yitiao3 = yitiaoBatis.getYitiao("yitiao");
System.out.println("第三次查询:"+yitiao3);
输出结果
重点看第三次查询,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.再次测试
不用改测试类,直接看一下结果:
从输出结果可以看出第三次查询id
依然是1
,没有脏缓存现象。
基于原型模式的克隆思想,我可以快速拿到和「本体」一模一样的「克隆体」,而且对象也只被new
了一次。
不知道大家是否好奇对象是怎么被创建出来的,那我们就一起看一下「深拷贝」和「浅拷贝」是怎么回事。
深拷贝和浅拷贝
定义
深拷贝:不管拷贝对象里面是基本数据类型还是引用数据类型都是完全的复制一份到新的对象中。
浅拷贝:当拷贝对象只包含简单的数据类型比如int、float 或者不可变的对象(字符串)时,就直接将这些字段复制到新的对象中。而引用的对象并没有复制而是将引用对象的地址复制一份给克隆对象。
好比两个兄弟,深拷贝是年轻的时候关系特别好,衣服买一样的,房子住一块。浅拷贝是长大了都成家立业,衣服可以继续买一样的,但房子必须要分开住了。
实现
在代码上区分深拷贝和浅拷贝的方式就是看引用类型的变量在修改后,值是否发生变化。
浅拷贝
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);
}
输出结果
结论:基本类型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);
输出结果
与测试1无异
深拷贝
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);
输出结果
结论,引用对象也完全复制一个新的,值不变化。
不过要注意的是,如果某个属性被transient
修饰,那么该属性就无法被拷贝了。
应用场景
我们说回原型模式。
原型模式在我们的代码中是很常见的,但是又容易被我们所忽视的一种模式,比如我们常用的的
BeanUtils.copyProperties
就是一种对象的浅拷贝。看看有哪些场景需要原型模式
- 资源优化
- 性能和安全要求
- 一个对象多个修改者的场景。
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时可以考虑使用原型模式拷贝多个对象供调用者使用。
原型模式已经与 Java 融为浑然一体,可以随手拿来使用。
更多设计模式
7.SSM框架
这对于初学者来说,是一个坎,前几年学完这些,已经可以开始找工作了,所以恭喜你能坚持带这里,胜利就在前方。
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缓存数据库来存储热点数据。
什么是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作为统一配置文件管理和集群管理框架,是后续学习其他框架的基础,在微服务中,还可以用来做注册中心。
什么是zookeeper
zookeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 google chubby 的开源实现,是 hadoop 和 hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
zookeeper 的功能
集群管理:监控节点存活状态、运行请求等。
主节点选举:主节点挂掉了之后可以从备用的节点开始新一轮选主,主节点选举说的就是这个选举的过程,使用 zookeeper 可以协助完成这个过程。
分布式锁:zookeeper 提供两种锁:独占锁、共享锁。独占锁即一次只能有一个线程使用资源,共享锁是读锁共享,读写互斥,即可以有多线线程同时读同一个资源,如果要使用写锁也只能有一个线程使用。zookeeper可以对分布式锁进行控制。
命名服务:在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。
zookeeper 的部署模式
zookeeper 有三种部署模式:
单机部署:一台集群上运行;
集群部署:多台集群运行;
伪集群部署:一台集群启动多个 zookeeper 实例运行。
10.Kafka
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——单体架构
如果现在让你实现几十人的抽奖系统,简单死了吧,直接重拳出击!
两猫一豚走江湖,中奖入库,调通知服务,查库通知,完美!
相信大家学java时可能都做过这种案例,思考🤔一下存在什么问题?
- 单体服务,一着不慎满盘皆输
- 抽了再抽,一个人就是一支军队
- 恶意脚本,没有程序员中不了的奖
接下来就聊聊怎么解决这些问题?
V1——负载均衡
当一台服务器的单位时间内的访问量越大时,服务器压力就越大,大到超过自身承受能力时,服务器就会崩溃。
为了避免服务器崩溃,让用户有更好的体验,我们通过负载均衡的方式来分担服务器压力。
负载均衡就是建立很多很多服务器,组成一个服务器集群,当用户访问网站时,先访问一个中间服务器,好比管家,由他在服务器集群中选择一个压力较小的服务器,然后将该访问请求引入该服务器。
如此以来,用户的每次访问,都会保证服务器集群中的每个服务器压力趋于平衡,分担了服务器压力,避免了服务器崩溃的情况。
负载均衡是用「反向代理」的原理实现的。具体负载均衡算法及其实现方式我们下文再续。
负载均衡虽然解决了单体架构一着不慎满盘皆输的问题,但服务器成本依然不能保护系统周全,我们必须想好一旦服务器宕机,如何保证用户的体验。
即如何缓解开奖一瞬间时的大量请求。
V2——服务限流
限流主要的作用是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量实效),造成不可用。
还可用于平滑请求。
在上一小节我们做好了负载均衡来保证集群的可用性,但公司需要需要考虑服务器的成本,不可能无限制的增加服务器数量,一般会经过计算保证日常的使用没问题。
限流的意义就在于我们无法预测未知流量,比如刚提到的抽奖可能遇到的:
- 重复抽奖
- 恶意脚本
其他一些场景:
- 热点事件(微博)
- 大量爬虫
这些情况都是无法预知的,不知道什么时候会有10倍甚至20倍的流量打进来,如果真碰上这种情况,扩容是根本来不及的(弹性扩容都是虚谈,一秒钟你给我扩一下试试)
明确了限流的意义,我们再来看看如何实现限流
防止用户重复抽奖
重复抽奖和恶意脚本可以归在一起,同时几十万的用户可能发出几百万的请求。
如果同一个用户在1分钟之内多次发送请求来进行抽奖,就认为是恶意重复抽奖或者是脚本在刷奖,这种流量是不应该再继续往下请求的,在负载均衡层给直接屏蔽掉。
可以通过nginx
配置ip
的访问频率,或者在在网关层结合sentinel
配置限流策略。
用户的抽奖状态可以通过redis来存储,后面会说。
拦截无效流量
无论是抽奖还是秒杀,奖品和商品都是有限的,所以后面涌入的大量请求其实都是无用的。
举个例子,假设50万人抽奖,就准备了100台手机,那么50万请求瞬间涌入,其实前500个请求就把手机抢完了,后续的几十万请求就没必要让他再执行业务逻辑,直接暴力拦截返回抽奖结束就可以了。
同时前端在按钮置灰上也可以做一些文章。
那么思考一下如何才能知道奖品抽完了呢,也就是库存和订单之前的数据同步问题。
服务降级和服务熔断
有了以上措施就万无一失了吗,不可能的。所以再服务端还有降级和熔断机制。
在此简单做个补充,详细内容请持续关注作者。
有好多人容易混淆这两个概念,通过一个小例子让大家明白:
假设现在一条粉丝数突破100万,冲上微博热搜,粉丝甲和粉丝乙都打开微博观看,但甲看到了一条新闻发布会的内容,乙却看到”系统繁忙“,过了一会,乙也能看到内容了。
(请允许一条幻想一下😎)
在上述过程中,首先是热点时间造成大量请求,发生了服务熔断,为了保证整个系统可用,牺牲了部分用户乙,乙看到的”系统繁忙“就是服务降级(fallback),过了一会有恢复访问,这也是熔断器的一个特性(hystrix)
V3 同步状态
接着回到上一节的问题,如何同步抽奖状态?
这不得不提到redis
,被广泛用于高并发系统的缓存数据库。
我们可以基于Redis来实现这种共享抽奖状态,它非常轻量级,很适合两个层次的系统的共享访问。
当然其实用ZooKeeper也是可以的,在负载均衡层可以基于zk客户端监听某个znode节点状态。一旦抽奖结束,抽奖服务更新zk状态,负载均衡层会感知到。
V4线程优化
对于线上环境,工作线程数量是一个至关重要的参数,需要根据自己的情况调节。
众所周知,对于进入Tomcat的每个请求,其实都会交给一个独立的工作线程来进行处理,那么Tomcat有多少线程,就决定了并发请求处理的能力。
但是这个线程数量是需要经过压测来进行判断的,因为每个线程都会处理一个请求,这个请求又需要访问数据库之类的外部系统,所以不是每个系统的参数都可以一样的,需要自己对系统进行压测。
但是给一个经验值的话,Tomcat的线程数量不宜过多。因为线程过多,普通服务器的CPU是扛不住的,反而会导致机器CPU负载过高,最终崩溃。
同时,Tomcat的线程数量也不宜太少,因为如果就100个线程,那么会导致无法充分利用Tomcat的线程资源和机器的CPU资源。
所以一般来说,Tomcat线程数量在200~500之间都是可以的,但是具体多少需要自己压测一下,不断的调节参数,看具体的CPU负载以及线程执行请求的一个效率。
在CPU负载尚可,以及请求执行性能正常的情况下,尽可能提高一些线程数量。
但是如果到一个临界值,发现机器负载过高,而且线程处理请求的速度开始下降,说明这台机扛不住这么多线程并发执行处理请求了,此时就不能继续上调线程数量了。
V5业务逻辑
抽奖逻辑怎么做?
好了,现在该研究一下怎么做抽奖了
在负载均衡那个层面,已经把比如50万流量中的48万都拦截掉了,但是可能还是会有2万流量进入抽奖服务。
因为抽奖活动都是临时服务,可以阿里云租一堆机器,也不是很贵,tomcat优化完了,服务器的问题也解决了,还剩啥呢?
Mysql,是的,你的Mysql能抗住2万的并发请求吗?
答案是很难,怎么办呢?
把Mysql给替换成redis,单机抗2万并发那是很轻松的一件事情。
而且redis的一种数据结构set
很适合做抽奖,可以随机选择一个元素并剔除。
V6流量削峰
由上至下,还剩中奖通知部分没有优化。
思考这个问题:假设抽奖服务在2万请求中有1万请求抽中了奖品,那么势必会造成抽奖服务对礼品服务调用1万次。
那也要和抽奖服务同样处理吗?
其实并不用,因为发送通知不要求及时性,完全可以让一万个请求慢慢发送,这时就要用到消息中间件,进行限流削峰。
也就是说,抽奖服务把中奖信息发送到MQ,然后通知服务慢慢的从MQ中消费中奖消息,最终完成完礼品的发放,这也是我们会延迟一些收到中奖信息或者物流信息的原因。
假设两个通知服务实例每秒可以完成100个通知的发送,那么1万条消息也就是延迟100秒发放完毕罢了。
同样对MySQL的压力也会降低,那么数据库层面也是可以抗住的。
看一下最终结构图:
17.Linux
作为Java程序员,不会用Linux会让人笑掉大牙的。我们不必像运维兄弟一样精通,基本的命令还是要熟练掌握。
- 理解一切皆文件
- 文件操作命令
- 权限管理命令
- 网络命令
- 系统磁盘命令
- ……
18.Git
网上经常传出不会git被开除的新闻,所以还不学起来?
命令大全
19.数据结构和算法
问一下各位什么是程序?——数据结构+算法。所以,学吧,刷题吧
排序算法
《八大排序》源码](https://pan.baidu.com/s/1woTgwkVUT1xtgMB1ha36Uw),提取码:5ehp
准备
古语云:“兵马未动,粮草先行”。想跟着一条一块把「排序算法」弄明白的,建议先准备好以下代码模板。
📢 观看本教程需知道基本循环语法、两数交换、双指针等前置知识。
📚 建议先看完代码和逐步分析后再尝试自己写。
- 新建一个
Java
工程,本文全篇也基于Java语言实现代码。 - 建立如下目录结构
- 在
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.冒泡排序
基本思想
通过对乱序序列从前向后遍历,依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部。
像水底下的气泡一样逐渐向上冒一样。
动图讲解
代码实现
不理解的小伙伴可以用
debug
模式逐步分析。
/\*\*
\* 冒泡排序
![img](https://img-blog.csdnimg.cn/img_convert/7e4d4e62f849f8a695e10d3c258464ef.png)
![img](https://img-blog.csdnimg.cn/img_convert/3f726c4bf1868e37152c2e1ad88a5f31.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
6.png)
* 理解一切皆文件
* 文件操作命令
* 权限管理命令
* 网络命令
* 系统磁盘命令
* ……
### 18.Git
>
> 网上经常传出不会git被开除的新闻,所以还不学起来?
>
>
>
#### 命令大全
>
> [200条Git命令大全](https://bbs.csdn.net/topics/618545628)
>
>
>
### 19.数据结构和算法
>
> 问一下各位什么是程序?——数据结构+算法。所以,学吧,刷题吧
>
>
>
#### 排序算法
>
> 《八大排序》源码](https://pan.baidu.com/s/1woTgwkVUT1xtgMB1ha36Uw),提取码:5ehp
>
>
>
[外链图片转存中...(img-fBvzwudc-1715726356218)]
**准备**
>
> 古语云:“兵马未动,粮草先行”。想跟着一条一块把「排序算法」弄明白的,建议先准备好以下代码模板。
>
>
> 📢 观看本教程需知道基本**循环语法**、**两数交换**、**双指针**等前置知识。
>
>
> 📚 建议先看完**代码**和**逐步分析**后再尝试自己写。
>
>
>
* 新建一个`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`模式逐步分析。
>
>
>
/**
* 冒泡排序
[外链图片转存中…(img-TZ5ZBZkR-1715726356219)]
[外链图片转存中…(img-WWi76Icc-1715726356219)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!