后端萌新自备题库

个人认为的八股考点:

专业课+Spring框架基础+Redis原理[项目用到的中间件]

JAVA基础+JVM+JUC

MySQL语句操作;索引;相关股股股股 股股股股

但会问的专业课知识,数据结构,计网和操作系统,似乎都分布在这些八股以及算法里了。

我会把除了JVM,JUC的JAVA八股塞进其他部分里。

零碎基础部分

TCP与UDP

DNS与HTTP

MySQL

1. 索引

1.1 JAVA集合与搜索树【数据结构警告】

计划:JAVA集合hashmap, concurrenthashmap, 红黑树B树,B+树,对B+树的修改删除,

MySQL使用的B+树

1.什么是集合?

集合就是一个存数据的容器,分为Collection和Map两类接口,展开来就是List,  set, Map三种。

常见的实现类有[ArrayList],[HashSet],[HashMap(ConcurrentHashMap), HashTable.](当然都可以加链表)

(所以用法就是List<> XX = new ArrayList<>();)

ArrayList扩容机制是 size = size + (size >>1); 比如15--15+7【1.5倍】

hashSet就是把HashMap的value屏蔽掉了……变成个PRESENT

这样就能当不可重复集合使了,它们仨都能和链表结合(LINKEDXX)

数组和链表一个查找效率高,一个插入删除效率高,而JAVA中的哈希表(hashtable)结合了两者的优势,以数组和链表形式构成,增删改查都快。

单元数据里存放键值对[键值都必须是对象],但是hashtable现在几乎很少用了。

原罪1:使用syncronized,线程安全但效率很低

原罪2:1.8不像hashMap更新红黑树

赎罪:不支持null键值(hashmap支持一个null键和多个null值)

concurrentHashMap也不支持,这是为了防止引发并发环境下的歧义,hashMap可以,是因为containskey能处理一下null的逻辑,但并发条件下不好说。

2.HashMap底层数据结构有什么特点?
  • 1.7采用了数组+单向链表的形式,
  • 1.8采用数组+单向链表+红黑树形式。
  • 如果没有哈希冲突,哈哈哈O(1)就查到数据了,用红黑树是为了优化哈希冲突问题,其查询效率为O(log n)(而链表则是n)

3.介绍一下hashMap的参数?

HashMap的主干是一个Entry数组,长度默认为16,在插入元素前会先判断,如果插入后元素数量超过【装载因子】*数组长度,数组会自动扩容(翻倍)。

扩容的过程就是先把Entry数组翻倍,然后搬迁数据,比如有的哈希值是15%4 = 3 现在要挪到7那边。这个过程可能涉及到红黑树解体。

loadFactor因子默认为0.75。

当数组长度达到64时,开放红黑树功能,在同一数组索引长度到8时,链表转为红黑树。

如果小于6,则退化为链表。

根据泊松分布,如果索引分布均匀的话,长度为8的概率是非常非常低的。这也是为了保证一般情况下用不到这个,(说白了红黑树是速效救心丸,但产生红黑树开销并不低,哈希冲突能通过算法避免就算法避免吧,如果冲突了也可以通过线性寻址、二次探测、随机探测。)

+当我们用HashMap存入自定义的类时,如果不重写这个自定义类的equals和hashCode方法,得到的结果会和我们预期的不一样。

因为HashMap的key有个特征,就是key值不能重复,否则add的时候会覆盖。那么如果使用自定义的类做HashMap的key,就需要用到equals方法判断两个对象是否相等,判断的条件就是hashCode生成的哈希值。

如果不重写hashcode方法,那么即便是两个内容完全一致的key对象,它仍会视为是不同的key,导致数据存取无法进行;

如果不重写equals方法,那么equals方法只是简单的将两个对象间的内存地址进行比较。

4.介绍一下树树?二叉查找树,二叉平衡(查找)树,红黑树?

它们都是特殊的树型数据结构,用来方便地进行操作

AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树,开销较大。红黑树是一种平衡二叉查找树的变体,它的左右子树高差有可能大于 1,所以红黑树不是严格意义上的平衡二叉树(AVL),但 对之进行平衡的代价较低, 其平均统计性能要强于 AVL 。

红黑树是一种近似平衡的二叉查找树,他并非绝对平衡,但是可以保证任何一个节点的左右子树的高度差不会超过二者中较低的那个的一倍。红黑树有这样的特点:

每个节点要么是红色,要么是黑色。
根节点必须是黑色。叶子节点必须是黑色NULL节点。
红色节点不能连续(黑色节点可以连续)
对于每个节点,从该点至叶子节点的任何路径,都含有相同个数的黑色节点
能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。此外,任何不平衡都会在3次旋转之内解决

5.ConcurrentHashMap

ConcurrentHashMap介绍一下?它的底层实现原理是什么?如何保障线程安全?

【拆解:数据结构+并发安全的实现+性能方面的优化】

解答角度【线程安全】:

HashMap在并发条件下可能会出现死循环或者数据丢失的情况,而ConcurrentHashMap能够完美地解决这一问题。

对于并发安全的实现呢,1.7版本和1.8版本的实现方式不太一样。

解答角度【介绍,本质】ConcurrentHashMap在本质上和HashMap是一样的,但它提供了并发条件下的线程安全功能。

JDK1.7中,它的数据存储结构由segment分段构成,每个分段里又是类似1.7hashmap那样的拉链式结构。而1.8以后放弃了分段,采用了数组+链表+红黑树的形式,使用红黑树能够提高大量数据场景下的搜索效率避免遍历长链表带来的耗时问题

对于并发安全的实现呢,1.7版本和1.8版本的实现方式不太一样

它会只对操作的segment锁定,其他的不受影响;数组扩容也是每个segment独立进行。

使用到的锁是可重入锁。理想情况下,分段有多少个,它就能支持多少个并发写操作。

在JDK1.8版本中,他会采用乐观锁 + 自旋/同步锁 实现并发插入或更新操作。相比分段锁,锁的粒度是入口的一个node节点,因此性能更高。

什么时候会启用同步锁?

当发生了hash碰撞的时候说明容量不够用了或者已经有大量线程访问了,因此这时候使用synchronized来处理hash碰撞比CAS效率要高,因为发生了hash碰撞大概率来说是线程竞争比较强烈。

元素计数上也使用了CAS原理:

baseCount CounterCell[]

每次元素元素变更之后,将会使用 CAS方式更新该值。如果多个线程并发增加新元素,baseCount 更新冲突,将会启用 CounterCell,通过使用 CAS 方式将总数更新到 counterCells 数组对应的位置,减少竞争。

如果 CAS 更新 counterCells 数组某个位置出现多次失败,这表明多个线程在使用这个位置。此时将会通过扩容 counterCells方式,再次减少冲突。

通过上面的努力,统计元素总数就变得非常简单,只要计算 baseCount 与 counterCells总和,整个过程都不需要加锁。

仔细回味一下,counterCells 也是通过类似分段锁思想,减少多线程竞争。

写到这的时候,笔者建议大家去了解下Redis的渐进式扩容,是另一种思想,都值得学习。一句话帮助理解Redis的渐进式扩容:由于Redis是单线程,而且数据量较大时,无法一次性快速扩容,所以Redis首先申请一个新的容量加倍的哈希表,然后在插入,删除,更新操作的时候,调用rehash函数(dictRehash函数),将原有操作单元的链表移植到新的哈希表中,当原有哈希表全部移植过去,扩容结束。

两个版本都有<K,V>的结构,都有next,value属性,也都使用了volatile保证并发的可见性

1.2 MySQL索引原理【从读盘,I/O,B+树】层次回答

一般的数据库会采用B树或者B+树这样的多路查找树,在数据量很大的时候,相较于二叉查找树,树的高度会低不少,查找效率很高。

数据通常是存储在磁盘上的,磁盘读取的性质决定了随机IO效率会很低,而高度低意味着IO次数会少,读取时间就小。

在MySQL的innoDB引擎中,它采用了B+树的数据结构实现索引和数据存储。它是一种增强版的B树。

那么相较于B树的数据结构,它的结构做了一些优化:

首先是非叶子节点不再保存数字,只保存键值,这样一个节点最多可以存储400个键值,也就意味着可以有400个子节点,这能够进一步降低整个查找树的高度;

其次是解决了范围查找的困难,它把叶子节点之间用指针连接起来,减少查找次数,同时这些数据在磁盘中也往往是相邻的,IO开销会很低。

附加:存储引擎

MyISAM存储引擎只在叶子节点存放数据地址,而innoDB直接存储整行数据

.frm存储了表结构信息,.ibd存储了表的数据和索引。

在5.7以后innoDB存储引擎使用.ibd存储每个表的信息,不再使用共享的系统表空间。所以每个.ibd都是一个独立表空间。

.ibd是由许许多多的页组成的。页是存储引擎磁盘与内存交互的最小存储单元。大小为16kb(磁盘的页则为4kb)。

每个页内部存储了数据行,页和页之间在磁盘上的存储是连续的。

当发生跨页读取的时候,磁头需要移动,会大大降低性能,于是又进化出了区,一个区大小为1MB,一个组又存放256个区。这样,哪怕跨页跨区,也能保障数据之间在磁盘的相邻区域,减少开销。

一个页对应一个B+树节点。

1.3 聚集索引和非聚集索引

每一张表都有且只有一个聚集索引,它是通过表主键来构建的。如果缺少主键呢,它会再考虑UNIQUE列,如果这个也没有,就会创建一个隐藏的递增列,叫DB_ROW_ID

它会按照主键的大小对数据页进行排序。

对于聚集索引,索引和数据是在一起的;非聚集索引,叶子节点存放主键值,在进行查找时,先快速在这棵树中查找到主键值,再做“回表”操作。

非聚集索引可以通过普通的指令创建。 

1.4 使用索引的注意事项

总的来说分为两点:

创建更需要的索引;

充分利用已有的索引。

 1. 确定需要索引的列
  • 确定数据表中经常用于 JOIN、WHERE 或 ORDER BY 子句的列。
  • 这些列在查询中使用频率较高,索引可以提高它们的检索速度。
2. 联合索引

回表虽然本身效率就比较高,但尽量避免回表(避免不了就减少次数),而使用联合索引就可以实现一定程度的覆盖索引。

3. 索引失效

索引失效是优化器导致——在它的分析看来,你这么做还不如分析全表扫描更快。

联合索引具有最左匹配原则,即最左优先:这也是为什么添加联合索引的时候是可以选择顺序的。

在搜索与比较的过程中 ,对于bcd的联合索引,会先判断 b 再判断 c 然后是 d 。

所以如果你的查找条件不包含b,联合索引就会失效。

而如果包含了b,哪怕WHERE语句把b放后面了,优化器也会帮你调整,唉,VIP。

除此之外,NULL,%,LIKE,对索引字段进行表达式运算,也可能会造成索引失效。

LIKE"abc%",以abc开头,这是没问题的,二级索引匹配起来好匹配

LIKE"%abc",以abc结尾,索引则大部分情况下失效,除非:查询的条件都被索引覆盖到了,所以直接遍历二级索引树就好了,不用遍历全表。

还有个特殊情况,SQL支持字符串比较,是因为他会按规则转换为数字。所以int的索引使用字符串不会失效,而反过来则会失效。

4. 查看计划

explain指令可以了解到这个命令的执行计划

包括单表查询,联合查询,子查询,预期和实际采用的索引,便于我们进行相关优化。

5. 索引并非越多越好

任何事情都有两面,过度使用索引会在空间和时间上都付出代价。

每个索引都要对应一个B+树,创建表的时候都会注意对字段长度进行限制,以减小磁盘开销,更不用说索引了。

时间上,索引太多,也会造成优化器计算成本的时间增加。

JUC

1.进程与线程

1.1 OS的进程与线程

线程,进程,协程的区别:
  • 进程是操作系统中进行资源分配和调度基本单位,每一个程序本身是没有生命周期的,但运行以后就成为了一个进程实体。进程具有独立性,不会互相干涉。稳定性和安全性相对较高,但同时上下文切换的开销也较大,因为需要保存和恢复整个进程的状态。

在用户态下,CPU只能执行部分指令集,无法直接访问硬件资源。这种模式下的操作权限较低,主要用于运行用户程序。进程是由内核管理和调度的,所以进程的切换只能发生在内核态。

分配资源的最小单位的“资源”:虚拟内存、文件句柄、信号量等资源。

  • 线程是CPU调度分派的基本单位,线程依赖于进程,同时一个进程可以拥有多个线程。线程间会共享进程里的资源。每个线程又会拥有自己的寄存器上下文和栈。

由于多个线程共享内存空间,各个线程是由操作系统随机进行调度,并且各个线程是抢占式执行,因此存在数据竞争和线程安全的问题,需要通过同步和互斥机制来解决。

  • 协程可以理解为用户态的轻量级线程。协程可以完全由用户进行调度,而不需要内核的参与。协程拥有自己的寄存器上下文和栈,协程的切换开销非常小。

进程状态

对于并发任务,进程来回切换,操作系统需要事先帮 CPU 设置好 CPU 寄存器和程序计数器

所以说,CPU 寄存器和程序计数是 CPU 在运行任何任务前,所必须依赖的环境,这些环境就叫做 CPU 上下文

上面说到所谓的「任务」,主要包含进程、线程和中断。所以,可以根据任务的不同,把 CPU 上下文切换分成:进程上下文切换、线程上下文切换和中断上下文切换

中断是什么?

中断是指在处理器执行程序时,由于某种事件的发生而打断当前程序的正常执行流程,转而去处理发生的事件。中断可以是来自外部设备的信号,如键盘输入、鼠标操作,也可以是来自处理器内部的异常情况,如除零错误、越界访问等。中断可以分为硬中断和软中断两种类型。

无论是硬件中断还是软件中断,一旦中断请求被处理器接收到,处理器会在合适的时机暂停当前正在执行的程序,保存当前的运行现场(包括指令指针、寄存器状态等),跳转到中断处理程序来处理中断事件。处理完中断事件后,处理器会恢复之前保存的运行现场,继续原来的程序执行。

进程调度算法

1/优先调度算法

先来先服务最短作业优先,都没有很好的权衡短作业和长作业。

2/高优先权优先

非抢占式/抢占式优先权算法

高响应比优先算法:优先权会考虑到等待时间,这样长作业等久了会快点处理。

3/基于时间片轮转

用得最多的:时间片 轮转调度算法

多级反馈队列调度算法

每个进程被分配一个时间段,允许该进程在该时间段中运行。

  • 如果时间片用完,进程还在运行,那么将会把此进程从 CPU 释放出来,并把 CPU 分配另外一个进程;
  • 如果该进程在时间片结束前阻塞或结束,则 CPU 立即进行切换

另外,时间片的长度就是一个很关键的点:

  • 如果时间片设得太短会导致过多的进程上下文切换降低了 CPU 效率
  • 如果设得太长又可能引起对短作业进程的响应时间变长。20-50ms比较适中。

Java线程调度

  1.抢占式调度:指的是每条线程执行的时间、线程的切换都由系统控制。系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞

  2.协同式调度:指某一线程执行完后主动通知系统切换到另一线程上执行。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命缺点:如果一个线程编写有问题,运行到一半就一直阻塞,那么可能导致整个系统崩溃。

  JVM:Java使用的线程调度使用抢占式优先权调度算法,Java中线程会按优先级分配CPU时间片运行,且优先级越高越优先执行(可以调),但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。

更多详见2.1部分

进程同步

进程具有异步性的特征,异步性是指各并发执行的进程以各自独立的、不可预知的速度向前推进,可能导致我们的程序不按期望的顺序前进,进而产生错误的结果。

我们把异步环境下的一组并发进程因直接制约而互相发送消息、进行互相合作、互相等待,使得各进程按一定的步骤执行的过程称为进程间的同步

所以同步是和异步相对的概念,保证执行次序一定!而非什么同步进行的那种(其实意思更接近anti异步)

这里最重要的一个就是对临界资源(比方说摄像头)的访问控制,需要实现进程互斥。

线程同步

线程同步就是为了对共享资源的访问进行保护,目的是解决数据一致性问题。

Linux系统提供了多种实现线程同步的机制,常见的有互斥锁、条件变量、自旋锁以及读写锁等。

自旋锁与互斥锁使用层面比较相似,但实现层面上完全不同:当加锁失败时,互斥锁用「线程切换」来应对,自旋锁则用「忙等待」来应对

1)互斥锁在无法获得锁时会让线程陷入阻塞等待状态,自旋锁在无法获取到锁时,将会在原地“自旋”等待。

2)对同一自旋锁两次加锁必然导致死锁,而对同一互斥锁两次加锁不一定导致死锁(互斥锁有多种类型)。

自旋锁的优缺点:

优点:自旋锁开销少,适合异步、协程等在用户态切换请求的编程方式。而且它的实现很底层,也适合内核态的一些开发。

缺点:如果被锁住的代码执行时间过长,自旋的线程会长时间占用 CPU 资源,所以自旋的时间和被锁住的代码执行的时间成「正比」。

对同一自旋锁两次加锁必然导致死锁,而对同一互斥锁两次加锁不一定导致死锁(互斥锁有多种类型)。

读写锁有多种状态.

一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。——更高的并行性

规则:

1)当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读 模式加锁还是以写模式加锁)的线程都会被阻塞。

2) 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以 写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。

读写锁非常适合于对共享数据读的次数远大于写的次数的情况

死锁:

引发死锁需要同时满足四个条件:

(1)互斥锁,多个线程不能同时使用同一个资源

(3)不可抢占,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取

(2)试图多要【持有并等待】当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 B 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1

(4)循环等待,两个线程获取资源的顺序构成了环形链

进程通信与线程通信

管道,消息队列,共享内存。

或者socket。

I/O多路复用

多路复用的本质是同步非阻塞I/O,I/O多路复用技术通过把多个I/O的阻塞复用到同一个select阻塞上,一个进程监视多个描述符,一旦某个描述符就位, 能够通知程序进行读写操作。因为多路复用本质上是同步I/O,都需要应用程序在读写事件就绪后自己负责读写。

select, poll在函数返回后需要查看所有监听的fd看哪些就绪

而epoll只返回就绪的描述符,所以应用程序只需要就绪fd的命中率是百分百。(?)

【待更新】

线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。只需要将数据复制到共享(全局或堆)变量中即可。但是需要避免出现多个线程试图同时修改同一份信息

父子进程

fork()可以创建子进程。JAVA默认会有一个主线程main。

孤儿进程:指的是父进程死掉后还在执行自己任务的子进程。一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。

孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。 

守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束

由于守护进程是脱离控制终端的,因此首先创建子进程,终止父进程,使得程序在shell终端里造成一个已经运行完毕的假象。之后所有的工作都在子进程中完成,而用户在shell终端里则可以执行其他的命令,从而使得程序以孤儿进程形式运行,在形式上做到了与控制终端的脱离。

僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。 

相比来说,僵尸进程后果更大。通过信号机制,杀死父进程,重启,来解决。

银行家算法

被动的话可以找出死锁,并尝试破坏四个条件之一;而银行家算法可以主动避免死锁,它搞放贷信用机制,防止死锁发生。

加餐:OS的内存管理

每个进程都有自己的独立的虚拟内存,我们所写的程序不会直接与物理内打交道。有了虚拟内存之后,它带来了这些好处:

  • 第一,虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域
  • 第二,由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。
  • 第三,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性

Linux 是通过对内存分页的方式来管理内存,分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页. (4kb)

虚拟地址与物理地址之间通过页表来映射,页表是存储在内存里的,内存管理单元 就做将虚拟内存地址转换成物理地址的工作。

【对比和优缺点】

分页式存储管理可以有效地提高内存利用率,而分段存储管理能反应程序的逻辑结构并有利于段的共享。把这两种方式结合起来,就是段页式存储管理方式

【具体机制】

在分页机制下,虚拟地址分为两部分,页号页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,这个基地址与页内偏移的组合就形成了物理内存地址

虚拟地址也可以通过段表与物理地址进行映射,分段机制会把程序的虚拟地址分成 4 个段,每个段在段表中有一个项,在这一项找到段的基地址,再加上偏移量,于是就能找到物理内存中的地址,如下图

如果要访问段 3 中偏移量 500 的虚拟地址,我们可以计算出物理地址为,段 3 基地址 7000 + 偏移量 500 = 7500。

1.2 JAVA多线程安全

线程安全问题,指的是在多线程环境当中,线程并发访问某个资源,从而导致的 原子性,(内存)可见性顺序性问题。以上这三个问题,会导致程序在执行的过程当中,出现了超出预期的结果。

根本原因是在于各个线程是由操作系统随机进行调度,并且各个线程是抢占式执行的。

原子性问题——i++是有三个步骤的,可能执行一半被加塞了,或者被调度了,万一这个操作出现问题了,可能会出现丢失写值的情况,就导致操作部分完成。可以使用原子类操作。

可见性问题——堆的最新值看不到,因为没访问,线程里的还是本地缓存

顺序性问题——多线程执行的顺序性不能保障。

JAVA提供了几种同步机制,来解决这三个问题。

synchronized

同步锁,隐式锁

监视器锁,是互斥锁

正常执行结束,自动释放锁
执行过程中发生异常,JVM让线程自动释放锁
synchronized的同步效率很低,如果某个代码块被其修饰,当一线程进入synchronized修饰的代码块,那么其余线程只能一直等待,等待持有锁的线程释放锁,才能进入同步代码块。

Lock——显示锁
是接口,如果持有锁的线程由于要等待IO或其他原因(如调用sleep方法),被阻塞了,但是没有释放锁,其他线程就只能等待,非常影响程序性能。

因此需要一种机制可以不让等待的线程一直无期限的等待下去(如只等待一定时间,或能够响应中断),通过Lock可以解决。如lock可以判断线程是否成功获取到锁,而synchronized无法做到。

缺点是需要手动控制开关,可能引发死锁。

如果使用了lock,必须主动释放锁,就算发生了异常,也需要手动释放,因为lock不会像synchronized一样自动释放锁。所以使用lock必须在try{}catch(){}中进行并在finally{}中释放锁,防止死锁。

Lock lock = new ReentrantLock();
try {
    lock.lock();
    System.out.println("上锁了");
}catch (Exception e){
    e.printStackTrace();
}finally {
    lock.unlock();
    System.out.println("解锁了");
}

ReentrantLock可重入锁

ReentrantLock是唯一实现了Lock接口的类,且提供了更多的方法。

可重入锁:某个线程已经获得某个锁,可以再次获取锁而不会死锁。(@自旋锁)

ReentrantLock类可以作为公平锁或非公平锁进行配置,而synchronized关键字在某种程度上可以被视为一种非公平锁,因为它并不保证等待线程按照请求顺序获得锁。联想一下进程/线程调度算法

ReadWriteLock是读写锁

可以实现共享锁+排他锁(独占锁),第一个想到的改造对象就是可重入锁,

这样就可以变成可重入读写锁,读是共享的,写是排他的,这样对于大量读的方法来说上锁性能会更好。

互斥锁(监视器锁)是用在同步锁的概念,指的是锁只能给一个对象支配;

排他锁是和共享锁相对的概念。

volatile

为了提高性能,编译器和处理器常常会对指令做重排,volatile 通过实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象,保证一定的顺序性

load:从内存读到寄存器,也就是加载操作
store:从寄存器写入到内存,就就是存储操作

屏障:强行要求先完成xx才能通过屏障

编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序

在每个volatile写操作的前面插入一个StoreStore屏障,后面插入一个StoreLoad屏障。

(Store-Store★-Load)

在每个volatile读操作的后面插入一个LoadLoad屏障,一个LoadStore屏障。(顺序无所谓)

(Load★-Load-Store)

当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到 CPU 缓存中。如果计算机有多个CPU,每个线程可能在不同的 CPU 上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步,保障了可见性

对 volatile 变量进行写操作时,会在写操作后加一条 store 屏障指令,将工作内存中的共享变量刷新回主内存;

对 volatile 变量进行读操作时,会在写操作后加一条 load 屏障指令,从主内存中读取共享变量;

原子性不保障

它和synchronized都是JVM级别的关键字,而可重入锁是API级别。

CAS
是乐观锁思想的一种体现,在分布式数据库中可以使用版本号来进行。

CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果

原子类大量使用了CAS技术。

里面有三个元素:内存值,预期原值,更新值。

例:AtomicInteger a = new AtomicInteger();

boolean success = a.compareAndSet(int a.get()[预期原值], int newValue[新值]);

比较内存值和预期原值,来决定要不要更新,防止并发冲突。

ABA问题,如果变量的值变化顺序刚好是:A-B-A,CAS可能觉得没事。

这时候可以引进版本号。

AtomicStampedReference(带版本号的引用类型原子类)

2.并发编程

2.1 Thread

创建和使用

创建

  • 继承Thread类
  • 重写runnable callable接口里的run()方法(有无返回值的区别)
  • 使用线程池

方法

JAVA的线程调度算法决定了线程可以根据优先级先安排,甚至把低优先级的挤走。

thread.join() 

调用该方法的线程强行进入运行状态,将上个线程阻塞。直到这个线程完成。

thread.sleep() 

调用该方法的线程会抱着锁进入睡眠,期间释放CPU资源,

thread.yield()

调用该方法的线程会暂时放出使用权,但不意味着睡眠,线程让出资源后,自己会再次抢占。

thread.setDaemon(true)

守护线程,即该线程会随着其他前台线程都结束而慢慢结束

synchronized的方法

wait()、notify()和notifyAll()这三个方法都依赖于Java对象的互斥锁。调用这些方法时,当前线程必须持有该对象的互斥锁,所以通常它们三都与synchronized关键字一起使用

当线程调用对象的wait()方法时,它会释放该对象的互斥锁。这允许其他线程能够进入同步代码块或方法,并获得锁以执行其操作。(与此相反sleep方法不会释放互斥锁

wait()方法通常与notify()或notifyAll()方法一起使用,以在多个线程之间实现协作。一个线程可以调用wait()来等待另一个线程

而另一个线程可以在条件成立后

调用notify()或notifyAll()来唤醒使用wait()而休息的线程。

使用方法

(任务1)synchronized ( Object o) {o.wait(); ……}

(任务2)synchronized ( Object o) {……o.notify(); }

JAVA的线程生命周期

NEW(new Thread())

RUNNABLE(Thread.start()/Thread.run())

然后可能到BLOCKED,也可能到WAITTING或TIMED_WAITTING

区别是一类是阻塞

一类是等待(根据有无超时参数分类)(比如.join()方法,.wait())

最后都是TERMINATED

Thread.start()/Thread.run()的区别:

1. 一个自带了synchronized,一个没有,所以start是同步方法,另一个不是。

因此Thread.start()可以保证new出来的线程按顺序执行,Thread.run()不行。

2.Thread.start()其实用了run()的接口,它包含了更多方法,比如线程名,它可以拥有自己独立的线程名,而Thread.run()会使用父线程的名字(默认main)

3.Thread.run()可以执行无数次,Thread.start()只有一次


2.2 ThreadLocal

介绍一下ThreadLocal?

ThreadLocal通过为每个线程提供独立的变量副本,提供了一种更加轻量级和简单的解决方案,让每个线程持有其自己的变量副本,避免了线程间的数据竞争和同步问题

如果需要在多个线程之间共享数据并保证线程安全,可以使用 synchronized;而ThreadLocal 适用于变量在线程间隔离,而在方法或类间共享的场景,所以ThreadLocal 变量通常被private static修饰

​ThreadLocalMap 是 ThreadLocal 的内部实现类,用于存储每个线程的变量副本。它是一个散列表(HashMap),以 ThreadLocal 实例作为键(Key)。

private static ThreadLocal1<String> aaa = new ThreadLocal<String>();

比如说一个类里建立了三个ThreadLocal

那么一个线程里会有一张独立共享的​ThreadLocalMap,分别存储了对应实例的值,如上图。

某教程里写的一个工具类
ThreadLocal与内存泄露[JVM]

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

但因为线程的创建比较昂贵,所以 Web 服务器往往会使用线程池来处理请求,这就意味着线程会被重用。这时,使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。如果在代码中使用了自定义的线程池,也同样会遇到这个问题。

可能发生内存泄漏的原因是?

由于ThreadLocalMap的key是弱引用,而Value是强引用,这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露

当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本才被回收。

详细版(待删减):

ThreadLocal 在 ThreadLocalMap 中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它

那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况

外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来

这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。

2.3 线程池

引用:https://blog.csdn.net/sixteen_16/article/details/122897988

2. 线程池创建
7大参数、4种拒绝策略
1)线程池原理

2)创建线程池 ThreadPoolExecutor()

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.corePoolSize = corePoolSize;
       this.maximumPoolSize = maximumPoolSize;
       this.workQueue = workQueue;
       this.keepAliveTime = unit.toNanos(keepAliveTime);
       this.threadFactory = threadFactory;
       this.handler = handler;
}

3. 线程池特点与应用
池化技术是创建和管理一个连接的缓冲池的技术,可以对程序中珍惜重要的资源进行有效管理。线程的创建、销毁以及切换都会带来开销,影响系统整体性能,通过线程池技术可以维持多个线程重复使用,并对线程的 分配回收、切换 进行 管理,不仅能够保证内核的充分利用,还能防止过分调度。

降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行。
提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

JVM

https://www.canva.cn/design/DAGOGcI58TY/9pt40l9vXHSLgg6EBcQByQ/view?utm_content=DAGOGcI58TY&utm_campaign=designshare&utm_medium=link&utm_source=editor

框架类

1.Springboot 注解

1,包扫描+组件标注注解
@Component:泛指各种组件
@Controller、@Service、@Repository都可以称为@Component。
@Controller:控制层
@Service:业务层
@Repository:数据访问层

2,@Bean

3,@Import
@Import(要导入到容器中的组件)
@ImportSelector:返回需要导入的组件的全类名数组
@ImportBeanDefinitionRegistrar:手动注册bean到容器中

4,注入bean的注解
@Autowired:由bean提供
@Autowired可以作用在变量、setter方法、构造函数上; 有个属性为required,可以配置为false; @Inject:由JSR-330提供
@Inject用法和@Autowired一样。
@Resource:由JSR-250提供 @Autowired、@Inject是默认按照类型匹配的,@Resource是按照名称匹配的,@Autowired如果需要按照名称匹配需要和@Qualifier一起使用,@Inject和@Name一起使用。
@Primary

5,Java配置类相关注解
@Configuration 声明当前类为配置类;
@Bean注解在方法上,声明当前方法的返回值为一个bean,替代xml中的方式; @ComponentScan用于对Component进行扫描;

6,切面(AOP)相关注解 Spring支持AspectJ的注解式切面编程。
@Aspect 声明一个切面
@After 在方法执行之后执行(方法上)
@Before 在方法执行之前执行(方法上)
@Around 在方法执行之前与之后执行(方法上)
@PointCut 声明切点

7,@Value注解
(1)支持如下方式的注入: 注入普通字符,注入操作系统属性,注入表达式结果,注入其它bean属性,注入文件资源,注入网站资源,注入配置文件
(2)@Value三种情况的用法。 ${}是去找外部配置的参数,将值赋过来 #{}是SpEL表达式,去寻找对应变量的内容 #{}直接写字符串就是将字符串的值注入进去

8,环境切换
@Profile 指定组件在哪个环境的情况下才能被注册到容器中,不指定,任何环境下都能注册这个组件。
@Conditional 通过实现Condition接口,并重写matches方法,从而决定该bean是否被实例化。

9,异步相关
@EnableAsync 配置类中通过此注解开启对异步任务的支持;
@Async在实际执行的bean方法使用该注解来声明其是一个异步任务(方法上或类上所有的方法都将异步,需要@EnableAsync开启异步任务)

10、定时任务相关
@EnableScheduling 在配置类上使用,开启计划任务的支持(类上)
@Scheduled 来申明这是一个任务,包括cron,fixDelay,fixRate等类型(方法上,需先开启计划任务的支持)

11,Enable***注解 这些注解主要是用来开启对xxx的支持:
@EnableAspectAutoProxy:开启对AspectJ自动代理的支持;
@EnableAsync:开启异步方法的支持;
@EnableScheduling:开启计划任务的支持;
@EnableWebMvc:开启web MVC的配置支持;
@EnableConfigurationProperties:开启对@ConfigurationProperties注解配置Bean的支持; @EnableJpaRepositories:开启对SpringData JPA Repository的支持; @EnableTransactionManagement:开启注解式事务的支持; @EnableCaching:开启注解式的缓存支持;

12,测试相关注解 @RunWith 运行器,Spring中通常用于对JUnit的支持
@ContextConfiguration用来加载配置配置文件,其中classes属性用来加载配置类。

13,SpringMVC常用注解
@EnableWebMvc在配置类中开启Web MVC的配置支持。
@Controller
@RequestMapping用于映射web请求,包括访问路径和参数。
@ResponseBody 支持将返回值放到response内,而不是一个页面,通常用户返回json数据。 @RequestBody允许request的参数在request体中,而不是在直接连接的地址后面。(放在参数前)
@PathVariable 用于接收路径参数,比如@RequestMapping(“/hello/{name}”)声明的路径,将注解放在参数前,即可获取该值,通常作为Restful的接口实现方法。
@RestController 该注解为一个组合注解,相当于@Controller和@ResponseBody的组合,注解在类上,意味着,该Controller的所有方法都默认加上了@ResponseBody。
@ControllerAdvice 全局异常处理,全局数据绑定,全局数据预处理
@ExceptionHandler 用于全局处理控制器里的异常。
@InitBinder 用来设置WebDataBinder,WebDataBinder用来自动绑定前台请求参数到Model中。 @ModelAttribute

2.设计模式

2.1 单例模式

参考链接:

一文带你彻底搞懂设计模式之单例模式!!由浅入深,图文并茂,超超超详细的单例模式讲解!!-CSDN博客

单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式是在内存中 仅会创建一次对象 的设计模式

简单来说单例模式的简单实现就是
成员是 私有的静态的
构造方法是 私有的
对外暴露的获取访问是 公有的静态的

单例模式分类
  • 饿汉式类加载就会导致该单实例对象被创建

  • 懒汉式类加载不会导致该单实例对象被创建,而是首次使用该对象时被创建,创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象。,否则则先执行实例化操作。

public class Singleton{

    //在该类中创建一个该类的对象供外界去使用
    private static Singleton instance= new Singleton();

    // 构造方法 private 化
    private Singleton(){

    }

    // 得到 Singleton 的实例(唯一途径)
    public static Singleton getInstance() {
        return instance;
    }
}

饿汉式↑ 懒汉式↓

public class Singleton {

    //在该类中创建一个该类的对象供外界去使用
    private static Singleton instance;

    // 构造方法 private 化
    private Singleton(){ 

    }

    // 得到 Singleton 的实例(唯一途径)
    public static Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

验证:

public class Main {
    public static void main(String[] args) {
        // Singleton s0 = new Singleton(); // 原先的实例化方法
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        if (s1 == s2){
            System.out.println("两个对象是相同的实例");
        }
    }
}
  • 成员变量 instance 为什么是私有的?

最直接的原因就是 防止外部直接访问,如果不声明为 private ,其他类将能直接访问和修改它,而这就违反了单例模式的原则,因为单例模式要求类只有一个实例化

  • 成员变量 instance 为什么是静态的?

使用 static 修饰后,意味着该变量是属于类的,而不是类的实例
你可以通过 类名.变量名 的方式直接访问
同时,将只存在一个 instance
你可以像
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
这样创建两个实例化对象,但他们都共享一个 instance 对象,确保只有一个单例

  • 构造方法 Singleton() 为什么是私有的?

与成员变量相似,是为了防止外部实例化 ,防止其他类通过 new 关键字直接创建该类的实例(new 关键字本质是调用了类的构造方法),而应该通过特定途径(通常是类提供的静态方法,也就是 getInstance()

  • getInstance() 方法为什么是公有的?

很显然,这是我们暴露给其他类调用来创建实例的方法,因此必须是公有的,如果是私有那不就是成黑盒搁这里圈地自萌了~

  • getInstance() 方法为什么是静态的?

与成员变量相似,因为构造方法被私有化了,我们无法通过 new 关键字来实例化对象,而通过 类名.方法名 的方式可以直接访问

线程安全

因为饿汉式单例模式下,单例实例在类加载的时候就已经创建,并在静态化容器中完成初始化,所以他通常是线程安全的

public class Main1 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            Singleton s = Singleton.getInstance();
            System.out.println("Thread 1 created instance: " + s);
        });

        Thread t2 = new Thread(() -> {
            Singleton s = Singleton.getInstance();
            System.out.println("Thread 2 created instance: " + s);
        });

        Thread t3 = new Thread(() -> {
            Singleton s = Singleton.getInstance();
            System.out.println("Thread 3 created instance: " + s);
        });

        // 启动线程
        t1.start();
        t2.start();
        t3.start();


        // 等待线程结束
        try {
            t1.join();
            t2.join();
            t3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

而懒汉式单例模式可能导致几个线程同时判定对象为空,然后各自创建实例,这是线程不安全的。

解决方法:加锁,比如在类的定义里加一个关键字,但并发性能很差

 

public static synchronized Singleton getInstance() {
    if (instance== null) {
        instance= new Singleton();
    }
    return singleton;
}

// 或者

public static Singleton getInstance() {
    synchronized(Singleton.class) {   
        if (instance== null) {
            instance = new Singleton();
        }
    }
    return instance ;
}

性能提升:
如果没有实例化对象则加锁创建
如果已经实例化了,则不需要加锁,直接获取实例

注意到两次判断是为了防止那些没拿到实例化的线程,拿到锁以后误会。

public static Singletion getInstance() {

if (instance == null) {
synchronized(Singleton.class){
if (instance == null){instance = new Singletion();}
}
return instance;

}
}

因为需要两次判空,且对类对象加锁,该懒汉式写法也被称为:Double Check(双重校验) + Lock(加锁)

破坏单例模式

利用反射,强制访问类的私有构造器,去创建另一个对象

对于序列化与反序列化破坏单例模式的问题,主要是通过readObject()方法,出现了破坏单例模式的现象,主要是因为这个方法最后会通过反射调用无参数的构造方法创建一个新的对象,从而每次返回的对象都不一致。

对于反射破坏单例模式是因为单例模式通过 setAccessible(true) 指示反射的对象在使用时,取消了 Java 语言访问检查,使得私有的构造函数能够被访问,

而单例模式的设计在于只保留一个公有静态函数来获取唯一的实例,其他方法(构造函数)或字段为私有,外界不能访问。

而反射破坏了这一原则,它突破了构造函数私有的限制,可以获取单例类的私有构造函数并使用其创建多个对象。

我们可以简单地理解枚举实现单例的过程:在程序启动时,会调用Singleton的空参构造器,实例化好一个Singleton对象赋给INSTANCE,之后再也不会实例化

防止反射攻击:
枚举类型的实例在编译后会被编译成静态常量,并且在运行时通过Enum.valueOf()方法来获取实例。这个方法在内部会检查传递的名称是否与枚举定义中的名称匹配,并且直接返回对应的枚举实例,而不允许创建新的枚举实例。
因此,即使调用者尝试使用反射创建新的枚举实例,也会因为名称不匹配而失败。例如,Enum.valueOf(MyEnum.class, "NEW_VALUE")将会抛出IllegalArgumentException。
防止序列化/反序列化攻击:
枚举实例在序列化时会被转换为其名称,而不是实例本身。这意味着,即使反序列化时提供了枚举实例的字节流,也会根据名称来查找对应的枚举实例,而不是创建一个新的实例。
由于枚举实例是静态的,反序列化时并不会创建新的实例,而是恢复之前序列化时的那个实例。这样,即使通过序列化/反序列化机制,也无法创建新的枚举实例。
防止直接实例化:
枚举类型默认有一个私有构造器,这意味着不能直接实例化枚举类型,只能通过枚举的静态方法来获取实例。
例如,对于public enum MyEnum { INSTANCE },你不能写代码MyEnum myEnum = new MyEnum();,因为这会编译错误,因为MyEnum的构造器是私有的。
通过这些机制,枚举类有效地阻止了通过反射、序列化和反序列化机制来破坏单例的行为。这使得枚举类成为实现单例模式的一种非常安全和简洁的方法。

(1)单例模式常见的写法有两种:懒汉式、饿汉式

(2)懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题

(3)饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。

(4)在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;

(5)如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题

(6)为了防止多线程环境下,因为指令重排序导致变量报NPE,需要在单例对象上添加volatile关键字防止指令重排序

(7)最优雅的实现方式是使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。

2.2 常见几种设计模式

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式

结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式解释器模式。

策略模式(Strategy Pattern)

概念: 策略模式定义了一系列算法,并将每个算法封装起来,使它们可以互相替换。策略模式使得算法的变化独立于使用算法的客户。

特点:

  • 封装算法: 每个策略实现一个算法,并可以被替换。
  • 独立性: 客户端不需要知道具体的算法实现,只需要知道如何使用策略接口。

应用场景: 需要在运行时选择算法,或需要在不同的算法间切换时。

示例: 付款方式(信用卡支付、支付宝支付、微信支付等)。

责任链模式(Chain of Responsibility Pattern)

概念: 责任链模式创建了一个链表结构,使得请求沿着链传递直到某个处理者对象处理它。每个处理者都有机会处理请求或将其传递给链中的下一个处理者。

特点:

  • 动态分配处理者: 请求在责任链中逐级传递,直到被处理或链尾。
  • 解耦: 发送请求的对象和处理请求的对象解耦。

应用场景: 多个对象可以处理一个请求时,例如处理文件格式转换或用户请求的权限检查。

示例: 事件处理系统中的事件处理链。

装饰器模式(Decorator Pattern)

概念: 装饰器模式允许在不改变对象自身的情况下,动态地添加额外的功能。它通过将对象包装在装饰器对象中来实现功能的扩展。

特点:

  • 动态扩展: 可以在运行时增加功能。
  • 透明性: 装饰器对象和原始对象具有相同的接口,客户端可以通过装饰器使用原始对象的功能。

应用场景: 需要动态增加对象功能时,例如在图形用户界面中为按钮增加边框或滚动条。

示例: Java I/O流中的装饰器(如 BufferedReaderPrintWriter)。

适配器模式(Adapter Pattern)

概念: 适配器模式将一个类的接口转换成客户端所期望的另一种接口。适配器使得原本接口不兼容的类可以协同工作。

特点:

  • 接口转换: 使不兼容的接口能够互操作。
  • 封装: 适配器封装了需要适配的类的接口。

应用场景: 需要将旧系统与新系统集成时,或将现有接口适配到新接口时。

示例: 电源适配器(将不同电压的电源转换为设备所需的电压)。

代理模式(Proxy Pattern)

概念: 代理模式为其他对象提供一个替代者或占位符,以便控制对这个对象的访问。代理可以在访问真实对象之前或之后执行一些额外的操作。

特点:

  • 控制访问: 代理可以控制对实际对象的访问。
  • 延迟加载: 代理可以在需要时才加载实际对象(虚代理)。

应用场景: 需要对访问对象进行控制时,例如在网络中访问远程对象,或在资源消耗较大的对象中实现延迟加载。

示例: 网络代理服务器(缓存网络请求,控制网络流量)。

总结与区别

  • 策略模式: 主要关注在运行时选择和切换算法。
  • 责任链模式: 关注于在一系列对象中传递请求,直到某个对象处理它。
  • 装饰器模式: 允许动态添加功能而不改变对象自身。
  • 适配器模式: 使得接口不兼容的对象可以协同工作。
  • 代理模式: 控制对对象的访问,可以添加额外的操作或延迟加载。

这些模式在不同的设计问题中各有适用,通过合理使用它们可以使系统更具灵活性和可维护性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值