geekymv

Do one thing, do it well.


  • 首页

  • 分类

  • 归档

  • 标签

  • 关于

mysql-innodb

发表于 2021-04-30

MySQL 体系结构和存储引擎

MySQL独有的插件式存储引擎。

存储引擎是基于表的,而不是数据库。
数据库与传统文件系统的最大差别是数据库是支持事务的。

1
show engines

查看当前MySQL数据库支持的存储引擎。

MySQL 示例数据库
https://dev.mysql.com/doc/index-other.html
Example Databases

进程间通信方式:
TCP/IP
命名管道和共享内存
Unix域套接字

InnoDB 存储引擎

MySQL5.5版本开始是默认的表存储引擎。
完整支持ACID事务;
行锁设计、支持MVCC、外键
提供一致性非锁定读

插入/更新操作高达800次/秒

高性能、高可用、高可扩展

Master线程
负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲(insert buffer)、undo页的回收等。

IO线程

内存

1、缓冲池
InnoDB 存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。
由于CPU速度与磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲池技术来提高数据库的整体性能。

1
2
show variables like 'innodb_buffer_pool_size';
show variables like 'innodb_buffer_pool_instances';

在InnoDB 存储引擎中,缓冲池中页的大小默认为16KB,使用LRU算法对缓冲池进行管理。

LRU列表中的页被修改之后,称该页为脏页(dirty page)。即缓冲池中的页和磁盘上的页的数据产生了不一致。
数据库会通过checkpoint机制将脏页刷新回磁盘。

redo log buffer

Write Ahead Log

checkpoint 将缓冲池中的脏页刷回磁盘。
当缓冲池中脏页的数量占据75%时,强制进行checkpoint。

Java面试题汇总

发表于 2021-04-21

Java并发篇

一、Java如何开启线程?怎么保证线程安全?
线程和进程的区别:进程是操作系统进行资源分配的最小单元,线程是操作系统进行任务调度的最小单元。线程属于进程。
如何开启线程?
1、继承Thread类,重写run方法;
2、实现Runnable接口,实现run方法;
3、实现Callable接口,实现call方法,通过FutureTask创建一个线程,获取到线程执行的返回值;
4、通过线程池来开启线程;
怎么保证线程安全?
加锁
1、JVM提供的锁,也就是synchronized 关键字;
2、JDK提供的各种锁;

二、volatile 和 synchronized 有什么区别?volatile 能不能保证线程安全?DCL(Double Check Lock)单例为什么要加volatile?
1、synchronized 关键字,用来加锁。volatile 只是保证变量的线程可见性,通常适用于一个线程写,多个线程读的场景。
2、volatile 不能保证线程安全,volatile 关键字只能保证线程可见性,不能保证原子性。
3、volatile 防止指令重排,在DCL中,防止高并发下,指令重排造成的线程安全问题。

三、Java线程锁机制是怎样的?偏向锁、轻量级锁、重量级锁有什么区别?锁机制是如何升级的?
1、Java的锁就是在对象的MarkWord中记录一个锁状态。无锁、偏向锁、轻量级锁、重量级锁对应不同的锁状态。
2、Java的锁机制就是根据资源竞争的激烈程度不断进行锁升级的过程。
3、无锁、偏向锁、轻量级锁、重量级锁。

四、谈谈你对AQS的理解。AQS如何实现可重入锁ReentrantLock?
1、AQS是一个Java线程同步的框架,是JDK中很多锁工具的核心实现框架。
2、在AQS中,维护一个整型变量state 和 一个线程组成的双向链表队列。其中,这个线程队列用来给线程排队的,而state用来控制线程排队或放行的。在不同场景下,有不同的意义。
3、在可重入锁这个场景下,state就用来表示加锁的次数,0表示无锁,每加一次锁,state就加1,释放锁state就减1。

五、有A、B、C 三个线程,如何保证三个线程同时执行?如何在高并发情况下保证三个线程顺序执行?如何保证三个线程有序交错执行?
CountDownLatch、CyclicBarrier、Semaphore
1、CountDownLatch、CyclicBarrier
2、join
3、wait、notify

六、如何对一个字符串快速排序?
Fork/Join框架
归并排序

Java网络通信篇

一、TCP和UDP有什么区别?TCP为什么是三次握手,而不是两次?
TCP Transfer Control Protocol 是一种面向连接的、可靠的、传输层通信协议。
特点:面向连接的,点对点的通信,高可靠的,效率比较低,占用系统资源比较多。

UDP User Datagram Protocol 是一种无连接的,不可靠的传输层通信协议。
特点:不需要连接,发送方不管接收方有没有准备好,直接发消息,可以进行广播发送,传输不可靠,有可能丢失消息;效率比较高;协议比较简单,占用系统资源比较少。

TCP建立连接三次握手,断开连接四次挥手。
如果是两次握手,可能造成连接资源浪费的情况。

二、Java有哪几种IO模型?有什么区别?
BIO 同步阻塞IO
可靠性差,吞吐量低,适用于连接比较少且比较固定的场景,JDK1.4之前唯一的选择。编程模型最简单。

NIO 同步非阻塞IO
可靠性比较好,吞吐量比较高,适用于连接数比较多,并且连接比较短(轻操作)的场景,例如聊天室。JDK1.4开始支持。
编程模型复杂。

AIO 异步非阻塞IO
可靠性是最好的,吞吐量非常高。适用于连接比较多,并且连接比较长(重操作)的场景。例如相册服务器。
编程模型比较简单,需要操作系统支持。

三、Java NIO 的几个核心组件是什么?分别有什么作用?
Channel、Buffer、Selector
Channel 类似于流,每个Channel对应一个Buffer缓冲区,Channel会注册到Selector 上。
Selector 会根据Channel上发生的读写事件,将请求交给某个空闲的线程处理。Selector 对应一个或多个线程。
Buffer 和 Channel 都是可读可写的。

四、Select Poll和EPoll 有什么区别?
它们是NIO中多路复用的三种实现机制,是由操作系统提供的。

Java 的NIO当中使用哪种机制?可以查看JDK中 DefaultSelectorProvider 源码,在Windows和Linux 实现方式是不同的。
在Windows下是 WindowsSelectorProvider,而Linux下,根据Linux内核版本,2.6版本以上就是EPollSelectorProvider,否则就是默认的 PollSelectorProvider。

五、描述Http 和 Https的区别
HTTP: 是互联网上应用最为广泛的一种网络通信协议,基于TCP,可以使浏览器工作更为高效,减少网络传输。
HTTPS: 是HTTP的加强版,可以认为是HTTP+SSL(Secure Socket Layer)。在HTTP的基础上增加了一系列的安全机制。一方面保证
数据传输安全,另一方面对访问者增加了验证机制。是目前现行架构下,最为安全的解决方案。

主要区别:
1、HTTP的连接是简单无状态的,HTTPS的数据传输是经过证书加密的,安全性更高。
2、HTTP是免费的,而HTTPS需要申请证书,而证书通常是需要收费的,并且费用一般不低。
3、它们的传输协议不同,所以它们使用的端口也不一样,HTTP默认是80端口,而HTTPS默认是443端口。

HTTPS的缺点:
1、HTTPS的握手协议比较耗时,所以会影响服务的响应速度和吞吐量;
2、HTTPS也并不是完全安全的,它的证书体系并不是完全安全的,HTTPS在面对DDOS这样的攻击时,几乎起不到任何作用。

消息队列篇

一、MQ有什么用?有哪些具体的使用场景?

MQ:Message Queue ,消息队列。队列是一种FIFO先进先出的数据结构。消息由生产者发送到MQ进行排队,然后由消费者对消息进行处理。

MQ的作用主要有三个方面:

1、异步

作用:异步将提高系统的响应速度和吞吐量。

2、解耦

作用:服务之间进行解耦,可以减少服务之间的影响,提高系统的稳定性和可扩展性。

另外,解耦之后可以实现数据分发,生产者发送一个消息后,可以由多个消费者来处理。

3、削峰

作用:以稳定的系统资源应对突发的流量冲击。

MQ的缺点:

1、系统可用性降低,一旦MQ宕机,整个业务系统就会产生影响。高可用

2、系统的复杂度提高:引入MQ之后,数据链路就会变得很复杂。如何保证消息不丢失?消息不会重复调用?怎么保证消息的顺序性?

3、数据一致性:A系统发消息,需要由B、C两个系统一同处理。如果B系统处理成功、C系统处理失败,这就会造成数据一致性的问题。

二、如何进行产品选型?

Kafka

优点:吞吐量非常大,性能非常好,集群高可用。

缺点:会丢数据,功能比较单一。

使用场景:日志分析,大数据采集。

RabbitMQ

优点:消息可靠性,功能全面。

缺点:吞吐量比较低,消息积累会严重影响。erlang语言不好定制。

使用场景:小规模场景。

RocketMQ

优点:高吞吐、高性能、高可用,功能非常全面。

缺点:开源版功能不如商业版。官方文档和周边生态还不够成熟,客户端只支持java。

使用场景:几乎是所有场景。

三、如何保证消息不丢失?

1、生产者发送消息不丢失

Kafka:消息发送 + 回调。

RocketMQ:1)、消息发送 + 回调;2)、事务消息。

RabbitMQ:

1)、消息发送 + 回调;

2)、手动事务,

channel.txSelect() 开启事务,channel.txCommit()提交事务,channel.txRollback()回滚事务。这种方式对channel是会产生阻塞的,造成吞吐量下降。

3)、Publish Confirm,整个处理流程跟RocketMQ的事务消息基本是一致的。

2、MQ主从消息同步不丢失

RocketMQ:1)、普通集群中,同步同步消息、异步同步消息。异步同步消息效率更高,但是由丢失消息的风险。同步同步消息不会丢消息。2)、Dledger集群-两阶段提交。

RabbitMQ:普通集群,消息是分散存储的,节点之间不会主动进行消息的同步,是有可能丢失消息的。镜像集群,会在节点之间主动进行数据同步,这样数据安全性得到提高。

Kafka:通常都是在允许消息少量丢失的场景。acks 0 1 all 参数配置。

3、MQ消息存盘不丢失

RocketMQ:同步刷盘、异步刷盘,异步刷盘效率更高,但是有可能丢消息。同步刷盘消息安全性更高,但是效率会降低。

RabbitMQ:将队列配置成持久化队列。新增的Quorum类型的队列,会采用Raft协议来进行消息同步。

4、MQ消费者消费消息不丢失

RocketMQ:使用默认的方式消费就行,不要采用异步方式。

RabbitMQ:autoCommit -> 手动提交offset。

Kafka:手动提交offset。

四、如何保证消息消费的幂等性?

其实就是解决消费者重复消费消息的问题。

所有MQ产品都没有提供主动解决幂等性的机制,需要由消费者自行控制。

RocketMQ:给每个消息分配MessageID,这个MessageID就可以作为消费者判断幂等的依据,这种方式不太建议。

最好的方式就是自己带一个有业务标识的ID来进行幂等判断,比如订单ID。

五、如何保证消息的顺序消费?

全局有序和局部有序:MQ只需要保证局部有序,不需要保证全局有序。

生产者把一组有序的消息放到同一个队列当中,而消费者一次消费整个队列当中的消息。

RocketMQ中有完整的设计,但是在RabbitMQ和Kafka当中并没有完整的设计,需要自己进行配置。

RabbitMQ:要保证目标exchange只对应一个队列,并且一个队列只对应一个消费者。

Kafka: 生产者通过定制partition分配规则,将消息分配到同一个partition。topic下只对应一个消费者。

消息顺序消费会影响性能。

六、如何保证消息的高效读写?

零拷贝、顺序写。

七、使用MQ如何保证分布式事务的最终一致性?

分布式事务:业务相关的多个操作,保证它们同时成功或同时失败。

最终一致性:与之对应的就是强一致性。

MQ中要保证事务的最终一致性,就需要做到两点

1、生产者要保证100%的消息投递。事务消息机制。

2、消费者需要保证幂等消费,唯一ID + 业务自己实现幂等。

分布式MQ的三种语义:

at least once

at most once

exactly once

八、让你设计一个MQ,你会如何设计?

数据库篇

一、索引的基本原理

缓存篇

一、如何避免缓存穿透、缓存击穿、缓存雪崩?

缓存雪崩是指缓存同一时间大面积失效,导致后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案:

  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  • 缓存预热
  • 互斥锁

缓存穿透是指缓存和数据库中都没有的数据,导致所有请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案:

  • 接口增加校验,如用户授权校验,id<=0的直接拦截;
  • 从缓存和数据库都取不到的数据,这时也可以将key-value对写成key-null 写入缓存,缓存有效时间可以设置的短点,如30秒(设置太长会导致后面这个ID对应的数据存在了也没法使用)。这样可以防止攻击用户反复用同一个ID暴力攻击。
  • 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmp中,一个一定不存在的数据会被这个bitmap拦截,从而避免了对底层存储系统的查询压力。(由于存在哈希冲突,一个bitmap中存在的数据不一定真的存在,而一个bitmap中不存在的数据肯定不存在)。

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时如果并发访问特别多,同时读取缓存没有读到数据,又同时去数据库去读取,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查询同一条数据,缓存雪崩是不同数据大面积过期,很多数据都查不到从而查数据库。

解决方案:

  • 设置热点数据永不过期;
  • 加互斥锁,只允许一个请求查询数据库,其他排队等待,做double check。

二、分布式系统中常用的缓存方案有哪些

客户端缓存:页面和浏览器缓存,APP缓存,H5缓存,localStorage 和 sessionStorage;
CDN缓存:内容存储:数据的缓存,内容分发:负载均衡;
nginx缓存:静态资源;
服务端缓存:本地缓存、外部缓存;
数据库缓存:持久层缓存(mybatis, hibernate 多级缓存),mysql查询缓存。
操作系统缓存:Page Cache、Buffer Cache。

三、缓存过期都有哪些策略

定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除,该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性过期:只有访问一个key时,才会判断该key是否已经过期,过期则删除。该策略可以最大化地节省CPU资源,但是很消耗内存、许多的过期数据都还存在内存中,极端情况可能出现大量的过期key没有再次被访问,从而不回被清除,占用大量内存。
定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key(随机的),并清除其中已过期的key。该策略是定时过期和惰性过期的这种方案。通过调整定时扫描的时间间隔和每次扫码的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

四、常见的缓存淘汰算法

FIFO(Fitst In First Out,先入先出),根据缓存被存储的时间,离当前时间最远的数据优先被淘汰;
LRU(Least Recently Used,最近最少使用),根据最近被使用的时间,离当前时间最远的数据优先被淘汰;
LFU(Least Frequently Used,最不经常使用),在一段时间内,缓存数据被使用次数最少的会被淘汰。

五、如何保证数据库和缓存的一致性

由于缓存和数据库是分开的,无法做到原子性的同时进行数据修改,可能出现缓存更新失败,或者数据库更新失败的情况,这时候会出现数据不一致,影响业务。

  • 先更新数据库,再更新缓存。缓存可能更新失败,读到老数据;
  • 先删缓存,再更新数据库。并发时,读操作可能还会将旧数据读回缓存;
  • 先更新数据库,再删除缓存。也存在缓存删除失败的可能。

最经典的缓存+数据库读写的模式:Cache Aside Pattern。
读的时候先读缓存,缓存没有的话就读数据库,然后取出数据放入缓存,同时返回响应。
更新的时候,先更新数据库,然后再删除缓存。

为什么是删除而不是更新?
删除更加轻量,延迟加载的一种表现,更新可能涉及多个表,比较耗时。

延时双删:先删除缓存,再更新数据库,休眠1s、再次删除缓存。
写数据的休眠时间则在读数据业务逻辑的耗时基础上加几百毫秒即可。这么做的目的就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据,并发还是可能读到旧值覆盖缓存。

终极方案:
将访问操作串行化
1、先删缓存,将更新数据库的操作放进有序队列中
2、从缓存查询不到的查询操作,都进入有序队列
面临的问题:
1、读请求积压,大量超时,导致数据量的压力:限流、熔断
2、如何避免大量请求积压:将队列水平拆分,提高并行度
3、确保相同的请求路由正确

六、布隆过滤器原理,优缺点
bitmap
int[10] 每个int类型的整数是4*8=32个bit,则int[10]一共有320 bit,每个bit非0即1,初始化时都是0
添加数据时。

在开发中遇到过什么问题,怎么解决的?

1、Redis 设置最大内存和淘汰策略;

2、Quartz定时任务,使用Redis的zset替代订单超时未支付检查。

https://www.cnkirito.moe/about/

zoom 一面

dubbo 服务拆分
dubbo 线程池、通信协议
http接口安全

线程池
线程池生命周期
线程生命周期
线程池大小设置
阻塞队列
线程池退出钩子方法
synchronized 修饰静态方法和非静态方法
synchronized 锁升级
ReentrantLock 公平锁与非公平锁

redis
缓存一致性
淘汰策略
分布式锁
缓存穿透、击穿、雪崩

MySQL
隔离级别
死锁
锁类型
日期类型 timestamp datetime

Spring
设计模式
Spring事务失效
Spring事务注解

Springboot
配置文件优先级
自定义 starter

SpringCloud

JVM内存模型
垃圾收集器

CPU100%问题排查

Netty
核心组件
心跳机制

ReentrantLock 源码分析

发表于 2021-04-06

AQS 是 AbstractQueuedSynchronizer 类的简称,AbstractQueuedSynchronizer 继承 AbstractOwnableSynchronizer。
通过Volatile + CAS 实现,AQS 内部维护着 volatile int state 变量 和 FIFO 的双向的链表。

AbstractOwnableSynchronizer 内部维护 exclusiveOwnerThread 变量,表示独占模式同步的持有者。

1
2
3
4
/**
* The current owner of exclusive mode synchronization.
*/
private transient Thread exclusiveOwnerThread;

ReentrantLock 就是使用AQS实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建锁
Lock lock = new ReentrantLock();

// 获取锁
lock.lock();
try {

// 业务代码

}finally {
// 释放锁
lock.unlock();
}

阅读全文 »

管程

发表于 2021-03-19

Monitor

Monitor 可以翻译为监视器或管程。
每个Java对象都可以关联一个Monitor对象,如果使用 synchronized 给对象上锁(重量级)之后,
该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。

Monitor

WaitSet
EntryList
Owner

1
2
3
synchronized(obj) {
// 临界区代码
}

obj 是一个Java对象,当第一个线程t1执行 synchronized 代码块时,obj的对象头(Object Header) 中的 MarkWord 与 操作系统底层的 Monitor 对象关联。
刚开始 Monitor 对象中的 Owner 属性为null,这个时候 Owner属性指向了线程t1,线程t1执行临界区代码。

接下来,线程t2 过来执行 synchronized 代码块时发现 obj对象已经关联了一个Monitor对象了,
并且 Owner 不等于空,也不是自己,那么线程t2 进入 Monitor 对象中的 EntryList 中排队BLOCKED。
同样的后面过来的线程t3也是进入EntryList 中排队。

当线程t1执行完了临界区代码,则释放锁,Owner 属性值为空,同时通知 EntryList 中等待的线程。
比如线程t2被唤醒了,争夺CPU时间片。

synchronized 必须是进入同一个对象的Monitor 才有上述效果。
不加 synchronized 的对象不会关联监视器,不遵循以上规则。

若线程调用 wait()方法,将释放当前持有的 monitor, Owner 属性恢复为null,该线程进入 WaitSet 集合中等待被唤醒,处于 WAITTING 状态。
WaitSet 中存放的是之前获得过锁,但条件不满足进入 WAITING 状态的线程。

EntryList 可能是公平的,也可能是非公平的。

synchronized 底层原理

synchronized 代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* javap -v target/classes/com/geekymv/concurrent/lock/MonitorDemo.class
*/
public class MonitorDemo {

private static Object lock = new Object();

private static int counter = 0;

public static void main(String[] args) {

synchronized (lock) {
counter++;
}

System.out.println(counter);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field lock:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter
6: getstatic #3 // Field counter:I
9: iconst_1
10: iadd
11: putstatic #3 // Field counter:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any

monitorenter、monitirexit 指令实现

monitorenter 将lock 对象的 MarkWord 置为 Monitor 指针
monitirexit 将lock 对象的 MarkWord 重置,唤醒 EntryList

最后一个 monitorexit 异常退出时释放锁。

synchronized 修饰方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MonitorSyncDemo {

private static int counter = 0;

public static void main(String[] args) {

MonitorSyncDemo demo = new MonitorSyncDemo();

demo.test();
}

public synchronized void test() {
counter++;
System.out.println("counter = " + counter);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: getstatic #5 // Field counter:I
3: iconst_1
4: iadd
5: putstatic #5 // Field counter:I
8: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
11: new #7 // class java/lang/StringBuilder
14: dup
15: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
18: ldc #9 // String counter =
20: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: getstatic #5 // Field counter:I
26: invokevirtual #11 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
29: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
35: return
LineNumberTable:
line 15: 0
line 16: 8
line 17: 35
LocalVariableTable:
Start Length Slot Name Signature
0 36 0 this Lcom/geekymv/concurrent/lock/MonitorSyncDemo;

synchronized 修饰的方法并没有使用 monitorenter、monitirexit 指令, 而是通过 ACC_SYNCHRONIZED 标志判断是否为同步方法。

在 Java6 之前,Monitor 的实现完全依赖操作系统内部的互斥锁,需要进行用户态到内核态的切换,同步操作时一个重量级的操作。
现在的 Oracle JDK 中,JVM 提供了三种不同的Monitor 实现,也就是常说的三种不同的锁:偏向锁、轻量级锁和重量级锁,

锁升级过程
无锁、偏向锁、轻量级锁、重量级锁

1
2


一个对象被new 出来的时候是正常(normal)状态,或者无锁状态。

偏向锁 threadId

轻量级锁
轻量级锁的使用场景,如果一个多线有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是通明的,仍然使用 synchronized。

线程和进程

发表于 2021-03-18

进程

程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。
在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存和IO的。

当一个程序被运行,从磁盘加载这个程序的代码到内存,这时就开启了一个进程。

进程可以视为程序的一个实例,大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),
也有的程序只能启动一个实例进程(例如微信、360安全卫士等)。

线程

一个进程内可以分为一到多个线程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。
Java中,线程作为最小调度单位,进程作为资源分配的最小单位。

进程和线程对比

进程基本上相互独立,而线程存在于进程内,是进程的一个子集。
进程拥有共享的资源,如内存空间等,供其内部的线程共享。
进程间通信较为复杂,同一台计算机的进程通信称为IPC(Inter-process communication),
不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP。
线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量。
线程更轻量,线程上下文切换成本一般要比进程上下文切换要低。

并行与并发

单核CPU下,线程实际还是串行执行的,操作系统中有一个组件叫作任务调度器,将CPU的时间片分给不同的线程使用。

查看进程线程的方法

Linux

1
2
3
4
5
6
7
8
9
ps -fe 查看所有进程

ps -fT -p pid 查看某个进程(pid)的所有线程

kill 杀死进程

top 按大写 H 切换是否显示线程

top -H -p pid 查看某个进程(pid)的所有线程

Java

1
2
3
4
jps 查看所有Java进程
jps -l 显示完整的包名

jstack pid 查看某个Java进程(pid)的所有线程状态

jconsole 远程连接配置

1
2
3
4
5
6
nohup java -jar \
-Djava.rmi.server.hostname=10.0.231.7 -Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=8999 \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.authenticate=false \
geek-1.0-SNAPSHOT.jar > /dev/null 2>&1 &

sharding-jdbc

发表于 2021-03-12

垂直分表

将一个表按照字段分成多个表,每个表存储其中一部分字段。
它带来的提升:
1、为了避免IO争抢并减少锁表的几率,查看详情的用户与商品信息浏览互不影响。
2、充分发挥热门数据的操作效率,商品信息的操作的高效不会被商品描述的低效率所拖累。

为什么大字段的IO效率低?
由于数据量本身大,需要更长的读取时间;
跨页,页是数据库存储单位,很多查找及定位操作都是以页为单位的,单页内的数据行越多数据库整体性能越好,而大字段占用空间大,单页内存储行少,因此IO效率较低。
数据库以行为单位将数据加载到内存中,这样表中字段长度较短且访问频率较高,内存能加载更多的数据,命中率更高,减少了磁盘IO,从而提升了数据库性能。

一般来说,某业务实体中的各个数据项的访问频次是不一样的,部分字段可能是占用存储空间比较大的BLOB或TEXT。例如商品描述。所以,当表数据量很大时,可以将表按
字段切开,将热门字段、冷门字段分来放置在不同表中,这些表可以放在不同的存储设备上,避免IO争抢。垂直切分带来的性能提升主要集中在热门数据的操作效率上,而且
磁盘争用情况减少。

通常我们按以下原则进行垂直拆分:
1、把不常用的字段单独放在一张表;
2、把text、blob 等大字段拆分出来放置在附表中;
3、经常组合查询的列放在一张表中。

垂直分库

通过垂直分表性能得到了一定程度的提升,但是还没有达到要求,并且磁盘空间也快不够了,因为数据始终在一台服务器,垂直分表只解决了单一表数据量过大问题,但没有将表
分布到不同的服务器上,因此每个表还是竞争同一个物理机的CPU、内存、网络IO、磁盘。
根据业务可以拆分为用户库、商品库、订单库。并把这些库分散到不同服务器上。

垂直分库是指按照业务将表进行分类,分布到不同的数据库上,每个库可以放在不同的服务器上,它的核心理念是专库占用。
它带来的提升是:
1、解决业务层面的耦合,业务清晰;
2、能对不同业务对数据进行分级管理、维护、监控、扩展等
3、在高并发场景下,垂直分库一定程度的提升IO、数据库连接数、降低单机硬件资源的瓶颈。

垂直分库通过将表按业务分类,然后分布在不同数据库,并且可以将这些数据库部署在不同服务器上,从而达到多个服务器共同分摊压力的效果,
但是依然没有解决单表数据量过大的问题。

水平分库

水平分库是把同一个表的数据按一定的规则拆分到不同的数据库中,每个库可以放在不同的服务器上。
它带来的提升是:
1、解决了单库大数据、高并发的性能瓶颈。
2、提高了系统的稳定性及可用性(稳定性体现在IO冲突减少,锁定减少,可用性是指某个库出问题,部分可用)。

水平分表

水平分表是在同一个数据库内,把同一个表的数据按一定的规则拆分到多个表中。
它带来的提升是:
1、优化单一表数据量过大而产生的性能瓶颈
2、避免IO争抢并减少锁表的几率。
库内的水平分表,解决了单一表数据量过大的问题,分出来的小表中只包含一部分数据,从而使得单个表的数据量变小,提高检索性能。

总结:

  • 垂直分表:可以把一个宽表的字段按访问的频次、是否是大字段的原则拆分为多个表,这样既能使业务清晰,还能提高部分性能。拆分后,尽量从业务角度
    避免联查,否则性能方面将得不偿失。

  • 垂直分库:可以把多个表按业务耦合松紧归类,分别存放在不同的库,这些库可以分布在不同服务器,从而使访问压力被多服务器负载,大大提升性能,
    同时能提高整体架构的业务清晰度,不同的业务库可根据自身情况定制优化方案。但是它需要解决跨库带来的所有复杂问题。

  • 水平分库:可以把一个表的数据(按数据行)分到不同的库,每个库只有这个表的部分数据,这些库可以分布在不同服务器,从而使访问压力被多服务器
    负载,大大提升性能。它不仅需要解决跨库带来的所有复杂问题,还要解决数据路由问题。

  • 水平分表:可以把一个表的数据(按数据行)分到同一个库的多张表中,每个表只有这个表的部分数据,这样能小幅提升性能,它仅仅作为水平分库的一个
    补充优化。

一般来说,在系统设计阶段就应该根据业务耦合松紧来确定垂直分库,垂直分表方案。在数据量及访问压力不是特别大的情况下,首先考虑缓存、读写分离、
索引技术等方案。若数据量极大,且持续增长,再考虑水平分库、水平分表方案。

分库分表带来的问题

分库分表能有效的缓解单机和单库带来的性能瓶颈和压力,突破网络IO、硬件资源、连接数的瓶颈,同时也带来了一些问题。

  • 事务一致性问题
    由于分库分表把数据分布在不同库甚至不同服务器,不可避免会带来分布式事务问题。

  • 跨节点关联查询
    在没有分库前,可以通过SQL关联查询,但垂直分库后的数据不在一个数据库,甚至不在一台服务器,无法进行关联查询。
    解决方案:可以将原关联查询分为两次查询,第一次查询的结果集中找出关联数据id,然后根据id发起第二次请求得到关联数据,最后将获得的数据进行拼接。

ArrayList类执行remove操作竟然抛出了ConcurrentModificationException 异常

发表于 2021-03-11

我们一般会有这么一个场景,比如从数据库中查询出一些数据,放入了ArrayList中。现在要对这些数据进行过滤,删除那些不满足条件的元素。
为了简化,我这里就不从数据库中查询了,而是模拟一些数据,ArrayList中存放了5个用户信息,现在要求将年龄小于18的用户从ArrayList中移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public class User {

// id
private int id;
// 姓名
private String name;
// 年龄
private int age;

public User() {
}

public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}

// 省略 getter setter 方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
List<User> list = new ArrayList<>();

// 模拟数据
list.add(new User(1000, "张三", 22));
list.add(new User(1001, "李四", 27));
list.add(new User(1002, "王五", 16));
list.add(new User(1003, "赵六", 18));
list.add(new User(1004, "孙七", 22));

Iterator<User> iterator = list.iterator();

while (iterator.hasNext()) {
User user = iterator.next();

// 删除不满足条件的用户
if(user.getAge() < 18) {
list.remove(user);
}
}

执行会抛出如下异常信息

1
2
3
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)

从异常信息可以看出是ArrayList内部类Itr的checkForComodification方法内部抛出了异常。

对比Vector、ArrayList、LinkedList 的区别

发表于 2021-03-11

三者都实现了List接口。

Vector 是线程安全的动态数组,内部使用 Object[] 数组存放元素,可以根据需要自动扩容到原来的2倍。
线程安全的集合类,通过在方法上使用 synchronized 实现同步,通常性能上要比ArrayList 差。

ArrayList 不是线程安全的,也可以根据需要调整容量,增加到原来的1.5倍。

LinkedList Java提供的双向链表,不是线程安全的,链表不需要调整容量。

Vector 和 ArrayList 内部元素以数组形式顺序存储的,适合随机访问的的场合,除了尾部插入和删除外,往往性能比较差,需要移动元素。
LinkedList 进行节点插入、删除高效得多,但是随机访问性能要比动态数组慢,需要从头开始遍历。

如果遇到多线程操作同一容器的场景,可以使用 Collections 类 提供的 synchronizedList() 方法 将 ArrayList 和 LinkedList 转换成线程安全的容器来使用。

HotSpot 虚拟机在Java堆中对象分配、布局和访问过程

发表于 2021-03-10

内存分配方式:

  • 指针碰撞(Bump The Pointer)
  • 空闲列表(Free List)

垃圾收集器(是否带有空间压缩整理,Compact)决定Java堆是否规整。

谈谈JVM如何判断一个对象是否可以被回收?

发表于 2021-03-10 | 分类于 JVM
1234…11

geekymv

110 日志
8 分类
23 标签
© 2022 geekymv
由 Hexo 强力驱动
| 总访问量次 | 总访客人 |
主题 — NexT.Muse v5.1.4