谈谈Java Volatile的设计

java volatile为什么要这么设计?

最近在知乎上看见一个有意思的问题:

java volatile为什么要这么设计?
如图所示,这种指令重排规则背后设计的是出于什么原因考虑?我想知道why? 而不是What/How

这个问题看似简单,但是后面其实隐藏着计算机架构的演变:

CPU的多核心时代

说到这儿,就要开始聊一下多线程/多处理器的发展史了。初期计算机性能的提升,主要靠的是主频的提升,后来主频提升遇到了困难,然后就开始了多核处理器的时代。

这里面有一个问题,一个程序可不是天然就能同时跑在多个CPU核心上的,如何让程序员利用多核心处理器提升效率,是一个难题。

这个问题乍一看很简单,我们已经有了线程模型了,直接套用到多核架构上不就可以了么?

但是不行,在原来的单核心架构中,我们的内存模型是 顺序一致性(Sequential consistency)

  1. 每个线程内部的指令都是按照程序规定的顺序(program order)执行的(单个线程的视角)
  2. 线程执行的交错顺序可以是任意的,但是所有线程所看见的整个程序的总体执行顺序都是一样的(整个程序的视角)
阅读更多

听说你没法在JRE中使用arthas?不,你可以

本文是《容器中的Java》系列文章之 5/n ,欢迎关注后续连载 :) 。

之前经常遇到的问题是,排查问题需要挂arthas,但客户用的是JRE,没法挂载arthas。就只能让客户更换成JDK,再重新部署、排查问题。

很多有用的现场,在这个过程中也会丢失,最终导致问题排查效率降低。于是就探索了下如何在JRE环境中,使用artahs。

复现问题

如果一个Bug 没法复现,研发大概率是无法修复的。 —— by 网友

我们写一个Java例子和Dockerfile:

./src/main/java/Main.java
1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) throws Exception {
while (true) {
System.out.println("hello!");
Thread.sleep(30 * 1000);
}
}
}
./Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM openjdk:8-jdk-alpine as builder
COPY ./ /app
WORKDIR /app/src/main/java/
# 编译java文件
RUN javac Main.java

# 运行时容器使用JRE
FROM openjdk:8-jre-alpine
RUN apk add bash curl busybox-extras
WORKDIR /app/src/main/java/
# 将arthas copy 到容器中
COPY --from=hengyunabc/arthas:latest /opt/arthas /opt/arthas
COPY --from=builder /app/src/main/java/ /app/src/main/java/
CMD ["java", "Main"]

构建并正常启动应用,并尝试用arthas attach,此处为了便于了解原理,我们使用as.sh来执行:

阅读更多

为什么在容器中1号进程挂不上arthas?

本文是《容器中的Java》系列文章之 4/n ,欢迎关注后续连载 :) 。

最近在容器环境中,发现在Java进程是1号进程的情况下,无法使用arthas,提示AttachNotSupportedException: Unable to get pid of LinuxThreads manager thread。具体操作和报错如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.5.6
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 1 com.alibabacloud.mse.demo.ZuulApplication
1
[INFO] arthas home: /home/admin/.opt/ArmsAgent/arthas
[INFO] Try to attach process 1
[ERROR] Start arthas failed, exception stack trace:
com.sun.tools.attach.AttachNotSupportedException: Unable to get pid of LinuxThreads manager thread
at sun.tools.attach.LinuxVirtualMachine.<init>(LinuxVirtualMachine.java:86)
at sun.tools.attach.LinuxAttachProvider.attachVirtualMachine(LinuxAttachProvider.java:78)
at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:250)
at com.taobao.arthas.core.Arthas.attachAgent(Arthas.java:117)
at com.taobao.arthas.core.Arthas.<init>(Arthas.java:27)
at com.taobao.arthas.core.Arthas.main(Arthas.java:166)
[INFO] Attach process 1 success.

之前也遇到过,总是调整了下镜像,让Java进程不是1号进程就可以了。但这个不是长久之计,还是要抽时间看下这个问题。

复现问题

我们创建如下项目,来复现这个问题:

./src/main/java/Main.java
1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) throws Exception {
while (true) {
System.out.println("hello!");
Thread.sleep(30 * 1000);
}
}
}
./Dockerfile
1
2
3
4
5
FROM openjdk:8u212-jdk-alpine
COPY ./ /app
WORKDIR /app/src/main/java/
RUN javac Main.java
CMD ["java", "Main"]

然后正常启动应用,并尝试用arthas,或者jstack:

阅读更多

Z-Library挂了?怎么办?

Z-Library

Z-Library 是一个免费下载电子书的网站。不过最近由于Z-Library的域名被查封,所以现在没法访问了。

Z-Library 刚开始的时候是 LibGen 的镜像站点,正因如此,Z-Library 上的大部分图书,都能在 LibGen找到;但有一些书是用户自己上传到 Z-Library 的,所以就没法在 LibGen 找到了。

这种情况,一般可以在维基百科上找下官方信息,就能发现 Z-Library 还提供了一个 onion 域名可以访问。

所以,我们可以下载一个 Tor 浏览器,或者 Brave 浏览器,然后访问 http://zlibrary​24tuxziyiyfr7​zd46ytefdqbqd2axkmxm​4o5374ptpc52fad.onion 来继续访问了:

当然, Tor 网络虽然链路是分布式的,但是其中的 server 却是集中式的,个人觉得不算是“分布式Web”

  • 网上也会有一些 Z-Library 镜像,也可以搜索下使用,比如 zlib.wikizlib.quest等。

Z-Library 存档

当然,这种网站一般也会有人收集离线存档,可以在 pilimi.org 找到 Z-Library 存档。

阅读更多

如何使用rust写内核模块

近年来,Rust语言以内存安全、高可靠性、零抽象等能力获得大量开发者关注,而这些特性恰好是内核编程中所需要的,所以我们来尝试下如何用rust来写Linux内核模块。

Rust与内核模块

虽然Rust支持已经在Linux Kernel 6.1版本合并到主线了,所以理论上来说,开发者可以使用Rust来为Linux 6.1写内核模块。
但实际开发工作中,内核版本不是最新的,比如Debian 11的内核就是5.10版本的,那么在这种情况下,该如何用Rust写内核模块呢?

原理

  1. Rust如何向内核注册回调、如何调用内核代码。Rust和C的互操作性
  2. Rust如何编译到目标平台上。Rust的target配置
  3. Rust如何申明内核模块入口、并添加特殊section。Rust内核模块的二进制约定

Rust和C的互操作性

第一个问题基本上就是C和Rust的互操作性了。
得益于Rust的抽象层次,C语言和Rust的互相调用都是比较容易的。rust官方也提供了bindgen这样,根据.h文件生成.rs文件的库。
这样一来,貌似直接使用bindgen将内核头文件翻译成.rs就可以了?
但还有一个问题,如何获取内核头文件路径呢?
可以使用一个dummy内核模块,在编译过程中把编译参数导出来,其中包含了头文件路径,编译参数等,用于bindgen生成代码。

Rust的target配置

内核模块和普通的程序相比,主要的不同在于:

  1. 内核模块是freestanding的,没有libc、内存分配也比较原始
  2. 内核模块对于异常处理等有特殊约定
阅读更多

如果java虚拟线程稳定了,是不是有一大批框架和工具要重写?

知乎上有一个提问,“如果java虚拟线程稳定了,是不是有一大批框架和工具要重写?”,我整理了下我的知乎回答:

首先,抛开虚拟线程、协程,我们看看现在的架构。

在顶层,开辟出很多调度单位(线程、协程);在底层,有很多的IO原语(accept、read、write);在中间,有很多的组织逻辑。

协程主要做的就是,在底层执行IO原语的时候,暂时保存上下文,等执行结束后,再恢复上下文继续执行。

那么在现有的协程实现方案上,就出现了两种模式:

有栈协程,就是协程上下文包括了callstack,当IO完成、恢复上下的时候,连带着callstack恢复。那么对于callstack中的各个caller(调用者)和callee(被调用者),都感知不到整个协程的调用过程,自然代码就不用修改了。

有栈协程的优点就是不需要现有的代码改动太多,只需要调度单位创建+调度器+IO操作方面改动即可。但开发者的把控就比较弱,不能干涉调度过程。

Go和Java的Loom,都是有栈协程。

无栈协程,就是协程上下问不包含callstack,恢复上下文的时候,也只是通知上下文完成了。接下来干什么事情,还是得重新构建callstack。

比如 A -> B -> C -> IO函数,当出现IO调用,需要保留上下文,那么就需要C->B->A逐个返回上下文,恢复的时候也是 A -> B ->C去恢复执行。

阅读更多

让 Java Agent 在 Dragonwell 上更好用

本文是《容器中的Java》系列文章之 3/n ,欢迎关注后续连载 :) 。

背景

Java Agent 技术能够动态修改 Java 应用程序行为,而不用重新修改代码。

正是因为这些特点,很多中间件团队、云厂商团队、开源产品,开始使用 Java Agent 技术来提供一些基础能力,比如 Apache Skywalking、OpenTelemetry 都提供了 Java Agent。

在早前,中间件团队通过SDK提供能力(比如可观测、微服务治理能力等);但中间件团队每次新增特性、修复缺陷,都需要各个业务方更新SDK版本、重新发布。

随着公司架构越来越复杂,随着云厂商开始提供中间件能力,这种逐个推动SDK使用方更新的方式越来越麻烦。

而用了 Java Agent 之后,业务同学只需要写业务代码;中间件能力通过设置环境变量来动态注入Java Agent来实现。Java Agent的更新,也只需要重启应用即可。

问题

我们以一个微服务demo为例。先在一个Kubernetes集群中部署demo,然后通过JAVA_TOOL_OPTIONS使用用Java Agent:

1
2
$ echo $JAVA_TOOL_OPTIONS
-javaagent:/home/admin/.opt/ArmsAgent/arms-bootstrap-1.7.0-SNAPSHOT.jar ...
阅读更多

LeetCode 周赛 311

第一题 2413. Smallest Even Multiple

给你一个正整数 n ,返回 2 和 n 的最小公倍数(正整数)。

显而易见的:

1
2
3
4
5
6
7
func smallestEvenMultiple(n int) int {
if n%2==1{
return n*2
}else{
return n
}
}

第二题 2414. Length of the Longest Alphabetical Continuous Substring

最近恰好做过dp的题目,所以立马dp:

  • 状态定义为 下标i -> 以i结尾的string中,字母序连续的字符串最大长度
  • 状态转移 s[i]和s[i-1]连续,那么在前面基础上+1,如果不连续,则置为1

最后,遍历i,拿最大长度即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func longestContinuousSubstring(s string) int {
dp := make([]int, len(s))
dp[0] = 1
for i := 1; i < len(s); i++ {
if s[i] == s[i-1]+1 {
dp[i] = dp[i-1] + 1
} else {
dp[i] = 1
}
}
res := 1
for i := 0; i < len(dp); i++ {
if res < dp[i] {
res = dp[i]
}
}
return res
}
阅读更多

Java Agent踩坑之appendToSystemClassLoaderSearch问题

本文是《容器中的Java》系列文章之 2/n ,欢迎关注后续连载 :) 。

从Java Agent报错开始,到JVM原理,到glibc线程安全,再到pthread tls,逐步探究Java Agent诡异报错。

背景

由于阿里云多个产品都提供了Java Agent给用户使用,在多个Java Agent一起使用的场景下,造成了总体Java Agent耗时增加,各个Agent各自存储,导致内存占用、资源消耗增加。

所以我们发起了one-java-agent项目,能够协同各个Java Agent;同时也支持更加高效、方便的字节码注入。

其中,各个Java Agent作为one-java-agent的plugin,在premain阶段是通过多线程启动的方式来加载,从而将启动速度由O(n)降低到O(1),降低了整体Java Agent整体的加载时间。

问题

但最近在新版Agent验证过程中,one-java-agent的premain阶段,发现有如下报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2022-06-16 09:51:09 [oneagent plugin a-java-agent start] ERROR c.a.o.plugin.PluginManagerImpl -start plugin error, name: a-java-agent
com.alibaba.oneagent.plugin.PluginException: start error, agent jar::/path/to/one-java-agent/plugins/a-java-agent/a-java-agent-1.7.0-SNAPSHOT.jar
at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:113)
at com.alibaba.oneagent.plugin.PluginManagerImpl.startOnePlugin(PluginManagerImpl.java:294)
at com.alibaba.oneagent.plugin.PluginManagerImpl.access$200(PluginManagerImpl.java:22)
at com.alibaba.oneagent.plugin.PluginManagerImpl$2.run(PluginManagerImpl.java:325)
at java.lang.Thread.run(Thread.java:750)
Caused by: java.lang.InternalError: null
at sun.instrument.InstrumentationImpl.appendToClassLoaderSearch0(Native Method)
at sun.instrument.InstrumentationImpl.appendToSystemClassLoaderSearch(InstrumentationImpl.java:200)
at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:100)
... 4 common frames omitted
2022-06-16 09:51:09 [oneagent plugin b-java-agent start] ERROR c.a.o.plugin.PluginManagerImpl -start plugin error, name: b-java-agent
com.alibaba.oneagent.plugin.PluginException: start error, agent jar::/path/to/one-java-agent/plugins/b-java-agent/b-java-agent.jar
at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:113)
at com.alibaba.oneagent.plugin.PluginManagerImpl.startOnePlugin(PluginManagerImpl.java:294)
at com.alibaba.oneagent.plugin.PluginManagerImpl.access$200(PluginManagerImpl.java:22)
at com.alibaba.oneagent.plugin.PluginManagerImpl$2.run(PluginManagerImpl.java:325)
at java.lang.Thread.run(Thread.java:855)
Caused by: java.lang.IllegalArgumentException: null
at sun.instrument.InstrumentationImpl.appendToClassLoaderSearch0(Native Method)
at sun.instrument.InstrumentationImpl.appendToSystemClassLoaderSearch(InstrumentationImpl.java:200)
at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:100)
... 4 common frames omitted

熟悉Java Agent的同学可能能注意到,这是调用Instrumentation.appendToSystemClassLoaderSearch报错了。

阅读更多

kill-port 清理占用端口的进程

背景

在日常开发中,经常出现端口莫名被占用的情况。比如要启动一个java服务,报错8080端口被占用,这时候就想着快速清理掉占用该端口的进程。

在Linux下,我们可以用熟悉的ss命令来找出进程并删除,但是macOS下,netstat我用的比较少,而且速度也很慢。

最近正好在学习Rust,所以就用Rust来写一个kill-port小工具,也算是入门系统编程了吧。

安装

1
2
3
$ git clone https://github.com/robberphex/kill-port.git
$ cargo build --release
# 将 ./target/release/kill-port 添加到PATH路径中。

使用方法

1
2
3
4
5
6
7
kill-port 0.1.0

USAGE:
kill-port [PORT]...

ARGS:
<PORT>... ports to find

例子:

1
2
# kill掉占用8080端口的进程
$ kill-port 8080
阅读更多