一 、java的虚拟线程和go协程测试效果
选用一个简单的sleep场景,基础条件是一个i7 16线程的cpu, 32g内存
1 jdk23
public static void compared() {
ExecutorService es = Executors.newVirtualThreadPerTaskExecutor();
int rounds = xxx;
CountDownLatch latch = new CountDownLatch(rounds);
long start = System.nanoTime();
for (int i = 0; i < rounds; i++) {
es.execute(()-> {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
});
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println((end - start) / 1000000 + "ms");
}
2 go1.17.8
func compared() {
var wg sync.WaitGroup
rounds := xxx
wg.Add(rounds)
start := time.Now()
for i := 0; i < rounds; i++ {
go func() {
time.Sleep(time.Millisecond * 100)
wg.Done()
}()
}
wg.Wait()
elapsed := time.Since(start)
fmt.Println(elapsed)
}
3 效果对比
可以看出来, 效果还是不错的
| rounds | go | java |
|---|---|---|
| 5000 | 107ms | 108ms |
| 10000 | 107ms | 108ms |
| 15000 | 108ms | 111ms |
| 20000 | 109ms | 121ms |
| 30000 | 115ms | 124ms |
| 40000 | 120ms | 124ms |
| 50000 | 124ms | 138ms |
二 、http请求场景探索
这里选用URLConnection,通过大量虚拟线程来发起http请求,会有一些异常。 相比之下, go的协程基本没有异常发生。
java.net.ConnectException: Connection refused: getsockopt
at java.base/sun.nio.ch.Net.pollConnect(Native Method)
at java.base/sun.nio.ch.Net.pollConnectNow(Net.java:682)
at java.base/sun.nio.ch.NioSocketImpl.timedFinishConnect(NioSocketImpl.java:542)
at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:592)
at java.base/java.net.Socket.connect(Socket.java:760)
at java.base/sun.net.NetworkClient.doConnect(NetworkClient.java:178)
at java.base/sun.net.www.http.HttpClient.openServer(HttpClient.java:531)
at java.base/sun.net.www.http.HttpClient.openServer(HttpClient.java:636)
at java.base/sun.net.www.http.HttpClient.<init>(HttpClient.java:280)
at java.base/sun.net.www.http.HttpClient.New(HttpClient.java:386)
at java.base/sun.net.www.http.HttpClient.New(HttpClient.java:408)
at java.base/sun.net.www.protocol.http.HttpURLConnection.getNewHttpClient(HttpURLConnection.java:1295)
at java.base/sun.net.www.protocol.http.HttpURLConnection.plainConnect0(HttpURLConnection.java:1228)
at java.base/sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConnection.java:1114)
at java.base/sun.net.www.protocol.http.HttpURLConnection.connect(HttpURLConnection.java:1043)
使用wireshark看一下tcp层面的内容。
两种情况, 一种是建连成功后的交互过程中发生, 另一种是建连失败。
分析第一种情况:
- 第一个ACK包(编号10105)确认了客户端已经成功接收到了服务器的数据,并且这些数据应该已经存放在客户端的TCP接收缓冲区中。
- 第二个RST包(编号10112)在大约0.5秒后发送,表明客户端决定重置连接。
分析第二种情况:
- 半连接/全连接队列满了
- 其它可能。
三 、Virtual Thread一些细节
### Thread-local variables
Virtual threads support thread-local variables ([`ThreadLocal`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ThreadLocal.html)) and inheritable thread-local variables ([`InheritableThreadLocal`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/InheritableThreadLocal.html)), just like platform threads, so they can run existing code that uses thread locals. However, because virtual threads can be very numerous, use thread locals only after careful consideration. In particular, do not use thread locals to pool costly resources among multiple tasks sharing the same thread in a thread pool. [Virtual threads should never be pooled](https://openjdk.org/jeps/444#Do-not-pool-virtual-threads) since each is intended to run only a single task over its lifetime. We have removed many uses of thread locals from the JDK's `java.base` module in preparation for virtual threads in order to reduce memory footprint when running with millions of threads.
The system property `jdk.traceVirtualThreadLocals` can be used to trigger a stack trace when a virtual thread sets the value of any thread-local variable. This diagnostic output may assist with removing thread locals when migrating code to use virtual threads. Set the system property to `true` to trigger stack traces; the default value is `false`.
Scoped values ([JEP 429](https://openjdk.org/jeps/429)) may prove to be a better alternative to thread locals for some use cases.
### `java.util.concurrent`
The primitive API to support locking, [`java.util.concurrent.LockSupport`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/locks/LockSupport.html), now supports virtual threads: Parking a virtual thread releases the underlying platform thread to do other work, and unparking a virtual thread schedules it to continue. This change to `LockSupport` enables all APIs that use it (`Lock`s, `Semaphore`s, blocking queues, etc.) to park gracefully when invoked in virtual threads.
Additionally, [`Executors.newThreadPerTaskExecutor(ThreadFactory)`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Executors.html#newThreadPerTaskExecutor(java.util.concurrent.ThreadFactory)) and [`Executors.newVirtualThreadPerTaskExecutor()`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/Executors.html#newVirtualThreadPerTaskExecutor()) create an `ExecutorService` that creates a new thread for each task. These methods enable migration and interoperability with existing code that uses thread pools and `ExecutorService`.
### Networking
The implementations of the networking APIs in the `java.net` and `java.nio.channels` packages now work with virtual threads: An operation on a virtual thread that blocks, e.g., to establish a network connection or read from a socket, releases the underlying platform thread to do other work.
To allow for interruption and cancellation, the blocking I/O methods defined by [`java.net.Socket`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/Socket.html), [`ServerSocket`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/ServerSocket.html), and [`DatagramSocket`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/DatagramSocket.html) are now specified to be [interruptible](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#interrupt()) when invoked in a virtual thread: Interrupting a virtual thread blocked on a socket will unpark the thread and close the socket. Blocking I/O operations on these types of sockets when obtained from an [`InterruptibleChannel`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/channels/InterruptibleChannel.html) have always been interruptible, so this change aligns the behavior of these APIs when created with their constructors with their behavior when obtained from a channel.
### `java.io`
The `java.io` package provides APIs for streams of bytes and characters. The implementations of these APIs are heavily synchronized and require changes to avoid pinning when they are used in virtual threads.
As background, the byte-oriented input/output streams are not specified to be thread-safe and do not specify the expected behavior when `close()` is invoked while a thread is blocked in a read or write method. In most scenarios it does not make sense to use a particular input or output stream from multiple concurrent threads. The character-oriented reader/writers are also not specified to be thread-safe, but they do expose a lock object for sub-classes. Aside from pinning, the synchronization in these classes is problematic and inconsistent; e.g., the stream decoders and encoders used by `InputStreamReader` and `OutputStreamWriter` synchronize on the stream object rather than the lock object.
To prevent pinning, the implementations now work as follows:
- `BufferedInputStream`, `BufferedOutputStream`, `BufferedReader`, `BufferedWriter`, `PrintStream`, and `PrintWriter` now use an explicit lock rather than a monitor when used directly. These classes synchronize as before when they are sub-classed.
- The stream decoders and encoders used by `InputStreamReader` and `OutputStreamWriter` now use the same lock as the enclosing `InputStreamReader` or `OutputStreamWriter`.
Going further and eliminating all this often-needless locking is beyond the scope of this JEP.
Additionally, the initial sizes of the buffers used by `BufferedOutputStream`, `BufferedWriter`, and the stream encoder for `OutputStreamWriter` are now smaller so as to reduce memory usage when there are many streams or writers in the heap — as might arise if there are a million virtual threads, each with a buffered stream on a socket connection.
### Java Native Interface (JNI)
JNI defines one new function, `IsVirtualThread`, to test if an object is a virtual thread.
The JNI specification is otherwise unchanged.
以上摘自openjdk中关于虚拟线程的一些说明。这里面有三个参数
- jdk.virtualThreadScheduler.parallelism,调度器并行度
- jdk.virtualThreadScheduler.maxPoolSize,最大平台(carrier)线程数
- jdk.virtualThreadScheduler.minRunnable,最小平台(carrier)线程数
这几个参数可通过
-Djdk.virtualThreadScheduler.maxPoolSize=2 -Djdk.virtualThreadScheduler.parallelism=2
进行控制
如果parallelism为4, maxPoolSize为8还有用吗?有的,比如Pinning,如果虚拟线程在执行同步块或本地方法时被固定到其载体平台线程,它可能会阻塞该平台线程。在这种情况下,调度器可以创建新的平台线程来确保其他虚拟线程继续执行。如何来模拟这个情况呢?
直接上代码:
Thread.ofVirtual().start(() -> {
System.out.println(System.currentTimeMillis() +"start 1.");
sync();
System.out.println(System.currentTimeMillis() + "end 1");
});
Thread.ofVirtual().start(() -> {
System.out.println(System.currentTimeMillis() +"start 2.");
sync();
System.out.println(System.currentTimeMillis() + "end 2");
});
public static synchronized void sync() {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
vm参数:
-Djdk.virtualThreadScheduler.maxPoolSize=1
-Djdk.virtualThreadScheduler.parallelism=1
-Djdk.tracePinnedThreads=short
打印:
vm参数:
-Djdk.virtualThreadScheduler.maxPoolSize=2
-Djdk.virtualThreadScheduler.parallelism=1
-Djdk.tracePinnedThreads=short
打印:
可以看到, jvm添加了一个新的carrier thread. 使用synchronized会导致a virtual thread is pinned to the carrier thread. 替换成lock锁,则不会有pinned。
接下来将synchronized修饰符去掉,
当maxPoolSize=1 && parallelism=1时
1727339925496start 1.
1727339925499start 2.
1727339926504end 1
1727339926504end 2
当maxPoolSize=2 && parallelism=2时
1727340126241start 1.
1727340126241start 2.
1727340127252end 1
1727340127252end 2