Java Collections.emptyList() 方法的使用及注意事项

Java Collections.emptyList方法的使用及注意事项

一、emptyList()

作用:

返回一个空的List(使用前提是不会再对返回的list进行增加和删除操作);

好处:

1. new ArrayList()创建时有初始大小,占用内存,emptyList()不用创建一个新的对象,可以减少内存开销;
2. 方法返回一个emptyList()时,不会报空指针异常,如果直接返回Null,没有进行非空判断就会报空指针异常;

注意:此List与常用的List不同,它是Collections类里的静态内部类,在继承AbstractList后并没有实现add()、remove()等方法,所以返回的List不能进行增加和删除元素操作。

示例:

增删操作:

结果:

Java Collections.emptyList() 方法的使用及注意事项

如果需要对collections.emptyList()进行增删操作的话,就需要将collections.emptyList()转换成ArrayList()进行操作。

示例:

结果:

Java Collections.emptyList() 方法的使用及注意事项

0

JAVA8 ConcurrentHashMap.computeIfAbsent 的使用及说明

1.简述

在JAVA8的Map接口中,增加了一个方法computeIfAbsent:

如果mappingFunction(key)返回的值为null或抛出异常,则不会有记录存入map
此方法首先判断缓存MAP中是否存在指定key的值,如果不存在,会自动调用mappingFunction(key)计算key的value,然后将key = value放入到缓存Map。

ConcurrentHashMap中重写了computeIfAbsent 方法确保mappingFunction中的操作是线程安全的。

2.使用方法

可以将原始代码为:

使用computeIfAbsent 代替:

注:mappingFunction方法的结果会作为值直接插入到map中,无需在mappingFunction中再执行put。

3.风险

不能在ConcurrentHashMap.computeIfAbsent方法中进行递归调用,及在mappingFunction方法中不能在进行put相关的操作会造成死循环。

例如:

This is fixed in JDK 9 with JDK-8071667 . When the test case is run in JDK 9-ea, it gives a ConcurrentModification Exception.
https://bugs.openjdk.java.net/browse/JDK-8172951
好在这个问题在java 1.9中已经基本修复了。

0

docker基本命令的使用

01. 使用容器运行Hello World

使用命令行运行以下命令,此命令将启动一个ubuntu的容器并在其中输出 Hello World文本,执行完毕后,容器自动退出。

02. 与容器进行交互

使用命令行运行以下命令,此命令将启动一个ubuntu容器并在其中运行bash交互命令行界面,你可以尝试运行pwd,ls,ps等命令查看容器内环境,就如同远程操作一台服务器一样。

03. 在容器中运行持续任务并管理容器生命周期

使用命令行运行以下命令,此命令将启动一个ubuntu容器并在其中持续运行echo hello world,启动后容器会持续输出hello world文本。

注意当你运行以上命令后,命令行直接退出并没有持续输出hello world文本,这是因为我们使用了-d参数,这时容器在后台运行,你可以通过docker logs命令获取容器内的日志输出,注意替换c3a2为你的容器ID的前四位,如下:

为了查看当前正在运行状态的容器,你可以使用docker ps命令,如下:

你也可以查看到那些没有在运行状态的容器,使用docker ps -a命令,如下:

注意以上出了第一个容器正在运行意外,另外2个ubuntu容器都已经停止,但是容器仍然存在。你可以理解为他们是没有被运行中的应用,而应用的文件存在于你的docker环境中。

现在,你可以通过docker stop {id}命令来停止正在运行的容器,如下:

然后,通过docker rm {id}命令来删除所有未运行的容器,(注意将id替换成你自己的容器ID的前四位)如下:

也可以通过这个命令自动枚举所有容器并停止,删除:

04. 运行web应用并通过浏览器访问

使用命令行运行以下命令

完成后打开浏览器并导航到 http://localhost:8080,你应该可以看到类似以下页面

注意以上命令与之前的最大区别在于使用了-p参数来映射网络端口,这样我们就可以通过容器主机的8080端口来访问容器的80端口,类似于实现了一个简单的NAT。你也可以使用-P(大写)参数来让docker自动分配主机端口,这样可以避免我们手动分配端口造成冲突。

你可以尝试使用以上实验中的docker logs和docker ps等命令查看此正在运行容器的状态和其中的web服务器所输出的日志,如下

运行以下命令式请注意替换dda5为你自己的容器id,同时可以尝试刷新浏览器看到日志的实时输出

另外,你还可以使用docker top {id}命令查看容器中的进程列表

或者通过 docker exec 命令直接进入容器进行操作

小结

至此,你应该已经基本掌握了运行容器的主要命令,下一节中,我们将尝试完成一个新的容器镜像的构建并运行我们自己构建的容器镜像。

0

Java 类加载编程面试题

首先看一段代码

它的输出结果是多少?

还是上面这段代码,把private static SingleTon singleTon = new SingleTon();移到类里的第一行。

打印结果又是什么呢?

 

Java 类加载编程面试题

类加载

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是jvm的类加载机制。

运行时的动态加载给java语言提供了高度的灵活性,动态拓展也成为了java语言的特性之一。

类的生命周期

类的初始化时机

1、jvm 遇到 new 、 putStatic、 getStatic 、invokeStatic 指令时

2、使用反射java.lang.reflect时

3、初始化子类时,若父类还没初始化,那也要初始化父类

4、启动jvm虚拟机时,需要初始化指定的主类

5、java 7 动态语言的 MethodHandler 在解析时,也要对对应的类进行初始化

以上五种类型称为对一个类进行主动引用,除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。

1、通过子类引用父类的静态变量,不会导致子类初始化

2、通过数组定义来引用类,不会触发此类的初始化

0

Git远程协作的工作流程

Git远程协作的工作流程

流程细节

1. Fork

在github上你要贡献的repo(eg.http://github/remote/test.git)之后称上游仓库。点击fork,将上游仓库fork到你的github,之后称为远程库(eg.http://github/chercher/test.git)

2. Clone

选择本地文件夹,之后称为本地库

3. 创建dev分支

进入文件夹中,创建dev分支作为你的开发分支,当你完成了这个开发分支的时候直接将这个分支的内容push到你的远程库。一般一个分支对应一个issue,开发完毕后即可销毁 git checkout -b dev 创建并切换至dev分支,是git branch dev + git checkout dev

4. 创建upstream分支

upstream分支是用于同步上游仓库的,可以同步其他人对上游仓库的更改 git remote add upstream http://github/remote/test.git 这时候用git remote 可以查看远程分支,git remote -v 可以查看具体路径 这时候应该有origin、upstream两种分支且分别有fetch和push的路径,origin是你的远程库,upstream是你的上游仓库 tips: 如果远程分支路径出错了,git remote set-url branch_name new_url 替换为具体的你的出错的分支名和新的路径即可

5. 同步上游仓库

在提交自己的修改之前,先同步上游仓库到master

6. 修改文件push到远程库

对本地库进行修改后,git add changed_file & git commit -m"message" 添加文件到暂存区然后提交,写入相应信息。 git push origin dev:dev 这时你的远程库将会多出一个dev分支

7. 提出pull request

这时候在你的远程库中点击create pull request,就可以等待别人review你的代码后merge入上游仓库了

8. 合并commit

一个issue有时候并不是一次commit就可以完成的,这时候就涉及到洁癖患者们用rebase合并commit的过程了 第一次commit的时候并不需要做rebase的操作,rebase是将之后的多次commit合并到之前的一个commit当中 以第二次修改为例,在commit之后进行 git rebase -i HEAD~2

补充

git 不留记录回滚到指定版本,并推送到远程分支

 

0

MySQL中truncate函数自动转型的精度问题

MySQL中truncate函数的精度问题

truncate函数

TRUNCATE(X,D) 是MySQL自带的一个系统函数。
其中,X是数值,D是保留小数的位数。
其作用就是按照小数位数,进行数值截取(此处的截取是按保留位数直接进行截取,没有四舍五入)。

现在有这样一个需求,某个表有一个varchar类型的字段,字段内存了一串浮点数。 现在我想要截取到该字段的小数点后两位。

得到的结果毫无疑问,应该是5.02,结果也确实为5.02

那下面这条SQL的结果,你能猜到么?

你可能会说,这还不简单,肯定还是5.02啊。

可是,神奇的一幕出现了,该SQL的执行结果为5.01。 为什么会少了0.1呢,这时就要说到MySQL的TRUNCATE函数做了什么。 因为TRUNCATE函数会对varchar类型做自动转型,产生了精度损失。 在转型后得到的结果其实是5.019999999。然后再做截取,那么结果自然是5.01了。

下面来执行这条SQL

由于没有使用varchar类型,所以TRUNCATE函数没有做自动转型操作,所以并没有产生精度损失,结果为5.02.

cast函数

cast是一种数据类型转换的函数,函数将任何类型的值转换为具有指定类型的值,语法格式如下所示:

CAST ( expression  AS  data_type)

  • expression:任何有效的MySQL表达式或者一些字符串数据。
  • AS:用于分隔两个参数,在AS之前的是要处理的数据,AS之后是要转换的数据类型。
  • data_type:系统所提供的数据类型,这里不能使用用户定义的数据类型。
  • MySQL所能使用的可以是以下类型之一:CHAR(字符型)、DATE(日期)、TIME(时间)、DATETIME(日期时间型)、DECIMAL(浮点数 float)、SIGNED(整数 int)。

依然用5.02这个数字举例:

看似没什么问题,结果也确实是5.02。 但如果使用的是5.029呢?

你会发现,结果变成5.03了,没错,四舍五入了。 执行出来的结果和truncate函数的结果不尽相同,所以也并不能完美满足一开始提出的需求。

总结

这个问题产生的根本问题是使用了varchar类型来存浮点数,并且还需要在数据库内做运算导致的。
我认为解决这个问题的最好时机是在设计时,使用浮点类型来存储该字段。

0

Java的四种引用类型

前言

上一篇文章我们讨论了ThreadLocal相关的问题,其中内存泄漏的原因是因为Thread对象内部维护的ThreadLocalMap,这个Map的Key是弱引用类型(WeakReference),而Value是强引用类型,如果Key被回收,Value却不会被回收。本期让我们详细分析一下Java的四种引用。

Java的引用

从 JDK1.2 开始,对象的引用从原来的1种级别增加到了4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用
Java.lang.ref 是 Java 类库中比较特殊的一个包,它提供了与 Java垃圾回收器密切相关的引用类。StrongReference(强引用),SoftReference(软引用),WeakReference(弱引用),PhantomReference(虚引用)。这四种引用的强度按照上面的顺序依次减弱。

引用类图

Java的四种引用类型

强引用(StrongReference)为JVM内部实现。其他三类引用类型全部继承自Reference父类。

强引用

最常用到的引用类型,StrongReference这个类并不存在,而是在JVM底层实现。默认的对象都是强引用类型,继承自Reference、SoftReference、WeakReference、PhantomReference的引用类型非强引用。
示例:

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

软引用

软引用是一种比强引用生命周期稍弱的一种引用类型。在JVM内存充足的情况下,软引用并不会被垃圾回收器回收,只有在JVM内存不足的情况下,才会被垃圾回收器回收。 所以软引用的这种特性,一般用来实现一些内存敏感的缓存,只要内存空间足够,对象就会保持不被回收掉,比如网页缓存、图片缓存等。
示例:

弱引用

弱引用是一种比软引用生命周期更短的引用。他的生命周期很短,不论当前内存是否充足,都只能存活到下一次垃圾收集之前。 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用, 如果弱引用所引用的对象被垃圾回收, Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
示例:

执行结果大概率为:

示例中有一个细节,如果不加Thread.sleep(20000);这段代码,WeakReference大概率不会被回收,因为System.gc();仅仅是提醒JVM该执行垃圾回收了。 但JVM具体什么时候执行,并不由我们的程序控制,所以在发起提醒后,要sleep一段时间等待JVM发生垃圾回收。

虚引用

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue) 联合使用。 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

上面程序执行的结果一直是null,和没有引用几乎一致。

引用类型对比

引用类型 取得目标对象方式 垃圾回收条件 是否可能内存泄漏
强引用 直接调用 不回收 可能
软引用 通过 get()方法 视内存情况回收 不可能
弱引用 通过 get()方法 发生gc时回收 不可能
虚引用 无法取得 随时可能被回收 不可能

各种引用的使用场景

我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

最容易想到的就是缓存了,内存不足时释放部分数据,类似Redis/Ehcache之类的淘汰策略。

下面列出一下JDK/框架中的应用场景:

  • java.util.WeakHashMap – jdk
  • java.util.concurrent.ArrayBlockingQueue – jdk
  • org.springframework.util.ConcurrentReferenceHashMap – spring中大量使用了此缓存,包括spring-BeanUtils
0

Redis缓存雪崩、击穿、穿透理解

前言

写这篇文章的契机是同事最近面试遇到的一道神奇面试题。 面试官先让同事解释了下Redis缓存雪崩,击穿,穿透的含义和解决方案。 然后刁钻的问题来了,缓存雪崩后如何恢复。下面会对上述问题逐一分析。

缓存雪崩

服务中的热点数据会做缓存,一般就是两种逻辑,第一种就是通过定时任务去刷新,第二种就是查缓存查不到后更新数据。

缓存雪崩指的是缓存同时大面积失效的情况,一般存在于上面说的第一种缓存逻辑中。如果能够通过定时任务刷新缓存, 刷新后缓存数据的失效时间会集中在一个很小的时间区间,如果这个时候同时有大流量打进来(比如秒杀场景),后果不堪设想。

轻则数据库cpu飙升,服务报警的轰炸会让半夜正在睡觉的你不得安宁,重则数据库被打挂,缓存这块的责任人当月绩效直接拿C。 用户点了半天没反应,睡觉去了,并且卸载了你们的app。

但其实解决这种情况的方法很简单。既然不可以同时大面积失效,那我把缓存失效的时间段拉长不就好了。每个Key的失效时间都加个随机值, 这样就可以让缓存不会大面积同时失效,数据库的压力也会小很多。

上面的解决方案已经很简单了,但还有个更简单的方法,就是热点数据永不过期,有更新操作就更新缓存。 这块可以抽取出来一个服务,专门做缓存的更新,传输使用MQ,用多Topic,多partition分片路由的方式保证顺序。

缓存穿透

缓存穿透指的是缓存和数据库中都没有数据,而用户/黑客不断发起查询请求去查询不存在数据,导致数据库压力过大,最终把数据库打卦的情况。

缓存穿透的影响比缓存雪崩的影响要小一些,现在云服务器都自带防御,花点钱还能买高防, 如果没有比较大的ip肉鸡资源池,想打透服务器的防御,其实并不简单。 但我们还可以使黑客的攻击成本变得更高,就是添加各种校验,业务相关的,有的没的都校验上,还可以增加用户鉴权。

还有一个方案就是取不到就写null或对应的错误信息到redis,缓存有效时间设置的短一些,防止影响到正常的业务。

如果你还嫌不够安全,Redis自己实现了一个布隆过滤器(Bloom Filter),原理就是利用它特有的数据结构和算法快速判断出key 在库中是否存在,如果不存在直接return。这么做如果库中的数据量特别大的时候,其实成本是相对高的, 虽然布隆过滤器的内存占用相较HashMap要低很多,但一般情况下,我们并不太需要这种级别的防御。

传统的布隆过滤器并不支持删除操作。但是名为 Counting Bloom filter 的变种可以用来测试元素计数个数是否绝对小于某个阈值,它支持元素删除。

这个世界上其实没有绝对安全的系统,黑客的玩法五花八门,你在软件系统上防住了,人家还可以玩社会工程学。 这块就算做了这么多其实就是提升黑客的攻击成本,假设黑客要花100W RMB的资源攻击才能给你攻破,但只能赚10W RMB, 冒着入狱的风险亏90W,是个正常人都不会去做的。只要攻击者获取的收益远低于攻击成本,你的系统就是相对安全的。

缓存击穿

缓存击穿指的是一个非常热点的Key失效了,巨大的并发量一下都打到了数据库,这种情况其实不是很容易给数据库打挂, 但会把数据库的连接占满,造成服务假死,等缓存的数据进了Redis,就会恢复。但这样造成的用户体验也是极差的。

这种情况的解决方案也是很简单的,就是热点数据永不过期,然后通过分布式锁的方式查库更新数据。 下面是一段伪代码。

缓存雪崩后如何恢复

上面的问题都不是很难,也都有很明确的解决方案,但是这个问题有所不同,雪崩如果提前准备比较充分的情况下, 基本是不可能发生的,但如果真的发生了,数据库已经被冲垮的情况下要如何让服务恢复,如何不让用户的大并发给数据库一次又一次的打死, 都是比较有意思的问题。

虽然我没有真正遇到过缓存雪崩的情况(生产中,这个级别的生产事故真的很难碰到),但是基本是执行这几个步骤。

第一步,修改入口网关的限流策略,给个错误页面反馈给用户(比如系统繁忙,请稍后再试什么的),虽然体验很差,但当务之急是把系统恢复过来。

第二步,把挂掉的数据库启动起来,通过脚本或者什么方式,把热点数据写到缓存中,即缓存快速预热 (缓存都雪崩了,说明预防不到位,那写热点数据的脚本或工具是否存在,这里画个问号)

接着就分两种情况,缓存已预热的情况,和缓存未预热的情况。如果是已预热的情况, 那么限流策略可以放开的稍微激进一点,并且时刻关注数据库的各项指标,让用户进来刷缓存,如果这波抗住了,基本系统就恢复正常了, 趁着这段时间赶紧亡羊补牢,找到雪崩的原因,补救好后紧急上线,这次危机就算彻底渡过了。

如果未预热的情况,那就只能用保守一点的限流策略,一点点的通过用户访问的方式把数据写到缓存。 阶梯式的放开限流后,找原因,fix bug,系统最终也会恢复正常,但是系统不可用的时间相对第一种情况要多一些。

总之,缓存雪崩重在预防,在已经雪崩的情况下,补救的方案基本都大同小异,用户体验必然是不好的。

总结

缓存雪崩指的是缓存同时大面积失效的情况,通过过期时间加随机数的方式解决。

缓存穿透指的是缓存和数据库中都没有数据,而用户/黑客不断发起查询请求去查询不存在数据,导致数据库压力过大,最终把数据库打卦的情况。 通过服务器自身防御,写null,布隆过滤器的方式解决。

缓存击穿指的是一个非常热点的Key失效了,巨大的并发量一下都打到了数据库的情况。 通过加分布式锁的方式解决。

缓存雪崩后恢复,即限流,缓存预热,阶梯式放开限流,修复bug,紧急上线,系统恢复。

0

Intellij Idea debug 的异常断点调试使用

有时候我们预估系统代码会出现什么异常,但又不知道异常具体会发生在哪,就需要用到异常断点,这个断点和普通断点不大一样,不需要在代码行左侧打,而是直接通过图中所示配置:


当程序走到异常的那一行,会自动停留在那一行。

RunTimeException 比较宽泛,我们可以缩小自己的范围,比方时间格式化,若是传的时间格式不对,会抛出java.time.DateTimeException,那我们就可以针对该异常进行定位debug调试。

0