开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Mysql高并发解决方案 Mysql高并发解决方案

发表于 2021-03-19

Mysql高并发解决方案

前言

随着近些年来分布式的应用,其伴随而来的是系统的数据量也越来越大,为了可以提升系统的整体性能,我们对以Mysql为代表的关系型数据库也提出了“分布式”的要求。

MyCat入门可参考我的博客:MyCat入门

使用HaProxy实现Mycat的高可用可参考我的博客: 使用HaProxy实现Mycat的高可用

正文

Mysql高并发解决方案

Mysql单节点的性能极限:

  • mysql默认的最大连接数为151,上限为100000。
  • 如果借助InnoDB的行级锁来实现“减库存”的话:由于数据库中InnoDB的行锁针对同一行数据的更新操作是串行执行的,那么某一个线程在未释放锁之前,其余的线程将会全部阻塞在队列中等待拿锁,并发越高时,等待的线程也就会越多,这会严重影响数据库的TPS,从而导致RT线性上升,最终可能引发系统出现雪崩。

由于Mysql单节点存在性能瓶颈,所以要进一步通过增加节点的方式来提升并发量,于是可以采用主从复制进而实现读写分离,缓解单节点的IO压力。

MySQL主从复制实现读写分离

事务的两阶段提交

Mysql使用两阶段提交解决故障恢复、主从、主备复制时,保持了数据的一致性,即实现binlog和InnoDB redo log的数据一致性。

  • 阶段一:redo log 写盘,InnoDB 事务进入 prepare 状态
  • 阶段二:如果前面prepare成功,binlog 写盘,那么再继续将事务日志(redo log)持久化到binlog,如果持久化成功,那么InnoDB事务 则进入commit 状态

每个事务binlog的末尾,会记录一个 XID event,标志着事务是否提交成功。

MySql数据库主从复制的方式

主从复制是基于日志(binlog)主要有以下四种:

  • 异步复制(默认) :MySQL 默认的复制策略,Master处理事务过程中,将其写入Binlog就会通知Dump thread线程处理,然后完成事务的提交,不会关心是否成功发送到任意一个slave中。
  • 半同步复制:Master处理事务过程中,提交完事务后,必须等至少一个Slave将收到的binlog写入relay log返回ack(提交后才同步)才能继续执行处理用户的事务。
  • 增强半同步复制:半同步的问题是因为等待ACK的点是Commit之后,此时Master已经完成数据变更,用户已经可以看到最新数据,当Binlog还未同步到Slave时,发生主从切换,那么此时从库是没有这个最新数据的,用户又看到老数据。增强半同步将等待ACK的点放在提交Commit之前(同步后再提交),此时数据还未被提交,外界看不到数据变更,此时如果发送主从切换,新库依然还是老数据,不存在数据不一致的问题。
  • 组复制:MySQL在引擎层完成Prepare操作写Redo日志之后,会被MySQL的预设Hook拦截进入MGR层,MGR层将事务信息打包通过Paxos协议发送到全部节点上,只要集群中过半节点回复ACK(半数写成功),那么将告诉所有节点数据包同步成功,然后每个节点开始自己认证(certify)通过就开始写Binlog,提交事务或者写relay log,数据同步,如果认证不通过则rollback。

MySql主从复制的涉及三个行为:

  1. 数据存储:Master将数据改变记录到二进制日志(binary log)中,也就是配置文件log-bin指定的文件,这些记录叫做二进制日志事件(binary log events);
  2. 数据传输:Slave 通过 I/O 线程读取 Master 中的 binary log events 并写入到它的中继日志(relay log);
  3. 数据重放:Slave 重做中继日志中的事件,把中继日志中的事件信息一条一条的在本地执行一次,完成数据在本地的存储,从而实现将改变反映到它自己的数据。

分表

水平分表的特点:

  • 将一张表拆分成多张表,每张表的结构是一样(使用的是同一个FRM文件,但每一子表存放在在同一个磁盘中一个独立MYD文件和一个独立的MYI文件,MRG文件记录分表信息);
  • 利用主表作为查询的接口(没有数据文件),表一表二作为存储数据的实际表单 ;
  • 决定数据放在那一张实际的表,往往采用对ID取模(即求余)或者对业务主键hash运算,确保有关联性的数据在同一个子表中;
  • merge来分表,是最简单的一种方式实现分区;

以对ID取模的方式举例:

1
2
3
4
5
6
java复制代码#向表一插入数据:
insert into tb_member1(id,name,sex) select id,name,sex from member where id%2=0; //这里区分表一表二
#向表二插入数据:
insert into tb_member2(id,name,sex) select id,name,sex from member where id%2=1;
#查看一下主表的数据:
select * from tb_member;

分区

水平分区的特点:

  • 分区一张表的数据分成N多个区块,这些区块可以在同一个磁盘上,也可以在不同的磁盘上,但实际上还是同一张表,只是把MYD、MYI同时切分了很多份,Par文件会记录分区信息;
  • 分区使用partition by比较容易实现,水平分区支持range分区、list分区、hash分区等方式;
  • 采用那种方式进行分区取决于业务,比如以权益中心为例:通常来说我们希望同一个用户的数据会存在同一个物理表中,可以采用以手机号进行hash的方式进行分区;
1
2
3
4
java复制代码#向表插入数据,分区相对于分表是无感知的:
insert into member(id,name,sex) values (1,’luo’,’男’);
#查看一下主表的数据:
select * from tb_member;

分表分库

分表分库是一种分区+分表结合起来的方式:

  • 原本存储于一个库的数据分块存储到多个库上;
  • 把原本存储于一个表的数据分块存储到多个表上;
  • 分库可以解决单台服务器性能不够,或者成本过高问题;
  • 分表分库可以是水平分表分库,也可以是垂直分表分库;

分表分库常见的问题

跨库join

当数据分到不同的库上,一般是禁止跨库join的,一般会采用以下方式:

  • 全局表:所谓全局表,就是有可能系统中所有模块都可能会依赖到的一些表。比较类似我们理解的“数据字典”。为了避免跨库join查询,我们可以将这类表在其他每个数据库中均保存一份;
  • 字段冗余:比如“订单表”中保存“卖家Id”的同时,将卖家的“Name”字段也冗余,这样查询订单详情的时候就不需要再去查询“卖家用户表”;
  • 数据同步:使用ETL工具(数据迁移)做表数据同步,定时A库中的tab_a表和B库中tbl_b有关联,可以定时将指定的表做同步;

跨库事务

由于数据存储到了不同的库上,数据库事务管理出现了困难。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价,所以实际上我们只会对历史数据进行分表分库拆分。

跨库事务

由于数据存储到了不同的库上,数据库事务管理出现了困难。跨库事务的核心是保障一致性问题,主要有两种方式:

  • 分布式事务:分布式事务能最大限度保证了数据库操作的原子性,但是成本过高,一般不采用。
  • 最终一致性:采用本地事务,本地落地,补偿发送。

如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价,所以实际上我们只会对历史数据进行分表分库拆分。

跨库分页、排序、函数问题

跨节点多库进行查询时,会出现limit分页、order by排序等问题:

  • 当排序字段就是分片字段时:通过分片规则就比较容易定位到指定的分片;
  • 当排序字段非分片字段时:需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序,最终返回给用户。

唯一全局主键问题

在分库分表环境中,由于表中数据同时存在不同数据库中,主键值平时使用的自增长将无用武之地,某个分区数据库自生成的ID无法保证全局唯一,常见有两种方案:

  • UUID:主键是最简单的方案,本地生成,性能高,没有网络耗时。但缺点也很明显,由于UUID非常长,会占用大量的存储空间;另外,作为主键建立索引和基于索引进行查询时都会存在性能问题,在InnoDB下,UUID的无序性会引起数据位置频繁变动,导致分页。
  • snowflake雪花算法:解决了分布式系统生成全局ID的需求,生成64位的Long型数字,生成的ID整体上按时间趋势递增;

分表分库的业务场景

对数据库进行分表分库时可参考一下原则:

  • 能不切分尽量不要切分:不到万不得已不用轻易使用分表分库,避免“过度设计”和“过早优化”。
  • 数据量过大,正常运维影响业务访问:单表太大,备份时需要大量的磁盘IO和网络IO等。
  • 业务发展,需要对某些字段垂直拆分:比如我们有时候会把用户登陆这部分从用户表独立出来,做成登陆表。
  • 数据量快速增长:如果由于数据量的增长,性能接近瓶颈,就需要考虑水平切分,要根据业务选择合适的规则切分,如对ID取模或者对手机号做hash运算等。
  • 安全型和可用性:将数据切分到不同的数据库可以降低数据库宕机对业务的影响。

MyCat实现Mysql的读写分离本质上其实就是分表分库的实践:具体可参考我的博客:MyCat入门

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【实战问题】-- 并发的时候分布式锁setnx细节

发表于 2021-03-19

前面讲解到实战问题】– 设计礼品领取的架构设计以及多次领取现象解决?,如果出现网络延迟的情况下,多个请求阻塞,那么恶意攻击就可以全部请求领取接口成功,而针对这种做法,我们使用setnx来解决,确保只有一个请求可以进入接口请求。

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
28
29
30
java复制代码    public String receiveGitf(int activityId,int giftId,String uid){
// isExist判断活动是否存在,内部包括redis和数据库请求,省略
if(isActivityExist(activityId,giftId)){
// 活动和礼品有效,判断是否领取过
if(!userReceived(uid,activityId,giftId)){
// 没有领取过,调用C系统
try {
// setnx
if(redis.setnx("uid_activityId_giftId")){
boolean receivedResult = Http.getMethod(C_Client.class, "distributeGift");
if(receivedResult){
// 领取成功更新mysql
updateMysql(uid,activityId,giftId);
}else{
// 领取成功更新redis
deleteRedis(uid,activityId,giftId);
return "已经领过/领取失败";
}
}else{
return "已经领过/领取失败";
}
}catch (Exception e){
// 记录日志
logHelper.log(e);
return "调用领券系统失败,请重试";
}
}
}
return "领取失败,活动不存在";
}

下面,我们就专门讲解一下setnx,setnx可以用作分布式锁,但是这个场景并不是分布式锁的一个较好的实践,因为每个用户的key都是不一样的,我们主要是防止同一个用户恶意领取,setnx本身是一个原子操作,可以保证多个线程只有一个能拿到锁,能返回true,其他的都会返回false。

但是上面的做法,没有设置过期时间,在生产上一般是不可以这么使用。不设置过期时间的key多了之后,redis服务器很容易内存打满,这时候不知道哪些是强制依赖的,只能扩容,从代码层面去清理,如果直接清理不常用的,也很难保证不出事。(基本不允许这么干,除非是基础数据,跟着服务器启动,写入redis的,不会变更的,比如城市数据,国家数据等等,当然,这些也可以考虑在本地内存中实现)

如果在上面的代码中,加入超时时间,假设是一个月或者半年,流程变成这样:

设置key的超时时间使用expire,但是这样还有缺陷么?

在redis 2.6.12之前,setnx和expire都不是原子操作,也就是很有可能在setnx成功之后,redis当季,expire设置失败,也就不会有超时时间了。虽然这个影响在当前业务不是很大,但是还是一个小缺陷。

Redis2.6.12以上版本,可以用set获取锁,set包含setnx和expire,实现了原子操作。也就是两步要么一起成功,要么一起失败。

除此之外,上面的流程可能还存在的一个问题,是请求C服务的时候出现超时,然后删除key,恰好这个时候redis有问题,删除失败了,这个key就永远存在了。表现在业务上,就是A用户点击了领取,领取失败了,但是后面再怎么点,都是已经领取的状态了。

那这种现象怎么优化呢?

这种情况,其实已经是很少见的情况,按照我们当前的业务场景也看,就是当前的用户,redis记录了它已经领取过了,但是由于接口的失败,成功之后还没将mysql/其他数据库更新,两个数据库不一致了。

我能想到的一个方法,就是再删除失败的时候,告警,并且将业务相关的数据记录下来,比如key,uid等等,针对这部分数据,做一次补发,或者手动删除key。

或者,启动一个定时任务或者lua脚本,去判定redis和数据库不一致的情况,但是切记不要全部查询,应该是隔一段时间,查询最后增加的部分,做一个校验以及相应的处理。枚举key是十分耗时的操作!!!

setnx 除了解决上面的问题,还可以应用在解决缓存击穿的问题上。

譬如现在有热点数据,不仅在mysql数据库存储了,还在redis中存了一份缓存,那么如果有一个时间点,缓存失效了,这时候,大量的请求打过来,同时到达,缓存拿不到数据,都去数据库取数据,假设数据库操作比较耗时,那么压力全都在数据库服务器上了。

这个时候所有的请求都去更新数据,明显是不合适的,应该是使用分布式锁,让一个线程去请求mysql一次即可。但是为了避免死锁的情况,如果超时,得及时额外释放锁,要不可能请求mysql都失败了,其他线程又拿不到锁,那么数据就会一直为null了。

可以使用以下的命令:

1
shell复制代码SETNX lock.foo <current Unix time + lock timeout + 1>

关于这个场景下的setnx先讲到这里,后面再讲讲分布式锁相关的知识。

【刷题笔记】
Github仓库地址:github.com/Damaer/code…

笔记地址:damaer.github.io/codeSolutio…

【作者简介】:

秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。

2020年我写了什么?

开源刷题笔记

平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 多线程 迟来的 Future 一 Futu

发表于 2021-03-18

写了这么多终于来到了future 这里, 这个题目真的和它很贴切 , 我们之前在线程池已经多次见到这个对象了 , 这一篇我们重点看一看他

一 . Future 是什么

1.3 Future Task 简述

作用 : future 可以用于异步获取多线程任务结果 , Callable 用于产生结果,Future 用于获取结果
流程 : 流程类似于叫好等餐 , 等餐是花费时间的过程,但是不妨碍我们叫号

  • 当 Future 进行 submit 开始 , 业务处理已经在多线程中开始 , 而 Get 即从多线程中获取数据
  • 当 Get 获取时业务还未处理完 , 当前线程会阻塞 , 直到业务处理完成 . 所以需要注意 future 的任务安排

使用 future 会有以下效果:

  • 1 启动多线程任务
  • 2 处理其他事情
  • 3 收集多线程任务结果

Future 对应的方法 :

  • cancel(boolean) : 取消操作
  • get() : 获取结果
  • get(long,TimeUtil) : 指定时间获取
  • isCancelled() : 该任务是否在完成之前被取消
  • isDone() :判断是否有结果

Future 接口的作用就是先生成一个 Future 对象 ,将具体的运行放入future 对象中 ,最终通过future 对象的 get 方法来获取最终的结果

1.2 Future Task

FutureTask 表示一个可以取消的异步运算 ,提供了Future 完整的流程

  • 它有启动和取消运算、查询运算是否完成和取回运算结果等方法。
  • 只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞

我们关注一下以下源码细节点 :

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
java复制代码
// Node 1 : 实现了 RunnableFuture

// ------------------------------------

// Node 2 : 提供了7种状态
private static final int NEW = 0; // 新建
private static final int COMPLETING = 1; // 完成
private static final int NORMAL = 2; // 正常
private static final int EXCEPTIONAL = 3; // 异常
private static final int CANCELLED = 4; // 取消
private static final int INTERRUPTING = 5; // 中断(中)
private static final int INTERRUPTED = 6; // 打断

// 过程的流转 :
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED

// ------------------------------------

// Node 3 : 内部属性
// 底层Callable 对象
private Callable<V> callable;
// 输出对象
private Object outcome;
// 运行线程
private volatile Thread runner;
// 等待线程的Treiber堆栈
private volatile WaitNode waiters;

// ------------------------------------

// Node 4 : 内部方法
// 方法一 : 获取参数
V report(int s) // 为已完成的任务返回结果或抛出异常
1. Object x = outcome;
2. return (V)x;
// 注意 : 状态为 CANCELLED 时会抛出异常 CancellationException

// 方法二 : 取消
public boolean cancel(boolean mayInterruptIfRunning) // 取消
1. UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
// 这里神奇的做了一个CAS操作, 判断当前的状态
2. Thread t = runner; + t.interrupt();
// 打断线程
3. UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
// CAS 方式修改状态

// 方法三 : get 类型
public V get() throws
public V get(long timeout, TimeUnit unit)
1. if (s <= COMPLETING &&(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
// 核心代码就是关注是否完成


// 方法四 : run
Step 1 : CAS 操作状态
Step 2 : 准备一个 Callable<V> c = callable;
Step 3 : state == NEW 后 , result = c.call();
// 此时阻塞等待
Step 4 : 设置结果 : set(result);
Step 5 : 当然是修改状态啦
Over !!!

// 方法五 : runAndReset
// 在不设置结果的情况下执行计算,然后将这个future重置为初始状态 (其实主要是结尾修改了状态)

核心 : return ran && s == NEW;


// 方法五 : finishCompletion
// 删除并通知所有等待的线程,调用done(),并使callable为空

// 2个for 循环保证执行
for (WaitNode q; (q = waiters) != null;) {
// CAS 保证操作的准确性
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
for (;;) {
Thread t = q.thread;
if (t != null) {
q.thread = null;
// 提供许可
LockSupport.unpark(t);
}
WaitNode next = q.next;
if (next == null)
break;
q.next = null; // unlink to help gc
q = next;
}
break;
}
}

二 . Future 使用

Future 使用其实比较简单 , 发起等待即可 , 但是注意Future 是会阻塞主线程的

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
java复制代码public class FutureService extends AbstractService implements ApplicationRunner, Callable<String> {

private Logger logger = LoggerFactory.getLogger(this.getClass());

private static Long startTime;
private static Long endTime;

private String salt;
private Integer sleepNum;

public FutureService() {
}

public FutureService(String salt, Integer sleepNum) {
this.salt = salt;
this.sleepNum = sleepNum;
}

@Override
public String call() throws Exception {

logger.info("------> 业务逻辑开始执行 :{} <-------", salt);
StringBuffer sb = new StringBuffer();
for (int i = 0; i < sleepNum; i++) {
sb.append(salt);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {

}
}
endTime = System.currentTimeMillis();
logger.info("------> {} - 业务执行完成 :{} <-------", salt, sb.toString());
getTime(startTime, endTime);
return sb.toString();
}


@Override
public void run(ApplicationArguments args) throws Exception {
logger.info("------> 创建一个初始连接池 <-------");
ExecutorService executor = Executors.newFixedThreadPool(3);

logger.info("------> 开始业务一 future - a : <-------");
FutureTask<String> future = new FutureTask<String>(new FutureService("a", 10));
startTime = System.currentTimeMillis();
executor.submit(future);
logger.info("------> 业务一请求完毕!主线程执行 <-------");

logger.info("------> 开始业务二 future - b : <-------");
FutureTask<String> future2 = new FutureTask<String>(new FutureService("b", 5));
startTime = System.currentTimeMillis();
executor.submit(future2);
logger.info("------> 业务三请求完毕!主线程执行 <-------");

logger.info("------> 开始业务三 future - c : <-------");
FutureTask<String> future3 = new FutureTask<String>(new FutureService("c", 3));
startTime = System.currentTimeMillis();
executor.submit(future3);
logger.info("------> 业务三请求完毕!主线程执行 <-------");

logger.info("------> future2 数据处理完成:{} <-------", future2.get());
logger.info("------> 2-1 测试主线程是否阻塞 <-------");
logger.info("------> future1 数据处理完成:{} <-------", future.get());
logger.info("------> 1-3 测试主线程是否阻塞 <-------");
logger.info("------> future3 数据处理完成:{} <-------", future3.get());
}
}

19.839 [ main] this is run <-------
19.839 [ main] 开始业务一 future - a : <-------
19.839 [ main] 业务一请求完毕!主线程执行 <-------
19.839 [ main] 开始业务二 future - b : <-------
19.840 [ main] 业务三请求完毕!主线程执行 <-------
19.840 [ main] 开始业务三 future - c : <-------
19.840 [ main] 业务三请求完毕!主线程执行 <-------
19.840 [pool-4-thread-2] 业务逻辑开始执行 :b <-------
19.840 [pool-4-thread-1] 业务逻辑开始执行 :a <-------
19.840 [pool-4-thread-3] 业务逻辑开始执行 :c <-------
22.843 [pool-4-thread-3] c - 业务执行完成 :ccc <-------
22.843 [pool-4-thread-3] time is :3.0 <-------
24.844 [pool-4-thread-2] b - 业务执行完成 :bbbbb <-------
24.844 [pool-4-thread-2] time is :5.0 <-------
24.844 [ main] future2 数据处理完成:bbbbb <-------
24.844 [ main] 2-1 测试主线程是否阻塞 <-------
29.847 [pool-4-thread-1] a - 业务执行完成 :aaaaaaaaaa <-------
29.847 [pool-4-thread-1] time is :10.0 <-------
29.847 [ main] future1 数据处理完成:aaaaaaaaaa <-------
29.847 [ main] 1-3 测试主线程是否阻塞 <-------
29.847 [ main] future3 数据处理完成:ccc <-------

// 流程 :
// Main 线程中 , 哭有看到 abc 时顺序执行的 , 从 submit 开始 , 开始多线程执行 (所以顺序不再固定, 变成了 bac)
// 从多线程里面看 , c > b > a 执行完成
// 当 b 业务完成后 , 因为main 一直阻塞到 futurb.get 的阶段 , 所以B future 获取值 , main 线程遇到 a future 继续阻塞
// 当 a future get 完成后 , c 才能get

// 总结 :
> future submit 多线程执行
> future get 会阻塞主线程等待 , 当 get 时 , 多线程才会把数据提供出来

三 . Future 问答

按照线程池最常见的用法 , 我们通过 executor.submit 时会返回一个 Future 对象 ,然后通过 Future 对象获取

First : 我们以这个方法去推理
ExecutorService executor = Executors.newFixedThreadPool(1);
Future future = executor.submit(A Callable Object);

问题一 : Future 底层基于什么 ?

  • Step 1 : 当通过 submit 调用的时候 , 底层会调用 :
    return new FutureTask<T>(runnable, value);
  • Step 2 : 在外层会被提升为父类 RunnableFuture , 在返回的时候又会被提成 Future
    RunnableFuture<T> ftask = newTaskFor(task, result);

总结 : 所以 , 底层的实现类主要可以看成 FutureTask , 而task 实际上可以算 Runnable 的实现类

问题二 : Future 怎么运行 ?

Future 运行主要基于 run()

  1. 通过调用 callable.call() 完成
  2. 如果call执行成功,则通过set方法保存结果 ,将 result 保存到 outcome;
  3. 如果call执行有异常,则通过setException保存异常;

问题三 : future 回调基于什么 – get(long,TimeUtil) ?
通过调用 report(s) 完成调用

问题四 : future 阻塞的方式 ?

当判断未完成时 , 会调用 awaitDone 等待 , 具体的逻辑以后分析

1
2
3
java复制代码if (s <= COMPLETING){
s = awaitDone(false, 0L);
}

awaitDone 中主要做了以下几件事 :

  1. 如果主线程被中断,则抛出中断异常;
  2. 判断FutureTask当前的state,如果大于COMPLETING,说明任务已经执行完成,则直接返回;
  3. 如果当前state等于COMPLETING,说明任务已经执行完,这时主线程只需通过yield方法让出cpu资源,等待state变成NORMAL;
  4. 通过WaitNode类封装当前线程,并通过UNSAFE添加到waiters链表;
  5. 最终通过LockSupport的park或parkNanos挂起线程;

问题五 : future 怎么取消 ?

  • cancel(boolean mayInterruptIfRunning) 中 t.interrupt()
  • 并且 UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); 修改状态

问题六 : future 怎么判断取消和结果 isCancelled – isDone ?

return state >= CANCELLED;

四 . Future 与 Callable

这个需要从 ExecutorService.submit() 来看 , ExecutorService 有2个主要的 submit 方法 , 不论是 Callable , 还是Runnable , 通过返回值就不难发现 ,其最终都变成了一个 Future

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码<T> Future<T> submit(Callable<T> task);
- RunnableFuture<T> ftask = newTaskFor(task);
- return new ForkJoinTask.AdaptedCallable<T>(callable);
- execute(ftask);

<T> Future<T> submit(Runnable task, T result);
- RunnableFuture<T> ftask = newTaskFor(task, result);
- execute(ftask);

// 例如这里可以直接将 Callable 作为参数传进去 :
Future<String> future = executor.submit(createCallable());
public Callable createCallable() {
Callable<Module> call = new Callable<Module>() {
public Module call() throws Exception {
// .....
}
};
return call;
}

五 . 衍生用法 ScheduledFutureTask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码// ScheduledFutureTask 简介
• time:任务执行时间;
• period:任务周期执行间隔;
• sequenceNumber:自增的任务序号。

// 执行顺序 : 在等待队列里调度不再按照FIFO,而是按照执行时间,谁即将执行,谁就排在前面。
M- getDelay(TimeUnit unit)
M- int compareTo(Delayed other)


// ScheduledFutureTask 主要在 ScheduledThreadPoolExecutor中


// Node 1 : ScheduledThreadPoolExecutor内部类 ScheduledFutureTask
private class ScheduledFutureTask<V>
extends FutureTask<V> implements RunnableScheduledFuture<V>

核心代码参考线程池这篇文档

致谢 :

多线程集合

芋道源码

死磕系列

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

超高性能发号器框架——彻底解决雪花算法问题 Butterfl

发表于 2021-03-18

Butterfly 简介

Butterfly(蝴蝶)是一个超高性能的发号器框架。框架通过引入多种新的方案不仅解决了雪花算法存在的所有问题,而且还能够提供比雪花算法更高的性能。在单机版QPS理论值为51.2(w/s)这种情况下,新的方案在一些机器上可达 1200(w/s) 甚至更高。
起名Butterfly是用世界上没有完全相同的蝴蝶翅膀来表示该算法的唯一性。

雪花算法是twitter提出的分布式id生成器方案,但是有三个问题,其中前两个问题在业内很常见:

  • 时间回拨问题
  • 机器id的分配和回收问题
  • 机器id的上限问题

其中业内针对前两个问题都有个自己的解决方式,但是都不是很完美,或者说没有完全解决。我们这里从新的思路出发,通过改造雪花算法以及其他相关方式彻底解决了以上的三个问题。
该方案算是对雪花算法比较完美的一种实现方式。方案请见方案介绍

新的方案

对于以上三个问题,我们这里简述下我们的方案。

  1. 时间回拨问题

这里采用新的方案:大概思路是:启动时间戳采用的是“历史时间”,每次请求只增序列值,序列值增满,然后“历史时间”增1,序列值重新计算。
2. 机器id分配和回收

这里机器id分配和回收具体有两种方案:zookeeper和db。理论上分配方案zk是通过哈希和扩容机器,而db是通过查找机制。回收方案,zk采用的是永久节点,节点中存储下次过期时间,客户端定时上报(设置心跳),db是添加过期时间字段,查找时候判断过期字段。
3. 机器id上限

这个采用将改造版雪花+zookeeper分配id方案作为服务端的节点,客户端采用双Buffer+异步获取提高性能,服务端采用每次请求时间戳增1。

框架指标

全局唯一:最基本的要求,目前是对一个业务而言是唯一性

超高性能:纯内存化操作,性能特别高。采用时间预留,解决时钟回拨问题,同时可以使得qps达到更高,理论上最高可到 51.2w/s ~ more(不同的机器上限不同,自己的笔记本可达 1200(w/s))

趋势递增:整体递增,对用Mysql这种用b+树作为索引的结构可以提高性能

信息安全:自增位放在高位,id不是完全连续的,防止外部恶意的数据爬取

易用性:开发接入非常简单

快速入门

对于使用根据机器id的分配方式不同,这里有三种方式:

  • (单机版)zookeeper分配workerId
  • (单机版)db分配workerId
  • (分布式版)distribute分配workerId

目前已发布到maven中央仓库

zookeeper分配workerId

1
2
3
4
5
6
xml复制代码<dependency>
<groupId>com.github.simonalong</groupId>
<artifactId>butterfly-zookeeper-allocator</artifactId>
<!--替换为具体版本号-->
<version>${last.version.release}</version>
</dependency>

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Test
public void test(){
ZkButterflyConfig config = new ZkButterflyConfig();
config.setHost("localhost:2181");

ButterflyIdGenerator generator = ButterflyIdGenerator.getInstance(config);
// 设置起始时间,如果不设置,则默认从2020年2月22日开始
generator.setStartTime(2020, 5, 1, 0, 0, 0);

// 添加业务空间,如果业务空间不存在,则会注册
generator.addNamespaces("test1", "test2");
Long uuid = generator.getUUid("test1");
System.out.println(uuid);
}

db分配workerId

1
2
3
4
5
6
xml复制代码<dependency>
<groupId>com.github.simonalong</groupId>
<artifactId>butterfly-allocator-db</artifactId>
<!--替换为具体版本号-->
<version>${last.version.release}</version>
</dependency>

使用示例

在对应的公共库中创建表

1
2
3
4
5
6
7
8
9
10
11
sql复制代码CREATE TABLE `butterfly_uuid_generator` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`namespace` varchar(128) DEFAULT '' COMMENT '命名空间',
`work_id` int(16) COMMENT '工作id',
`last_expire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下次失效时间',
`uid` varchar(128) DEFAULT '0' COMMENT '本次启动唯一id',
`ip` varchar(20) NOT NULL DEFAULT '0' COMMENT 'ip',
`process_id` varchar(128) NOT NULL DEFAULT '0' COMMENT '进程id',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_name_work` (`namespace`,`work_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='发号器表';

编写db方式的测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Test
public void test(){
DbButterflyConfig config = new DbButterflyConfig();
config.setUrl("jdbc:mysql://127.0.0.1:3306/neo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&&allowPublicKeyRetrieval=true");
config.setUserName("neo_test");
config.setPassword("neo@Test123");

ButterflyIdGenerator generator = ButterflyIdGenerator.getInstance(config);
// 设置起始时间,如果不设置,则默认从2020年2月22日开始
generator.setStartTime(2020, 5, 1, 0, 0, 0);

// 添加业务空间,如果业务空间不存在,则会注册
generator.addNamespaces("test1", "test2");
Long uuid = generator.getUUid("test1");
System.out.println(uuid);
}

分布式模式

分布式模式客户端一个,但是对应的服务端(代码很简单可以也自己写服务端)这边有两个:

  • dubbo方式的:服务端采用的是butterfly-allocator-zk方式获取workerId和time字段,然后将对应字段给客户端
  • restful方式的:服务端采用的是butterfly-allocator-db方式获取workerId和time字段,然后将对应字段给客户端
1
2
3
4
5
6
xml复制代码<dependency>
<groupId>com.github.simonalong</groupId>
<artifactId>butterfly-allocator-distribute</artifactId>
<!--替换为具体版本号-->
<version>${last.version.release}</version>
</dependency>

使用示例 - dubbo方式获取

首先启动服务端butterfly-server模块,然后客户端这边使用如下即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Test
public void test(){
DistributeDubboButterflyConfig config = new DistributeDubboButterflyConfig();
config.setZkHoseAndPort("localhost:2181");

ButterflyIdGenerator generator = ButterflyIdGenerator.getInstance(config);
// 设置起始时间,如果不设置,则默认从2020年2月22日开始
generator.setStartTime(2020, 5, 1, 0, 0, 0);

// 添加业务空间,如果业务空间不存在,则会注册
generator.addNamespaces("test1", "test2");
Long uuid = generator.getUUid("test1");
System.out.println(uuid);
}

使用示例 - restful方式获取

首先启动服务端butterfly-server模块,然后客户端这边使用如下即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Test
public void test(){
DistributeRestfulButterflyConfig config = new DistributeRestfulButterflyConfig();
config.setHostAndPort("localhost:8800");

ButterflyIdGenerator generator = ButterflyIdGenerator.getInstance(config);
// 设置起始时间,如果不设置,则默认从2020年2月22日开始
generator.setStartTime(2020, 5, 1, 0, 0, 0);

// 添加业务空间,如果业务空间不存在,则会注册
generator.addNamespaces("test1", "test2");
Long uuid = generator.getUUid("test1");
System.out.println(uuid);
}

更多内容

对于详细内容介绍,请见文档Butterfly说明文档

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

免费金融数据API(基金,股票),基金股票网格交易模拟服务

发表于 2021-03-18

文档地址 <www.doctorxiong.club/api>

网站主页 <www.doctorxiong.club>

使用真实数据对基金股票进行网格交易模拟,可以自己设置本金,时间跨度,持有份额等,模拟真实的网格交易
image.png

API介绍

  1. 通过基金代码获取基金详情;
  2. 通过股票代码获取股票详情;
  3. 按条件获取基金排行;
  4. 按条件获取股票排行;
  5. 获取热门基金(属于基金规模较大,收益良好);
  6. 获取热门股票;
  7. 获取基金持仓详情;

获取基金详情

请求方式GET
api.doctorxiong.club/v1/fund/det….
会返回这个基金所有信息的json数据,具体参看文档,数据太多,这里只贴出和解释部分数据

返回结果示例:所有字段在文档中都有详细的解释,货币基金与非货币基金是两种不同的数据格式,。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
javascript复制代码{
"code": 200,
"message": "操作成功",
"data": {
"code": "003171",
"name": "信达澳银慧理财货币",
"type": "货币型",
"monthGrowth": 0.1,
"threeMonthGrowth": 0.28,
"sixMonthGrowth": 0.7,
"yearGrowth": 1.98,
"buyMin": 100,
"buySourceRate": 0,
"buyRate": 0,
"manager": "孔学峰",
"fundScale": "0.20亿(2019-03-31)",
"millionCopiesIncomeData": [
[
"160923",
0.4773
],
[
"160925",
0.9546
]
],
"millionCopiesIncomeDate": "2019-06-27 00:00",
"sevenDaysYearIncome": 1.073,
"sevenDaysYearIncomeDate": [
[
"160923",
2.131
],
[
"160925",
1.706
]
]
},
"meta": "003171"
}

获取基金排行

基金的排行条件数据较多,目前只支持json格式的数据请求
可以支持基金类型,基金公司,基金评级,成立年限,基金规模等按收益进行排行

比如下面这个请求就是获取嘉实,易方达,博时,汇添富的指数型,股票型,混合型基金,成立年限小于5,基金规模小于100的基金按照近一周的收益进行排行。

1
2
3
4
5
6
7
javascript复制代码{
"fundType": ["指数型","股票型","混合型"],
"sort":"lastMonth",
"creatTimeLimit":5,
"fundCompany":["嘉实","易方达","博时","汇添富"],
"fundScale": 100
}

返回示例

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
28
29
30
31
32
33
34
javascript复制代码{
"code": 200,
"message": "操作成功",
"data": [
{
"code": "004424",
"name": "汇添富文体娱乐混合",
"netWorth": "1.2547",
"dayGrowth": "0.2637",
"lastWeekGrowth": "3.60",
"lastMonthGrowth": "10.3227",
"lastThreeMonthGrowth": "10.5560",
"lastSixMonthGrowth": "32.2268",
"lastYearGrowth": "25.47",
"thisYearGrowth": "31.8793"
},
{
"code": "000950",
"name": "易方达沪深300非银ETF联接",
"netWorth": "1.0310",
"dayGrowth": "0.1165",
"lastWeekGrowth": "-0.2998",
"lastMonthGrowth": "9.6459",
"lastThreeMonthGrowth": "9.7392",
"lastSixMonthGrowth": "42.9166",
"lastYearGrowth": "40.0054",
"thisYearGrowth": "42.9166"
}
],
"meta": {
"pageIndex": 1,
"pageSize": 2
}
}

获取股票详情

两个参数code为股票代码,year为查询K线查询年限,目前只支持五年内的数据,默认查询今年的,数据的刷新间隔为每分钟。

查询000001,也就是大家说的上证指数
api.doctorxiong.club/v1/stock/de….

真实数据过长,当日分时数据和K线数据只保留了部分

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
javascript复制代码{
"code": 200,
"message": "操作成功",
"data": {
"code": "000001",
"name": "上证指数",
"open": 2992.2426,
"close": 2996.7926,
"price": 2978.88,
"priceChange": -13.3626,
"changePercent": -0.447,
"high": 2992.2426,
"low": 2961.2225,
"volume": 1814315.23,
"amount": 18364176.74,
"date": "2019-06-28 15:00",
"dayMinData": [
[
"0930",
2992.24,
5366055
],
[
"1500",
2978.88,
181431523
]
],
"dailyData": [
[
"190628",
2992.24,
2978.88,
2992.24,
2961.22,
181431523
]
]
},
"meta": "sh000001"
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

用泊松分布来解释为什么HashMap的链表变树的阈值是8

发表于 2021-03-18

前言

有网友指出《面试Java——集合之HashMap和ConcurrentHashMap》一文,关于为什么是8,还可以加一句符合泊松分布。于是我了解一下泊松分布后,确实和网友说的一致,同时非常感谢网友指出文章存在的瑕疵。接下来的内容,大头菜将试图用泊松分布来论证HashMap的链表变树的阈值为什么是8。

泊松分布

首先,什么是泊松分布?

维基百科官方解释:

泊松分布(法语:loi de Poisson,英语:Poisson distribution)又称Poisson分布、帕松分布、布瓦松分布、布阿松分布、普阿松分布、波以松分布、卜氏分布、帕松小数法则(Poisson law of small numbers),是一种统计与概率学里常见到的离散概率分布,由法国数学家西莫恩·德尼·泊松在1838年时发表。

泊松分布适合于描述单位时间内随机事件发生的次数的概率分布。如某一服务设施在一定时间内受到的服务请求的次数,电话交换机接到呼叫的次数、汽车站台的候客人数、机器出现的故障数、自然灾害发生的次数、DNA序列的变异数、放射性原子核的衰变数、激光的光子数分布等等。

泊松分布的概率质量函数为:

图片摘自网络

我总结一下,简单点来说:泊松分布就是描述单位时间内,独立事件发生的次数。

简单举个例子,让大家熟悉一下:

比如:某医院平均每小时出生3个婴儿,请问下一个小时,会出生几个?

有可能出生6个,也有可能一个都不出生,这是我们没法知道的。

如果尝试用泊松分布来表示:等号的左边,P 表示概率,N表示某种函数关系,t 表示时间,n 表示数量,1小时内出生3个婴儿的概率,就表示为 P(N(1) = 3) 。等号的右边,λ 表示事件的频率。

接下来两个小时,一个婴儿都不出生的概率是0.25%,基本不可能发生。

图片摘自网络

接下来一个小时,至少出生两个婴儿的概率是80%。

图片摘自网络

泊松分布的图形大概是下面的样子。

图片摘自网络

可以看到,在频率附近,事件的发生概率最高,然后向两边对称下降,即变得越大和越小都不太可能。每小时出生3个婴儿,这是最可能的结果,出生得越多或越少,就越不可能。

看完例子后,你应该对泊松分布有一个大概的认识了。接下来,我们尝试使用泊松分布去分析HashMap的链表变树。

HashMap的链表变树阈值为什么是8

根据泊松分布:单位时间内,独立事件发生的次数。

HashMap的key碰撞问题,每次key的碰撞,都可以认为是一次独立事件。与上次或下次是否发生key碰撞,都无关系。

图片摘自网络

因此,用泊松分布尝试表示:P 表示概率,N表示某种函数关系,t 表示时间,n 表示数量,每1秒key发送碰撞的次数为k,就表示为 P(N(1) = k) 。等号的右边,λ 表示事件的频率。关于一个key是否发生碰撞的概率为0.5。

  • e^-0.5 = 0.6065

把相应数值代入泊松分布公式:

P(N(1)=k)=0.6065∗(0.5k)/k!P(N(1)=k) = 0.6065*(0.5^k)/k!P(N(1)=k)=0.6065∗(0.5k)/k!

  • 当k=0时,P= 0.6065
  • 当k=1时,p= 0.3032
  • 当k=2时,p= 0.0758
  • 当k=3时,p= 0.0126
  • 当k=4时,p= 0.0015
  • 当k=5时,p= 0.0001
  • 当k=6时,p= 0.000013
  • 当k=7时,p= 0.0000009
  • 当k=8时,p= 0.00000006
  • 当k=9时,p= 0.000000003

有没有发现上述的结果有点熟悉:

对比一下HashMap的注释:

1
2
3
4
5
6
7
8
9
10
markdown复制代码    * 0:    0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million

当k=9时,也就是发生的碰撞次数为9次时,概率为亿分之三,碰撞的概率已经无限接近为0。

如果设置为9,意味着,几乎永远都不会再次发生碰撞,换句话说,链表的长度此时为8,要发生碰撞才会从链表变树。但永远都不会变树,因为概率太小了。因此设置为9,实在没必要。

这就是链表变树的阈值为8的原因。

参考资料

  • 维基百科
  • 阮一峰的网络日志——《泊松分布和指数分布:10分钟教程》

絮叨

非常感谢你能看到这里,如果觉得文章写得不错 求关注 求点赞 求分享 (对我非常非常有用)。

如果你觉得文章有待提高,我十分期待你对我的建议,求留言。

如果你希望看到什么内容,我十分期待你的留言。

各位的捧场和支持,是我创作的最大动力!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

产品经理说以后接口不找你写了!啥情况?我要凉了?

发表于 2021-03-18

产品:小哥哥,有个临时需求要搞一下,不用做界面,搞个接口就好,统计一些数据,我需要每周处理一下,然后给领导看的。

file

开发:临时加需求不太好吧?

file

产品:那我自己搞一下吧。

开发:哈~你自己搞?

产品:之前一直提取数需求,学过一些SQL,可以试一下

开发:那你用啥写后端啊?Java吗?

产品:飞算全自动软件工程平台上拖拉拽搞一下SQL就好了,不用写Java,我又不会…

开发:飞算全自动软件工程平台是什么鬼?来抢我饭碗吗?我这是要凉了吗?

产品:额,你竟然还不知道?

开发:…

这是最近发生在某机构开发中心的一幕…

飞算全自动软件工程平台到底是个什么样的东西,可以让产品经理有底气说自己来完成一个后端接口?现在可登录其官网申请产品试用:feisuanyz.com/ ,小编申请注册了个试用账号来一探究竟!

飞算全自动软件工程平台

一番体验下来,最大的感受是飞算全自动软件工程平台真的可以编程逻辑可视化、降低开发上手门槛、将各种复杂的开发工作自动化的运作起来,从而大幅度地提高开发效率。

小编发现,使用飞算全自动软件工程平台开发后端接口与我们使用IDEA的传统开发模式有很大的不同。使用人员完全不需要具备很强的Java编码能力,甚至一些简单需求都不需要掌握Java就可以快速的上手参与到后端编码工作中去。

所以,也就有了文首的场景,一个略懂SQL的产品经理通过拖拉拽实现编程逻辑可视化,写写查询语句即可完成一个后端接口的开发。

这到底有多神奇呢?来直观感受下整个开发模式:

可以看到,我们常见的数据访问(SQL、事务)都被封装成了图形化的组件,我们只需要拖拽不同的组件,然后填入要执行的SQL,来编排接口的业务逻辑就能完成整个后端接口的逻辑开发。

而在这个过程中,你不需要再去思考是使用JdbcTemplate,MyBatis,还是JPA。只要你会SQL,就能实现一个后端接口。

完成了开发,不知道对错?还要求测试人员帮忙支持一下?估计还得被测试怼!当我们用飞算的时候,我们也可以轻松的完成仿真测试,操作模式同样轻松实现:

如果你也是一名产品经理,是不是有感觉很好上手呢?如果有这样一个神器加持,当开发拒绝需求的时候,有没有自己搞一把来实现的冲动呢?

飞算全自动软件工程平台能改变什么?

人人都能开发后端了?

既然产品经理也能来写后端接口了,那我们后端开发都要凉了?人人都能做后端开发么?

小编认为目前还不会。由于我们在实际的开发过程中,还是会碰到很多特殊的场景,比如:高并发的场景、大数据量的场景。整个解决方案就可能变的特别复杂,其中也会引入各种不同的前沿中间件或自研中间件。在这个平台的帮助下,程序员完全可以从繁杂重复的打代码的工作中解脱出来专注这些“高级活”,告别996,找对象不再发愁没时间了。

同时,也不是所有的产品经理都如本篇开头那位一样,可以快速的上手这个平台的。由于后端开发除了需要Java的编码知识之外,还有很多其他知识要求,比如:数据库、缓存、高并发等其他高级内容的支持。对于非开发人员(如:产品、运营)要参与到其中,也是需要有一定的背景知识,这位产品经理最核心的是他已经掌握了SQL,虽然他不会Java,但在飞算全自动软件工程平台的帮助下,是可以让他快速上手后端开发的。

所以,飞算全自动软件工程平台并不能完全取代后端开发,而是把后端开发的门槛降低了。

可以提高效率吗?

既然飞算全自动软件工程平台还无法取代后端开发,那他能提高我们的研发效率吗?

答案是肯定的!

这类平台的核心就是降低开发的上手门槛,所以我认为平台可以推向更多依靠降低门槛,入门开发的场景去尝试。

用来帮助团队中原本开发能力较弱,开发效率较低的人群,或是具备一定开发知识,但因知识不完备而暂时无法胜任开发工作的成员,让更多的人能够直接参与到一些研发工作中去。

而具备专业知识的成员可以从繁重的重复性工作中解放出来,更多的去从事架构设计等具有创造性的工作,从而使得团队整体的生产工具升级!

当大部分成员都拥有了编码思维,信息化思维,不论是后续参与到研发工作中去,还是结合当前自身工作,由于思维模式的改变,就很容易结合出新的效率提升想法,激活各领域的创新动力。

平台虽好,但还是要理性思考,任何工具都不是银弹。

我比较不提倡的是将这类平台推向原本开发实力很强,具备很多前沿技术且创造力丰富的场景下。因为当一个为了降低上手门槛而出现的平台,推向了已经具备高超技术的团队时候(由于他们整体本身都在门槛之上,并不需要降低),反而可能会造成反效果,比如:大量前沿的创新性的尝试变得困难。

所以,能不能真实的提高效率,就看团队如何使用它,用对了就是超级神器,用错了就是潘多拉魔盒。

技术管理者一定要结合自己的业务、团队实际的情况去选择人群、选择场景来部署平台,以帮助他们提高自身能力,从而实现效率的提升,让这类科技产品落到真正弥补短板的地方,实现最大的产品价值!

说了那么多,那么你觉得飞算全自动软件工程平台会是未来吗?

它能够帮助你的团队加速业务需求的研发吗?欢迎留言说说你的观点!

欢迎关注我的公众号:程序猿DD,获得独家整理的学习资源、日常干货及福利赠送。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

2021年,SQL注入死透了么?

发表于 2021-03-18

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

很长一段时间,我认为后端开发,在安全性方面最容易出问题的地方就在于SQL注入。通过 where 1=1这种魔幻的SQL写法,就可以很容易的对一个存在问题的系统进行攻击,以至于最终演进出sqlmap这样的神器存在。

sqlmap-screen.png

后来的fastjson刷新了我的认知,这个框架也算是对互联网安全概念的一种推动。连不懂技术的老板,都知道fastjson快的要命,作为程序员安全理念就得到了一次提升。

为什么对sql注入情有独钟?因为开发人员和SQL打交道的地方太多了。甚至有的专门开发报表的同学,写的SQL行数,比写的代码行数还多!

问题是。很久很久之前,早在10年前,就有人在喊SQL注入已经死掉了,但时至今日,依然有一大批的SQL注入教程和SQL注入的案例。

SQL注入是漏洞之王,这可不是吹的。

开源一套以教学为目的系统,欢迎star:github.com/xjjdog/bcma…。它包含ToB复杂业务、互联网高并发业务、缓存应用;DDD、微服务指导。模型驱动、数据驱动。了解大型服务进化路线,编码技巧、学习Linux,性能调优。Docker/k8s助力、监控、日志收集、中间件学习。前端技术、后端实践等。主要技术:SpringBoot+JPA+Mybatis-plus+Antd+Vue3。

当然在这方面,PHP的贡献最大,Java甘拜下风。

SQL注入流行的原因,就是开发人员对自己太自信了,或者使用的工具太原始了,没有经过框架层进行一次过滤。如果你用了Java界的MyBatis或者JPA,发生SQL注入的可能性就变的非常的低。现在PHP也有了类似于thinkphp一样的框架,代表着能搞的SQL注入漏洞已经越来越少了。

但不代表着没有,只是门槛提高了。我们以MyBatis为例,看一下到底还能不能发生SQL注入。

MyBatis依然存在SQL注入

使用Mybatis的同学,第一个接触的概念,就是#和$的区别。这两个符号非常的像Shell中的魔幻符号,但好在只有两种情况。

  • # 代表的是使用sql预编译方式,安全可靠
  • $ 代表着使用的是拼接方式,有SQL注入的风险

比如下面这个xml配置,就是一个绝对安全的写法。因为整个#{id}会被替换成?。

1
2
3
xml复制代码<select id="queryAll"  resultMap="resultMap">
SELECT * FROM order WHERE id = #{id}
</select>

但可惜的是,有些场景,并不能使用预编译方式(或者你仅仅是不知道或者懒)。像一些代码重构,把表名/列名/排序等字段,动态传入的时候,不可避免的就需要SQL拼接的方式,SQL注入依然有搞头。

但更容易发生问题的,还是LIKE和IN等类似的语句。

下面是两句Like模糊查询的写法,实际测试会发现,使用#竟然不好使了,会报错,需要使用sql拼接的$。问题由此发生。

1
2
sql复制代码SELECT * FROM order WHERE name like '%#{name}%'  //会报语法错
SELECT * FROM order WHERE name like '%${name}%' //可以运行

而正确的写法,应该使用函数拼接。但是工期压死人,在不知不觉间,大多数人就选择了简单的写法。毕竟功能第一嘛,也是体现工作量的最主要方式。

1
sql复制代码SELECT * FROM order WHERE  name like concat(‘%’,#{name}, ‘%’) //正确的写法

同样的问题,存在于IN语句。

1
2
sql复制代码in (#{tag}) //报错
in (${tag}) //可以运行

既然几个字符就可以运行,当然没人选择下面复杂的写法。

1
2
3
4
xml复制代码tag in
<foreach collection="tag" item="item" open="("separatosr="," close=")">
#{tag}
</foreach>

还有order by,也千万不要掉以轻心,一不小心就会万劫不复。

1
2
sql复制代码SELECT * FROM order order by createDate #{sortType} //报错
SELECT * FROM order order by createDate ${sortType} //正常

这种情况下,就需要把sortType搞成白名单了。不就一个ASC和DESC了,你给我传一个长长的串,是怎么回事?

总结

SQL注入在2021年,依然存在,只不过门槛提高了。现在SQL注入减少,都是框架的功劳,和程序员的水平没半毛关系。sql拼接的情况永远不会消失,因为这是最快捷简单的方式,会让人欲罢不能。无数的外包项目,十几年躺尸不动的系统比比皆是,寄希望于在框架层全部消灭SQL注入,是一个梦想。

因为它的对手,是人性的懒惰。谁也无法战胜它。

开源一套以教学为目的系统,欢迎star:github.com/xjjdog/bcma…。它包含ToB复杂业务、互联网高并发业务、缓存应用;DDD、微服务指导。模型驱动、数据驱动。了解大型服务进化路线,编码技巧、学习Linux,性能调优。Docker/k8s助力、监控、日志收集、中间件学习。前端技术、后端实践等。主要技术:SpringBoot+JPA+Mybatis-plus+Antd+Vue3。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,​进一步交流。​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

奉劝那些想把编程学好的学弟学妹们!

发表于 2021-03-18

掘金的小伙伴们,大家好,我是沉默王二。

作为一名在掘金上拥有 15 万+关注者的技术博主(自认为做到这一点并不容易,害羞脸),我接触了太多太多想学编程、想把编程学好的人,有从别的专业转过来的,有零基础自学的,有科班出身的。他们当中的一部分人,学着学着就放弃了,或者还在放弃的路上。

所以真的想掏心掏肺给大家谈一谈,在学好编程这条路上,我们该做好哪些心理准备,该怎么去学。

01、很遗憾

我上大学那会,学校的计算机专业刚成立两年,也就是说,我们是第二批。据说,第一批做小白鼠的学长学姐们,很多在毕业的时候都没从事计算机专业方面的工作。倒不是他们不想找这方面的工作,而是本地的工作机会寥寥无几。

很幸运,我是班里面为数不多坚持下来的。准确来说,这个数不超过 10。剩余的呢?能在大一的时候转专业的都转了,转不了的毕业后都干别的去了。

2008 年那会,郑州(老家河南洛阳的,老乡可以关注下哈)几乎没有计算机专业方面的工作,于是我们都莫名其妙地丧失了对未来的信心。老师们呢,也不吭声,哪怕说一句,“你们去北上广深闯一闯吧”,没准我们都能成为时代的弄潮儿,毕竟一线城市的工作机会还是挺多的,把握住的话,还真能成就一番作为,毕竟早就是优势啊。

现在想起来,不仅为我自己感到遗憾,也为班里面的同学们感到遗憾。我们的专业是学校最不看好的,但却是那个时代最有前景的,十多年的时间也证明了这一点。但我们还是因为自身的局限性错过了,错过了去一线城市闯荡的最佳时机。

以前,程序员稀缺,但机会也少;现在,机会多,程序员也多。每个时代都有每个时代的局限性,那还要学不学编程了呢?

02、选择比努力更重要

时不时就会有人问我,“25 岁了,学编程还来得及吗?”“30 岁了,学编程晚吗?”也有人问我,“正在上高中,我想以后从事软件开发,我该学些什么呢?”

每个人,不管处于什么样的年龄段,都有选择的权力。

大家应该听过这句话,“互联网时代,选择比努力更重要。”于是,大多数人就在纠结,到底该选择什么才能不那么努力就轻而易举的得到,到底选择什么才能不后悔。

但大多数人往往会忽略一件事,说“选择比努力更重要”这句话的人,其实付出了很多很多常人看不到的努力。

很多人听说 IT 行业很吃香,于是倔强的选择了 IT,这是对的,互联网虽然已经很卷了,但仍然处在蓬勃发展的阶段,很多地方都充满了机会。

我要告诉大家的是,既然选择了,就要付出努力,不要轻言放弃。

很多事情,短时间内都是没有答案的,只有把时间线放得长一点,才能验证选择到底是否正确。

03、编程难吗

我妹学了有小半年的编程了,她就经常给我抱怨,“编程太难了!”

PS:非忽悠,真的,大家可以点击链接看我另外一篇文章,送我妹上了大学。

说句实在话,编程确实不是一件容易的事儿,我上大学那会,也觉得编程难,难得想要放弃!真的,一点不骗大家。

现在不是放寒假了嘛,我就没看见过我妹打开电脑敲过代码,每天用得最多的一个软件,大家不用猜应该就知道,它叫“抖音”。

抖音是一件国民级的软件,的确给我们带来了很多欢乐和感动,我不讨厌它也不喜欢它,因为对于那些自制力差的人来说,抖音悄无声息地就把他们的时间消耗殆尽。

卡耐基的《人性的弱点》里阐述了一个发人深省的观点:

99% 的情况下,不管犯下多么严重的错误,人们都会优先归咎于他人。

例子我就不再举了,我也有过这样的想法。一开始学编程很难,于是同学们不约而同地都把责任归咎到了老师那边,认为是老师自己学艺不精,所以才没办法把我们教好。

但事实上,这就是人性的弱点,我们都在想方设法地为自己找借口,找一个看起来很合理的接口。

编程难不难,我估计没人敢说容易,哪怕是出了名的大佬们。可正因为难,才有价值,不是吗?因为难,就不学了吗?

04、努力就真的能成功吗

答案也显而易见,“不一定。”

我每周会去三次健身房,每次都会见到一个人,他的肚子好像一直就那么大。他没有请私人教练,也没有练习器械,单纯的就是在跑步机上慢走,走大概 20 分钟左右。

在我看来,他很努力也很自律,然而这样的努力,往往收不到任何的效果。

这也就是为什么,常常有人会抱怨,“为什么那谁谁谁努力了就成功了,而我没有呢!”

每天把输出“hello world”的程序敲上一百遍,坚持一百天,这样学编程的话,效果可想而知,是学不好的。努力,并不是不断地做重复性的工作。

真正的努力,需要花费足够多的时间,并且要不断的寻求突破。

就拿学习 Java 来说吧,一开始可能要先学习 C 语言,打下坚实的基础,如果没有学的话,直接开始学 Java 也行,但以后有时间的话,还是要补一补的,好处很多。从 Java 基础知识,到面向对象编程,到网络编程,到多线程并发,到 Java 虚拟机,到性能优化。

这一条线下来,还不够。还要学习工具,比如说 IDE、Git、Maven;还要学习框架,比如说 Spring、MyBatis、Spring Boot;还要学习数据库,比如说 MySQL、Redis 等等。

我在知乎上有一个 3100 多赞的 Java 自学路线,这里推荐给大家。

自学java,学多久可以自己找到工作?

PS:说句心里话,在知乎上拿到 3100+ 赞真的不容易,尤其是对于计算机领域的博主来说。真的帮助了许许多多的读者,希望大家不要错过。

我也同步到了掘金上,大家可以点击链接看一看了。

拜托,别再问我怎么自学 Java 了!和盘托出

除此之外,数据结构与算法、设计模式、计算机网络、计算机组成原理、操作系统等等,这些也要学,也只有这样,不断地走出舒适区,不断地突破,不断地寻求边界,然后才能真正地把编程学好。

说到设计模式,我这里有一份好朋友小傅哥重写的 Java 设计模式,已经下载了2 万多次,同样强烈推荐给大家。可以通过下面的链接获取(无套路,没有解压密码)。

设计模式,牛逼!

05、目标如何定

对,我们常说,做任何事情之前,都要先定个目标,这样做起事情来就有了动力,不至于漫无目的。

但,如果做什么事情都奔着一个目标去,会感觉很累,累到有时候你会丧失信心。

我们可以把“我要学好编程”作为目标,也可以把这个目标简单做下拆分,比如说先入门再进阶,然后再拓展。更甚至,可以抱着一种“玩一玩”的心态,也没什么大不了的。

我喜欢看王小波的书,但一开始,我并不知道有王小波这个人,而在通过一个叫《一个人的书房》的播客节目了解到的。这个播客里提到了《沉默的大多数》,于是我就买来读了读,觉得有趣得不得了,然后我就又买了时代三部曲,然后就读啊读。

等到我要写作的时候,莫名其妙地,就受到了王小波的影响,写出来的文字就带有一些“幽默风趣”在里面。

但如果一开始,我是抱着一种我要把文章写得有趣,再去读王小波的作品的话,没准我会读得很困难,因为我可能会静不下心来,我想从书里面挖掘“有趣”,可能就失去了阅读的兴趣。

同样的,如果我们抱着一种“玩一玩”的心态来学习编程的话,没准真能把编程学好。但如果我们抱着一种“我一定要把编程学好”这种心态的话,没准在遇到困难的时候就放弃了,因为我们定下来的目标很难完成,以至于我们有一种负罪感,这种学习状态下,学好是一件很难的事,学不好倒是一件很容易的事。

把目标降到最低,也是为什么我们在学习一门编程语言的时候要敲“hello world”的原因。

最后,希望大家都能把编程学好,从一键三连做起吧。另外推荐 2 篇在掘金上点赞数超过 500 个的旧文给大家读一读:

狂补计算机基础知识,让我上了瘾

GitHub上最励志的计算机自学教程(重制版)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 多线程 简简单单原子类

发表于 2021-03-17

前言

原子类继承于 Number 接口 , 归属于java.util.concurrent.atomic 包中 , 原子类的主要作用是为了解决 synchronized 处理原子操作 资源消耗过高 的问题 ,原子类的CAS 基于 Unsafe 实现 .

一. 原子类的简述

1.1 原子类的应用场景

原子类适用于需要原子操作而有需要减少资源消耗时 , 原子类相当于 volatile 和 CAS 的工具类 .

1.2 原子类的类型

基本类型 : 使用原子的方式更新基本类型

  • AtomicInteger:整形原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean :布尔型原子类

数组类型 : 使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray :引用类型数组原子类

引用类型

  • AtomicReference:引用类型原子类
  • AtomicStampedRerence:原子更新引用类型里的字段原子类
  • AtomicMarkableReference :原子更新带有标记位的引用类型

对象的属性修改类型

  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
  • AtomicLongFieldUpdater:原子更新长整形字段的更新器
  • AtomicStampedReference :原子更新带有版本号的引用类型。
    • 该类将整数值与引用关联起来
    • 可用于解决原子的更新数据和数据的版本号
    • 可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

1.3 . AtomicInteger 类常用方法

  • public final int get() : 获取当前的值java
  • public final int getAndSet(int newValue) : 获取当前的值,并设置新的值
  • public final int getAndIncrement(): 获取当前的值,并自增
  • public final int getAndDecrement() : 获取当前的值,并自减
  • public final int getAndAdd(int delta) : 获取当前的值,并加上预期的值
  • boolean compareAndSet(int expect, int update) : 如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
  • public final void lazySet(int newValue) : 最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
  • public final int incrementAndGet() : 以原子方式给当前值加1并获取新值
  • public final int decrementAndGet() : 以原子方式给当前值减1并获取新值
  • public final int addAndGet(int delta) : 以原子方式给当前值加delta并获取新值
  • public final boolean compareAndSet(int expect, int update) : CAS 比较方法

二 . 原子类的原理

2.1 原理简述

原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 contextswitch切换到另一个线程 , 之所以称为原子变量,是因为其包含一些以原子方式实现组合操作的方法

回顾 CAS : CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值

原子类主要通过 CAS (compare and swap) + volatile 和 native 方法来保证原子操作 !

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
JAVA复制代码// 案例 : AtomicInteger

// 它的主要内部成员是:
private volatile int value;
注意,它的声明带有volatile,这是必需的,以保证内存可见性。

// 它的大部分更新方法实现都类似,我们看一个方法incrementAndGet,其代码为:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}

// 重点 :
1 . value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值
2 . UnSafe 类的objectFieldOffset()方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址 返回值是 valueOffset
  1. 先获取当前值current,计算期望的值next
  2. 然后调用CAS方法进行更新,如果当前值没有变,则更新并返回新值,否则继续循环直到更新成功为止

2.2 AtomicInteger 原理深入

我们从源码看看那些之前被我们忽略的东西 , 此类可以代表大多数基本类型

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
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码
// Node 1 : 原子类支持序列化
implements java.io.Serializable

---------------------->

// Node 2 : CAS 对象 Unsafe , Unsafe 之前已经说过了, 其中有很多 Native 犯法
private static final Unsafe unsafe = Unsafe.getUnsafe();

---------------------->

// Node 3 : 偏移量 valueOffset
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// objectFieldOffset() : 获取某个字段相对Java对象的“起始地址”的偏移量 , 后续通过便宜量获取方法
// getDeclaredField() : 返回一个字段对象,该对象反映由这个类对象表示的类或接口的指定声明字段

---------------------->

// Node 4 : 核心值 Value ,可以看到 value 使用 volatile 进行修饰
private volatile int value;

---------------------->

// Node 5 : 操作方法 , 可以看到 valueOffset 此时已经发挥了作用
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}


---------------------->
// Node 6 : 值转换 , AtomicInteger 提供了以下四个值得固有转换方法
public int intValue() ;
public long longValue() ;
public float floatValue();
public double doubleValue();

2.3 AtomicReference 深入

现在看一下 AtomicReference 有什么特别之处

1
2
3
4
5
6
7
8
9
java复制代码
// Node 1 : 不再继承 Number 接口

// Node 2 : 使用泛型方法
public class AtomicReference<V> implements java.io.Serializable
private volatile V value;

// Node 3 : 比对时使用 putOrderedObject
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);

2.4 AtomicIntegerArray 深入

相对而言 AtomicIntegerArray 有更多得变化 , 其他的大同小异

1
2
3
4
5
6
7
8
java复制代码
// Node 1 : 对数组元素偏移进行了记录 , 此处不再是 "value" 偏移
private static final int base = unsafe.arrayBaseOffset(int[].class);
private static final int shift;
private final int[] array;

// Node 2 : 比较使用了 getAndSetInt
unsafe.getAndSetInt(array, checkedByteOffset(i), newValue)

三 . 原子类的操作

3.1 原子类常见案例

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
java复制代码AtomicInteger integer = new AtomicInteger();

logger.info("------> 1 > 获取原子变量 :[{}] <-------", integer.get());

// Step 2 : 设置参数
integer.set(999);

logger.info("------> 2 > 获取原子变量 :[{}] <-------", integer.get());


logger.info("------> 失败比较获取 : 测试比较判断 :[{}] <-------", integer.compareAndSet(0, 998));
logger.info("------> 3 > 获取原子变量 :[{}] <-------", integer.get());

logger.info("------> 成功比较获取 : 测试比较判断 :[{}] <-------", integer.compareAndSet(999, 998));
logger.info("------> 4 > 获取原子变量 :[{}] <-------", integer.get());


// Step 3 : 获取当前的值,并设置新的值
logger.info("------> 测试比较判断 :[{}] <-------", integer.getAndSet(888));
logger.info("------> 5 > 获取原子变量 :[{}] <-------", integer.get());


// Step 4 : 获取当前的值,并设置新的值
logger.info("------> 测试比较判断 :[{}] <-------", integer.getAndIncrement());
logger.info("------> 6 > 获取原子变量 :[{}] <-------", integer.get());

// 以原子方式给当前值加1并获取新值
logger.info("------> 测试比较判断 :[{}] <-------", integer.incrementAndGet());
logger.info("------> 6-1 > 获取原子变量 :[{}] <-------", integer.get());

// Step 5 : 获取当前的值,并设置新的值
logger.info("------> 测试比较判断 :[{}] <-------", integer.getAndDecrement());
logger.info("------> 7 > 获取原子变量 :[{}] <-------", integer.get());

// 以原子方式给当前值减1并获取新值
logger.info("------> 测试比较判断 :[{}] <-------", integer.decrementAndGet());
logger.info("------> 7 > 获取原子变量 :[{}] <-------", integer.get());

// Step 6 : 获取当前的值,并设置新的值
logger.info("------> 测试比较判断 :[{}] <-------", integer.getAndAdd(99));
logger.info("------> 8 > 获取原子变量 :[{}] <-------", integer.get());

// 以原子方式给当前值加delta并获取新值
logger.info("------> 测试比较判断 :[{}] <-------", integer.addAndGet(99));
logger.info("------> 8 > 获取原子变量 :[{}] <-------", integer.get());
}

3.2 原子类测试多线程情况

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
java复制代码 /**
* 测多线程方式
*/
public void testThead() throws Exception {

InnerTO innerTO = new InnerTO();
MyThread[] threadDSS = new MyThread[1000];

for (int i = 0; i < 1000; i++) {
threadDSS[i] = new MyThread(innerTO);
}
for (int i = 0; i < 1000; i++) {
threadDSS[i].start();
}

logger.info("------> 原子类线程 Start 完成 :{} <-------", innerTO.getInteger().get());
for (int i = 0; i < 1000; i++) {
if (i % 100 == 0) {
Thread.sleep(1);
logger.info("------> 测试原子类 :{} <-------", innerTO.getInteger().get());
}
}
}

/**
* 包含原子类的对象
*
**/
class InnerTO {
AtomicInteger integer = new AtomicInteger();
public AtomicInteger getInteger() {
return integer;
}

public void setInteger(AtomicInteger integer) {
this.integer = integer;
}
}

/**
* 运行线程类
*
**/
class MyThread extends Thread {

public InnerTO innerTO = new InnerTO();

public MyThread(InnerTO innerTO) {
this.innerTO = innerTO;
}

@Override
public void run() {
int i = innerTO.getInteger().getAndIncrement();
if (i == 999) {
logger.info("------> 线程执行完成 <-------");
}
}
}

// 可以看到在没有锁的情况下 ,数据保证了原子性
------> 原子类线程 Start 完成 :876 <-------
------> 测试原子类 :918 <-------
------> 测试原子类 :950 <-------
------> 测试原子类 :973 <-------
------> 测试原子类 :989 <-------
------> 线程执行完成 <-------
------> 测试原子类 :1000 <-------
------> 测试原子类 :1000 <-------
------> 测试原子类 :1000 <-------
------> 测试原子类 :1000 <-------
------> 测试原子类 :1000 <-------
------> 测试原子类 :1000 <-------

欢迎大家关注我的相关文档 多线程集合

参考文档

1
2
3
java复制代码[芋道源码](http://www.iocoder.cn/JUC/sike/aqs-3/)

[死磕系列](http://cmsblogs.com/?cat=151)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…703704705…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%