多线程相关问答

阿里开发手册上写着关于线程创建的问题#

【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors返回的线程池对象的弊端如下:

1)CachedThreadPool 和 ScheduledThreadPool :
允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

2)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

Executors的四个线程池构造方法#

Executors.newCachedThreadPool();

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

Executors.newScheduledThreadPool(1);

1
2
3
4
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}

CachedThreadPool 和 ScheduledThreadPool :
允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

Executors.newSingleThreadExecutor();

1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

Executors.newFixedThreadPool();

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

ThreadPoolExecutor都有哪些参数#

核心线程数, 最大线程数,非核心线程空闲存活时间,阻塞队列,拒绝策略,有必要的话还有ThreadFactory线程工厂.

使用ThreadPoolExecutor创建线程池示例#

1
2
3
4
5
6
7
8
9
10
private static int corePoolSize = 5;
private static int maximumPoolSize = 100;
private static long keepAliveTime = 60L;
private static ExecutorService executorService = new ThreadPoolExecutor(
corePoolSize, // 核心线程
maximumPoolSize, // 最大线程数,在队列满的时候,进行扩容的最大容量 (core、max 可以更好的利用线程资源)
keepAliveTime, // 非核心线程数,空闲回收时间 (增加线程利用,减少线程重复创建)
TimeUnit.SECONDS, // 空闲回收时间的单位
new ArrayBlockingQueue(10), // 阻塞队列 (设置队列大小,很关键)
new ThreadPoolExecutor.AbortPolicy()); // 当队列已经满了,执行的处理

还有一个可传参数是 ThreadFactory threadFactory, 我们通常使用 Executors.defaultThreadFactory()

使用ThreadPoolExecutor的好处#

参数带来的好处, 线程池就是为了更好更充分的利用线程资源,线程资源是宝贵的

相比Executors创建线程,ThreadPoolExecutor 可定制化线程池,创建符合场景下的线程池。可以通过参数配置避免Executors的OOM

  1. corePoolSize, maximumPoolSize 弹性控制线程数量,可伸缩,可扩容可释放
  2. keepAliveTime, TimeUnit.SECONDS 设置后在回收前可让其他任务使用,减少重新创建线程的开销
  3. BlockingQueue, 设置队列大小,起到缓冲的作用
  4. 自定义拒绝策略

总结: 弹性线程数、增加空闲线程再利用、队列缓冲任务量、自定义拒绝策略

线程池原理#

把一个任务放入线程池的execute()函数中,线程池会为我们选择一个线程来执行我们提交的任务。在这个选择线程的过程中,如果线程池中线程数量小于corePoolSize,那么将创建新线程执行任务;当线程池数量大于等于corePoolSize并且小于maximumPoolSize,线程池会把任务放到阻塞队列workQueue中直到workQueue满了去创建新线程;当线程池线程数量等于maximumPoolSize并且workQueue满时会执行拒绝策略

四种拒绝策略#

AbortPolicy#

ThreadPoolExecutor中默认的拒绝策略就是AbortPolicy。直接抛出异常

1
2
3
4
5
6
7
8
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
CallerRunsPolicy#

CallerRunsPolicy在任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务。

1
2
3
4
5
6
7
8
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
DiscardPolicy#

这个策略的处理就更简单了,看一下实现就明白了:

1
2
3
4
5
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
DiscardOldestPolicy#

DiscardOldestPolicy策略的作用是,当任务呗拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去。

1
2
3
4
5
6
7
8
9
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}

读《自控力》

前言

看了本周”李自然说”讨论学习方法,里面有一句话非常喜欢,人和人的差距大于人和猪的差距。

这句话夸张的表达了人和人的差距会有多大,人和人理解问题的深度、处事的态度、说话的逻辑和好听程度,有趣指数,相信大家都想做人上人,百里挑一的人,不止是为了获得更多的财富,更是让自己的生活满意,做事情处事不惊,游刃有余。 今天开始阅读《自控力》,不是说这一本书能带我走向人生巅峰,只希望是个好的开始。

01 意志力是什么,为什么至关重要

###意志力

就是驾驭 “我不要”, “我要做”,”我想要” 这三种力量。

“我不要” : 抵制诱惑!

抵制甜甜圈,香烟,清仓大甩卖,或一夜情等等很诱人的东西,这些东西阻碍你实现你真正想要的,比如瘦身,好的肺,幸福的家庭等,如果你一面对就会无法抵抗,那就要提升意志力,提升“我不要”的能力。

这些东西可能会带来即时的快感,但只是因为真正美好的东西暂时离你远,因为时空的距离感,让你选择了当前的选项,但如果思维清晰,目标明确,就不要为眼前的小利益而放弃大目标。

比如你想换工作、换城市,考虑清楚之后,知道换了之后的结果是好的,但过程中有困难和安逸给你选,如果选了安逸,就是当前的诱惑赢了,如果选了困难,就是意志力的体现,对当前诱惑说出 “我不要” !

“我要做” :今日事今日毕 !

“明日复明日”,拖延症晚期患者有些事情可能拖到下辈子再做,是这部分意志力薄弱,我理解不可能每天真毕,但“做” 和 “拖”的差距是很大的,是“我要做”意志力的体现。

“我想要”: 真正要的是什么!

当我们在冲动的时候有时会觉得这就是自己想要的,想要玩游戏、想要喝酒、巧克力蛋糕、骂人等等,但一定得想清楚,你真正要的是什么样的身材,什么样的素养,什么样的生活,是否希望苗条、升职加薪、家庭美满,是否吃”巧克力蛋糕”, 吃多少,要在关键时刻遏制住一时冲动,明确自己的目标,强大的目标明确能力,是“我想要”意志力的体现。

小总结

个人理解意志力的体现肯定不是要让我们当机器人或当狗(加班狗、奋斗狗),而是像某口号一样,集中力量办大事,只是集中的是自己的力量,办的是“我想要”的事。

两个自我

大脑中有两个部分,一部分是控制自己,一部分是冲动的自己。冲动为原始的冲动,我们的本能; 控制自己的就是我们的前额皮质。

“两个自我发生分歧的时候,总会有一方击败另一方。决定放弃的一方并没有做错,只是双方觉得重要的东西不同而已。”

原始的本能冲动是我们必不可少的,没有欲望,人就会变得沮丧,没有恐惧,就没法保护自己,没有厌恶,就没法提高自己,我们不是要消灭冲动的自己,而是要学会利用冲动的自己,换句话就是在合适的时候冲动,合适的时候控制。

原始的冲动带着自己进步,而当冲动要带我们走向错误的时候,控制自己的能力需要体现。

自我意识

自我意识体现在做决定的时候,意识到要做自己的选择,生活才是自己的。

这里重点是意识到自己可以做的选择。一件事,我根据自己的想法做出选择,是自我意识,而我如果没思考,下意识走了简单舒适的选择时,就是自我意识的缺失,一种随波逐流,这样选的结果不一定不好,但不会一直是自己想要的。

做一个有自我的人

冥想

冥想通过坐着闭眼专注于”呼“ ”吸“, 锻炼自己的控制能力,注意力集中能力,意志力。

冥想我相信对应现实中的注意力集中能力,自控能力都会有一定提升。

今天进行了人生第一次冥想,虽然大部分时间都无法专注于呼吸,但我看到了挑战,当我能完全专注于呼吸的时候,那我的集中注意力的能力就会变得很强。

mark一篇完整冥想步骤文章

https://mp.weixin.qq.com/s/2nV2u66ScuLJEHEXcrHkMQ

innodb事务隔离级别、MVCC

隔离级别

未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据

提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)

可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读

串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

MVCC

InnoDB是一个多版本的存储引擎:为了支持事务的一些特性诸如并发和回滚,它保持着被修改行的旧版本信息。这些信息被存储在一个被叫做回滚段(rollback segment)的表空间中。InnoDB在回滚段中用这些信息来执行undo操作,以此支持事务回滚。它也用这些信息来构造行的更早的版本,以此支持一致性读(快照读)。

在内部,InnoDB为数据库中存储的每一行添加三个隐藏字段。

DB_TRX_ID:表明插入或者修改这一行的最后一个事务的事务标识符。如何查看行事务ID

DB_ROLL_PTR:指向回滚段中的一个undo log记录,如果行被修改了,那么这个undo log记录包含的信息必须先于行修改被重新修改。

DB_ROW_ID:单调递增的行ID。如果InnoDB自动生成了一个聚集索引,那么这个索引包含行ID值,否则DB_ROW_ID列不会出现在任何索引中。

锁类型

共享锁(读锁,S锁):若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放S锁。

排他锁(写锁,X锁):若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他事务不能再对A加作何类型的锁,直到T释放A上的X锁。

意向共享锁(IS锁):事务T在对表中数据对象加S锁前,首先需要对该表加IS(或更强的IX)锁。

意向排他锁(IX锁):事务T在对表中的数据对象加X锁前,首先需要对该表加IX锁。

意向锁补充
  • InnoDB 支持多粒度锁,特定场景下,行级锁可以与表级锁共存。
  • 意向锁之间互不排斥,但除了 IS 与 S 兼容外,意向锁会与 共享锁 / 排他锁 互斥。
  • IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
  • 意向锁在保证并发性的前提下,实现了行锁和表锁共存且满足事务隔离性的要求。

2pl 两阶段锁

两段锁协议,先随便加,最后commit才能一起释放.

GAP锁有何用

其实这个多出来的GAP锁,就是RR隔离级别,相对于RC隔离级别,不会出现幻读的关键。

确实,GAP锁锁住的位置,也不是记录本身,而是两条记录之间的GAP。

所谓幻读,就是同一个事务,连续做两次当前读 (例如:select * from t1 where id = 10 for update;),那么这两次当前读返回的是完全相同的记录 (记录数量一致,记录本身也一致),第二次的当前读,不会比第一次返回更多的记录 (幻象)。

如何保证两次当前读返回一致的记录,那就需要在第一次当前读与第二次当前读之间,其他的事务不会插入新的满足条件的记录并提交。为了实现这个功能,GAP锁应运而生

快照读 & 当前读

快照读:就是select

1
select * from table ….;

当前读:特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。

1
2
3
4
5
select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update ;
delete;

聊天讨论

全快照、全当前,不幻读, 先快照,再当前,幻读

「 球球: 其实产生幻读的原因,就是以为读走了mvcc的副本, 」


读如果也加gap锁,就真的不会幻读了,但代价太大

「 宏伟: 张三账户余额有10000元;要转账给李四9000元,转给王五5000元;这两次转账肯定有一次因余额不足转账失败;如果在 rr 下,可重复读, 两次update 张三都会成功? 」


这个如果两个事务并发,都先读的1w元快照读,然后第1个事务直接update张三减钱, commit, 第2个事务 接着update张三减钱,都会成功,除非
1 用数据库乐观锁
2 自己搞个分布式锁,将读写操作原子化
3 用kafka让同一用户操作串行化

敏: 交易相关的就不要快照读了, 交易要算钱,select的时候就要锁,不然这条数据之后未必可用

总结

RR级别下,通过MVCC, 解决了两个快照读的幻读问题,
通过next-key锁,解决了两个当前读的幻读问题,但是快照读后的当前读是会幻读的.

netty模型

定义

netty是一个高性能、异步事情驱动的网络通信框架
NIO IO多路复用 、事情驱动模型

1.设置主从reactor模式
2.指定IO类型
3.指定handler
4.绑定端口

netty优点

  1. API使用简单,开发门槛低;

  2. 功能强大,预置了多种编解码功能,支持多种主流协议;

  3. 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;

  4. 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优;

  5. 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;

  6. 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入;

  7. 经历了大规模的商业应用考验,质量得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它已经完全能够满足不同行业的商业应用了。

高性能三要素

  1. reactor模型
  2. IO多路复用
  3. 协议

reactor模型

单线程模型

多线程模型

主从线程模型

组件

Bootstrap/ServerBootstrap

顾名思义就是启动类,分别负责启动客户端和服务器端。这个类用来配置相关参数,比如设置EventLoopGroup,IO类型,handler等等,Netty的一切都从这里开始

EventLoopGroup

主从多线程Reactor模型,分别有两个线程池: EventLoopGroupA和EventLoopGroupB。一个用于接收请求,另一个用于处理IO操作。

一个EventLoopGroup就相当于一个线程池,而每一个EventLoop就是一个线程,当新的Channel被创建时(有新的请求进来),就会在EventLoopGroup里注册一下,同时会分配一个EventLoop给这个Channel,

从此开始直到这个Channel被销毁,这个Channel只能被它绑定的这个EventLoop执行,这也就是为什么Netty可以不用考虑并发的原因。

EventLoop是处理各个event的具体线程。除了处理IO读写等event外,EventLoop还需要进行系统任务和定时任务进行执行

Channel

Netty的Channel接口所提供的的API,大大降低了直接使用Socket类的复杂性

ChannelPipeline & ChannelHander

Netty采用了一种叫做数据流(data flow)的处理机制,类似于Unix中的管道。即每一个Channel都有一个自己的ChannelPipeline,每一个pipeline里会有多个ChannelHandler。数据会像水流一样依次通过每一个handler被逐一处理。
流处理是双向混合的,分为Inbound和Outbound, 分别对应request和response。

这个handler被分成两类:ChannelOutboundHandler和ChannelInboundHandler。当服务器处理进来的请求时,则只会调用实现了ChannelInboundHandler的handler;当服务器返回信息给客户端时,则只会调用实现了ChannelOutboundHandler的handler

Encoders & Decoders

我们在解析处理请求时通常需要对数据格式进行转换,比如把字节变成对象,或者把对象转换为字节。针对这种常见的场景,Netty提供了编码和解码的接口:MessageToByteEncoder和ByteToMessageEncoder。

其实两个抽象类分别继承了ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter,说白了,使用起来和普通的handler没什么区别。自己写的类只要重写decode()或者encode()方法对数据进行处理即可

策略模式

tair & redis 的选择

tair & redis 的选择

  1. 延迟敏感程度, 延迟敏感,说什么也得redis
  2. 数据量大的话,数据量超过100GB全内存太浪费资源,延迟没有那么敏感,使用tair
  3. 使用复杂数据结构 redis
  4. 容忍数据丢失

redis单线程还这么快

为什么Redis使用单线程模型会达到每秒万级别的处理能力呢?可以将其归结为三点使Redis具有很高的吞吐量?

1. 纯内存访问

Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础。

纯内存数据库,如果只是简单的 key-value,内存不是瓶颈。一般情况下,hash 查找可以达到每秒数百万次的数量级。瓶颈在于网络 IO 上。每次请求需要通过网络把请求发送到 redis 所在的机器,然后等待 redis 返回数据。时间大部分消耗在网络传输中。如果把 redis 和客户端放在同一台机器,网络延迟会更小,一般情况下可以打到 60000 次每秒甚至更高,取决于机器性能。

2. I/O多路复用、事件驱动模型

旨在解决IO的问题。多路I/O复用模型是非阻塞IO,内部实现采用epoll和自己实现的事件分离框架。

其利用select、poll、epoll 可以同时检测多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。

“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗)。

总结就是Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间,如下图所示。

3. 单线程避免了线程切换和竞态产生的消耗。

线程模型

主线程

其它

  1. Redis会fork得到的子进程,用来处理RDB持久化以及AOF持久化等任务

  2. 一组异步任务处理线程,即Redis异步化组件——BIO组件

BIO组件目前包括三个线程,分别处理三种类型的任务:
1)文件句柄关闭任务
2)AOF持久化任务
3)空间懒释放

持久化

Redis 提供了两种持久化策略

RDB 持久化机制,会在一段时间内生成指定时间点的数据集快照(snapshot)

AOF 持久化机制,记录 server 端收到的每一条写命令,当 server 重启时会进行重放以此来重建之前的数据集。AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加(append)到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写(rewrite) ,使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。

如果你仅使用 Redis 作为缓存加速访问,你可以关闭这两个持久化设置

你也可以同时开启这两个持久化设置,但是在这种情况下,Redis 重启时会使用 AOF 文件来重建数据集,因为 AOF 文件保存的数据往往更加完整

单线程利弊

单线程模型能带来几个好处:
第一,单线程可以简化数据结构和算法的实现。并发数据结构实现不但困难而且开发测试比较麻烦。
第二,单线程避免了线程切换和竞态产生的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。

但是单线程会有一个问题:
对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,对于Redis这种高性能的服务来说是致命的,所以Redis是面向快速执行场景的数据库。

BIO NIO AIO 阻塞 非阻塞

BIO:同步阻塞IO(面向流的)

特点:一个连接建立一个线程,连接如果没有IO请求时,则会浪费线程资源开销,可以通过线程池技术来改善

适用场景:连接数小,并发数小,架构固定(java1.4之前唯一的IO)

NIO:同步非阻塞(面向Buffer的)

特点:客户的发送的连接请求会注册到多路复用器上,多路复用器轮询到连接存在有效请求时才会启动一个线程来进行处理
适用场景:连接数目多,且连接比较短的架构,比如聊天服务器(java1.4开始支持)

AIO:异步非阻塞

特点:针对客户端连接发出的IO请求,会由OS先完成IO操作后,在通知服务器启动线程进行处理
适用场景:连接数目多,连接比较长,比如相册服务器,充分调用OS参与并发操作(java1.7开始支持)

同步、异步(关注点:消息通信机制、IO请求发送)

同步:发送一个请求后,会不断的去轮询、等待请求结果返回后再发送下一个请求,可以避免死锁,脏读的发生

异步:发送一个请求后,不需要等待结果返回也可以发送下一个请求,可以提高效率,保证并发,OS执行完成后会通过回调函数通知应用程序获取结果

阻塞、非阻塞(关注点:程序在等待调用结果时的状态、IO操作结果获取)

阻塞:程序在等待请求结果的时候,线程会被挂起,调用线程只有在得到结果之后才会返回

非阻塞:虽然不能立马得到结果,但是该调用不会阻塞当前线程,此线程还可以干其他事

NIO IO多路复用

NIO是概念,IO多路复用是一套方案

当某个连接发送请求到服务器,服务器把这个连接请求当作一个请求“事件”,并把这个“事件”分配给相应的函数处理。我们可以把这个处理函数放到线程中去执行,执行完就把线程归还,这样一个线程就可以异步的处理多个线程

而阻塞式 I/O 的线程的大部分时间都被浪费在等待请求上了

对于同步非阻塞,一个线程可以执行多个连接中的请求,而同步阻塞则时一个线程对应一个连接,所以阻塞会很严重, 线程浪费严重

jvm相关问答

锁相关问答

用过哪些锁#

锁分类

synchronized 偏向、轻量、重量锁
reentryLock
condition
mysql乐观锁version

synchronized & reentryLock#

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×