文章来源于公众号的一位粉丝,作者:fanyank,他的简书https://www.jianshu.com/p/72e803674228,任何转载者务必保留原出处
楼楼双非渣本,实习的机会没有好好珍惜,一心想着考研,后来因为种种原因在暑假的时候又放弃考研,此时已经接近9月,大部分互联网公司的提前批秋招已经结束,对我这个笔试渣渣秋招直接进入了地狱模式。
offer.pngJVM和绝大多数用户自定义的类在JVM启动的时候被加载,少量用户的类在运行的时候被动态的加载。比如说tomcat中的jsp就是通过运行的时候动态的加载到JVM中,这样可以避免用户每次修改完jsp之后频繁的重启tomcat。其实就是一个热部署的过程。
Tomcat5.0类加载器
BootStrap
Extension
Application
Common
Catalina Shared
WebApp
JSP
为什么tomcat会要用到这么多的类加载器呢?
因为对于一个web容器来讲,两个web应用所使用的类库应当被隔离,所以每个web应用都有一个webapp classloader
两个web应用使用的类库应当共享,所以每个web应用都有一个shared classloader
1. 加载
2. 验证
3. 准备
4. 解析
5. 初始化
把一个方法调用和方法的主体关联起来叫做绑定。那么动态绑定就是在程序运行的时候根据对象的类型进行绑定。Java中除了static,final,private(编译时为final),其他的方法都为动态绑定。
1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
2. 静态变量引用的对象
3. 方法区常量引用的对象
4.本地方法栈中的引用对象
每一个类加载器收到一个类的加载请求时,不会尝试自己去加载这个类,而是交由自己的父类加载器去完成这个类加载的请求,只有当父类加载器完成不了这个请求时,才会交由子类加载器去加载。
这样做可以保证Java体系最基本的行为,比如java.lang.object类不会被篡改。如果你自己写了一个java.lang.object类,那么会发现可以正常编译但是永远无法被执行。
1. 自定义ClassLoader类加载器,实时监听class文件的变化
2. 改变对象的创建方式,改为通过ClassLoader动态创建,虚拟机在碰到一个new指令时,会先去方法区检查是否有该类的符号引用,并且检查该类是否已经被加载,解析和初始化,如果没有,就会执行这个类的加载过程
3. 在JVM启动时使用JavaAgent拦截默认类加载器行为
HashMap 可以 hashtable 不可以
1. 处理哈希码的方式
2. 扩容时链表的插入方式
3. 数据结构
-Xmn20M 新生代20M
-Xmx2G 堆内存最大为2G
-Xms2G 堆内存初始大小为2G
-XX:+PrintGCDetails 打印GC日志
-XX:SurvivorRatio=8 新生代Eden区和Survivor区的比值为 8:1:1 6:2:2
ConcurrentHashMap与HashTable都是线程安全的容器,在面对多线程竞争激烈的情况下,HashTable的性能相比于ConcurrentHashMap要逊色的多,原因在于HashTable采用的是synchronize对整个容器进行加锁,而ConcurrentHashMap采用的是锁分段技术,大大提高了多线程下的读写性能。
1. ConcurrentHashMap由多个Segment组成,而每个Segment又由多个HashEntry组成
2. 每个Segment都配有一把锁,对某个Segment进行加锁的同时不影响其他Segment的读写
3. get()操作不用加锁,原因在于HashEntry***享变量count,value都是volatile类型的,即使在读的时候有一个线程在写,也能保证读取到的value是最新的,因为volatile保证了Java内存模型中的happen-before原则,即写操作总是优先于读操作的
4. get()时会先进行一次再散列运算定位到某个Segment,然后再进行一次再散列定位至某个HashEntry,之后进行读操作即可
5. put()操作需要加锁,这是为了防止同时有两个线程同时对某个HashEtry进行写操作
6. put()操作同样需要先进行一次再散列定位至某个Segment,然后判断其是否需要扩容,如果需要扩容,待其进行扩容操作后再进行插入操作
7. size()操作先采用两次不加锁的方式统计每个Segment的count,如果统计的过程中发现count发生了,变化,再采用加锁的方式统计每个Segment的count
数据结构不同
保证并发的方式不同
初始化map的时机不同
1. 降低资源消耗
2. 提高响应速度
3. 提高线程的可管理性
单机高性能的本质问题就是解决I/O,进程,线程的问题。多线程为了提高资源的利用率和进行资源管控都会用到线程池,要说典型的应用场景,比如说某个容器吧,一般几百的并发量直接进进程就可以,但是量大了就会使用到线程池。
1. 直接抛异常 AbortPolicy
2. 直接抛弃 DiscardPolicy
3. 运用调度线程执行该任务 CallerRunsPolicy
4. 丢掉队列中最近的一个任务,直接执行当前任务 DiscardOldestPolicy
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
coreSize,即使其他线程空闲也会创建一个新线程来执行新来任务
maxSize,当线程池大小达到coreSize,并且有界队列满的情况下会继续创建线程直到达到maxSize
keepAliveTime,线程空闲时存活时间,如果任务多而短的话,可以把这个值调大提高线程的利用率
timeUnit,
blockingQueue,new ArrayBlockingQueue(),
rejectHandler,new ThreadPoolExecutor.DiscardPolicy()
);
1. Lock可以中断式的获取锁
2. Lock可以尝试非阻塞的获取锁
3. Lock可以超时的获取锁
Java中的锁都是基于队列同步器AQS实现的
独占锁
共享锁
可重入锁
读写锁
提供可重写的方法
tryAcquire()
tryRelease()
tryAcquireShared()
tryReleaseShared()
isHeldExclusively()
提供的模板方法
1. acquire() {
if(!tryAcquire()&&acquireQueued(addWaiter(Node.Exclusive,arg))) {
selfInterrupt();
}
}
2. acquierInterruptibly()
3. tryAcquirNanos()
4. acquireShared() {
if(tryAcquireShared() < 0) {
doAcquireShared();
}
}
5. acquireSharedInterruptibly()
6. tryAcquireSharedNanos()
7. release() {
if(tryRelease()) {
Node h = head;
if(h!=null && h.waitStatus != 0) {
unparkSuccessor(h);
}
return true;
}
}
8. releaseShared() {
if(tryReleaseShared()) {
doReleaseShared();
}
}
9. getQueuedThreads()
独占锁
public class MyLock {
private static class Sync {
public boolean tryAcquire() {
if(compareAndSetState(0,1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
public boolean tryRelease() {
if(getState() == 0) {
throw new Exception();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
}
}
共享锁
public class TwinsLock {
private Sync sync = new Sync(2);
private static final class Sync extends AbstactQueuedSynchronizer {
Sync(int count) {
if(count < 0)
throw new Excetion();
setState(count);
}
public int tryAcquireShared(int reduceCount) {
for(;;) {
int current = getState();
int newCount = current - reduceCount;
if(newCount < 0 || compareAndSetState(current,newCount)) {
return newCount;
}
}
}
public boolean tryReleaseShared(int returnCount) {
for(;;) {
int current = getState();
int newCount = current + returnCount;
if(compareAndSetState(current,newCount)) {
return true;
}
}
}
}
public void lock() {
acquireShared(1);
}
public void unLock() {
releaseShared(1);
}
}
可重入锁
public class ReentryLock{
private static class Sync {
public boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if(c == 0) {
if(compareAndSetState(0,acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else {
if(current = getExclusiveOwnerThread()) {
int nextc = c + acquires;
setstate(nextc);
return true;
}
return false;
}
}
public boolean tryRelease(int releases) {
int c = getState() - release;
if(Thread.currentThread() != getOwerExclusiveThread()) {
throw new Exception();
}
boolean free = false;
if(c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
}
读写锁
tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if(c != 0) {
if(w == 0 || current != getOwnerExclusiveThread()) {
return false;
} else {
setState(c+acquires);
return true;
}
}
if(compareAndSetState(c,c+acquires)) {
setOwnerExclusiveThread(current);
}
return true;
}
tryAcquireShared(int acquires) {
int c = getState();
int nextc = c + (1<<16);
if(nextc < c) {
throw new Exception();
}
if(exclusiveCount(c) != 0 && getOwnerExclusiveThread() != Thread.currentThread()) {
return -1;
}
if(compareAndSetState(c,nextc)) {
return 1;
}
}
1. condition可以拥有多个等待队列(确切的说是一个lock可以有多个等待队列,一个condition只有一个等待队列),Object只有一个
2. condition可以在当前线程释放锁进入等待状态,并且在等待状态不响应中断
3. condition支持当前线程释放所并等待到将来的某个时间
public final void await() {
if(Thread.interrupted()) {
throw new Exception();
}
//构造等待节点,并加入到等待队列中
Node node = addConditionWaiter();
//saveState释放节点前的状态,用于acquireQueued(node,saveState)
int saveState = fullyRelease(node);
int interruptMode = 0;
//如果在等待队列一直循环
while(!isOnSyncQueue()) {
LockSupport.park(this);
if((interruptMode = checkInterruptWhileWaiting(node) != 0)) {
break;
}
}
//某个线程调用signal方法,将该等待节点从等待队列中移除,移除之后加入同步队列中去获取锁
if(acquireQueued(node,saveState) && interruptMode != THROW_IE) {
interruptMode = REINTERRUPT;
}
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
1. CountDonwLatch只能使用一次,而CyclicBarrier可以reset()方法使用多次
2. CyclicBarrier可以使用getNumberWaiting()查看被阻塞线程的数量
3. CyclicBarrier可以使用isBroken()方法检查被阻塞线程是否被中断
分布式系统中存在三个理论,即CAP理论
一致性 Consistency
可用性 Availability
分区容错性 Partition Tolerance
Raft算法是一种强一致性算法
领导选举(lead election)
1 每个节点都有三种状态-跟随者,候选者,领导者
2 每个节点的初始状态都为跟随者,如果一段时间内没有收到来自领导者的消息,就会变为候选者
3 成为候选者之后就会给其他节点发送投票消息,其他节点接受后只能发送同意的响应,当有超过半数的节点同意之后,这个后选者节点就成为了领导者,成为领导者之后就会向所有的跟随者定时发送心跳包
日志同步(log replication)
1. 所有数据的变化都要通过这个领导者来完成
2. client发出一个数据变更信息后,领导者收到这个信息然后把变更信息写入日志,并通过心跳包发送给
3. 领导者如果收到过半跟随者的响应后,对自身数据做变更并给client端发送响应,之后通知其他跟随者进行数据变更,此时,数据一致性就得到了保证;如果没有收到,数据变更将仍然存储到日志中
其他(other)
1. 每个跟随者在没有收到领导者的心跳包时,都会随机产生一个倒计时(150ms-300ms),倒计时结束之后
2. 如果某个跟随者在倒计时结束之前没有收到来自领导者的心跳包,那么他就会变成候选者,并把任期数加1,然后向其他跟随者发送投票信息
3. 其中可能出现的情况是,有两个跟随者同时成为了同一时期的候选人,并且得到的投票数还相同,此时,这两个
4. 如果集群网络出现了故障,导致集群被分成了A和B两个分区,原来的leader在A区,那么B区就会重新进行一轮选举,显然B区leader的任期要高于A区,经过一段时间之后,两个分区都进行了数据变更,如果此时网络恢复,分区消失,那么leaderB因为任期高于leaderA,所以leaderA收到leaderB的心跳包之后会对之前的数据进行回滚,然后按照leaderB发送过来的日志进行复制操作
因为CAS底层采用的是总线锁,即一个CPU在对一个共享变量进行输出时,其他CPU的请求将会被阻塞,这样一个CPU可以独占整个共享内存
1. System.gc()
1. 强引用,永远不会被垃圾收集器回收
1. IOC即控制反转,用来解决复杂系统对象耦合度太高的问题,常见的解决方式是DI(依赖注入),即用一个容器统一维护bean,方便在其他地方使用,而不是用的时候直接new
2. AOP即面向切面编程,我们知道代码一定要力求简洁,比如我们的所有的动物类都有run(),eat()方法,这时候我们就可以抽象出来一个动物类来声明这个方法,这样子类就没有必要再对这些方法进行声明。那么,对于一些业务来说,我们可能要实现一些监控和日志打印,那么此时用父类抽象就无能为力了,对应的解决办法就是使用AOP把这些非业务代码给抽象出来,使代码保持简洁
3. 所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。这样上层类就不需要关心下层类是如何实现的,依赖注入是IOC的一种实现方式
重载即为某个类存在多个相同的函数名,但是每个函数的入参却各不相同
重写即为子类继承父类的时候对父类方法的重定义
1 管道通信
2 消息传递通信
3 共享内存通信
4 信号量通信
P,V操作
p(Samephore s) {
s.value = s.value - 1;
if(s.value < 0) {
block();
}
}
v(Samephore s) {
s.value = s.value + 1;
if(s.value <= 0) {
wake(Q);
}
}
本质上是两个进程基于单个socket进行远程通信的问题,主要解决两个问题
短时间内如果有多个线程对服务端进行调用,那么socket连接上就会存在大量的消息传递,client端的某个线程如何知道server端返回的消息是给自己发送的呢?
客户端的某个线程发送完消息之后如何进行等待?(Dubbo底层采用的是apache mina框架进行通信)
client中的一个线程调用远程接口,生成一个唯一的ID
将调用方法的信息(接口,方法,入参)以及处理结果的回调对象callback打包成一个对象object
向专门存放调用信息的concurrentHashmap中进行put(ID,object)
将ID和调用方法的信息打包成一个connRequest对象,之后使用Apache Mina框架中的IOSession.write(connRequest)方法异步发送出去
当前线程使用get()方法获取远程调用的结果,在get()方法内部先使用synchronized获取对象锁,之后如果没有获取到结果就调用callback.wait()方法,释放当前线程获取的锁,并进入等待状态
服务端收到消息之后进行处理,并把处理结果进行回传(包含UUID和处理结果),客户端的监听线程收到消息之后,根据UUID找到对应的callback,并把结果设置到callback对象中
监听线程使用synchronized获取回调对象的锁,并使用notify()方法唤醒等待线程,等待线程唤醒之后执行get()方法即可取到服务端的结果,之后代码即可往后执行
可以,订阅者缓存有发布者的服务地址,可以通过缓存地址找到对应的服务发布者并进行调用,但是新增的服务发布者不能被调用到,因为注册中心已经挂掉,不能通过长连接及时的把发布者的变更推送给订阅者。
Dubbo通过设置安全令牌防止用户绕过注册中心直连,然后在注册中心管理授权;Dubbo还提供黑白名单来控制服务所允许的调用方
一般在开发和测试环境下,采用的是点对点的直连的方式,直连将忽略注册中心提供的服务者列表。
采用连接注册中心的方式,调用者将基于注册中心提供的服务提供者列表,基于软负载均衡算法,选择一台服务提供者进行调用,如果调用失败,再选择另外一台进行调用。注册中心不转发请求,消费者和提供者只在启动的时候与注册中心进行交互。注册中心通过长连接感知服务提供者的存在,如果发生变更,立即推送变更给消费者。
传输数据包小,消费者比提供者个数多
如果使用dubbo协议传输大文件或者大字符串,那么将会影响服务提供者的TPS
dubbo的核心配置
<dubbo:application name="ele"/>
<dubbo:service interface="com.ele.UserService" ref="com.ele.UserServiceImpl"/>
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<dubbo:protocol name="dubbo" port="20881" />
<dubbo:reference id="UserService" interface="com.ele.UserService"
loadBalance="random"/>
random: 随机调用
roundRobin: 轮询,一般设置性能最好的机子权重最大,这样每次请求过来优先轮询这个机子
leastActive: 最少活跃调用数,越慢的机子调用的次数越少
consistentHash: 一致性hash,含相同参数的总是调用同一台机子
进程集合中的每个进程都在等待另一个进程释放资源而造成的无限等待下去的僵持局面
悲观锁:假设会发生并发冲突,屏蔽一切可能违反数据完整性的操作
乐观锁:假定不会发生冲突,只是在提交操作时检查是否违背数据完整性
ACID 原子性,一致性,隔离性,持久性
udp
UDP是无连接的,即发送数据之前不需要事先建立连接
UDP尽最大努力交付,不保证可靠交付
UDP是面向报文的,应用层交给它多长的报文,它就发送多长的报文
UDP支持一对一,一对多,多对多的通信
UDP首部较小8个字节
源端口,目的端口
长度,检验和(检测数据传输中是否有差错)
TCP
TCP是面向连接的,发送数据之前需要经过三次握手使得双方建立可靠的连接
TCP提供可靠的数据交付
TCP提供全双工通信
TCP面向字节流
TCP首部较大,20个字节
源端口,目的端口
序号,确认号
数据偏移,保留字段
URG(插队),ACK,SYN,FIN,RST(TCP连接出现严重差错,必须先释放然后重新建立连接),PSH(双方都不用等到缓存区满就进行报文交付)
窗口,发送该报文段一方的接收窗口,即接收方告诉发送方允许对方发送的数据量
MSS(Max Segment Size)=536字节+20(首部) = 556字节
流量控制是为了解决发送速率过快,而接收方来不及接受的情况,这是一种端对端的控制。
1. 连接建立时,接收方B就给A发送接受窗口(receive window)的大小
2. 发送方的窗口大小不能超过接收方的窗口大小
3. 当接受方来不及接受时,便在发送ACK确认报文段时缩小接受窗口的值
4. 发送方收到确认报文以后及时调整发送窗口的大小
5. 考虑一种极端情况,接受窗口的值为0,并把这个ACK数据报成功发送给了发送方A,过了一段时间之后,接收方B又有了缓存空间,想发送方A发送rwnd=400的报文段,但是这个报文段在传输过程中丢失了,此时,发送A在等待接收方B发送新的接受窗口,接受方B在等待A发送新的数据,这样就形成了死锁的局面。
6. 打破死锁的解决方案就是任意一方收到0窗口值时,都立即设定一个计时器,计时器结束的时候就向对方发送一个零窗口探测报文段,对方收到后给出现在的窗口值,如果还为零重置计数器继续上述流程,直到窗口值不为0为止。
解决的是整个网络对资源的需求>实际可用资源的问题。解决的思路就是减少对网络资源的需求。
1. 慢开始
2. 拥塞避免
3 快重传
4 快恢复
握手:
1.png重点关注client端发送的最后一个ACK确认报文段之后的状态为Time_wait,client端需要等待2MSL(报文段最大生存时间)时间之后才会彻底关闭,这么做的目的主要如下:
最后发送的ACK报文段可能会在网络中丢失,造成server端一直等待client端的ACK报文段而无法关闭的情况,因为server端在发送FIN关闭请求之后会进行一个倒计时,倒计时结束还没有收到ACK确认就会重新发送FIN关闭请求,此时client端就可以利用这2MSL时间对这个重发的FIN关闭请求进行确认
防止“已失效的连接请求”,经过2MSL之后,该次连接所产生的请求都将失效(保证都传输到server端,不会阻塞在某个网络节点上),保证下一次连接中不会收到旧的连接请求
1. 脏读
2. 不可重复读
3. 幻读
Read Uncommited
Read Commited
Repeatable Read--MySQL默认隔离级别
Serialiable
设置会话的隔离级别:
SET TRANSCATION ISOLATION LEVEL READ COMMITED
InnoDB:
MyISAM:
SELCT lname FROM user WHERE fname = 'Peter';
优点:适用于精确查找,查找速度非常快
创建自定义hash索引,典型应用场景(表中存放的url需要建立索引),此时即可再增加一列用来存放hashCode,在进行查询时MySQL优化器会基于这个hashCode列的索引来完成查找,因为整数匹配远比字符匹配来的快,这样实现的缺陷是需要手动维护hash值,维护的实现方式是手动创建触发器
SELECT id FROM url WEHRE url = 'http://www.mysql.com' AND url_crc = CRC32('http://www.mysql.com');
MySQL中有两种存储引擎,MyISAM和InnoDB,两种存储引擎实现索引的方式都是采用B+Tree
1. MyISAM中索引文件和数据文件是相分离的
2. MyISAM索引中的叶节点的data域存放的是指向数据记录的地址
3. MyISAM主索引和二级索引没有区别,只是主索引不能重复
InnoDB(聚簇索引):
1. InnoDB数据文件本身就是索引文件
2. InnoDB索引中的叶节点包含了完整的数据记录
3. InnoDB索引按照主键进行聚集,如果没有主键就选一个非空的索引作为主键,如果还没有,InnoDB会隐式定义一个主键
4. 二级索引需要查找两次,第一次找到主键,之后通过主键找到数据
索引虽然可以加快查询速度,但是索引本身也是需要占一定的存储空间的,同时插入和删除记录时也会额外的耗费一些时间和资源,并且在MySQL运行的时候也需要消耗资源去维护索引以下两种情况不建议使用索引:
1. 表中的数据较少时(少于2000)
2. 索引的选择性较低时(选择性等于索引列的不重复数/表中的行数)
聚簇索引:
范围查找
返回结果集较多
非聚簇索引:
频繁更新的列
返回结果是少量的数据集
多表查询怎么优化?
1. 避免查询不必要的行,使用limit,当一个表的分页多达几万页的时候,此时使用limit的效率将变得非常低,因为limit每次都会从初始位置开始向后进行遍历,然后找到该页数据并返回。改善的方法有两种:1. 子查询使用索引先查找出偏移量,然后父查询通过limit限定取出的结果数 2. 如果某列有序,添加where语句使得直接从分页位置开始查找
select * from user where user_id >= (select user_id from user limit 100000,1) limit 30;
limit优化.png
2. 避免每次查询取出所有的行,这样做会使覆盖索引失效,但是却简化了开发,提高了代码的通用性
3. 查询相同的数据尽量使用缓存
4. 避免在SQL语句中使用函数或者表达式,会使索引失效
RocketMQ中总共有四种角色,producer,consumer,nameServer,broker
1. producer定期从nameServer中获取topic路由信息,并缓存到本地,包括可用的broker。当producer需要发送一个消息时,会先根据路由表查找某个topic对应的broker,然后根据某种策略将消息发送到该broker上,如果nameServer挂掉,那么producer可以继续使用本地缓存的路由表进行查找
2. consumer通过nameServer拿到路由表并缓存到本地,然后并发的从消息队列中获取消息并进行消费,随之而来的就是消费顺序的问题(先要创建订单,才能确认订单,最后进行付款),消息队列是支持消息顺序的
3. broker负责存储队列信息,维护消费进度,定时向NameServer上报topic路由信息,如果一台broker挂掉,那么producer不会立即感知,而是nameServer发现该broker心跳包超时的时候,会将该broker从集群中清除并告知相应的producer,如果producer已经把消息发送给挂掉的broker,那么肯定会发送失败,producer会重新选择另外一台broker发送消息,这样就避免了消息的丢失。
4. nameServer,可集群,一台nameServer挂掉之后,另一台顶上,broker会对nameServer集群中的每一个节点都发送心跳包,这样保证了集群中的每个节点的路由信息都是同步的。
假设一个场景,订单必须先创建,然后才能让用户确认订单,最后才能付款
方案一:
那么就可以根据每个订单的订单号进行hash运算,把相同订单号的消息放在同一个消息队列中
然后消费端的线程池降为单线程,每次只能从消息队列中取一条消息进行消费。
方案二:
比特币,比特币在2009年由中本聪发明,为什么会在2009年这个时间发明呢?因为2008年的金融危机导致了大量的银行倒闭甚至国家处于破产的边缘,是的民众不在相信自己存在银行里的钱是安全可靠的,所以促使了这种去中心化的货币出现。
比特币具有匿名性,不可抹灭性,公开透明性。
匿名性是指每个人都可以去申请一个比特币钱包,而这个钱包的地址是一串随机的字符,这样就没有人知道谁是谁了。
不可抹灭性是指一旦一笔交易被写入到区块链中,那么他就永远无法被修改,除非你所控制的算力能够达到51%
公开透明性指每个人都可以看到其他人的钱包里都多少币
比特币发行总量为2100万枚,现在已经发行了80%多,比特币所有的交易都会在区块链中进行存储,每笔交易都需要支付一定量的手续费,这样才会有节点帮助你进行存储这笔交易,一旦某个节点存储了某笔交易那么就会通知其他所有节点都去存储这笔交易。
区块链中的一个块大小为1M,每10分钟产生一个块,每个块中都有一定数量的比特币,当一个块存满的时候,需要暂停全部的交易,然后所有的节点都会去算这个块的nonce值,第一个算出来的将获得这个节点中存储的比特币,之后交易继续进行,现在大概已经有了50多个块
1. 利用top命令查看哪个进程占用了CPU(top -c P)
2. 找出进程中最耗CPU的线程(top -Hp 10675)
3. 将进程ID转化为16进制(printf "%x\n" 10765)
4. 然后使用jstack工具看看线程在干什么(jstack 10675 | grep '0x2a34'),同时可以配合使用jmap -histo:live pid 查看该进程中存活对象(如果发生OOM的话)
1. 开启-XX:+HeapDumpOnOutOfMemoryError参数使得JVM在发生OOM时生成一个dump(堆转储)文件
2. 可以使用VisualVM工具导入dump文件,然后进行查看
3. 首先确定是内存泄漏还是内存溢出
4. 如果是内存溢出那么就要增大堆的内存空间,如果是内存泄漏就要查看内存泄漏报告,分析是哪个线程的哪个对象出了问题
5. 如果发现一个对象非常大,那么就可以查看该对象的GC链,看看他到底引用了哪个GCRoots节点,然后在根据引用链去定位代码
1. Java应用被认为杀死(kill,kill -9)
2. Java应用发生OOM
3. 系统发生OOM
1. 静态集合类
2. 各种连接
3. 单例模式
性能调优的原则;
1. MinorGC回收原则:每次发生MinorGC时应该尽可能的回收垃圾对象,以减少应用发生FullGC的频率
2. GC内存最大化的原则:处理吞吐量和时延问题时,可供垃圾收集器使用的内存越大,垃圾的收集效果越好,应用程序也越来越流畅
3. GC调优3选2原则:在性能属性里面,内存,延迟,吞吐量三者我们只能选其中两者进行调优,不可三者兼得。
4. 调优需要按照先后顺序来,即内存>延迟>吞吐量
内存
1. 确定应用在压力测试下进入稳定运行时的内存占用,然后计算此时的对象活跃大小,如何确定应用已经进入了稳定阶段呢?那就是查看GC日志,多收集几次,然后取平均值即可获得老年代对象的平均活跃大小。没有GC日志的可以使用jmap -heap 查看堆的使用情况
2. 然后对各个区域的内存大小进行调整4 新生代 老年代1.5
3. 一般情况下,为了避免老年代扩容的时候触发fullGC,通常需要制定-Xms和-Xmx(-XX:PermSize和-XX:MaxPermSize)的大小一样,这样可以让JVM在启动的时候就把老年代和永久代的空间定下来,避免运行时自动扩展。这样可以避免在启动的时候发生多次fullGC
延迟
导致延迟的主要原因还是发生GC,导致系统出现某段STW(Stop the world)
主要关注以下四点:
1. minor gc的时间
2. minor gc的频率
3. full gc的时间
4. full gc的频率
主要是通过gc日志查看,得到的数据越平均越好,调小空间可以降低gc时间,但是会增加gc频率,根据具体需要动态调整
增加吞吐量
1. CMS垃圾收集器对CPU资源非常敏感
2. CMS垃圾收集器无法处理浮动垃圾
3. CMS垃圾收集器会产生内存碎片
1. 快速排序
2. 归并排序 采用分治法,先将子序列有序,然后再使子序列合并成为一个有序的子序列段,最后使整个序列有序
3. 希尔排序 把记录按照下标的一定增量进行分组,对每组使用直接插入排序,随着增量的逐渐减少,每组包含的关键词越来越多,当增量减小到1时,整个记录恰好被分成一组,算法终止
用来保存线程的局部变量,不会发生内存泄漏,因为Entry类继承自WeakReference,会在GC发生之前回收这部分数据。
适配器模式(HandlerAdpter,根据HandlerMapper找到对应的Adpter)
单例模式(容器中存在的实例基本都是单例,SpringMVC中的Controller)
工厂模式(BeanFactory)
装饰器模式(用来设置JavaBean属性的BeanWrapper)
模板模式(JDBCTemplate,1.获取数据库连接2.创建执行语句3.执行SQL语句4.处理结果集5.释放资源)
1. 创建型模式
2. 结构型模式
3. 行为型模式
4. J2EE模式
1. new
2. 反射(class.newInstance,class.getDeclaredConstructor().newInstance())
3. clone(),implements Cloneable接口,并且重写clone()方法,默认是浅拷贝
4. 反序列化
1. 使用Statement每次查询都会进行编译,一次查询只会产生一次网络请求,而且安全性很差
2. 使用PreparedStatement,对于相似的SQL只会编译一次,编译之后只要缓存命中,那么就可以跳过编译阶段直接运行,并且SQL语句使用占位符,提高了代码的可读性和安全性
3. 可以使用preparedStatement.addBatch()对SQL语句进行批处理,也就是一次给DB发送多条SQL,这样可以减少网络请求
负载均衡服务器需要实现两个功能:1. 根据负载均衡算法和应用服务器列表计算出处理该请求的web服务器地址 2. 将该请求发送到该web服务器上
1. 轮询(Round Robin) 硬件都一致
2. 加权轮询(Weight Round Robin) 硬件不一致
3. 随机(Random) 集群数量越高越均匀
4. 最少连接(Lest Connections) 最合理
5. 源地址散列(Source Hashing) 相同IP的请求总是转发到固定的节点上,保证会话的生命周期
分布式缓存集群和服务集群完全不同,分布式缓存集群的节点上每个节点存储的数据各不相同,必须先找到缓存有该数据的服务器,然后才能访问,并且从集群的伸缩性考虑,必须使新加入节点对整个集群的影响最小(使整个缓存集群中已经缓存的数据还能被访问到)
1. 构造一个长度为2的32次方的整数环
2. 根据节点名称的hash值将该服务器放置在这个hash环上
3. 根据请求的key的hash值在hash环上顺时针查找离它最近的服务器节点
采用上述算法会存在新增节点之后各个服务器负载压力不均衡的问题,此时可以通过增加虚拟节点来解决,即在hash环上存在的都是一些虚拟节点,多个虚拟节点映射到同一个物理服务器上,当增加一个物理服务器时,同时在虚拟环上增加多个虚拟节点,这样可保证增加节点后各个物理节点的负载是均衡的,在实战中为了权衡负载和性能的影响,通常虚拟节点的数量选择为150
虚拟节点hash环.JPG1. HTTP重定向负载均衡
2. DNS域名解析负载均衡
3. 反向代理服务器(通过应用进行请求的转发)
4. IP负载均衡
5. 数据链路层负载均衡
Diamond介绍
Diamond是一个管理持久配置的系统,从系统架构来看,总共分为三个角色
1. client:client是配置信息的使用者,client在启动并第一次获取数据的时候会将数据的MD5保存在内存中,还会在启动的时候创建一个定时任务(15s),定时检查配置是否发- 生了更新
2. http server:存放着server的地址,client通过http server找到某一台server并获取到配置信息,充当着一个路由的角色
3. diamond server:可集群,server在启动时,向http server注册自己的地址,然后把数据的MD5(从MySQL获取)保存在内存中,数据更新时,会更新内存中的MD5。
架构和过程:
1. 当收到client发出来的数据校验时会对MD5进行比对,如果没有变化,返回一个标识不变的字符串给clinet,如果有变化,返回变化的group和dataId,之后client会再次请求新的数据
2. diamond server可集群,集群中的每台机器都连接同一个mysql,集群中的数据同步方式有两种:1. 每台机器定时去MySQL dump数据到本地文件 2. 当一个server发生了数据变更以后,通知其他server去dump Mysql最新数据到本地
3. 发布数据时,数据先写到mysql,再写到本地文件,订阅数据时,直接查本地文件而不用去查数据库,最大程度减小mysql 的查询压力
容灾机制:
1. 数据库不可用
2. 所有的server不可用
3. clinet主动删除snapshot
4. client没有备份配置数据,导致其不能配置“容灾目录”
1. 线程池处理该请求
2. 解析器解析SQL语句(如果缓存命中可以跳过该步骤)
3. 是否命中缓存,如果命中缓存,跳过解析、优化和执行的过程,直接返回缓存中的结果集
4. 没有命中缓存进行语句解析,优化并执行
5. 通过存储引擎查询结果,并返回结果集
1. FROM 表之间做笛卡尔乘积产生虚表v1
2. ON 根据ON指定的条件进行筛选,产生虚表v2
3. JOIN 如果有外连接,会进行补偿(添加null值),产生虚表v3
4. WHERE 根据where条件筛选v3表,产生v4虚表
5. GROUP BY 根据某个字段对表进行分组,产生v5虚表
6. HAVING 对组进行过滤,产生v6虚表
7. SELECT 选择指定的列插入到v7虚表
8. DISTINCT 对v7虚表进行去重操作,然后生成v8虚表
9. ORDER BY 按照某个字段进行排序,生成v9虚表
LIMIT 限制查询的个数,生成v10虚表,并将结果返回
如果A线程执行了thread.join()语句,那么线程A会等待thread线程终止之后才能继续向下运行
1. id 查询的ID
2. select_type
simple 简单查询
primary 包含复杂的子查询,最外层的select_type为该值
subquery 在select或where里包含子查询
3. table 本次查询是哪张表
4. type 表的连接类型,性能由高到低排列如下
system 表中只有一行记录,相当于系统表
const 通过索引一次就能找到,只匹配一行数据
eq_ref 唯一性索引扫描,每个索引键只匹配表中一行数据
ref 非唯一性索引扫描,返回匹配某个索引键的所有行
range 范围索引,使用一个索引来选择行
index 只遍历索引树
ALL 全盘扫描
5. possible_keys 指出MySQL使用哪个索引在该表找到行记录,如果该值为NULL,说明没有用到索引,需要使用索引来提高性能
6. key MySQL实际使用的索引
7. key_len 使用索引的最大长度
8. ref 显示该表的索引字段关联了哪张表的哪个字段
9. rows 本次查找遍历的行数
10. extra 额外信息
using index 使用了覆盖索引
using where 使用where语句限定某一行
QLExpress是一个脚本引擎,在编写机审项目中的售卖规则时就使用到了该规则引擎,使用此规则引擎可以灵活的定义各种售卖规则,并且使这些配置即时生效。
1. 用户定义好自己的规则脚本之后,规则引擎对用户输入的脚本进行语法和词法的解析
2. 然后生成对应的指令集合
3. 输入上下文中变量的具体值,这些具体的变量值就是售卖规则中的某一个条件,
4. 当一个方案订单过来之后,解析脚本,并调用runner执行脚本,订单需要一一匹配脚本中变量的条件,当所有条件都符合时,方案才能提交成功。
不可以,因为泛型会在编译时期被擦除掉,这会使得数组中可以存放任意的数据类型,而Java中的数组是强类型的,在向数组中添加元素时会进行类型检查。
1. Executors.newCachedThreadPool()
2. Executors.newFixedThreadPool()
3. Execuotrs.newScheduledThreadPool()
4. Executors.newSingleThreadExecutor()
1. 偏向锁
2. 轻量级锁
偏向锁和轻量锁比较:
3. 自旋锁
4. 重量级锁
写锁可以降级为读锁,目的是为了保证刚写入的值能够被立刻读取
1. 使用反向代理和负载均衡实现分流
2. 通过限流保护应用免受雪崩之灾
3. 通过降级实现服务部分可用,有损服务
4. 通过隔离实现故障隔离
5. 通过设置合理的超时调用与重试机制避免请求堆积造成雪崩
6. 通过回滚机制快速修复错误版本
早期的数据库只支持读读并发操作,但是读写,写读,写写都会被阻塞,引入MVCC之后,只有写写被阻塞,其他三种操作是可以并行的。这样大幅提高了InnoDB存储引擎的并发量。
MVCC只在Read Commited,Repeatable Read隔离级别下工作
通过在每行中添加三个隐藏的列来实现MVCC(事务ID,回滚指针,DB_ROW_ID)
事务ID:用来标识最后一个对该行进行修改的事务ID
回滚指针:当提交时发现数据完整性被破坏,利用回滚指针指向的undo log进行回滚数据操作
DB_ROW_ID:当用户没有指定聚簇索引的主键或唯一的索引时,InnoDB会自动生成聚簇索引,并使用DB_ROW_ID作为聚簇索引的主键
修改数据时,拷贝出当前数据的版本,然后任意修改,各个事务之间互不影响
提交数据时,检查数据的版本号,如果成功,覆盖原纪录,失败通过指针进行数据回滚,
1. 二层负载均衡
2. 三层负载均衡
3. 四层负载均衡(IP地址+端口)
4. 七层负载均衡(URL进行转发)
关于本号
作者乔戈里亲历2019秋招,哈工大计算机本硕,百度java工程师,欢迎大家关注我的微信公众号:程序员乔戈里,公众号有3T编程资源,以及我和我朋友(百度C++工程师)在秋招期间整理的近200M的面试必考的java与C++面经,并有每天一道leetcode打卡群与技术交流群,欢迎关注。
3T编程资料等你来拿