原文链接:http://tutorials.jenkov.com/java-concurrency/same-threading.html
摘要:这是翻译自一个大概30个小节的关于Java并发编程的入门级教程,原作者Jakob Jenkov,译者Zhenning Lang,转载请注明出处,thanks and have a good time here~~~(希望自己不要留坑)
“同一线程”是这样一种并发方式:一个单线程系统被扩展成 N 个单线程系统。其结果是N个单线程系统并发的运行。
一个同一线程系统并不是一个单纯的单线程系统,因为他包含多个线程。然而其中的每一个线程都像一个单线程系统一样。
你可能正在疑惑为何如今竟然还有人设计单线程系统。单线程系统之所以流行是因为:基于单线程系统的并发模型相比多线程并发模型是非常简化的。单线程系统不需要在多个线程间共享任何数据。这使得基于单线程的并发可以使用非并发的数据结构,并且充分利用CPU和缓存的结构。
然而不幸的是,单线程系统并不能充分利用现代的多核CPU结构(双核、四核或更多)。而单线程系统只能利用其中的一个核,如下图所示:
为了充分的利用多核,单线程系统可以被规模化来充分利用计算的全部CPU资源。
在一个同一线程系统中,通常每个CPU中运行一个任务。如果一个计算机有四个CPU,或者一个CPU是四核的,那么这个计算机通常用来运行同一线程系统的4个实例(4个单线程系统),如下图所示
一个同一线程系统和一个多线程系统看起来是很相似的,因为二者都是在计算机中跑多条线程,但他们之间却存在着微妙的区别。同一线程系统和多线程系统的区别在于前者不共享状态,即不同的线程间不在同时拥有共享内存的使用权。如下图所示,
无共享数据使得同一线程系统中的每个线程表现得像单线程系统一样。然而,由于同一线程系统可以包含多于一个线程,所以它并非真正的单线程系统。由于缺乏专业术语,我觉得用“同一线程系统”(same-threaded system)比用“具有单线程设计的多线程系统”(multi-threaded system with a single-threaded design)来的更精确一些。同一线程系统这个称谓即容易说又容易懂。同一线程的基础定义是在单线程中进行数据处理,并且没有共享任何共享数据。
显而易见,同一线程系统需要在单线程的实例中分担任务。如果不这样做,可能会出现一个单独的实例承担全部的任务负载,那么系统将退化为单线程了。
你的系统设计决定了负载是如何在不同的实例间分配的,下面简述了其中的几种设计。
如果你的系统包含了多个微服务,每个微服务可以以单线程被运行,那么当你在同一台机器上部署多个单线程的微服务时,每个CPU可以用来运行一个单线程的微服务。
各个微服务之间本质上不共享任何数据,所以微服务系统很适合用同一线程来实现。
如果你的系统有共享数据的需求,或者至少要共享数据库,那么你可以“划破”(shard)数据库。“划破”意味着数据被在不同的数据库之间划分。一种典型的数据划分是将有相互关系的数据分配在同一个数据库中。例如,所有属于某个“拥有者”的数据被插入同一个数据库。“划破”超出了本教程的讨论范畴,如果感兴趣请搜索相关介绍。1
如果同一线程系统中的多个线程需要相互通信,他们通过传递消息来完成通信。
利用消息进行通信的过程如下图所示:
这种消息可以利用队列(queues)、管道(pipes)、嵌套字(unix sockets,tcp sockets)等技术来实现,这需要根据具体系统选择具体的消息实现机制。
同一线程模型实现的并发系统中,每个部分在其自己的线程中运行就好像单线程一样。无共享状态意味着这种模型是一种极简的模型 - 编程者不再需要担心共享数据和这导致的一系列问题。
对于单线程系统、多线程系统和同一线程系统,为了让读者可以更容易的构建起相关概念,请看下面的例子:
单线程系统:
共享数据的多线程系统:
同一线程系统(数据分离,通过消息机制通信)
译者的一点补充:
(1) 首先是在翻译过程中忽然想到Java的多线程真的可以运行在多个CPU中吗?
因为之前接触过OpenMP和MPI的入门,记得跨CPU的通信好像不是那么容易的事情,所以就会怀疑Java的多线程实际是在一个CPU内实现的。于是搜了一下,搜到这个帖子:
http://blog.csdn.net/ziwen00/article/details/38097297 。
但心动不如行动,我决定自己试一试(本人笔记本四核、4G内存),测试代码如下:
public class Test {
public static void main(String[] args) {
int threadNum = 1; // 2 3 ...
for(int i = 0; i < threadNum; i++){
System.out.println(i);
new Thread(new myThread(), "Thread" + i).start();
}
}
}
class myThread implements Runnable{
@Override
public void run() {
while(true);
}
}
通过改变程序中的threadNum,测试结果如下,
线程数 | CPU使用率 |
---|---|
1 | 33% |
2 | 56% |
3 | 80% |
4 | 100% |
又想到了某一节中作者说开好多线程的事(作者在介绍多线程的缺点时说可以尝试开100个线程看一看系统内存的负荷有多少),就蛋疼的试了。。。
public class Test {
public static void main(String[] args) {
int threadNum = 1; // 2, 3, ...
for(int i = 0; i < threadNum; i++){
System.out.println(i);
new Thread(new myThread(), "Thread" + i).start();
}
}
}
class myThread implements Runnable{
@Override
public void run() {
try {
Thread.sleep(1000000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
其结果如下
线程数 | 运行前(G) | 运行后(G) | 使用内存 |
---|---|---|---|
1 | 2.4 | 2.4 | 推算约为0.1M |
10 | 2.4 | 2.4 | 推算约为1M |
100 | 2.4 | 2.4 | 推算约为10M |
1000 | 2.4 | 2.5 | 0.1G |
10000 | 2.4 | 3.2 | 0.8G |
100000 | 2.4 | 大于4(实际是系统卡死了…) | 大于1.6G,推算约为 8G |
实验结果仅做参考
(2) 这一节介绍的内容实际应用还是很多的。本人研究生阶段做过一个大规模的仿真程序,然后并行化进行提速,用的就是这节介绍的同一线程模型。如果可以利用好问题特性,这种模型可以说无论用什么语言都是非常容易实现的。
尾注: