Netty4 实现数据传输中间层处理

Netty4 实现数据报文的接收/拆包/重组/转发


完整代码:netty4-datatrans


前言

由于项目中有对建筑的 GPS 定位模块,而 GPS 仪器作为客户端连接,传输的是标准的 GPGGA 语句,也就是多个客户端对一个服务端发送数据,节约端口资源故配置的是同一个端口,此时服务端接收到的 GPGGA 数据却并不能分辨出到底是哪一个客户端发送的,由此决定写一个数据中间层处理,给报文重组根据规则加上唯一标识符。

正题

根据实际需求我这写了服务端和客户端,即该脚本部署的机器同时作为 server 和 client。

可以进行对接收数据的拆包/逻辑重组/添加数据标识符等等 DIY 操作,再进行定向转发。

客户端处理

  • 添加了 Listener 启动时可监听判断 client 是否正常启动,即对应 server 端口是否启用监听
    • 若通道连通,正常连接进行数据传输
    • 若通道未连通,则调用 schedule 进行定时重连操作

GPSTransClientConnectionListener.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (!future.isSuccess()) {
final EventLoop loop = future.channel().eventLoop();
loop.schedule(new Runnable() {
@Override
public void run() {
System.err.println("client reconnecting ...");
try {
client.connect(GPSTransConsts.REMOTE_IP, Integer.parseInt(GPSTransConsts.REMOTE_PORT));
} catch (NumberFormatException | InterruptedException e) {
System.out.println("restart err...");
e.printStackTrace();
}
}
}, 5L, TimeUnit.SECONDS);
} else {
System.out.println("client connected ...");
}
  • 同时若是启动成功但是运行一段时间后 server 端口关闭监听了,那也要进行重连处理,可以根据实际需求更改

GPSTransClientHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.err.println("server disconnect ...");
success = false;
// 使用过程中断线重连
final EventLoop eventLoop = ctx.channel().eventLoop();
eventLoop.schedule(new Runnable() {
@Override
public void run() {
try {
client.connect(GPSTransConsts.REMOTE_IP, Integer.parseInt(GPSTransConsts.REMOTE_PORT));
} catch (Exception e) {
System.out.println("restart err...");
e.printStackTrace();
}
}
}, 5L, TimeUnit.SECONDS);
super.channelInactive(ctx);
}

由于是不停的进行转发操作,所以需要循环处理。

定义了 private static volatile boolean success; 作为数据发送线程的循环标志符。

当连接成功时,success 置为 true,当连接断开时,success 置为 false。

volatile 修饰故保证了其可见性。

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
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelActive ...");
success = true;
System.out.println("send data to server ...");
// 必须另开线程处理,否则会在这个方法中出不去
new Thread() {
@Override
public void run() {
while (success) {
if (!GPSTransConsts.NAME_MESS.isEmpty()) {
StringBuilder sb = new StringBuilder();
GPSTransConsts.NAME_MESS.values().forEach(value -> {
sb.append(value);
});
ByteBuf resp = Unpooled.copiedBuffer(sb.toString(), CharsetUtil.UTF_8);
ctx.writeAndFlush(resp);
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("client thread exit ...");
};
}.start();
super.channelActive(ctx);
}

服务端处理

接收多个客户端数据,根据其 IP 来定位设备,再进行报文拆包重组 DIY,存储到内存中便于 client 模块进行转发。

GPSTransServerHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

InetSocketAddress ipsocket = (InetSocketAddress) ctx.channel().remoteAddress();
// 获取客户端 IP
String clientIP = ipsocket.getAddress().getHostAddress();
int index = clientIP.lastIndexOf(".");
String ipNum = clientIP.substring(index + 1);
ByteBuf in = (ByteBuf) msg;
String message = in.toString(CharsetUtil.UTF_8);
if (message.startsWith("$")) {
message = message.replace("$", "#");
if (!GPSTransConsts.IP_NAME.containsKey(ipNum)) {
System.err.println(ipNum + "未配置!");
return;
}
String name = GPSTransConsts.IP_NAME.get(ipNum);
message = "#" + GPSTransConsts.IP_NAME.get(ipNum) + message + "\r";
GPSTransConsts.NAME_MESS.put(name, message);
}
// 释放
super.channelRead(ctx, msg);
}

因为我们没有进行 write 和 flush 操作,所以需要进行释放。

配置文件

为了方便配置的修改,可以把项目打成 jar 包,然后在同目录下新建一个 config 文件夹,把 gps.properties 丢进去,完事。


头晕九月(2019)

为什么现在这个头越来越疼了?

好难啊,好南啊。


最近因为好多电视剧都太优秀了,完全停不下来啊,一部接着一部,晚上回去好久都没看过书了……有几本买来都两年了,塑封都还在呢。一想我现在才25岁,正当青春活力年少,怎能沉迷于银幕中,看完这几部就决心告一段落了,一部有五六十集呢。

上次说的杨浦图书馆,好嘛,就那次去了一趟,环境很重要,在屋里虽也能看但心还是无法长静下来。这次中秋无所事事,无计可划,不如早些时辰去占个位儿,带上几个白面馍馍,看个一天大半天书的,也是个快乐的想法。

中秋

今年中秋不回家,国庆回吧,家离上海倒是不远,只是来回也得费点心力,在家歇一晚上,第二天又得忙活准备回来。到时候要给家里打个电话,家是温暖的港湾,是背后的倚靠。大中华中秋团圆夜,今年得独自倚窗赏月喝闷酒了。说到酒,我发现白酒喝多了我就会想吐。

不出意外,窝着呆了三天。

诸事不顺,病魔缠身。

感冒见好,又来低烧。

躺在床上,汗浸衣裳。

一觉醒来,面目荒凉。


祖国七十大寿!


Java 多线程程序的性能调校

本文摘抄自《Java 多线程编程实战指南》核心篇 第十二章小结

本章介绍了与 Java 多线程程序紧密相关的性能调校常用技术。


  Java 虚拟机自 Java 6 开始对内部锁进行了若干优化:锁消除、锁粗化、偏向锁以及适应性锁。除锁消除是 Java 7 开始引入的,其他优化均是在 Java 6 开始引入的,这些优化仅在 Java 虚拟机的 server 模式下起作用。这些优化默认都是开启的,且多数优化都可能依赖于 JIT 的内联优化,并且其本身也可能是通过 JIT 编译实现的。因此,这些优化都有其开销。锁消除优化能够彻底消除锁的开销,它依赖于逃逸分析技术。锁粗化优化能够减少线程申请/释放锁的频率,其代价是使临界区长度变大,从而可能导致线程在申请锁时的等待时间变长。偏向锁优化可以减小锁的申请/释放的开销,它不适用于争用程度较高的锁。适应性锁优化可以减小锁申请的开销,有利于减少上下文切换。

  锁的开销主要是由争用锁引起的。这些开销主要包括:上下文切换与线程调度开销、内存同步、编译器优化受限的开销以及限制可伸缩性。降低锁的开销可以从使用锁的替代品、降低锁的争用程度以及减少线程所需申请的锁的数量这几个方面入手。

  使用可参数化锁可以减少线程所需申请的锁的数量从而降低锁的开销,但是它在一定程度上破坏了封装性。

  减小临界区的长度可以减少锁的持有时间,从而降低锁的争用程度。减小临界区的长度有利于适用性锁优化发挥作用。在不影响线程安全的前提下,将临界区中的阻塞式 I/O 等阻塞操作以及较耗时的操作挪动到临界区之外可以减小临界区的长度。

  减小锁的粒度可以降低锁的申请频率从而降低锁的争用程度。减小锁的粒度常用技术包括锁拆分技术和锁分段技术。锁拆分技术在高争用情况下的效果可能并不明显;锁分段技术会使得对整个对象进行加锁比较困难乃至不可能。

  减少上下文切换可以从这几个方面入手:控制线程数量、避免在临界区中执行阻塞式 I/O 等阻塞操作、避免在临界区中执行比较耗时的操作和减少 Java 虚拟机垃圾回收。

  运用多线程设计模式也有助于提升多线程程序的性能,但是程序的复杂性也可能相应增加。

  伪共享产生的前提是多个线程访问被缓存到同一个缓存中的不同变量,它会导致大量的缓存未命中,从而增加内存访问操作的开销。了解 Java 对象的内存布局有助于分析与消除伪共享。Java 对象内存布局的规则包括:对象是以 8 字节为粒度进行对齐的、对象中的实例字段并非依照其源代码声明顺序排列以及继承自父类的实例字段不会与类本身定义的实例字段混杂在一起进行存储等。使用 jol 工具可以查看具体对象的内存布局情况。判断伪共享是否存在可以从分析多个线程是否存在共同的共享变量入手,并通过 jol 以及 Linux 内核工具 perf 来进一步分析与确认。伪共享可通过手工填充、自动填充以及降低共享变量的访问频率这几个方面来消除与规避。手工填充和自动填充可以在无须调整程序算法的前提下消除伪共享。手工填充的缺点比较多,使用该方法我们必须知道缓存行的宽度、Java 对象的具体内存布局,这使得该方法存在硬件、软件层面的可移植性问题,并对人员的要求比较高。并且,我们还需要避免手工填充的字段被 Java 虚拟机优化掉,自动填充依赖于 @Contented 注解,它避免了手工填充的缺点,但是其消耗的额外空间更多。Java 虚拟机对自动填充的支持需要通过 Java 虚拟机的开关 “-XX:-RestrictContended” 开启。虽然减少共享变量的访问频率所带来的效果可能比较明显,但是由于它可能涉及程序算法的调整,因此其适用范围比较有限。

本章知识结构图


多线程编程的硬件基础与 Java 内存模型

本文摘抄自《Java 多线程编程实战指南》核心篇 第十一章小结

本章介绍了多线程编程的硬件基础以及 Java 内存模型的基础知识。


  高速缓存是一个存取速率远比主内存大而容量远比主内存小的存储部件,其引入弥补了处理器与主内存处理能力之间的鸿沟。高速缓存相当于一个由硬件实现的散列表,其键为内存地址,其值为从内存读取或者准备写入内存的数据。高速缓存中的每个桶可包含若干缓存条目。缓存条目中的 Tag 部分包含了内存地址的高位部分比特;Flag 部分指示了缓存条目的有效性;缓存行用于存储从内存读取或者准备写入内存的数据,其容量在 16~256 字节之间不等,一个缓存行可用于存储多个变量。缓存命中意味着待读取或者写入内存的数据在高速缓存中存在相应的副本,这可以提升内存访问效率。缓存未命中包括读未命中和写未命中,它不利于性能,但是由于高速缓存容量的限制又往往是不可避免的。Linux 内核工具 perf 可用来查看缓存未命中情况。现代处理器多采用多级高速缓存,典型的高速缓存层级包括 L1 Cache/L2 Cache 和 L3 Cache。

  缓存一致性协议保障了多个处理器上高速缓存中的数据副本的数据一致性,避免了一个处理器读取到共享变量的旧值以及避免了一个处理器对共享变量所做的更新丢失。MESI 协议是一个广为使用的缓存一致性协议,在该协议下缓存条目的 Flag 可能值包括:M/E/S/I。内存读/写操作是通过处理器发送与接收相关消息并更新缓存条目的 Flag 实现的。这些消息包括:Read/Read Response、Invalidate/Invalidate Acknowledge、Read Invalidate、Writeback。

  写缓冲器与无效化队列的引入弥补了 MESI 协议的性能弱点。

  写缓冲器是处理器内部的一个容量比高速缓存还小的私有高速存储部件。其引入使得内存写操作的执行处理器无须等待其他处理器回复 Invalidate Acknowledge/Read Response 消息便可以执行其他指令,从而减小内存写操作的延迟。写缓冲器能导致写线程对共享变量所做的更新无法被其他处理器同步过去。存储转发技术使得一个处理器可以直接从写缓冲器中读取该处理器先前执行的写操作的结果,但是它也可能导致可见性问题。另外,写缓冲器还会导致 StoreLoad 重排序和 StoreStore 重排序。

  无效化队列的引入使得处理器在接收到 Invalidate 消息之后可以立即回复 Invalidate Acknowledge 消息,这减少了发送 Invalidate 消息的处理器的等待时间。无效化队列可能使写线程对共享变量所做的共享无法反映到读线程执行处理器的高速缓存中,即导致可见性问题。无效化队列可以导致 LoadLoad 重排序。

  从硬件的角度来看,可见性的保障是通过写线程和读线程配对使用存储屏障和加载屏障实现的。存储屏障能够冲刷写缓冲器使得写线程对共享变量所做的更新能够被其他处理器同步,加载屏障能够清空无效化队列,使得写线程对共享变量所做的更新能够反映在读线程执行处理器的高速缓存之中。

  获取屏障相当于 LoadLoad 屏障和 LoadStore 屏障的组合,释放屏障相当于 StoreStore 屏障和 StoreLoad 屏障的组合。LoadLoad 屏障相当于加载屏障;而 StoreLoad 屏障是“全能型”屏障,它既可以充当存储屏障,也可以充当加载屏障。

  Java 虚拟机(JIT 编译器)为了确保 final 关键字的语义,会在 final 字段初始化与构造器返回之前插入一个 StoreStore 屏障,这使得 final 字段初始化操作无法被重排序到构造器之外,从而确保了构造器返回之后相应对象的 final 字段总是初始化完毕的。有序性的保障是通过写线程与读线程配对执行释放屏障和获取屏障实现的,同样这些屏障也是 Java 虚拟机(JIT 编译器)替我们的应用程序插入的。Java 虚拟机(JIT 编译器)会在 volatile 变量写操作之后插入一个 StoreLoad 屏障,该屏障不仅充当了存储屏障以冲刷写缓冲器,它还充当了加载屏障以清空无效化队列从而消除了存储转发技术的副作用。Java 虚拟机(JIT 编译器)会在 volatile 变量读操作前插入一个 LoadLoad 屏障,该屏障充当了加载屏障,用于清空无效化队列。

  Java 内存模型从“什么”(What)的角度来回答线程安全有关问题,JSR 133 对 Java 内存模型进行了增强和修复。Java 内存模型规定,long/double 型变量以外的任何变量的读/写操作具有原子性;volatile 变量修饰的 long/double 型变量的读/写操作也具有原子性。long/double 型普通变量的读/写操作的原子性取决于具体的 Java 虚拟机。happens-before 从可见性的角度对有序性进行描述。happens-before 关系具有传递性和累积效果。Java 内存模型定义的 happens-before 规则包括:程序顺序规则/内部锁规则/volatile 变量规则/线程启动规则和线程终止规则。Java 标准库本身也定义了一些 happens-before 规则。从语言的层面来看,这些规则是通过使用 Java 的同步机制实现的;从底层的角度来看,这些规则是由 Java 虚拟机/编译器以及处理器一同协作来落实的,内存屏障则是 Java 虚拟机/编译器和处理器之间的“沟通”纽带。

本章知识结构图


Java 异步编程

本文摘抄自《Java 多线程编程实战指南》核心篇 第九章小结

本章介绍了同步计算与异步计算的概念,并介绍了 Java 平台对异步计算所提供的相关 API。


  从单个任务的角度来看,任务的执行方式可以是同步的,也可以是异步的。同步方式的优点是代码简单/直观,缺点是它往往意味着阻塞,因此不利于系统的吞吐率。异步方式的优点则是它往往意味着非阻塞,因此有利于系统的吞吐率,其代价是相对复杂的代码和额外的开销。阻塞/非阻塞是任务执行方式的属性,它们与任务执行方式没有必然的联系:同步任务既可能是阻塞的,也可能是非阻塞的;异步任务既可能是非阻塞的,也可能是阻塞的。对于同一个任务,我们既可以说它是同步任务也可以说它是异步任务,这取决于任务的执行方式以及我们的观察角度。

  Runnable/Callable 接口是对任务处理逻辑进行的抽象,而 Executor 接口是对任务的执行进行的抽象。Executor 接口使得我们能够对任务的提交与任务的具体执行细节进行解耦,这为更改任务的具体执行细节提供了灵活性与便利。ExecutorService 接口是对 Executor 接口的增强:它支持返回异步任务的处理结果/支持资源的管理接口/支持批量任务提交等。ThreadPoolExecutor 是 Executor/ExecutorService 接口的一个实现类。实用工具类 Executors 为线程池的创建提供了快捷方法。Completion Service 接口为异步任务的批量提交以及获取这些任务的处理结果提供了便利,其默认实现类为 ExecutorCompletionService。

  FutureTask 是 Java 标准库提供的 Future 接口实现类,它还实现了 Runnable 接口。因此,FutureTask 可直接用来获取异步任务的处理结果,它可以交给专门的工作者线程执行,也可以交给 Executor 实例执行,甚至由当前线程直接执行(同步)。一般来说,FutureTask 是一次性使用的,一个 FutureTask 实例代表的任务只能够被执行一次。如果需要多次执行同一个任务,那么可以考虑 AsyncTask 类。

  计划任务的执行方式包括延迟执行和周期性执行。ScheduledThreadPoolExecutor 是 ScheduledExecutorService 接口的默认实现类,它可以用于执行计划任务。ScheduledFuture 接口可用来获取延迟执行的计划任务的处理结果。如果要获取周期性执行的计划任务的处理结果,可以使用自定义的 AsyncTask 类。周期性执行的计划任务,其执行周期并不是固定的,而是受任务单次执行耗时的影响:提交给 scheAtFixedRate 方法执行的计划任务,其执行周期为 max(Execution Time,period);提交给 scheduleWithFixedDelay 方法执行的计划任务,其执行周期为 Execution Time + delay。计划任务在其执行过程中如果抛出未捕获的异常,那么该任务将不会再被执行。

本章知识结构图


线程管理

本文摘抄自《Java 多线程编程实战指南》核心篇 第八章小结

本章介绍了如何将线程管控起来以便高效/可靠地利用线程这种有限地资源。


  线程组是 Thread.UncaughtExceptionHandler 的一个实现类,它可以帮助我们检测线程的异常终止。多数情况下,我们可以忽略线程组这一概念以及线程组的存在。

  Thread.UncaughtExceptionHandler 接口使得我们能够侦测到线程运行过程中抛出的未捕获的异常,以便做出相应的补救措施,例如创建并启动相应的替代线程。一个线程在其抛出未捕获的异常而终止前,总有一个 UncaughtExceptionHandler 实例会被选中。被选中的 UncaughtExceptionHandler 实例的 uncaughtException 方法会被该线程在其终止前执行。UncaughtExceptionHandler 实例 > 线程所在线程组 > 默认 UncaughtExceptionHandler。

  线程工厂 ThreadFactory 能够封装线程的创建与配置的逻辑,这使得我们能够对线程的创建与配置进行统一的控制。

  利用条件变量我们能实现线程的暂挂与恢复,用于替代 Thread.suspend()/resume() 这两个废弃的方法。

  线程池是生产者——消费者模式的一个具体例子,它能够摊销线程的创建/启动与销毁的开销,并在一定程度上有利于减少线程调度的开销。线程池使得我们能够充分利用有限的线程资源。ThreadPoolExecutor 支持核心线程大小以及最大线程池大小这两种阈值来控制线程池中的工作者线程总数。ThreadPoolExecutor 支持对核心线程以外的空闲了指定时间的工作者线程进行清理,以减少不必要的资源消耗。RejectedExecutionHandler 接口使得我们能够对被线程池拒绝的任务进行重试以提高系统的可靠性。Future 接口使得我们可以获取提交给线程池执行的任务的处理结果/侦测任务处理异常以及取消任务的执行。当一个线程池实例不再被需要的时候,我们需要主动将其关闭以节约资源。ThreadPoolExecutor 提供了一组能够对线程池进行监控的方法,通过这些方法我们能够了解线程池的当前线程池大小/工作队列的情况等数据。同一个线程池只能用于执行相互独立的任务,彼此有依赖关系的任务需要提交给不同的线程池执行以避免死锁。我们可以通过线程工厂为线程池中的工作者线程关联 UncaughtExceptionHandler,但是这些 UncaughtExceptionHandler 只会对通过 ThreadPoolExecutor.execute 方法提交给线程池的任务起作用。

本章知识结构图


糊涂八月(2019)

稀里糊涂稀里糊涂啊

狂风暴雨 阳光烈焰 雾雨蒙蒙


昆明

不出意外,跟同事一起又跑了趟昆明。没想到的是呆了七天,干了七天的活,出去耍的时间都没有,惨兮兮。

呈贡大学城也确实偏了点,连市区都没得去,只能在附近随便吃点喝点。米粉确实是正宗还实惠,十三块钱吃得我啤酒肚了,可总不能天天搞米粉吃吧。

阴凉地小风吹得,舒服极了,站在太阳直射下却不一会就晒得通红,热得要炸。

从去的那天开始内火就开始旺啊,上次来也不见得体内火气这么大,还是天气太干燥了,不适应。

水果都是按公斤计价的,买了点桃子芒果,便宜倒也不是很便宜,总之比上海还是要便宜点的,在上海都不舍得买水果吃。

一直心心念念想要吃炸虫子,吃爆浆的那种,可是到走的那天也没能吃着,亏边上同事还是在昆明读过几年书的,真的一点不靠谱啊。后来去了家农家乐随便吃了点,大盘的菜,量大,当然相对昆明的物价来说也不能算便宜。

虽说附近是大学城,可真也不巧,刚好暑假。校园里也都没几个人,看到的大多是对对小情侣,不禁感叹:大学生活好啊!

地区会影响人的外貌习性,总的看来,个人还是较喜欢沿海的南方妹子,下次有机会去传说中的成都/长沙养养眼,见识见识。

唉,出差真的也是累得慌啊。

七夕

什么七不七的,那天半夜才在浦东下的飞机,热乎饭都没能吃上口。

台风利奇马

这个台风是真的厉害了,刚好周末登陆各种预警,那天真是狂风催嫩叶啊。刚登陆温岭的时候风速巨大,陆地后强度减弱,听新闻说也有不少人遭难,天灾。

已经造成浙江损失一百多亿了。

周五晚已经是暴雨连连了,想着周末也铁定出不了门了就在超市备了两天的伙食,自己随便开开火得了。周六确实是下了一天的雨,挂了一天的风,到了周日台风过去后雨过天晴,一切又像是都没发生过一样,只有地上的落叶能说明一切。

无锡

无锡太湖试验厅项目需要测个索力数据,没人啊,派我过去了呗。无锡太湖一日游,也就在边角眺望了一会。

跟同事晚上吃了个自助,喝了点小酒,本想着再去洗个脚按个摩,奈何吃饱喝足累得慌,还是回去早点休息吧。

做的事越来越杂,好像已经开始脱离了最初进来时的职责。

周末

现在有了个喜欢做饭的室友,周末比之前是好多了,对面就是超市买菜也方便。弄个一荤两素一汤,健康又实惠。不过这次周末有点奢侈,大荤大素的整了不少,室友还把“珍藏”的红酒给贡献了出来。说好的吃素,这整的,以后吃吃蔬菜就得了。

身上的湿气重啊,房间里放的除湿袋不到一星期就积满了水,也可能跟我那朝北的小房间有关系,不过最近感觉全身都难受,站着不动都能往外出水来着。看看找个时间需要去拔个罐去去湿,出门在外要对自己好点。

压抑

不知道为什么,最近情绪不太高总觉得很压抑,胸闷有时候喘不过气来。

很难受,周末就去找了同学一起聚一聚,也有好长时间没有聚过了,大家都多多少少有点压抑。

胡吃海玩了之后回来还是不太好,我这是怎么了?凌晨两点的杭州夜景也治愈不了我。

什么都没意思,好难受啊,何以解忧


线程的活性故障

本文摘抄自《Java 多线程编程实战指南》核心篇 第七章小结

本章介绍了常见的线程活性故障以及相应的规避措施。


  死锁会导致相关线程一直被暂停使得其任务无法进展。产生死锁的必要条件包括:资源互斥/资源不可抢夺/占用并等待资源以及循环等待资源。我们可以通过查看线程转储手工检测死锁,也可以利用 ThreadMXBean.findDeadlockedThreads() 方法进行死锁的自动检测。死锁的规避方法包括:粗锁法(使用一个粗粒度的锁代替多个锁)/锁排序法(相关线程使用全局统一的顺序申请锁)/使用 ReentrantLock.tryLock(long,TimeUnit)来申请锁/使用开放调用(在调用外部方法时不加锁)以及使用锁的替代品。使用内部锁或者使用 lock.lock() 申请的显式锁导致的死锁是无法恢复的;使用 lock.lockInterruptibly()申请的显式锁导致的死锁理论上是可恢复的,但实际可操作性不强——自动恢复的尝试可能是徒劳且有害的(导致活锁)。

  锁死是等待线程由于某种原因一直无法被唤醒而导致其任务无法进展的一种活性故障。信号丢失锁死是由于没有相应的通知线程来唤醒等待线程而使等待线程一直处于等待状态的一种活性故障。嵌套监视器锁死是嵌套锁导致通知线程无法获得其为唤醒等待线程所需的锁从而使其无法唤醒等待线程,最终使得通知线程与等待线程都一直处于等待状态的一种活性故障。嵌套监视器锁死可以通过查看线程转储进行检测。为规避嵌套监视器锁死,我们应该避免在嵌套锁的内层临界区内实现等待/通知。

  线程饥饿指线程一直无法获得其所需的资源而导致其任务一直无法进展的一种活性故障。把锁看成一种资源,那么死锁可被看作一种线程饥饿。饥饿可能演变成活锁。

  活锁是线程一直在做无用功而使其任务一直无法进展的一种活性故障。试图进行死锁故障恢复可能导致活锁。

本章知识结构图


Spring JPA Data with REST

逛 Spring 官网学习总结


@RepositoryRestResource 看到这个注解,之前一直没有用到过,所以想要自己试试效果,顺道做下总结。

不想要看我废话想要直接看官网的Accessing JPA Data with REST

不想要看我废话想看大佬的Spring Boot之@RepositoryRestResource注解入门使用教程


构建项目

用 Maven 构建项目,建一个 Person 实体类,再建一个 PersonRepository 接口,这个是关键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package hello;

import java.util.List;

import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource(collectionResourceRel = "people", path = "people")
public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {

List<Person> findByLastName(@Param("name") String name);

}

官网是这么介绍的:

1
At runtime, Spring Data REST will create an implementation of this interface automatically. Then it will use the @RepositoryRestResource annotation to direct Spring MVC to create RESTful endpoints at /people.

我们翻译过来就是:

1
在运行时,Spring Data REST将自动创建此接口的实现。然后它将会在使用 @RepositoryRestResource 注解的 Spring MVC 在 /people 创建 RESTful 端点。

点进 PagingAndSortingRepository 接口看:

1
2
3
4
5
6
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort var1);

Page<T> findAll(Pageable var1);
}

看到该接口还是继承了平时常使用的 CrudRepository,这就说明那些基本的增删改查方法也都有。

为了方便,我还写了一个接口用来保存 Person 实体,为后续测试使用。

1
2
3
4
5
6
7
8
9
10
@Service
public class PersonService {

@Autowired
PersonRepository personRepository;

public void savePerson(Person person){
personRepository.save(person);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/person")
public class PersonController {

@Autowired
PersonService personService;

@PutMapping("/save")
public String savePerson(Person person){
personService.savePerson(person);
return "success";
}
}

很简单的一个映射。

到这为止,我们的项目就全构建好了。

嫌麻烦不想自己动手建项目的GitHub

测试功能

我这里为了方便,用的是 Postman 进行测试。

首先访问 http://localhost:8080,返回的是:

1
2
3
4
5
6
7
8
9
10
11
{
"_links": {
"people": {
"href": "http://localhost:8080/people{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://localhost:8080/profile"
}
}
}

再访问 http://localhost:8080/people,返回的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"_embedded": {
"people": []
},
"_links": {
"self": {
"href": "http://localhost:8080/people{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://localhost:8080/profile/people"
},
"search": {
"href": "http://localhost:8080/people/search"
}
},
"page": {
"size": 20,
"totalElements": 0,
"totalPages": 0,
"number": 0
}
}

因为里面没有数据,我们先塞几个数据进去,这时候就利用我们之前写的接口来操作。如下:

1
2
3
我们用 PUT 方式去请求 http://localhost:8080/person/save?firstName=aaa&lastName=bbb

返回 success 就说明操作成功,成功塞入。

同样的方式我们再多塞几个。

塞完值之后我们接着去访问 http://localhost:8080/people,这时候发现结果与之前的稍有不同了:

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
{
"_embedded": {
"people": [
{
"firstName": "aaa",
"lastName": "bbb",
"_links": {
"self": {
"href": "http://localhost:8080/people/1"
},
"person": {
"href": "http://localhost:8080/people/1"
}
}
},
{
"firstName": "aaa",
"lastName": "ccc",
"_links": {
"self": {
"href": "http://localhost:8080/people/2"
},
"person": {
"href": "http://localhost:8080/people/2"
}
}
},
{
"firstName": "jay",
"lastName": "folger",
"_links": {
"self": {
"href": "http://localhost:8080/people/3"
},
"person": {
"href": "http://localhost:8080/people/3"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/people{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://localhost:8080/profile/people"
},
"search": {
"href": "http://localhost:8080/people/search"
}
},
"page": {
"size": 20,
"totalElements": 3,
"totalPages": 1,
"number": 0
}
}

是的,我们刚塞的值都查出来了。

接着测试,访问 http://localhost:8080/people/1,返回结果为:

1
2
3
4
5
6
7
8
9
10
11
12
{
"firstName": "aaa",
"lastName": "bbb",
"_links": {
"self": {
"href": "http://localhost:8080/people/1"
},
"person": {
"href": "http://localhost:8080/people/1"
}
}
}

是的,就是第一条数据被查出来了,同理 /2 /3 也是。

访问 http://localhost:8080/people/search,返回结果为:

1
2
3
4
5
6
7
8
9
10
11
{
"_links": {
"findByLastName": {
"href": "http://localhost:8080/people/search/findByLastName{?name}",
"templated": true
},
"self": {
"href": "http://localhost:8080/people/search"
}
}
}

对的,findByLastName{?name} 就是我们在里面新增的那个接口,那就顺着这个意思咱们继续来。

访问 http://localhost:8080/people/search/findByLastName?name=folger,返回结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"_embedded": {
"people": [
{
"firstName": "jay",
"lastName": "folger",
"_links": {
"self": {
"href": "http://localhost:8080/people/3"
},
"person": {
"href": "http://localhost:8080/people/3"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/people/search/findByLastName?name=folger"
}
}
}

与我们猜测无二。接口写的返回是集合,所以就返回了一个集合。

之前还看到了这么一条 link "href": "http://localhost:8080/people{?page,size,sort}"

有经验的程序员一眼就肯定知道这些个参数都是干什么用的,是啊,就是分页用的。

我们访问这个 url : http://localhost:8080/people?page=0&size=4&sort=lastName,返回的结果为:

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
{
"_embedded": {
"people": [
{
"firstName": "aaa",
"lastName": "bbb",
"_links": {
"self": {
"href": "http://localhost:8080/people/1"
},
"person": {
"href": "http://localhost:8080/people/1"
}
}
},
{
"firstName": "aaa",
"lastName": "ccc",
"_links": {
"self": {
"href": "http://localhost:8080/people/2"
},
"person": {
"href": "http://localhost:8080/people/2"
}
}
},
{
"firstName": "ccc",
"lastName": "ccc",
"_links": {
"self": {
"href": "http://localhost:8080/people/4"
},
"person": {
"href": "http://localhost:8080/people/4"
}
}
},
{
"firstName": "jay",
"lastName": "folger",
"_links": {
"self": {
"href": "http://localhost:8080/people/3"
},
"person": {
"href": "http://localhost:8080/people/3"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/people"
},
"profile": {
"href": "http://localhost:8080/profile/people"
},
"search": {
"href": "http://localhost:8080/people/search"
}
},
"page": {
"size": 4,
"totalElements": 4,
"totalPages": 1,
"number": 0
}
}

从返回的数据我们可以看出: page 当然是页数,size 就是每一页的个数,sort 就是排序的字段,默认为 asc。

这一波测试下来发现这样封装之后之前需要一大堆代码才能实现的分页,这里只需要一个注解即可。妙啊妙不可言。

相关链接


保障线程安全的设计技术

本文摘抄自《Java 多线程编程实战指南》核心篇 第六章小结

本章从面向对象编程的角度出发讲解了实现线程安全的几种常用技术。这些技术的运用通常可以产生具有固有线程安全性的对象,即这些对象本身无须借助锁就可以保障线程安全,从而有利于提高系统的并发性。本章还介绍了同步集合和并发集合。


  Java 运行时空间可分为堆空间/非堆空间以及栈空间。栈空间是线程的私有空间,而堆空间和非堆空间都是线程的共享空间。堆空间用于存储对象以及类的实例变量,它是 Java 虚拟机启动时分配的可以动态扩容的存储空间。非堆空间用于存储类的静态变量以及其他元数据,它是 Java 虚拟机启动时分配的可以动态扩容的存储空间。栈空间用于存储线程所执行的方法的局部变量/返回值等私有数据,它是线程创建时分配的容量固定不可变的存储空间。

  无状态对象不包含任何实例变量以及可更新的变量,它具有固有的线程安全性。无状态对象的客户端代码在调用该对象的任何方法时都无须加锁,而无状态对象自身的方法实现可能仍然需要借助锁。仅包含静态方法的类并不能取代无状态对象。Servlet 类通常需要被设计为无状态对象。

  不可变对象也具有固有的线程安全性。严格意义上的不可变对象需要同时满足这几个条件:类本身采用 final 修饰,所有字段都是 final 字段,在对象初始化过程中 this 代表的当前对象没有逸出,引用了状态可变的对象的字段不能直接暴露给其他对象。如果需要将引用了状态可变的对象的字段暴露给其他对象,那么需要在返回该对象前进行防御性复制,或者返回一个不支持 remove() 的 Iterator 实例。使用不可变对象建模时,系统状态的变化是通过创建新的不可变对象实现的。这种方式可能有利于提高垃圾回收效率,但也可能由于系统状态频繁变更/无状态对象占用较多内存空间等因素增加了垃圾回收的负担。不可变对象的典型应用场景包括:被建模对象的状态变化不频繁/同时对一组相关的数据进行写操作,因此需要保证原子性/使用不可变对象作为安全可靠的 Map 键。当被建模对象的状态变更比较频繁时,不可变对象也不见得就不能使用。此时,我们需要综合考虑被建模对象的规模/代码目标运行环境的 Java 虚拟机堆内存容量/系统对吞吐率和响应性的要求这几个因素。

  线程特有对象也具有固有的线程安全性。ThreadLocal 是线程访问其线程特有对象的代理。ThreadLocal 也被称为线程局部变量,一个线程可以通过使用不同的线程局部变量来访问不同的线程特有对象实例。多个线程即使是使用同一个线程局部变量,其访问到的对象也是各自的线程特有对象。线程局部变量通常作为一个类的静态字段来使用。为避免线程局部变量的使用导致内存泄露和伪内存泄露,我们需要确保在线程特有对象不再被需要的时候将其“删除”(即调用 ThreadLocal.remove())。线程特有对象的典型应用场景包括:需要使用非线程安全对象,但又不希望因此而引入锁;使用线程安全对象,但希望避免其使用的锁的开销和相关问题;实现方法间的隐式参数传递;实现特定于线程的单例模式。

  装饰器模式也能够用于实现线程安全。在使用装饰器模式的情况下,实现同一组功能的对象有非线程安全版和线程安全版两种。这两种对象具有相同的接口,其中非线程安全版对象仅关注功能的实现,而外包装对象(线程安全版)主要关注线程安全的保障。外包装对象在功能方面则是通过委托给相应的非线程安全对象来实现的。Java 并发集合就是使用装饰器模式来保障线程安全的。使用装饰器模式实现线程安全的优点是它支持关注点分离,并有利于降低开发难度和提高代码的可测试性,也有利于提高使用的灵活性。其缺点是并发性不高,并可能导致遍历操作是非线程安全的。

  并发集合一般可用于替代同步集合。其内部实现往往借助于 CAS 操作或者细粒度锁。并发集合支持线程安全的遍历操作,即对集合的遍历操作与更新操作是可以由不同线程并发执行的。并发集合实现线程安全的遍历操作由两种方式:快照和准实时。前者无法在遍历过程中反映其他线程对被遍历集合所做的更新,而后者在遍历过程中可能反映其他线程对被遍历集合所做的更新。CopyOnWriteArrayList 相当于 ArrayList 的线程安全版,它适用于遍历操作远比更新操作频繁或者不希望在遍历的时候加锁的场景,在其他场景下我们仍然要考虑使用相应的同步集合。CopyOnWriteArraySet 相当于 HashSet 的线程安全版,内部实现是基于 CopyOnWriteArrayList 的。因此,CopyOnWriteArraySet 适用场景与 CopyOnWriteArrayList 类似。ConcurrentLinkedQueue 相当于 LinkedList 的线程安全版,与 BlockingQueue 的实现类相比,ConcurrentLinkedQueue 适用于更新操作和遍历操作并发的场景。BlockingQueue 的实现类更适合于多个线程并发更新同一队列的场景,如生产者——消费者模式中。ConcurrentSkipListMap 相当于 TreeMap 的线程安全版。ConcurrentSkipListSet 相当于 TreeSet 的线程安全版。

本章知识结构图