具体问题
使用栈实现一个队列(手写代码),考虑线程安全的实现
思路:使用2个栈,一个用于存放数据,另外一个用于辅助在取数据或者删除时临时存放。线程安全通过加锁来实现。
代码:import java.util.Stack; import java.util.concurrent.locks.ReentrantLock; public class StackQueue<T> { Stack<T> data = null; Stack<T> temp = null; ReentrantLock lock = new ReentrantLock(); public StackQueue() { data = new Stack<T>(); temp = new Stack<T>(); } public T pop() { lock.lock(); if (temp.isEmpty()) swap(data, temp); if (temp.isEmpty()) { lock.unlock(); return null; } T top = temp.pop(); lock.unlock(); return top; } public T peek() { lock.lock(); if (temp.isEmpty()) swap(data, temp); if (temp.isEmpty()) { lock.unlock(); return null; } T top = temp.peek(); lock.unlock(); return top; } public void push(T element) { lock.lock(); if (data.isEmpty()) swap(temp, data); data.push(element); lock.unlock(); } void swap(Stack<T> src, Stack<T> dest) { while (!src.empty()) { dest.push(src.pop()); } } public boolean isEmpty(){ return data.isEmpty() && temp.isEmpty(); } }
java线程模型
线程实现:- 内核级线程:轻量级进程,会消耗一定的内核空间,由内核来完成线程切换
- 用户线程:完全建立在用户空间上的线程,更加轻量级,开销更低
- 用户线程和内核线程混合
线程调度:
- 抢占式:线程的执行时间由线程自己决定,在自己执行完后需要主动通知系统进行进程切换
- 协同式:线程的切换由系统控制,运行时间由系统分配
线程状态:
- 就绪态 Ready:等待分配运行时间
- 运行态 Running:正在运行
- 阻塞态 Blocking:等待IO等操作
- 等待态 Waiting、Timed Waiting:CPU调度、手动通过
sleep()
进入等待 - 终止态 Terminated:运行结束
线程通信方式
- 共享变量
- 关键字:
volatile
、synchronized
- 锁:
ReentrantLock
- 管道通信:
PipedInputStream
和PipedOutputStream
- 等待与通知:
object.wait()/object.notify()
- 条件变量:
Condition
- 倒计时协调器:
CountDownLatch
- 复用栅栏:
CyclicBarrier
- 信号量:
Semaphore
- 阻塞队列:
ArrayBolckingQueue
- 交换:
Exchanger
jvm中,
eden
、s1
、s2
的作用
在分代回收的机制中,eden
、s1
、s2
都用于保存新生代的数据(存活时间较短且不会占用很多空间的小对象),其中所分配的空间比例一般为。其中,
eden
用于存储新创建的对象,s1
用于保存上一轮回收后存活的对象,s2
用于下一次GC时的辅助空间。所采用的GC算法是复制算法,具体机制为:在发生垃圾回收时,
eden
和s1
中存活的对象复制到s2
中并对eden
和s1
进行清理,然后交换s2
和s1
的角色。在这样的机制下,虚拟机的空间利用率能达到
,只有
用于下一轮GC的辅助空间会被“浪费”,且由于会在
s1
和s2
交替的使用复制算法,所以不会存在较大的内存碎片,整体的空间利用率较高,。只用eden和s能否解决问题
能解决问题,但是效率较低。对比使用
eden
、s1
、s2
进行处理的方法,如果去掉其中一个Survivor
空间,则每一次GC都只能从eden
复制到s
中。如果对s
本身没有优化处理的话,在s
中容易产生内存碎片,导致eden
中稍大的存活对象都无法存放于s
,最终导致提前触发下一轮GC甚至报错。虽然空间是的用到了,但是整体的效率和可靠性还是较低。
TCP的拥塞控制机制
慢开始、拥塞控制、快重传、快恢复TCP连接为什么要使用三次握手
用于确认通信双发的接收和发送能力连接建立阶段 内容 发送方 接收方 1 发送方发送 SYN
发送方:
接收方:发送方:发送能力
接收方:接受能力2 接收方发送 ACK
和SYN
发送方:接受能力、发送能力
接收方:接受能力、发送能力发送方:发送能力
接收方:接受能力3 发送方发送 ACN
和SYN
发送方:接受能力、发送能力
接收方:接受能力、发送能力发送方:接受能力、发送能力
接收方:接受能力、发送能力解释一下servlet是什么
本质上就是一个实现了相关接口的类,运行在Tomcat容器中。维基百科:Servlet(Server Applet),全称Java Servlet,未有中文译文。是用Java编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。
servlet单例吗?为什么
不一定。
默认的情况是单例,如果在web.xml
中,给对一个Servlet
类进行多个配置,则会有多个实例。但是大部分情况下,只会配置一个,所以是单例的。对于映射到同一个
Servlet
类的多个请求,通过创建一个Servlet
实例,多个线程执行这个Servlet
的service
方法的实现。所以在Servlet
中,实例变量都不是线程安全的,只有局部变量(request
、session
等)才是线程安全的。servlet在处理请求前,tomcat在底层做了些啥,或者说实现一个Tomcat的大致思路
Tomcat的结构主要分为Server
、Service
、Connector
、Container
四部分。Server
: Tomcat服务器本身Service
:服务,默认的是Catalina
Connector
:用于处理网络连接,监听端口封装网络协议,屏蔽连接与处理的细节Container
:管理Servlet
并处理请求,由Engine
、Host
、Context
、Warrper
四个部分(层级)组成。参考:
数据库索引
B+树索引和哈希索引B+树
非叶子节点中可以保存多个指针数据,每一个数据都是一个指向其子节点的指针。
叶子节点用于保存真正的数据,与非叶子节点相同,每个叶子节点也能保存多个数据,且相邻叶子节点之间存在指针连接。聚簇索引与非聚簇索引
二者的数据结构都是B+树
聚簇索引:叶子节点保存实际的数据,所以索引的顺序决定了数据的顺序
非聚簇索引:叶子节点保存指向实际数据的指针,所以数据的顺序与索引无关非聚簇索引怎么指向具体数据
首先需要明确的是,非聚簇索引(也叫 二级索引 )中,叶子节点里面指向数据的“指针”,并不是真正意义上的指针,即 不是指向物理位置的指针。
在非聚簇索引中,数据和索引是分开存储的,索引的叶子节点保存的是索引列和数据列的主键值。在根据保存的主键,在聚簇索引中进行第二次查找。参考自《高性能MySQL》第三版:
这意味着通过二级索引查找行,存储引擎需要找到二级索引的叶子节点获得对应的主键值,然后更具这个值去聚簇索引中查找到对应的行。什么时候需要建立索引
考虑如下因素:- 查询频度:对于查询频度高或常用于连接查询的字段或组合字段,建立索引能提高查询的效率
- 删改频度:每一次删改都有可能需要对索引进行调整,频繁的删改会影响性能
- 重复程度:对于重复度较高的数据,其索引也具有较高的重叠性,对效率的提高不明显却要占用空间
- 数据类型:像
blob
、clob
等用于保存较大对象的特殊类型,不适合建立索引;较大的文本字段,建立索引也会占用较大的空间且效率不高 - 表的规模:在表中数据较多时,使用索引一般能提高检索效率,表规模较小时,索引对效率的影响不大
- 存储介质:当数据保存在内存中时,索引对效率的提升不会很明显
如何不使用递归遍历一颗二叉树
中序遍历思路:对于每一个节点,优先遍历左孩子,一直到最左边的节点遍历完。处理右边的节点,栈中保存的是左子树正在被遍历的节点。当一个节点的子树遍历完后,从栈中取一个节点(该节点的父节点),遍历其右子树。
中序遍历代码:public static void iterate(TreeNode root) { Stack<TreeNode> stack = new Stack<>(); TreeNode node = root; while (true){ // 不断往左下方遍历,中间的节点保存在栈中 while (node.left != null) { stack.push(node); node = node.left; } // 处理最左边的节点 process(node); // 从栈中取出父节点,并处理其右子树 while (!stack.isEmpty() && node.right == null) { node = stack.pop(); process(node); } if (node.right != null) { // 有右孩子,则处理右孩子 node = node.right; } else { // 栈空,所有节点处理完毕 break; } } } public static void process(TreeNode node) { System.out.print(node.val+ " "); }