java浮点型数值在运算中会出现精度损失的情况,在业务要求比较高比如交易等场景,一般使用BigDecimal来解决精度丢失的情况。最近一个同事在使用BigDecimal时仍然出现了精度损失,简略记录一下

测试用例

代码如下

1
2
3
4
5
6
7
@Test
public void fd() {
double abc = 0.56D;
System.out.println("abc: " + abc);
System.out.println("new BigDecimal(abc): " + new BigDecimal(abc));
System.out.println("BigDecimal.valueOf(abc): " + BigDecimal.valueOf(abc));
}

输出

1
2
3
abc: 0.56
new BigDecimal(abc): 0.560000000000000053290705182007513940334320068359375
BigDecimal.valueOf(abc): 0.56

可以看到在使用BigDecimal构造器转化浮点型仍然会有损失,而使用valueOf方法则不会出现精度损失。

深入源码

BigDecimal构造器,核心代码(BigDecimal(double val))如下

1
2
3
4
5
6
7
8
9
10
11
public BigDecimal(double val, MathContext mc) {
.....
long valBits = Double.doubleToLongBits(val);
int sign = ((valBits >> 63) == 0 ? 1 : -1);
int exponent = (int) ((valBits >> 52) & 0x7ffL);
long significand = (exponent == 0
? (valBits & ((1L << 52) - 1)) << 1
: (valBits & ((1L << 52) - 1)) | (1L << 52));
exponent -= 1075;
...
}

划重点, Double.doubleToLongBits返回根据IEEE754浮点“双精度格式”位布局,返回指定浮点值的表示

BigDecimal.valueOf核心代码

1
2
3
4
5
6
public static BigDecimal valueOf(double val) {
return new BigDecimal(Double.toString(val));
}
public BigDecimal(char[] in, int offset, int len, MathContext mc) {
....
}

可以看到使用valueOf方法实际上是把double转为String,再调用string构造器的。

那么为什么使用Double.doubleToLongBits会出现精度损失,而使用string构造器不会呢。主要原因是BigDecimal使用十进制(BigInteger)+小数点(scale)位置来表示小数,而不是直接使用二进制,如101.001 = 101001 * 0.1^3,运算时会分成两部分,BigInteger间的运算以及小数点位置的更新,这里不再展开。

原理浅析

Double.doubleToLongBits为什么会出现精度损失呢,主要原因是因为浮点型不能用精确的二进制来表述,就如十进制不能准确描述无穷小数一样。

浮点型转化为二进制的算法是乘以2直到没有了小数为止,举个栗子,0.8表示成二进制

0.8*2=1.6 取整数部分 1

0.6*2=1.2 取整数部分 1

0.2*2=0.4 取整数部分 0

0.4*2=0.8 取整数部分 0

可以看到上述的计算过程出现循环了,所以说浮点型转化为二进制有时是不可能精确的。

结论

如果想要把浮点型转化为BigDecimal,尽量选择使用valueOf方法,而不是使用构造器。

References

JAVA程序中Float和Double精度丢失问题

罗技鼠标的自定义按键很好远,但自从升级系统到10.15 catalina后自定义按键就失灵了,重启,重装Logitech Options,重新授权…一番折腾后终于找到了彻底的解决办法

环境配置

系统:MacOS 10.15 Catalina

鼠标:MX720

Logitech Options 版本:8.02.86

排查步骤

  1. 修改Security & Privacy 里的 Logi Options Daemon 和 Logi Options 权限,发现已经勾选了
  2. 重新安装Logitech Options勾选权限,仍然无法使用

解决办法

  1. 删除Logi Options和Logi Options Daemon后,再次添加这两项

logi.png

  1. Security & Privacy -> Privacy 中添加Input Monitoring权限

    input.png

  2. 如果需要自定义鼠标截图,还需要添加 Screen Recording权限

结论

仔细看了Catalina的新特性,新系统对于安全和权限管理更加严格了,所以需要单独处理,至于Logitech Options需要的权限,可以参考Logitech Options permission prompts on macOS Catalina and macOS Mojave,新系统中其他软件遇到类似的问题,都可以通过这种方式解决

References

Logitech Options 在 Mac 下的自定义按键经常会失灵

Logitech Options permission prompts on macOS Catalina and macOS Mojave

Nacos 1.2.0版本以前是不支持MySQL8.0,如果出现配置保存不了,500的错误,多是由于数据源的问题,需要修改源码以支持MySQL8.0。

从github克隆源码

git clone https://github.com/alibaba/nacos.git

修改pom驱动版本(最外层pom)

mysql mysql-connector-java 8.0.19

修改源码引用

位置 nacos/naming/src/main/java/com/alibaba/nacos/naming/healthcheck/MysqlHealthCheckProcessor.java

// import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;原本引用的类
import com.mysql.cj.jdbc.MysqlDataSource;

打包

1
2
3
cd nacos/
mvn -Prelease-nacos -Dmaven.test.skip=true clean install -U
ls -al distribution/target/nacos-server-1.2.0-SNAPSHOT/nacos

注意修改targeta下的jar包名为 nacos-server.jar

修改conf里的配置文件

启动

进入bin目录,以官方提供的方式启动 sh startup.sh -m standalone

异常

如果出现异常,可以通过logs/nacos.log查看具体的启动异常

References

Nacos 配置 MySQL8数据库

Nacos 快速开始

问题来源

问题来源于一次串行使用CompletableFuture和Stream导致CompletableFuture异步失效的问题,问题代码:

1
2
3
4
5
6
7
8
9
10
11
12
int size = 50;
List<Double> res = Stream.iterate(0, i -> i + 1).limit(size).map(i -> CompletableFuture.supplyAsync(() -> {
Double re = BigDecimal.valueOf(10 * (Math.sin(i) + 1)).setScale(2, RoundingMode.HALF_UP).stripTrailingZeros().doubleValue();
try {
TimeUnit.SECONDS.sleep(re.intValue());
} catch (InterruptedException e) {
e.printStackTrace();
}
return re;
}, ThreadUtil.fixed()))
.map(CompletableFuture::join)
.collect(Collectors.toList());

其中ThreadUtil.fixed()是自己封装的线程工具方法,也可以使用Executors.newFixedThreadPool代替,原本是想通过CompletableFuture拿到异步执行的结果并进行处理,串联使用stream后反而未起到作用,先说解决方法:

  1. 先收集 future 结果到list,再调用新的流运算,即 .map(CompletableFuture::join)方法
  2. limit(size).map 之间添加 parallel() 方法,形成 parallelStream()d的形式

原因分析:普通的stram可以理解为单纯的foreach循环,每生成一个future立即join,出现异步变同步的现象。

再进一步,既然CompletableFuture和parellStream都可以并行执行任务,有必要比较一下。

parallelStream

先试用 parallelStream 重写上述方法

1
2
3
List<Double> result = Stream.iterate(0, i -> i + 1).limit(50).parallel().map(i -> {
///.....
}).collect(Collectors.toList());

用时:54018ms

想要深入了解parallelStream,需要先了解ForkJoin框架和ForkJoinPool框架。这里简单介绍一下ForkJoinPool,真正了解 ParallelStream 还是需要先弄懂ForkJoinPool的,在此只是简单比较两者功能,不做深入探讨。

ForkJoinPool 使用分治法(Divide-and-Conquer Algorithm)来解决问题,实现了ExecutorService接口,线程数量可以通过构造器传入,默认使用机器的CPU数量。和ThreadPoolExecutor有一定区别,ForkJoinPool可以在运行线程中创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行,而ThreadPoolExecutor做不到这一点的。ForkJoinPool的核心算法是工作窃取算法,这样就可以在使用少量的线程来完成大量的任务。比如说ForkJoinPool 4个线程可以处理200完个任务,ThreadPoolExecutor显然是不可行的。

工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

ParallelStreams中java8为ForkJoinPool添加了通用线程池,默认线程数量为机器的处理器数量。可以通过 -Djava.util.concurrent.ForkJoinPool.common.parallelism=N来设置ForkJoinPool的线程数量。

拆分成两步重写上述方法

1
2
3
4
5
List<CompletableFuture<Double>> futures = Stream.iterate(0, i -> i + 1).limit(50).map(i -> CompletableFuture.supplyAsync(() -> {
///.....
}, ThreadUtil.fixed())).collect(Collectors.toList());
List<Double> ids = futures.stream().map(CompletableFuture::join)
.collect(Collectors.toList());

用时:57111ms, 通过增加线程数量可以减少执行时间。

References

Java 多线程中的任务分解机制-ForkJoinPool,以及CompletableFuture

CompletableFuture 组合式异步编程

翻译自 Under the Hood of Redis: Strings

你知道简单string strings 在redis里占用了56 bytes的内存吗?

我会试图告诉你为什么,了解redis运行原理时非常重要的。当你试图构建一个高负载的应用显的尤为重要,同时,你很快就会理解你的redis实例为什么会消费大量的内存?

阅读全文 »

  1. Query 和 Criteria 查询

    1
    2
    3
    Query query = new Query();
    query.addCriteria(Criteria.where("name").is("Eric"));
    List<User> users = mongoTemplate.find(query, User.class);

    支持的查询方法:is, regex, lt, gt, pageable, sort

  2. 生成query方法

    • findByX

      1
      List<User> findByName(String name);
    • startinggWith and endingWith

      1
      2
      List<User> findByNameStartingWith(String regexp);
      List<User> findByNameEndingWith(String regexp);
    • between

      1
      List<User> findByAgeBetween(int ageGT, int ageLT);
    • like and orderBy

      1
      List<User> users = userRepository.findByNameLikeOrderByAgeAsc("A");
  3. JSON Query methods : @Query

    1
    2
    @Query("{ 'name' : ?0 }")
    List<User> findUsersByName(String name);

    支持的查询方法: $regex, $gt, $lt

  4. QueryDSL Queries

    4.1 maven

    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
    <dependency>
    <groupId>com.mysema.querydsl</groupId>
    <artifactId>querydsl-mongodb</artifactId>
    <version>3.6.6</version>
    </dependency>
    <dependency>
    <groupId>com.mysema.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <version>3.6.6</version>
    </dependency>


    <plugin>
    <groupId>com.mysema.maven</groupId>
    <artifactId>apt-maven-plugin</artifactId>
    <version>1.1.3</version>
    <executions>
    <execution>
    <goals>
    <goal>process</goal>
    </goals>
    <configuration>
    <outputDirectory>target/generated-sources/java</outputDirectory>
    <processor>
    org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor
    </processor>
    </configuration>
    </execution>
    </executions>
    </plugin>

    4.2 class

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @QueryEntity
    @Document
    public class User {

    @Id
    private String id;
    private String name;
    private Integer age;

    // standard getters and setters
    }
  5. Implement QueryDslPredicateExecutor

    1
    2
    3
    QUser qUser = new QUser("user");
    Predicate predicate = qUser.name.eq("Eric");
    List<User> users = (List<User>) userRepository.findAll(predicate);

    支持的查询方法:is,startinggWith and endingWith, between

获取项目配置

curl -X GET http://admin:[email protected]:37555/job/geek-icem_backend_build-packages/config.xml

build

curl -X POST http://admin:[email protected]:37555/job/geek-icem_backend_build-packages/build -F ‘json={“parameter”: [{“name”: “Project”, “value”: “geek-icem-gateway”}, {“name”: “Branch”, “value”: “ad”}]}’

curl -X POST http://127.0.0.1:37555/job/geek-icem_backend_build-packages/build –user admin:Jenkins -H ‘cache-control: no-cache’ -F ‘json={“parameter”: [{“name”: “Project”, “value”: “geek-icem-gateway”}, {“name”: “Branch”, “value”: “ad”}]}’

获取构建信息

curl -X GET http://admin:[email protected]:37555/job/geek-icem_backend_build-packages/lastCompletedBuild/api/json

获取构建信息

curl -X GET http://admin:[email protected]:37555/job/geek-icem_backend_build-packages/lastBuild/api/json

获取控制台日志

curl -X GET http://admin:[email protected]:37555/job/geek-icem_backend_build-packages/17/consoleText

lastBuild, lastStableBuild, lastSuccessfulBuild, lastFailedBuild, lastUnstableBuild, lastUnsuccessfulBuild, lastCompletedBuild

获取描述

curl -X GET http://admin:[email protected]:37555/job/geek-icem_backend_build-packages/description

References

jenkins 出现“Error 403 No valid crumb was included in the request ”的解决方案

python-jenkins

使用 Python 操作 Git 版本库 - GitPython

Jenkins获取编译状态

GitPython Tutorial

背景

项目原本是用jedis连接redis,但考虑到需要用redis锁,因此替换为方便快捷的redisson,但是使用redisson之后会报decode error,具体信息如下:

1
2
3
4
5
6
7
8
9
10
11
2019-05-15 13:39:59.973 [redisson-netty-2-3] ERROR o.r.c.h.CommandDecoder [decodeCommand:203]     - Unable to decode data. channel: [id: 0x477c5ced, L:/192.168.4.94:57423 - R:10.10.10.43/10.10.10.43:6379], reply: ReplayingDecoderByteBuf(ridx=102, widx=102), command: (GET), params: [Geek:xxxxx:xxxx]
java.io.IOException: java.lang.NullPointerException
at org.nustaq.serialization.FSTObjectInput.readObject(FSTObjectInput.java:247)
at org.redisson.codec.FstCodec$1.decode(FstCodec.java:228)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:368)
at org.redisson.client.handler.CommandDecoder.decodeCommand(CommandDecoder.java:200)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:140)
at org.redisson.client.handler.CommandDecoder.decode(CommandDecoder.java:115)
at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502)
at io.netty.handler.codec.ReplayingDecoder.callDecode(ReplayingDecoder.java:366)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:278)
阅读全文 »