以下内容均来自于网络摘抄(会给出原链接),没有本人的思考,我是一个莫得感情的机器。
在IDEA里使用本地Tomcat部署的时候把applicationContext改成'/',要不然在页面里写根目录会跳出当前war的目录
通过注解的方式装配bean:@Component("beanID")
如果没有id,默认类的首字母小写作为这个bean的id
遇到了一个诡异的问题(也没有很诡异,下次注意吧):在没有创建package的情况下,即使把Component
和ComponentScan
的类都放在同一文件夹,还是会报错。解决方法是建个package就行了。
Spring里Bean的作用域:
singleton, prototype, request, session
Spring里xml配置的Bean其实还有一个<scope>属性,默认是singleton,也就是说Spring维护了一个单例(包括@Autowired
产生的),这个单例不是线程安全的,需要自己维护。
@Autowired: 可以放在构造函数,方法和字段上实现自动注入。默认是byType,按类型注入。如果一个接口类型有多个实现类,会发生冲突,需要@Qualifier指定。如果没有指定,会尝试byName通过bean名字匹配
SpringMVC里有获取Cookie值的简便方法(@CookieValue
),但没有设置Cookie的方法,所以还是用Servlet容器提供的HttpServletResponse.addCookie
配置数据库连接池: apache的DBCP连接池BasicDataSource
,阿里的Druid
mybatis
一级缓存(SqlSession级别):session内的查询结果会被缓存,当执行同样的查询语句时会直接返回缓存结果。这个缓存在session间不能共享。
二级缓存(SqlSessionFactory级别):在配置文件中添加<cache>
开启,要求被缓存对象支持序列化。自定义的缓存器要实现???接口,例如Redis的实现???
思考:
客户端收到了一个null,可能是什么原因导致的?
首先假设传输是稳定的,考虑服务器业务逻辑的问题:
1. 可能是服务器收到了一个未知或者错误的命令,没有响应
2. 服务器访问数据库发现对应的数据项不存在,返回空
3. 如果返回的数据形式是JSON,可能是在序列化的时候没有把对应的字段写进去
再假设服务器逻辑没有问题,考虑传输的原因。如果服务器和客户端走的是UDP协议,那么可能是出现了丢包。如果走的是TCP协议,那么可能是因为网络环境非常差,客户端掉线,超过了TCP的重传时间(一般是10分钟左右,如果没有数据传输的话是2小时,Keep-alive机制)
(注意TCP的Keep alive和HTTP的Keep alive是两样东西)
思考:
分布式情况下怎么进行session管理?
问题描述:分布式情况下,session分布在不同的服务器中,同一个用户的session第一次访问被存在了服务器A,第二次访问被映射到了服务器B,但这里没有它的session,会被要求再次登录。
答:
1. 利用应用服务器自身(如tomcat)的session复制特性(不推荐,占内存占带宽)
2. 修改负载均衡模块,每次都把同一个浏览器的同一个用户映射到固定的服务器
3. 把用户本身的信息存在Cookie中(不安全,且长度有限制)(注意:参考JWT, Java web token)
4. 将session剥离出来单独放在一个公用的存储器里(如redis)。推荐的方式是结合spring session,通过一个SessionFilter将所有请求拦截,查询redis
思考
URL重写,这个长度有限制吗?
HTTP报文构成:
首先,HTTP协议对于请求行/头/体的大小均没有限制。但实际浏览器和服务器可能会对URL有长度限制。例如Chrome: Chrome limits URLs to a maximum length of 2MB for practical reasons and to avoid causing denial-of-service problems in inter-process communication
引申:URL与URI的关系:URL(locator)是URI(identifier)的子集,地址是识别身份的信息之一
Spring Bean的初始化过程???
SpringMVC里一个请求的经过的流程
ArrayList默认初始容量10,超出容量会扩大原始容量*0.5+1,(10+10*0.5+1=16),线程安全的版本是table和vector
HashMap使用节点数组+链表实现,默认初始容量16, 装载因子0.75,超过0.75*16=12容量扩大为原来的两倍。在1.8中进行了优化,当链表长度大于8时,改用红黑树
ConcurrentHashMap在数据结构上也作了同样的改进,同时
类加载
垃圾回收
在jvm里 majorGC等同于 fullGC
线程的6种状态
Java线程有6种状态,如图:
注意Thread.sleep()
与Object.wait()
都会让出cpu,但前者不会释放锁。另外Thread.yield()
就是让出时间片,在1.7?的实现中,等同于Thead.sleep(0)
。
修正了一个误区(重要!):synchronzied修饰一般方法等价于 synchronized(this)
,锁的是类实例。只有在修饰静态方法的时候才锁的是class
synchronized原理:
1. 对于同步代码块,通过monitorenter/exit
指令进入退出监视器(Monitor)对象实现(javap
看class文件可以看出来)
2. 对于同步方法,通过读取运行时常量池里方法的ACC_SYNCHRONIZED标志来实现???
对象头与Monitor对象:
java对象在内存中的布局分为三块区域:对象头、实例变量和对齐填充。对象头包含 MarkWord 和 ClassMetadataAddress,其中MarkWord在32位jvm下的构造如图:
其中重量级锁也就是通常意义上的synchronized锁,可以看到每个对象都会有一个监视器对象与之关联,在HotSpot虚拟机中,监视器的实现是
MonitorObject
。Java1.6针对重量级锁进行了优化:
偏向锁:基于“大部分情况下锁都是由同一线程获得”的经验,MarkWord里会记录线程的ID,当这个线程再次获取时,无需同步操作即获取到锁。若失败,则升级为轻量级锁。
轻量级锁:基于“对绝大部分的锁,在整个同步周期内都不存在竞争”的经验,也就是说适用于线程交替访问同步块,不在同一时间竞争同一个锁的场景。
自旋锁:若获取轻量级锁也失败,为了避免线程在操作系统层面挂起[1][2],会再尝试循环等待一小段时间的方式(自旋)获取锁。这是基于“大部分情况下线程持有锁的时间都不会太长”的经验。如果尝试几次都失败,那么才会升级为重量级锁。
[1]:Java线程与操作系统线程的关系:目前主流的虚拟机实现都把Java线程一一映射到操作系统的线程上,把调度工作交给了操作系统,jvm本身只是对这一过程作了包装。
[2]:操作系统的线程切换涉及内核态与用户态的切换,耗时操作。
wait(), notify()与notifyAll()
这几个方法的调用需要先获取到当前对象的监视器对象,否则会抛 IllegalMonitorState 异常,这也是为什么这些方法必须要放在synchronized方法或者代码块里。监视器对象
MonitorObject
包含几个重要的属性:_owner
指向当前持有该监视器的线程;_WaitSet
(双向循环链表)等待队列,用于存放wait的线程以及_EntryList
同步队列,用于存放阻塞获取该对象锁的线程。wait()方法实现: 把当前线程包装成
ObjectWaiter
->加入等待队列->释放当前MonitorObject
,最终操作系统park()挂起当前线程notify()方法实现:从
_WaitSet
选一个线程(API文档里说随机选择,其实是选队头元素),放到_EntryList
notifyAll()方法实现:将
_WaitSet
队列中的所有线程放到_EntryList
注意 notify() 和notifyAll() 执行后并不会立即释放锁,只有退出同步区域后锁才被释放。
实现生产者消费者模型
- sychronized+wait+notifyAll:
调用wait和notify之前线程必须获取到这个对象的监视器锁,也就是他们必须在同步方法或同步块中使用,否则会抛IllegalMonitorStateException
两个坑: 1. 不加while判断而用if会超取/超放 2. 不用notifyAll而用notify会假死 - ReentrantLock+Condition
使用lock.lock/unlock()手动加/释放锁,注意定义了两个Condition,empty和full,分别用于通知生产者和消费者。
遇到有面试官问用第一种synchronzied实现有什么问题,盲猜一波是性能上的。因为只能靠一个对象通知,比如唤醒的时候不分生产者消费者都唤醒了,加剧了竞争。而用两个Condition分别通知生产者消费者会好一些。(我瞎猜的)
ReentrantLock原理???
ThreadLocal
http://www.jasongj.com/java/threadlocal/
CAS实现
CountdownLatch: 一个计数器,使用场景例如一个线程等待其他5个线程结束之后再执行。那么可以new一个CountdownLatch(5),其他线程在执行完之后加一句countdown()
, 而这个线程在一开始就await()
, 当计数减为0这个就被唤醒。
CyclicBarrier
线程池
Callable, Runnable: Callable有返回值,Runnable没有
单个任务的提交:submit(Runnable / Callable), execute(Runnable)
, 其中submit
会返回Future<T>
,可以cancel(), isDone(), get()
多个任务提交并等待结束: invokeAll()
Netty具有异步和零拷贝两种特性
- 关于异步:Netty是基于NIO实现的。NIO是IO多路复用,主要包含ByteBuffer, Channel, Selector,即所谓的Reactor模式。而异步是Proactor模式的,所以Netty为什么是异步这一点本人暂时还有疑问(???)。实际上NIO2才是真正的异步,在Linux上基于AIO库实现(Need ref),在Windows上基于IOCP实现,但Netty从4.0起抛弃了NIO2的支持(NeedRef),理由是在linux上AIO的性能被证实不高(NeedRef);对于Windows则因为其在服务器领域可怜的份额而不考虑支持……
- 关于零拷贝:Netty的零拷贝和传统意义上的零拷贝不太一样,主要是针对数据逻辑操作上的。例如传统方法要将两个缓冲区的内容合并,需要再分配一块更大的缓冲区,将两个缓冲区的内容拷贝进来。但Netty的
ByteBuf
可以通过包装使得这两个缓冲区在逻辑上是一个,避免了复制的过程。
零拷贝
关于零拷贝,在OS层面通常指避免在用户态(User-space) 与 内核态(Kernel-space) 之间来回拷贝数据,这里的两张图作了很形象的解释。以一个读取文件发送到网络的例子说明,传统方法必须要先从硬盘读取内容到内核态的缓冲区,然后复制到用户态的缓冲区进行操作,随后又复制到内核态的socket写缓冲区,这里涉及了两次内核态切换和两次复制。使用零拷贝,读取与拷贝直接在内核态进行,只需要一次复制。
零拷贝的一个真实例子是 Linux 提供的 mmap
系统调用, 它可以将一段用户空间内存映射到内核空间, 当映射成功后, 用户对这段内存区域的修改可以直接反映到内核空间; 同样地, 内核空间对这段区域的修改也直接反映用户空间. 正因为有这样的映射关系, 我们就不需要在用户态与内核态之间拷贝数据, 提高了数据传输的效率.
Java input streams can support zero-copy through the java.nio.channels.FileChannel
's transferTo()
method if the underlying operating system also supports zero copy
Netty里的FileRegion
包装了FileChannel.tranferTo()
,可以实现文件传输的零拷贝
引申:nio.ByteBuffer.allocateDirect()
在直接内存分配缓冲区,直接内存是通过JNI调用malloc()
分配的。如果不分配直接内存,而调用allocate()在java堆分配,那么会发生内核缓冲-->用户缓冲--> Java堆缓冲两次复制。
注意:看到这里可能需要区分一下堆内存、直接内存、用户空间和内核空间。Linux虚拟地址被划分为内核空间与用户空间,出于安全考虑,不允许直接访问内核空间。用户空间对于Java虚拟机而言,在虚拟机之外的是直接内存(需要C++ malloc),而堆内存就是虚拟机内的堆区域。
引申2:直接内存的垃圾回收?
Linux五种IO模型
这篇文章讲的很清楚,这里再简单总结一下:
网络IO分为两步:
1. 等待数据分组到达,然后复制到内核缓冲区
2. 将数据从内核缓冲区复制到用户空间
同步IO模型:
1. 同步阻塞(recvfrom
系统调用,阻塞直到两步完成)
2. 同步非阻塞(在第一步recvfrom
不断轮询,失败立即返回,进程做其他事情;若成功则阻塞等待第二步完成)
3. 多路复用IO(第一步select/poll/epoll
系统调用阻塞住,轮询多个连接,若有一个成功,调用recvfrom
进行第二步)
4. 信号驱动式IO
异步IO模型:用户进程发出aio_read
系统调用立即返回,转而做其他事情,内核在背后完成了两步之后再通知用户进程
select, poll, epoll ???
select的几大缺点:
1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
3. select支持的文件描述符数量太小了,默认是1024
三次握手:
SYN_SENT [SYN]
[SYN+ACK] SYN_RECVD (默认重试五次)
ESTABLISHED [ACK]
ESTABLISHED
Syn flood
四次挥手
(FIN_WAIT_1) [FIN]
FIN_WAIT_2 [ACK] CLOSE_WAIT
[FIN] LAST_ACK
TIME_WAIT [ACK] CLOSED
CLOSED(2MSL)
等待两个最大报文时长:
1. 防止服务器没有收到ACK,服务器再次发送FIN,收到RST认为连接错误,违反可靠性要求
2. 确保残余的数据包在网络中被抛弃
UDP是无连接的,且没有拥塞控制。对于应用层下发的数据包,UDP既不拆分也不合并,添加上首部后即下发给IP层。如果分组太大IP层会进行分片,如果太小则效率不高,所以使用UDP时需要注意应用层发送数据的大小。一些实时应用会对UDP的不可靠传输进行适当改进,减少数据包的丢失,例如前向纠错和重传。
TCP
重传时间 RTO:
RTT_s = (1-a)RTT_{old} + a·RTT_{new}, a = 1/8
RTT_D=(1-b)RTT_D_{old} + b·|RTT_s-RTT_{new}|,b = 1/4
RTO=RTT_s + 4·RTT_D
滑窗:连续ARQ,累积确认,Go-back-N。在TCP头部附带自己的接收窗口大小。零窗口的持续计时器,零窗口探测报文段(1字节)。
针对小包的Nagle算法:先发一字节,同时缓存后面的数据。收到ACK后,再把数据通过一个报文段发出去,同时继续缓存,只有在收到前一个报文段的ACK后才发下一个报文段。(好像回退到了停等式ARQ?)这种方法在数据到达较快但网络速率较慢的情况下能有效减少网络带宽。
糊涂窗口综合征:接收方缓存消化的速率太慢,刚有了一点小空间就急着通知给发送方。解决方法:让接收方等待一段时间,直到有一半的空闲空间或者能容纳一个更大的报文段。
保活计时器:每收到一次来自客户端的数据就重置该计时器。超过两小时,发送一次探测报文段,之后每隔75秒发送一次,共计10次,若超过10次没有响应(可能客户端故障或网络状况不佳),服务器就关闭连接
流量控制:通信双方的传输速率可能是不一致的,例如一方发送的太快,另一方来不及接受。因此双方在TCP头部附带自己的接受窗口大小,告诉对方自己最多能接受这么多字节。
拥塞控制:
流量控制与拥塞控制的区别:流量控制是针对端对端之间通信量的控制。拥塞控制是针对全局性的网络,防止过多的数据注入到网络中,使网络中的路由器和链路不至于过载。
拥塞控制方法:发送方维持一个拥塞窗口cwnd,并使自己的发送窗口等于拥塞窗口(考虑到前面的流量控制,实际上应该是拥塞窗口和对方接收窗口的最小值)
-
慢开始:初始时把
OSI各层的传输数据单元格式
每下降一层(除了物理层),都会在原数据包上添加新的控制头
应用层:[HTTP报文]
传输层:[TCP头(20Bytes)/UDP头(8Bytes)][HTTP报文]:TCP叫报文段(Segment),UDP叫数据报(Datagram)
网络层:[IP头(20Bytes)][TCP头][HTTP报文]:叫IP分组/数据报
数据链路层:[控制头][IP头][TCP头][HTTP报文][控制尾]:叫帧
数据链路层的帧有大小限制,叫MTU,以太网的MTU是1500字节。
传输层也有大小限制,TCP报文的长度限制叫MSS,在建立连接的时候会协商取双方的最小值(???)。实际上往往还是取决于MTU的大小,一般是1500-20(IP头)-20(TCP头)=1460Bytes
HTTP
HTTP1.0时代,HTTP对于每个请求都要新建连接,完成后立即断开连接。从HTTP1.1(RFC2616)开始,默认持久连接(Keep-Alive),简单说就是不去主动关闭TCP连接,让后续请求都沿用已有的连接。Keep-Alive并不能保证客户端和服务器的TCP连接是活跃的。
因为采用了持久连接,导致不能明确区分请求的结束标志。HTTP使用两种方法解决这个问题:
(注意请求头是不区分大小写的,所以这里也随便大小写了)
1. Content-length
:指明请求体的长度,在这个长度之后就结束了。缺点是需要提前知道整个请求体的大小
2.Transfer-Encoding: chunked
,目前主流的规范里只定义了一种分块编码,通过0分块标志结束,其格式如下:
require('net').createServer(function(sock) {
sock.on('data', function(data) {
sock.write('HTTP/1.1 200 OK\r\n'); // 请求行
sock.write('Transfer-Encoding: chunked\r\n'); // 请求体
sock.write('\r\n');
sock.write('5\r\n'); // 第一个块:5字节(不包括\r\n)
sock.write('abcde\r\n');
sock.write('0\r\n'); // 块结束
sock.write('\r\n');
});
}).listen(9090, '127.0.0.1');
需要注意区分Transfer-encoding
和Content-encoding
:Content-encoding
先对内容压缩编码(例如gzip),然后Transfer-encoding
指示传输分块编码。
状态码304应用于条件Get:
条件Get是HTTP的缓存策略,用于减少不必要的网络流量。当客户端第一次访问时,服务器会在响应头附带Last-Modified
(日期)和Cache-control
(缓存时长)。浏览器在Cache-control
指定的时间内会一直使用缓存而不用访问服务器。当缓存超时后,客户端再次访问该资源时,会在请求头附带If-Modified-Since
字段,服务器如果检查发现没有更改,那么会返回304 Not modified
,客户端可以继续使用缓存。
HTTP2
二进制分帧、多路复用(通过一条连接同时发送多个消息,每个消息分成多个帧乱序发送,最后根据数据流标识符重新组装)、头部压缩(服务端维护一个首部表,客户端只需传输需要更新的首部信息)、服务端推送(主动传输将来需要的资源)
SSL/TLS:
SSL1.0, 2.0, 3.0 均已被废弃,TLS是其升级版本。SSL/TLS是应用层和传输层之间的一种安全传输协议。SSL握手过程:
1. 客户端发起请求,带上自己生成的随机数rand1和SSL版本、支持的加密算法
2. 服务端响应请求,带上自己生成的随机数rand2并下发CA证书, 证书中包含公钥
3. 客户端验证CA证书,拿到公钥,生成一个随机数rand3并用公钥加密发送给服务端
4. 服务端
SQL注入、XML注入、ReDos
RabbitMQ
ConnectionFactory, Channel
结构:生产者-->Exchange-->多个队列-->多个消费者
过程:生产者将消息发送到Exchange,Exchange把消息路由到相应的队列,由绑定这个队列的消费者取出执行
一. 为了防止消费者取出消息后没执行完就宕机,造成消息丢失。要求消费者处理完之后发送一个消息回执给RMQ,RMQ在收到消息回执之后才会删除对应的消息。
如果RMQ没收到消息并且检测到这个消费者的连接断开,就会把消息分给其他消费者处理。注意只要连接不断开,消息处理时间再长也不会导致消息被发给其他消费者。
二. 防止RMQ本身造成消息丢失:将队列和消息设成可持久化的。更进一步,可以使用事务。
三. 路由规则由ExchangeType和生产者发送消息附带的routingKey与消费者绑定队列的bindingKey决定:
四种ExchangeType
- fanout:分配到所有队列
- direct:分配到和routingKey完全匹配的bindingKey队列
- topic:routingKey是.分隔的单词,bingdingKey可以带通配符(*匹配一个单词,#匹配多个单词)
- headers:根据消息内容里headers属性包含的键值对进行匹配
四:RPC
RMQ本身是异步的消息处理。如果需要同步,即生产者等待消费者处理完成再根据结果进行下一步处理,相当于RPC。
RMQ支持RPC的方式是:生产者发送的消息里附带replyTo和correlationId。replyTo是一个队列名称,表示消息处理完后把通知发送到这个队列;correlationId是请求的标识号,生产者据此判断是哪个消息完成了。
SpringBoot集成RMQ
思考:
如下是一个学生成绩数据表:1. 选出不及格科目多于两门的学生 2.选出不及格科目多于两门的学生的平均成绩
create table if not exists t_grade (
id int auto_increment,
name varchar(20),
subject varchar(20),
grade int,
primary key(id));
select name from t_grade where grade<60 group by name having count(grade)>2
-
select name, sum(grade<60) as numFailed, avg(grade) group by name having numFailed>2
(注意这里的sum不能换成count)
注:
where
不能使用聚合函数(avg
, sum
, ...)。
where
用于过滤行,having
用于过滤组(group by
)。
tab1 left [outer] join tab2
:返回左表所有行,即使在右表没有匹配
tab1 right [outer] join tab2
:返回右表所有行,即使在左表没有匹配
[inner] join
:求交
数据库的四种范式
B树(我终于也有b shu了)
性质:一个m阶的B树表示节点最多有m个子节点,每个节点最多有m-1个关键字,最少有ceil(m/2)-1个关键字(根节点可以最少有一个关键字),节点内的关键字都是排好序的。叶节点都处于同一层。
B树的特点是深度很小,这是为了减小磁盘寻道带来的时间开销。
插入:超过m-1个关键字会发生分裂:中间关键字上浮到父节点,然后左右分裂成两个子树。若父节点也超过m-1,那么对父节点重复这个过程。
删除:如果是非叶节点,那么用后继节点覆盖这个值,然后递归删除后继节点,直到到达叶节点。如果叶节点删除后小于ceil(m/2)-1个关键字:(1)如果兄弟节点大于ceil(m/2)-1,则父节点关键字下移,兄弟节点的一个关键字上浮 (2) 如果左右兄弟节点都没有大于ceil(m/2)-1的,则父节点关键字下移,并且选一个左或右兄弟一起合并成新的节点。
B+树:
和B树的主要区别在于:
1. 节点分为内部节点和叶节点。内部结点不保存数据,而叶节点才存储数据。
2. 叶节点之间通过指针形成一个有序链表
为什么要使用B+树作为索引结构:数据库的数据表和索引一般都很大,都是放在硬盘上的。而一次硬盘IO的时间(包括寻道时间(5ms左右)、旋转延迟(7200转约4ms)、传输时间(可忽略))很长,所以根据内存局部性原理一次读取多个连续的块放到内存页中[1]。B+树的每个节点对应一个页大小[NeedRef],相比于B树把数据也保存在节点中,B+树由于只存索引所以单节点保存的索引数量更多,减少了硬盘读写的次数。
- 一个表有且仅有一个聚簇索引[2]。一般情况,用主键来作为聚簇索引。如果没有定义主键,使用第一个
unique
且not null
的列来作为聚簇索引。否则,会内部根据行ID值生成一个隐藏的聚簇索引GEN_CLUST_INDEX
。
覆盖索引
最左前缀匹配
explain执行计划
[1] Linux下默认内存页大小是4k?
[2] 聚簇索引与非聚簇索引
数据库事务特性(ACID)
1. 原子性:整个事务中的操作要么都完成,要么都不完成。发生错误就会回滚成执行前的状态,不会处在中间某个操作状态
2. 一致性:数据库在事务执行前后都是一致的(AB转账,不管怎么转,账户总额都是一定的)
3. 隔离性(Isolation):多个事务操作同一张表时,单个事务感受不到其他事务的存在。即从单个事务角度来看,其他事务要么已经执行完毕,要么还没开始。
4. 持久性(Durability):事务提交后,提示操作完成;数据库一定会保证事务已经成功执行,即使数据库出现了故障。
数据库隔离级别
脏读:一个事务读取了另外一个事务还没有提交的数据
不可重复读:一个事务多次读取同一数据却得到了不同的结果,因为其他某个事务在读取间隔进行了修改并提交。(与脏读的区别是:脏读是读了其他事务没提交的数据,不可重复读是读其他事务已提交的数据)
幻读:一个事务批量修改了一些数据,但另外一个事务在其中插入了一条历史数据,如果这个事务再去读就会发现好像这条数据没有被修改一样(出现了幻觉?)
针对上述情况有四种隔离级别:
1. 读未提交(Read uncommited): 什么也无法保证
2. 读已提交(Read commited): 避免脏读
3. 可重复读(Repeatable read): 避免脏读和不可重复读
4. 串行化(Serializable): 串行执行,避免所有问题(效率也最差)
MySQL默认隔离级别是可重复读。InnoDB引擎在可重复读级别可以通过MVCC解决幻读???
redis单个命令都是原子的,因为redis是单线程的。但如果包含多个原子命令那就不是原子的了(废话),结果可能会错误,这还是因为客户端发送命令时候线程切换导致的。
redis的高可用
单例模式
一个线程安全的单例(double check):
适配器模式
将一个类的接口转换成客户希望的另外一个接口。例如,假如我们看到这样的接口:
public class ThinkpadComputer implements Computer {
@Override
public String readSD(SDCard sdCard) {
if(sdCard == null)throw new NullPointerException("sd card null");
return sdCard.readSD();
}
}
现在我们希望在不改变readSD(SDCard sdcard)
这个接口形式的情况下,读一个TFCard
,怎么办呢?
这个时候就需要适配器模式:使用方式是用一个Adapter
去实现SDCard
接口,内部持有一个TFCard
,readSD()
的时候使用TFCard
的实现就行了
代理模式
- 反转栈,只允许使用常数个变量
两次递归。先pop,再递归,然后push_bottom。 push_bottom也需要递归。
最小生成树
并查集求解岛屿数量
两个有序数组的中位数
1G大小的文件,每行一个单词,每个单词不超过16字节。可用内存1M,返回出现次数最多的100个单词
首先把1G文件分成n个小文件(1G=1024M,取大一点,比如2000吧),把每个单词通过hash(word) % n
映射过去,这样就保证了同样的单词都在同一个文件里。然后遍历,对每个文件建一个hashmap统计词频(如果不重复的单词太多hashmap放不下,就再重复上述分的过程; 或者把n取大一点),得到n个词频文件。弄一个大小100的最小堆,读入词频找出词频最多的100个。