145. linux打印前row行日志
参考:linux日志打印
- 前10行日志
head -n 10 xx.log
- 后10行日志
tail -n 10 xx.log
tail -10f xx.log
- 使用sed命令
sed -n '9,10p' xx.log #打印第9、10行
- 使用awk命令
awk 'NR==10' xx.log #打印第10行
awk 'NR>=7 && NR<=10' xx.log # 打印第7-10行
146. 数据库中文乱码问题
参考:mysql中文乱码
如果遇见插入中文乱码问题,先确定问题的产生肯定与编码方式有关,先检查数据库的编码方法,查看下数据库字符集,再查看下表的编码,最后检查下表中列的编码。检查这三个地方的编码,一般问题即可迎刃而解。
1)查看数据库编码:show variables like ‘character_set%’;
2) 查看表的数据集:show create table 表名;
3)修改字段编码方式:ALTER TABLE 表名 CHANGE 列名 列名 VARCHAR(45) CHARACTER SET UTF8 NOT NULL;
147. Spring的底层原理是什么
参考:Spring相关
148. 抽象类和接口的区别
参考:抽象类和接口区别
1)接口是行为的抽象,是一种行为的规范,接口是like a 的关系;抽象是对类的抽象,是一种模板设计,抽象类是is a 的关系。
2)接口没有构造方法,而抽象类有构造方法,其方法一般给子类使用
3)接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
4)抽象体现出了继承关系,继承只能单继承。接口提现出来了实现的关系,实现可以多实现。接口强调特定功能的实现,而抽象类强调所属关系。
5)接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。
149. 最长回文子串
150. 数组中前k大元素
参考:前k大元素
海量数据的topK问题
1)使用排序:冒泡、快排等
2)前k大就建立小根堆,可以使用优先级队列
151. 有大量查询如何优化?代码方面如何优化?数据库如何优化?
参考:大量查询优化
152. 重载、重写、重构的区别?
参考:重载、重写和重构
1)重载:构造方法可以重载,要求同名不同参、与返回值类型无关,一般是再同一个类中进行重载
2)重写:重写(又叫覆盖)是继承的实现方式之一,也就是说只有在子类中才会出现方法的重写。重写是指在子类中保留父类成员方法的名称不变,参数列表不变,重写成员方法的实现内容,修改成员方法的返回值类型(必须满足修改的返回值类型是父类中同一方法返回值类型的子类),或更改成员方法的存储权限。
子类不能重写父类的构造方法
3)重构:重构是继承中一种特殊的重写方式,只重写子类方法中的实现内容,成员方法的名称,参数类型、个数及成员方法的返回值都保持不变。
综上,重载可以出现在任意类的任意方法中,方法的名称相同,参数的类型,个数,顺序三个中只要有一个不同即可实现方法的重载。重写和重构是发生在子类中,也就是说只有出现继承才会需要重写和重构的实现。重写可以通过修改成员方法的实现内容,修改成员方法的返回值类型或更改成员方法的存储权限实现,但必须保证方法名和参数列表不变。而重构是一种特殊的重写方式,只可以重写实现内容,其他都不能改变。
153. 一个自然数n分解成若干个数相乘,求这些数的最小和
154. 一个英文句子全部逆序输出?句子中单词不变,句子逆序输出?如果多个空格怎么办?
155. 在linux上创建一个文件,创建失败了可能是什么原因?
1)当前目录对应的磁盘空间不足
# 查看当前目录所在磁盘的空间使用情况
df -h ./
2)inode不足:一个文件对应一个inood
# 查看当前目录所在磁盘的inode的使用情况
df -i ./
156. 在linux上运行一个二进制文件,如果结果不符合预期,怎么调试?
157. linux的读文本筛选,如果有4列,需要按照第三列降序排列,怎么实现?
参考:linux文本排序
sort -u -n -r -k 3 -t / file.txt
① -u:去重
② -n;按照数字
③ -r:降序,默认是升序
④ -k col:按照col列排序
⑤ -t ch:分隔符按照ch排序
158. 数据结构有哪些?关于数据结构有哪些知识点?
参考:数据结构
线性结构、非线性结构
数组、链表、队列、栈以及树等
159. 双向链表插入节点
参考:双向链表插入、删除节点
-
插入节点
-
删除节点
160. 双向链表循环的遍历如何遍历
161. 单链表如果有环,如何进行判断?
使用快慢指针
参考:判断链表是否有环
162. 青蛙跳台阶问题
斐波那契数列
参考:青蛙跳台阶🐸
163. 页面或网站打开特别慢的原因
参考:网站打开慢的原因
1)客户端:硬件配置低、资源不足
2)前端:浏览器页面渲染的过程中存在异常(JS代码bug or 插件)、调用耗时过长的接口或请求的资源过多
3)网络:用户带宽不足 or 网络环境差、DNS解析慢(可以Ping命令看耗时)、HTTP劫持、未配置CDN(CDN:将内容缓存在终端用户附近)
4)服务器:数据库、代码
164. Linux窗口端口被哪个进程占用
参考:Linux端口占用
1)lsof -i:端口号
2)netstat -tunlp | grep 端口号
-t (tcp) 仅显示tcp相关选项
-u (udp) 仅显示udp相关选项
-n 拒绝显示列名,能显示数字的全部转化为数字
-l 仅显示出在listen(监听)的服务状态
-p 显示潜力相关链接的程序名
165. linux文本字符串替换
166. 请求https底层流程
参考:https通信
167. 一个文件很大有1亿个ip地址,怎么用100M的内存找出出现次数最多的ip地址?
参考:海量数据找IP
其实就是使用分治思想:哈希模,小内存排序
168. nestat各个字段含义
参考:netstat字段详解
169. 进程状态?
参考:进程状态和转换
- 三态模型:进程状态:运行态、就绪态(具备运行条件,但是没有获得CPU;就绪队列)、阻塞态/等待态(即使获得CPU也无法运行)
-
五态模型:运行、就绪和阻塞状态都是可以直接到终止态的;某些操作系统允许父进程终结子进程(就绪和阻塞)
-
七态模型:
1)挂起就绪态:进程具备运行条件,但目前在外存中,只有它被对换到内存才能被调度执行。
2)挂起等待态:表明进程正在等待某一个事件发生且在外存中。
-
挂起进程具有如下特征:
1)该进程不能立即被执行
2)挂起进程可能会等待一个事件,但所等待的事件是独立于挂起条件的,事件结束并不能导致进程具备执行条件。 (等待事件结束后进程变为挂起就绪态)
3)进程进入挂起状态是由于操作系统、父进程或进程本身阻止它的运行。
4)结束进程挂起状态的命令只能通过操作系统或父进程发出。
170. 有哪些进程调度算法?具体说明
- 两种进程调度方式:
1)抢占式:对提高系统吞吐率和响应效率都有明显的好处,但抢占也要遵循一定原则。
2)非抢占式:即使有更紧急或优先级更高的任务进入处理机,也必须等待当前任务执行完成或阻塞。其优点是实现简单,系统开销小,适用于大多数批处理系统,但它不能用于分时系统和大多数实时系统。 - 调度的基本准则:
CPU利用率、系统吞吐量、周转时间、等待时间、响应时间 - 经典进程调度算法
1)先来先服务算法FCFS
① FCFS属于不可剥夺(抢占)算法。
② 特点分析:算法简单,但是效率低下;对长作业较为有利,对短作业不利;利于CPU繁忙型作业,不利于I/O繁忙型作业。
2)短进程优先算法SPF
① 短进程优先调度算法从进程的就绪队列中挑选那些运行时间(估计时间)最短的进程进入主存运行。
② 这是一个非剥夺算法,不适用于分时系统,不能及时回应。
③ 算法缺点:
A. 长进程会被饿死在就绪队列中。该算法对长作业不利,SJF中长作业的周转时间会增加。更糟的是,若一旦有长作业进入系统的后备队列,由于调度程序总是优先调度那些短作业(即使是后来的短作业也会被优先安排给处理机),导致长作业长期不被调度,饿死在后备队列中。
B. 完全没有考虑作业的紧迫程度,因而不能保证紧迫的作业会被及时处理。
C. 由于作业的长短只是根据用户所提供的预估的执行时间而定的,而用户又可能会有意无意地缩短其作业的估计运行时间,使得算法不一定能真正做到短作业优先调度。为此,当一个进程的运行时间超过所估计的时间时,系统将停止这个进程,或对超时部分加价收费。
④ 算法优点:平均等待时间、平均周转时间最少
3)优先级调度算法
① 在进程调度中,优先级调度算法每次从就绪队列中选择优先级最高的进程,并分配处理机,运行。
② 调度算法分为两种:抢占式和非抢占式
③ 根据进程创建后其优先级是否可以改变,可以将进程优先级分为两种:
A. 静态优先级:优先级是在创建进程时确定的,并且进程的整个运行期间保持不变。确定静态优先级的主要依据有进程类型、进程对资源的要求、用户要求。
B. 动态优先级:在进程运行过程中,根据进程情况的变化动态调整优先级。动态调整优先级的主要依据有进程占有CPU的时间的长短、就绪进程等待CPU时间的长短。
④ 一般来说,进程优先级可以参考以下原则:
A. 系统进程>用户进程。
B. 交互型进程>非交互型进程(前台进程>后台进程)
C. I/O型进程>计算型进程。
4)高响应比优先调度算法
① 是对FCFS调度算法和SJF调度算法的一种综合平衡,同时考虑了每个作业的等待时间和估计的运行时间。
② 是一种非抢占式调度算法
③响应比Rp = (等待时间+要求服务时间)/要求服务时间
。每次选取响应比最高的进程进行运行
④ 根据公式推断:
A. 作业的等待时间相同时,要求服务时间越短,响应比越高,有利于短作业。
B. 要求服务时间相同时,作业的响应比由其等待时间决定,等待时间越长,其响应比越高,因而它实现的是先来先服务。
C. 对于长作业,作业的响应比可以随等待时间的增加而提高,等待时间足够长时,其响应比便可升到很高,从而可以获得处理机,不会饿死。
5)时间片轮转调度算法
① 时间片轮转调度算法主要适用于分时系统。
② 在这种算法中,系统将所有就绪进程按到达时间的先后次序排成一个队列,进程调度程序总是选择就绪队列中的第一个进程执行,即先来先服务的原则,但是仅能运行一个时间片。在使用完一个时间片后,即使进程并未完成其运行,它也必须释放出(被抢占)处理机给下一个就绪的进程,而被抢占的进程返回到就绪队列的末尾重新排队,等候再次运行。
③ 时间片的选择要适当,可以根据系统响应时间、就绪队列中的进程数目和系统的处理能力等决定。
④ 时间片是否用完的判定程序是由时钟中断处理程序激活的,因此时间片值必须大于时钟中断间隔。
6)多级反馈队列调度算法
① 多级反馈队列调度算法是时间片轮转算法和优先级调度算法的综合与发展。
② 算法优点:
A. 终端型作业用户:短作业优先。
B. 短批处理作业用户:周转时间较短。
C. 长批处理作业用户:经过前面几个队列得到部分执行,不会饿死。
7)最短剩余时间优先调度算法
① 最短剩余时间优先调度算法是将短进程优先调度算法用于分时环境的变形
② 最短剩余时间优先调度算法允许被一个新进入系统的且其运行时间短于当前运行进程的剩余运行时间的进程所抢占。
③ 该算法使短进程一进入系统就能立即得到服务,从而缩短进程的平均等待时间
④ 缺点:系统开销增加。首先,要保存进程的运行情况记录,以比较其剩余时间长短:其次,剥夺本身也要消耗处理机时间。
171. IO缓冲和非缓冲,网络IO是缓冲吗
-
根据是否利用标准库缓存,可以把文件I/O分为缓冲I/O与非缓冲I/O。
1)缓冲I/O,是指利用标准库缓存来加速文件的访问,而标准库内部再通过系统调度访问文件。
2)非缓冲I/O,是指直接通过系统调用来访问文件,不再经过标准库缓存。
这里所说的“缓冲”,是指标准库内部实现的缓存 -
服务器端处理请求的大致流程
-
补充:fork函数fork()
1)fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程
2)一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。
3)fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
① 在父进程中,fork返回新创建子进程的进程ID;
② 在子进程中,fork返回0;
③ 如果出现错误,fork返回一个负值;(可能原因:进程数上限、内存不足)
可以通过fork返回的值来判断当前进程是子进程还是父进程。
4)创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
172. 平衡二叉树的概念,删除、插入、左旋和右旋操作
参考:二叉搜索树、二叉搜索树详解
平衡二叉树的左旋和右旋_理论、平衡二叉树代码实现
- 二叉搜索树:左孩子比父节点小,右孩子比父节点大,还有一个特性就是”中序遍历“可以让结点有序。
- 平衡二叉树:任意节点的左右子树高度差小于等于1。
173. 多态运行时怎么确定虚函数?如果一个类继承了多个包含虚函数的类,如何计算类的大小?
参考:C++多态虚函数表
174. 在一个文本文件中,统计“你好”这个词出现的次数
175. TCP和UDP在实际中的应用
参考:TCP和UDP的实际应用
1)TCP:一般用于准确性要求较高的场景,如收发邮件、文件传输和远程登录
2)UDP:一般用于对效率要求较高的场景,如即时通信(QQ聊天、视频以及语音电话)
176. 单例模式能解决说明问题,什么场景下会使用单例?实际中有哪些应用场景?
1)单例模式:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称
2)作用:避免频繁创建和销毁系统全局使用的对象
3)特点:
① 优点:节约资源,避免对资源的多重占用
② 缺点:不适用于可变的对象;无抽象层,不易扩展;若实例化对象长时间不使用会被回收,导致对象的丢失
4)适用:
① 需要频繁实例化然后销毁的对象。
② 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
③ 有状态的工具类对象:频繁访问数据库或文件的对象
5)场景:
① 外部资源如打印机,内部资源如系统配置
② 电脑的任务管理器、回收站
③ 应用程序的日志文件
④ 网站计数器
⑤ 数据库连接池、多线程的线程池
6)实现方式:懒汉模式(有线程安全问题)和饿汉模式
177. java有自动回收机制,那是不是不用考虑内存泄漏问题了呢?
参考:Java内存泄漏问题
- 判断对象可以回收:
① 计数器:有引用则计数器值加1,引用失效则计数器值减1;直到计数器值为0则释放回收该对象。但是无法解决对象之间的循环引用问题。
② 可达性分析:所有生成的对象都是一个称为"GC Roots"的根的子树。从GC Roots开始向下搜索,搜索所经过的路径称为引用链(Reference Chain);当一个对象到GC Roots没有任何引用链可以到达时,就称这个对象是不可达的(不可引用的),也就是可以被GC回收了。 - GC回收哪些内存:
1)内存运行时JVM会有一个运行时数据区来管理内存。它主要包括5大部分:程序计数器(Program Counter Register)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。
2)其中程序计数器、虚拟机栈、本地方法栈是每个线程私有的内存空间,随线程而生,随线程而亡。例如栈中每一个栈帧中分配多少内存基本上在类结构确定是哪个时就已知了,因此这3个区域的内存分配和回收都是确定的,无需考虑内存回收的问题。
3)方法区和堆就不同了,一个接口的多个实现类需要的内存可能不一样,我们只有在程序运行期间才会知道会创建哪些对象,这部分内存的分配和回收都是动态的,GC主要关注的是这部分内存。
4)GC主要进行回收的内存是JVM中的方法区和堆。 - 内存泄漏的根本原因:内存对象明明已经不需要的时候,还仍然保留着这块内存和它的访问方式(引用)。
- 内存泄漏常见领域:手机端对于内存和CPU的要求是比较严格的,当发生内存泄漏时会导致程序效率低下,占用很多额外的内存,甚至可能导致程序的崩溃。
- 避免内存泄漏:明确变量的生命周期,当内存对象不再使用时进行手动置空
- 引起内存溢出的原因:
1)内存中加载的数据过于庞大,一次从数据库中获取大量的数据
2)集合类中有对对象的引用,使用后没有清空,使JVM不能回收
3)代码中存在死循环或循环过程中产生过多重复的对象实体
4)引用的第三方软件中有bug
5)启动参数内存值设置的过小 - 内存泄漏解决方案:
1)修改JVM启动参数,直接增加JVM内存。
2)检查错误日志,查看“OutOfMemory
”是否有其他的异常或错误。
3)对代码进行走查和分析,找出可能发生内存溢出的位置。 - 重点排查:
1)检查数据库连接中的查询:尽量避免全量查询,对于数据库查询尽量采用分页的方式查询。
2)检查代码中是否有死循环或递归调用。
3)检查是否有大循环重复产生新对象实体。
4)检查List、Map等集合对象是否有使用完后未清除的问题。List、Map等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。 - 补充:内存和CPU检测功工具
178. 五个线程去切蛋糕,五个线程取蛋糕,会出现什么问题,如何进行优化?
参考:多线程的顺序执行
保证线程顺序:
1)join方法
thread1.start();
thread1.join();
2)ExecutorService方式
static ExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
public static void main(String[] args) throws Exception {
executorService.submit(thread1);
executorService.submit(thread2);
executorService.submit(thread3);
executorService.submit(thread4);
executorService.submit(thread5);
executorService.shutdown();
}
179. 主线程怎么知道五个线程已经切完蛋糕了呢?什么时候去取蛋糕?
1)通过Thread类中的isAlive()方法判断线程是否处于活动状态。
【注】如果只是要等其他线程运行结束之后再继续操作,可以执行t.join(),即:在t执行完毕前挂起。
2)通过Thread.activeCount()方法判断当前线程的线程组中活动线程的数目,为1时其他线程运行完毕。
3)过java.util.concurrent.Executors中的方法创建一个线程池,用这个线程池来启动线程。启动所有要启动的线程后,执行线程池的shutdown()方法,即在所有线程执行完毕后关闭线程池。然后通过线程池的isTerminated()方法,判断线程池是否已经关闭。线程池成功关闭,就意味着所有线程已经运行完毕了。
4)使用CountDownLatch:(其实就是计数器的功能)其工作原理是赋给CountDownLatch一个计数值,普通的任务执行完毕后,调用countDown()执行计数值减一。最后执行的任务在调用方法的开始调用await()方法,这样整个任务会阻塞,直到这个计数值(Count)为零,才会继续执行。
180. 堆和栈的区别?
1)栈和堆的概念
① 栈:由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈
② 堆: 一般由程序员分配释放, 若程序员不手动释放,大多数系统是不会进行回收的。分配方式类似于链表。
2)缓存方式的区别
① 栈使用的是一级缓存, 通常都是被调用时处于存储空间中,调用完毕立即释放;
② 堆是存放在二级缓存中,调用这些对象的速度要相对来得低一些。
Ps. 一级缓存的生命周期和SqlSession的生命周期相同,二级缓存和整个应用的生命周期相同(mapper级别的)
3)区别
① 管理方式不同:栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;
② 分配方式不同:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。
③ 空间大小不同:栈的大小要远远小于堆的大小。
④ 生长方向不同:堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低
4)堆和栈内存的主要区别
181. 项目怎么判断是http请求?
项目可以通过判断请求的头部信息来确定是否是HTTP请求。
1)可以检查请求头中的"Content-Type
"字段来确定请求的类型。如果"Content-Type"字段的值是"application/x-www-form-urlencoded"或"multipart/form-data",则可以判断为HTTP请求。
2)还可以通过检查请求头中的"X-Requested-With"字段来确定是否是AJAX请求。如果"X-Requested-With"字段的值是"XMLHttpRequest",则可以判断为AJAX请求。
182. 网络收发数据包的过程?
1)封装报文是从上层到下层(应用层 --> 传输层 --> 网络层 – > 数据链路层 --> 物理层),解封装报文是从下层到上层。
2)数据包传输的过程中,源IP和目标IP不会变,除非遇到NAT(SNAT或DNAT),源MAC和目标MAC遇到网关会变。
3)NAT:网络地址转换,是用于在本地网络中使用私有地址,在连接互联网时转而使用全局 IP 地址的技术。NAT实际上是为解决IPv4地址短缺而开发的技术。
4)数据链路层:帧; 网络层:数据包; 网络层以上:数据报。
帧、数据报和数据包
183. linux内核包含哪些模块?
参考:Linux内核
- Linux内核的作用是将应用层序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。
- Linux进程采用层次结构,每个进程都依赖于一个父进程。内核启动init程序作为第一个进程。该进程负责进一步的系统初始化操作。init进程是进程树的根,所有的进程都直接或者间接起源于该进程。
- 进程的调度和切换都需要使用到内核。
184. 服务器性能瓶颈
参考:服务器性能瓶颈
1)超过了服务器设置的网络请求最大连接数,报错:请求被拒绝403
2)服务的线程池最大线程数未设置适当,报错:连接超时(处理不过来的等待,等待时间太长超时)、请求失败(处理不过来的直接失败)
3)超过了redis最大连接数
4)接口直接访问数据库,超过了数据库最大连接数:暂停服务503
185. 线程池中线程数量怎么确定?
参考:线程池线程数量
- 线程池中线程数量主要与CPU、IO、并发等有关
- CPU密集型任务
① 要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
② 一般配置线程数=CPU总核心数+1 (+1是为了利用等待空闲) - IO密集型任务
① 这类任务的CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。常见的大部分任务都是IO密集型任务,比如Web应用。对于IO密集型任务,任务越多,CPU效率越高(但也有限度)。
② 一般配置线程数=CPU总核心数 * 2 +1 - 小结
① 最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1) CPU数目*
② 所以线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程
186. DoS攻击(拒绝服务)
参考:Dos和DDos攻击
187. HashMap线程不安全的原因以及CurrentHashMap线程安全的原因?
参考:HashMap线程不安全
ConcurrenttHashMap线程安全01、ConcurrentHashMap线程安全详解、ConcurrentHashMap突击
-
HashMap线程不安全的体现:
1)JDK1.7 HashMap线程不安全体现在:死循环、数据丢失
① HashMap的线程不安全主要是发生在扩容函数中,其中调用了JDK1.7 HshMap的transfer()函数
② HashMap的扩容操作:重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。
③ 在多线程操作时,如果在扩容的某个步骤被挂起而其他线程接入之后就可能会出现死循环以及数据丢失问题
2)JDK1.8 HashMap线程不安全体现在:数据覆盖
① JDK1.8直接在HashMap的resize()函数中完成了数据迁移,没有了transfer()
② 在putVal函数中需要判断是否存在hash碰撞,当两个线程计算得到的哈希值刚好一致,但是其中一个线程刚好被挂起,此时另一个线程完成了插入操作,当另一个线程获得时间片运行之后直接往后执行,就会像另一个线程插入的值进行覆盖;另外,在执行size++过程中如果其中一个线程刚好被挂起,此时前程就会一前一后执行然后将size协会内存,此时得到的size只是+1,也是存在覆盖作用
3)解决:在HashMap的所有方法上synchronized。
4)HashMap线程不安全原因
① JDK1.7 中,由于多线程对HashMap进行扩容,调用了HashMap#transfer(),具体原因:某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。
② JDK1.8 中,由于多线程对HashMap进行put操作,调用了HashMap#putVal(),具体原因:假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。 -
ConcurrentHashMap线程安全
1)解释1
① currenthashmap的线程安全保证主要通过synchronized,volatile,cas三种机制共同处理来保证整体的线程安全,线程安全问题主要出在put和扩容两方面。
② put的时候会保证当前数组在该列的hash处已经锁定,并且在没有扩容的前提下进行put,保证只有一个线程在该列put
③ 扩容问题是通过cas和锁来保证的,首先会new出一个二倍大小的数组(后面的rehash都是针对这个新的hash表操作,不涉及原hash表),且对扩容列加锁并且可以实现并发扩容,将以扩完的列设置为不可用,最后都扩容完之后将新数组替换到table处(用新数组来替换旧数组table)。
2)解释2
① ConcurrentHashMap 在 JDK 1.7 时使用的是数组加链表的形式实现的,其中数组分为两类:大数组 Segment 和小数组 HashEntry,而加锁是通过给 Segment 添加 ReentrantLock 锁来实现线程安全的。
② 而 JDK 1.8 中 ConcurrentHashMap 使用的是数组+链表/红黑树的方式实现的,它是通过 CAS 或 synchronized 来实现线程安全的,并且它的锁粒度更小,查询性能也更高。
188. 双亲委派
参考:双亲委派模型
双亲委派模型是 Java 类加载器的一种工作模式,通过这种工作模式,Java 虚拟机将类文件加载到内存中,这样就保证了 Java 程序能够正常的运行起来。
-
双亲委派模型针对的是 Java 虚拟机中三个类加载器的,这三个类加载器分别是:
① 启动类加载器(Bootstrap ClassLoader):启动类加载器(Bootstrap ClassLoader)是由 C++ 实现的,它是用来加载 \jre\lib\rt.jar 和 resources.jar 等 jar 包的
② 扩展类加载器(Extension ClassLoader):扩展类加载器是用来加载 \jre\lib\ext 目录下 jar 包的
③ 应用程序类加载器(Application ClassLoader):应用程序类加载器是用来加载 classpath 也就是用户的所有类的 -
双亲委派模型:
当加载一个类时,从应用程序类加载器开始查找,如果在类加载器的缓存中中查找到相应的类,此时就返回改对象,若未找到则继续向上扩展类加载器、启动类加载器;若在启动类加载的缓存中也没有找到该类,则向下查找并加载类,如果找到就返回对象并将该对象加入到缓存中,若找不到则继续向下应用程序类加载器,此时若再找不到就返回 ClassNotFound 异常 -
优缺点
1)优点:
① 安全:当使用双亲委派模型时,用户就不能伪造一些不安全的系统类了。如String类,按照双亲模型系统将不会加载用户自定义的String类
② 避免重复加载:当一个类被加载之后,因为使用的双亲委派模型,这样不会出现多个类加载器都将同一个类重复加载的情况了
2)缺点:比如 JNDI 和 JDBC 不能通过这个规则进行加载,它需要通过打破双亲委派的模型的方式来加载。(JNDI 的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部署在应用程序的 classpath 下的 JNDI 接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码)
189. 分布式锁
参考:分布式锁及实现方式
- 集群会带来数据的隔离性,需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题
- 分布式锁的三种实现方式:
1)基于数据库实现分布式锁;
① 基于数据库的实现方式的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
② 可优化点:数据库性能影响分布式锁的性能;不具备可重入特征,需要再增加一列记录当前获取到锁的机器和线程信息;没有锁失效机制,若服务器宕机,而插入的数据一直没有被删除,此时服务恢复后获取不到锁,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;不具备阻塞锁特性,应该进行多次循环来获取锁。
2)基于缓存(Redis等)实现分布式锁;
① 优点:Redis有很高的性能;Redis命令对此支持较好,实现起来比较方便
3)基于Zookeeper实现分布式锁;
① ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。
② 如果时最小子节点则获得锁
③ 优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
④ 缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。