公众号后台回复“
面试
”,获取精品学习资料
扫描下方海报
了解
专栏详情
本文来源:Java爱好者社区
本文导读:
-
应用异常监控
-
Redis客户端异常分析
-
Redis客户端问题引导分析
-
站在Redis客户端视角分析
-
站在Redis服务端视角分析
-
资源池生产配置合理性分析
-
本文总结
今天我们来聊聊线上环境遇到的一个问题以及分析过程。
这不,项目中有一个Redis客户端的异常在疫情期间,出现在了你的面前,虽然该异常是偶发,有必要仔细分析下该异常出现的原由。
具体异常信息如下所示:
大家看截图展示的异常信息,是不是很想问,这个异常显示怎么这么「友好」?
没错,是通过一款非常好用的
实时异常监控工具:Sentry
来监控到的,这款工具在我们的项目中已经接入并使用了很长一段时间了,对异常的监控非常到位。
比如针对发生的异常,将具体访问的整个URL、客户端上报的信息、设备型号等信息作为TAGS收集上来,尽情的展示给你,让你尽快结合这些信息快速定位问题。
该服务部署在k8s容器环境下,在截图中TAGS中,也能够看到
server_name
代表的是Pod的
hostname
,这样便能快速知道是哪个Pod出现的问题,进入容器平台直接进入到Pod内部进一步详细分析。
强烈推荐大家项目中接入
Sentry
,因为它不但有很好用的异常治理平台,更为重要的是Sentry支持跨语言客户端,比如支持Java、Andriod、C++、Python、Go等大部分语言,现成的客户端易于接入和使用。
我想只要你的服务不卡死,如果出现问题,项目里输出的日志中总会有一些
ERROR
级别的日志出现的,那么此时就交给Sentry,它会及时向你发出告警(邮件...)通知你。
本项目中使用的Jedis(Redis的Java客户端),提示异常信息
JedisConnectionException Unexpected end of stream
,在使用Redis过程中我还很少遇到这个问题,既然遇到了,这是不是缘分啊 :)
其实异常栈中已经给出了详细的调用过程,在哪里出现的问题,顺藤摸瓜根据这个堆栈去查找
线索
。
如何找到更为详细的堆栈?别担心,在上图中点击下
raw
会出现完整的异常堆栈的文本信息,也方便复制拷贝出来分析。
如下所示:
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:199)
at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40)
at redis.clients.jedis.Protocol.process(Protocol.java:151)
at redis.clients.jedis.Protocol.read(Protocol.java:215)
at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:340)
at redis.clients.jedis.Connection.getStatusCodeReply(Connection.java:239)
at redis.clients.jedis.BinaryJedis.auth(BinaryJedis.java:2139)
at redis.clients.jedis.JedisFactory.makeObject(JedisFactory.java:108)
at org.apache.commons.pool2.impl.GenericObjectPool.create(GenericObjectPool.java:888)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:432)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:361)
...
根据以上信息,发现是调用到
BinaryJedis.auth
验证Redis密码时出错的,而且有
GenericObjectPool.borrowObject
表示借用对象的方法,GenericObjectPool是Apache开源项目的线程池,在很多开源项目中都能看到它的身影。
说明是在伸手向资源池索要对象时,在资源池里没有拿到对象,那就只能创建一个,调用了
GenericObjectPool.create
,调用具体实现方法
JedisFactory.makeObject
创建Jedis对象时出错的。
哦?这么一看,简单一想猜测下,在创建新的对象时验证密码时,可能因网络不稳定,Redis-Server没有正常返回异常信息导致的。
在上文中,我们在异常堆栈中发现使用了线程池,如果不使用资源池管理这些对象,会发生什么情况?
如下所示,每次使用Redis连接都会在客户端重新创建Jedis对象,创建Jedis对象后,连接Redis Server,这个过程会建立TCP连接(三次握手),完成操作后,断开TCP连接(四次挥手),当遇到并发量稍大的请求,就会吃不消了,消耗资源的同时,无法满足应用性能上的要求。
如果使用了线程池,如下图所示的样子:
按需在资源池中初始化一定数量的对象,当有客户端请求到达时,从资源池里获取对象,对象使用完成,再将对象丢回到资源池里,给其他客户端使用。
这就是所谓的
「池化技术」
,相信在你的项目中一定会用到的,比如数据库连接池、应用服务器的线程池等等。
池化技术的优势就是能够复用池中的对象,比如上述图示中,避免了分配内存和创建堆中对象的开销;避免了因对象重复创建,进而能避免了TCP连接的建立和断开的资源开销;避免了释放内存和销毁堆中对象的开销,进而减少垃圾收集器的负担;避免内存抖动,不必重复初始化对象状态。
当然,我们也可以自己来实现,但是如果想写出比较完善的对象池的资源管理功能,也需要花费不少的精力,考虑的细节也是非常多的。
站在巨人的肩膀上,在前文中提到的Jedis内部是由 Apache Common Pool2 开源工具包来实现的,很多开源项目中应用也是很广泛的。
而Jedis客户端的很多参数都是来源于Apache Common Pool2的底层实现过程所需要的参数。
这也是Jedis或者说一些Redis客户端给用户使用简单的原因,但是简单的同时,我们也要根据不同场景去合理配置好连接池的参数,不合理的配置加上不合理的功能使用,可能会引起很多的问题。
在回归到前文的最开始的异常,这些异常跟什么有关系呢?
从图示中,我们能知道客户端使用了线程池,可能跟线程池有关系;创建对象时,auth 验证密码时出现了问题,而验证密码前已经发起了 connect 连接了,说明连接到了Redis Server,所以 Redis Server 也脱离不了干系的。
跟 Redis Client 有关系,我们就要进一步分析客户端的参数,连接池的参数是否合理。
跟 Redis Server 有关系,就要结合问题分析下服务端的参数,相关配置参数是否合理。
既然讲到了Redis客户端,首先想到的是从客户端配置的参数入手。
直接从参数入手,不如我们可以先接着对异常栈的分析,从对象资源池入手去分析,看看这个对象池到底是怎样管理的?
1、资源池对象管理
资源池中创建对象的过程如上图所示。
Apache Common Pool2 既然是一个通用的资源池管理框架,内部会定义好资源池的接口和规范,具体创建对象实现交由具体框架来实现。
1)从资源池获取对象,会调用ObjectPool#borrowObject,如果没有空闲对象,则调用PooledObjectFactory#makeObject创建对象,JedisFactory是具体的实现类。
2)创建完对象放到资源池中,返回给客户端使用。
3)使用完对象会调用ObjectPool#returnObject,其内部会校验一些条件是否满足,验证通过,对象归还给资源池。
4)条件验证不通过,比如资源池已关闭、对象状态不正确(Jedis连接失效)、已超出最大空闲资源数,则会调用 PooledObjectFactory#destoryObject从资源池中销毁对象。
ObjectPool 和 KeyedObjectPool 是两个基础接口。从定义的接口名上也能做下区分,ObjectPool 接口资源池列表里存储都是对象,默认实现类GenericObjectPool,KeyedObjectPool 接口用键值对的方式维护对象,默认实现类是GenericKeyedObjectPool。在实现过程会有很多公共的功能实现,放在了BaseGenericObjectPool基础实现类当中。
SoftReferenceObjectPool 是一个比较特殊的实现,在这个对象池实现中,每个对象都会被包装到一个 SoftReference 中。SoftReference 软引用,能够在JVM GC过程中当内存不足时,允许垃圾回收机制在需要释放内存时回收对象池中的对象,避免内存泄露的问题
PooledObject 是池化对象的接口定义,池化的对象都会封装在这里。DefaultPooledObject 是PooledObject 接口缺省实现类,PooledSoftReference 使用 SoftReference 封装了对象,供SoftReferenceObjectPool 使用。
2、对象池参数详解
查看对象池的参数配置,一种方式是直接查找代码或者官网文档中的说明去查看,另外介绍一种更为直观的方式,因为 Common Pool2 工具资源池的管理都接入到 JMX 中,所以可以通过如 Jconsole 等工具去查看暴露的属性和操作。
第一种方式:
查找对应配置类:
在 GenericObjectPoolConfig 和 BaseObjectPoolConfig 配置类对外提供的 setter 方法便是配置参数,并且代码里都有详细的注释说明。
第二种方式:
前提是你的应用暴露了 JMX 的端口和IP,允许外部连接。
JVM 参数如下所示:
-Dcom.sun.management.jmxremote
-Djava.rmi.server.hostname=IP地址
-Dcom.sun.management.jmxremote.port=端口
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
以上使用的是 Jconsole 工具类,点击
MBean
在左侧找到
org.apache.commons.pool2#GenericObjectPool#pool2
点击属性可以看到该类的所有属性信息,其中除包括核心的配置属性之外,还包括一些资源池的统计属性。
核心配置属性:
这些都是
重点关注
的属性,也是对外提供的可配置参数。
1)
minIdle
资源池确保最少空闲的连接数,默认值:0
2)
maxIdle
资源池允许最大空闲的连接数,默认值:8
3)
maxTotal
资源池中最大连接数,默认值:8
4)
maxWaitMillis
当资源池连接用尽后,调用者的最大等待时间,单位是毫秒,默认值:-1,建议设置合理的值
5)
testOnBorrow
向资源池借用连接时,是否做连接有效性检测,无效连接会被移除,默认值:false ,业务量很大时建议为false,因为会多一次ping的开销
6)
testOnCreate
创建新的资源连接后,是否做连接有效性检测,无效连接会被移除,默认值:false ,业务量很大时建议为false,因为会多一次ping的开销
7)
testOnReturn
向资源池归还连接时,是否做连接有效性检测,无效连接会被移除,默认值:false,业务量很大时建议为false,因为会多一次ping的开销
8)
testWhileIdle
是否开启空闲资源监测,默认值:false
9)
blockWhenExhausted
当资源池用尽后,调用者是否要等待。默认值:true,当为true时,maxWaitMillis参数才会生效,建议使用默认值
10)
lifo
资源池里放池对象的方式,
LIFO
Last In First Out 后进先出,true(默认值),表示放在空闲队列最前面,false:放在空闲队列最后面
空闲资源监测配置属性
当需要对空闲资源进行监测时,
testWhileIdle
参数开启后与下列几个参数组合完成监测任务。
1)
timeBetweenEvictionRunsMillis
空闲资源的检测周期,单位为毫秒,默认值:-1,表示不检测,建议设置一个合理的值,周期性运行监测任务
2)
minEvictableIdleTimeMillis
资源池中资源最小空闲时间,单位为毫秒,默认值:30分钟(1000
60L
30L),当达到该值后空闲资源将被移除,建议根据业务自身设定
3)
numTestsPerEvictionRun
做空闲资源检测时,每次的采样数,默认值:3,可根据自身应用连接数进行微调,如果
设置为 -1
,表示对所有连接做空闲监测
3、空闲资源监测源码剖析
在资源池初始化之后,有个空闲资源监测任务流程如下:
对应源代码:
创建资源池对象时,在构造函数中初始化配合和任务的。
this.internalPool
= new GenericObjectPool(factory, poolConfig);
public GenericObjectPool(final PooledObjectFactoryfactory,
final GenericObjectPoolConfig config) {
super(config, ONAME_BASE, config.getJmxNamePrefix());
if (factory == null) {
jmxUnregister(); // tidy up
throw new IllegalArgumentException("factory may not be null");
}
this.factory = factory;
// 创建空闲资源链表
idleObjects = new LinkedBlockingDeque>(config.getFairness());
// 初始化配置
setConfig(config);
// 开启资源监测任务
startEvictor(getTimeBetweenEvictionRunsMillis());
}
final void startEvictor(final long delay) {
synchronized (evictionLock) {
// 当资源池关闭时会触发,取消evictor任务
if (null != evictor) {
EvictionTimer.cancel(evictor, evictorShutdownTimeoutMillis, TimeUnit.MILLISECONDS);
evictor = null;
evictionIterator = null;
}
if (delay > 0) {
// 启动evictor任务
evictor = new Evictor();
// 开启定时任务
EvictionTimer.schedule(evictor, delay, delay);
}
}
}
Eviector 是个TimerTask,通过启用的调度器,每间隔
timeBetweenEvictionRunsMillis
运行一次。
class Evictor extends TimerTask {
@Override
public void run() {
final ClassLoader savedClassLoader =
Thread.currentThread().getContextClassLoader();
try {
...
// Evict from the pool
evict();
// Ensure min idle num
ensureMinIdle();
} finally {
// Restore the previous CCL
Thread.currentThread().setContextClassLoader(savedClassLoader);
}
}
}
evict() 移除方法源码:
@Override
public void evict() throws
Exception {
assertOpen();
if (idleObjects.size() > 0) {
PooledObjectunderTest = null;
// 获取清除策略
final EvictionPolicyevictionPolicy = getEvictionPolicy();
synchronized (evictionLock) {
final EvictionConfig evictionConfig = new EvictionConfig(
getMinEvictableIdleTimeMillis(),
getSoftMinEvictableIdleTimeMillis(),
getMinIdle());
final boolean testWhileIdle = getTestWhileIdle();
for (int i = 0, m = getNumTests(); i < m; i++) {
// ... 省略部分代码
// underTest 代表每一个资源
boolean evict;
evict = evictionPolicy.evict(evictionConfig, underTest,
idleObjects.size());
// evict为true,销毁对象
if (evict) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
} else {
// testWhileIdle为true校验资源有效性
if (testWhileIdle) {
boolean active = false;
try {
factory.activateObject(underTest);
active = true;
} catch (final Exception e) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
}
if (active) {
if (!factory.validateObject(underTest)) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
} else {
try {
factory.passivateObject(underTest);
} catch (final Exception e) {
destroy(underTest);
destroyedByEvictorCount.incrementAndGet();
}
}
}
}
//...
}
}
}
}
// ...
}
代码里的默认策略 evictionPolicy,由
org.apache.commons.pool2.impl.DefaultEvictionPolicy
提供默认实现。
// DefaultEvictionPolicy#evict()
@Override
public boolean evict(final EvictionConfig config, final PooledObjectunderTest,
final int idleCount) {
if ((config.getIdleSoftEvictTime() < underTest.getIdleTimeMillis() &&
config.getMinIdle() < idleCount) ||
config.getIdleEvictTime() < underTest.getIdleTimeMillis()) {
return true;
}
return false;
}
1)
当空闲资源列表大小超过 minIdle 最小空闲资源数时,并且资源配置的 idleSoftEvictTime 小于资源空闲时间,返回 true。
EvictionConfig 配置初始化时,idleSoftEvictTime 如果使用的默认值 -1 < 0,则赋予值为 Long.MAX_VALUE。
2)
当检测的资源空闲时间过期后,即大于资源池配置的最小空闲时间,返回true。表示这些资源处于空闲状态,该时间段内一直未被使用到。
以上两个满足其中任一条件,则会销毁资源对象。
ensureIdle() 方法源代码:
private void ensureIdle(final int idleCount, final boolean always) throws Exception {
if (idleCount < 1 || isClosed() || (!always && !idleObjects.hasTakeWaiters())) {
return;
}
// 资源池里保留idleCount(minIdle)最小资源数量
while (idleObjects.size() < idleCount) {
final PooledObjectp = create();
if (p == null) {
// Can't create objects, no reason to think another call to
// create will work. Give up.
break;
}
if (getLifo()) {
idleObjects.addFirst(p);
} else {
idleObjects.addLast(p);
}
}
if (isClosed()) {
// Pool closed while object was being added to idle objects.
// Make sure the returned object is destroyed rather than left
// in the idle object pool (which would effectively be a leak)
clear();
}
}
以上就是对线程池的基本原理和参数的分析。
4、线程池对象状态
线程池对象的状态定义在 PooledObjectState ,是个枚举类型,有以下值:
IDLE 处于空闲状态
ALLOCATED 被使用中
EVICTION 正在被Evictor驱逐器验证
VALIDATION 正在验证
INVALID 驱逐测试或验证失败并将被销毁
ABANDONED 被抛弃状态,对象取出后,很久未归还
RETURNING 归还到对象池中
一张图来了解下线程池状态机转换:
5、对象池初始化时机
思考个问题,资源池里对象什么时候初始化进去的?这里的资源池就是指上文图中的
idleObjects
空闲资源对象缓存列表。是在创建对象时还是归还对象时?
答案是
归还对象的时候
。
某些场景,启动后可能会出现超时现象,因为每次请求都会创建新的资源,这个过程会有一定的开销。
应用启动后我们可以提前做下线程池资源的预热,示例代码如下:
ListminIdleList = new ArrayList(jedisPoolConfig.getMinIdle());
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
Jedis jedis = null;
try {
jedis = pool.getResource();
minIdleList.add(jedis);
jedis.ping();
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
}
}
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
Jedis jedis = null;
try {
jedis = minIdleList.get(i);
jedis.close();
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
}
}
如果不了解原理,可能以为上面的预热代码不大对吧,怎么获取后又调用了
jedis.close()
呢?字面上理解是把资源关闭了嘛。
一起看下线程池资源归还对象的源码就明白了。
GenericObjectPool#returnObject() 归还对象方法源码:
// GenericObjectPool#returnObject() 归还方法
public void returnObject(final T obj) {
// allObjects是存储所有对象资源的地方
final PooledObjectp = allObjects.get(new IdentityWrapper(obj));
// ...
// 变更对象状态
synchronized(p) {
final PooledObjectState state = p.getState();
if (state != PooledObjectState.ALLOCATED) {
throw new IllegalStateException(
"Object has already been returned to this pool or is invalid");
}
p.markReturning(); // Keep from being marked abandoned
}
final long activeTime = p.getActiveTimeMillis();
// testOnReturn为true,返还时验证资源有效性
if (getTestOnReturn()) {
if (!factory.validateObject(p)) {
try {
destroy(p);
} catch (final Exception e) {
swallowException(e);
}
try {
ensureIdle(1, false);
} catch (final Exception e) {
swallowException(e);
}
updateStatsReturn(activeTime);
return
;
}
}
// ...
if (!p.deallocate()) {
throw new IllegalStateException(
"Object has already been returned to this pool or is invalid");
}
// 获取maxIdle,限制空闲资源保留的上限数量
final int maxIdleSave = getMaxIdle();
if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
try {
destroy(p);
} catch (final Exception e) {
swallowException(e);
}
} else {
// 重点在这里,如果没有超过maxIdle,则会将归还的对象添加到 idleObjects 中
if (getLifo()) {
idleObjects.addFirst(p);
} else {
idleObjects.addLast(p);
}
if (isClosed()) {
// Pool closed while object was being added to idle objects.
// Make sure the returned object is destroyed rather than left
// in the idle object pool (which would effectively be a leak)
clear();
}
}
updateStatsReturn(activeTime);
}
归还对象时,首先会变更对象状态从 ALLOCATED 到 RETURNING,如果 testOnReturn参数 为true,校验资源有效性(Jedis连接的有效性),如果无效,则调用 destroy() 方法销毁对象,当 maxIdle 未超过 idleObjects 资源列表大小时,则会将归还的对象添加到
idleObjects
中。
而在 borrorObject() 的借出对象方法中就是从
idleObjects#pollFirst()
获取对象的,没有的话就会去创建,对象最多不能超过 maxTotal 数量。
6、Jedis客户端线程池参数
我们了解完 Apache Common Pool2 框架的线程池原理之后,接下来看看 Jedis 里是如何包装的。
线程池里的参数都是基于
JedisPoolConfig
来构建的。
JedisPoolConfig Jedis资源池配置类默认构造函数:
public class JedisPoolConfig extends GenericObjectPoolConfig {
public JedisPoolConfig() {
// defaults to make your life with connection pool easier :)
setTestWhileIdle(true);
setMinEvictableIdleTimeMillis(60000);
setTimeBetweenEvictionRunsMillis(30000);
setNumTestsPerEvictionRun(-1);
}
}
JedisPoolConfig 继承了 GenericObjectPoolConfig,JedisPoolConfig 默认构造函数中会将 testWhileIdle 参数设置为true(默认为false),minEvictableIdleTimeMillis设置为60秒(默认为30分钟),timeBetweenEvictionRunsMillis设置为30秒(默认为-1),numTestsPerEvictionRun设置为-1(默认为3)。
每个30秒执行一次空闲资源监测,发现空闲资源超过60秒未被使用,从资源池中移除。
创建 JedisPoolConfig 对象后,设置一些参数:
// 创建 JedisPoolConfig 对象,设置参数
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig()
jedisPoolConfig.setMaxTotal(100);
jedisPoolConfig.setMaxIdle(60);
jedisPoolConfig.setMaxWaitMillis(1000
);
jedisPoolConfig.setTestOnBorrow(false);
jedisPoolConfig.setTestOnReturn(true);
JedisPool
管理了Jedis 的线程池:
// JedisPool 构造函数
public JedisPool(final GenericObjectPoolConfig poolConfig, final String host, int port,
int timeout, final String password) {
this(poolConfig, host, port, timeout, password, Protocol.DEFAULT_DATABASE, null);
}
public abstract class Pool<T> implements Closeable {
protected GenericObjectPoolinternalPool;
// 抽象 Pool 构造函数
public Pool(final GenericObjectPoolConfig poolConfig, PooledObjectFactoryfactory) {
initPool(poolConfig, factory);
}
}
既然猜测可能跟 Redis 服务端有关系,就需要从跟客户端的参数配置去分析下,是否会有所影响。
1、Redis客户端缓冲区满了
Redis有三种客户端缓冲区:
普通客户端缓冲区(normal):
用于接受普通的命令,例如get、set、mset、hgetall等
slave客户端缓冲区(slave):
用于同步master节点的写命令,完成复制。
发布订阅缓冲区(pubsub):
pubsub不是普通的命令,因此有单独的缓冲区。
Redis的客户端缓冲区配置具体格式是:
client-output-buffer-limit <class>limit>limit>seconds>
(1)class: 客户端类型:normal、 slave、 pubsub
(2)hard limit: 如果客户端使用的输出缓冲区大于hard limit,客户端会被立即关闭。
(3)soft limit和soft seconds: 如果客户端使用的输出缓冲区超过了soft limit并且持续了soft limit秒,客户端会被立即关闭
连接 Redis 查看 client-output-buffer-limit:
127.0.0.1:6379> config get client-output-buffer-limit
1) "client-output-buffer-limit"
2) "normal 0 0 0 slave 21474836480 16106127360 60 pubsub 33554432 8388608 60"
普通客户端缓冲区normal类型的class、hard limit、soft limit 都是 0,表示关闭缓冲区的限制。
如果缓冲期过小的,就可能会导致的
Unexpected end of stream
异常。
2、Redis服务器 timeout 设置不合理
Redis服务器会将超过
timeout
时间的闲置连接主动断开。
查看服务器的timeout配置:
127.0.0.1:6379> config get timeout
1) "timeout"
2) "600"
timeout 配置为 600 秒,同一个连接等待闲置 10 分钟后,发现还没有被使用,Redis 就将该连接中断掉了。
所以这里就会有个问题,这里的
timeout
时间是要与上文中的 Jedis 线程池里的
空闲资源监测任务
有关系的。
假设 JedisPoolConfig 里的
timeBetweenEvictionRunsMillis
不设置,会使用默认值 -1,不会启动 Evictor 空闲监测任务了。
当从资源池借出 Jedis 连接后,注意此时,如果过了 10 分钟,Redis 服务端已将这根连接给中断了。
而客户端还拿着这个 Jedis 连接去继续操作 set、get 之类的命令,就会出现
Unexpected end of stream
异常了。
示例演示:
为了方便演示,如下参数调整。
1)Redis服务器 timeout 初始化为 10秒
2)Java 测试代码如下所示
new Thread(new Runnable() {
public void run() {
for (int