Erlang进程与Java线程的区别

68
我正在阅读Saša Jurić的《Elixir实战》一书,第一章中提到:
Erlang进程完全隔离。它们不共享内存,一个进程的崩溃不会导致其他进程崩溃。
那Java线程也是这样的吗?我的意思是当Java线程崩溃时,它也不会影响其他线程 - 特别是如果我们考虑请求处理线程(让我们从此讨论中排除主线程)。

1
嗯,它描述了运行数百甚至数千个任务,使用调度程序,CPU 绑定。在我看来像是"线程"。 - user2982130
2
另一个比较可能是对象。除非它已经通过某些Java VM内部(但通常不是这样)公开了API,否则您无法更改另一个对象。Erlang进程在其抽象方面更类似于Java对象,并且经常被Erlang程序员用作此类对象。然后,最大的区别在于它们可以并发运行。 - Adam Lindberg
6
实际上,Erlang进程是面向对象编程(OOP)发明者所想象的唯一真正的对象。Java遵循C++的“亵渎”做法。 - Marko Topolnik
3
啊...我完全"哦不,又是这个话题...",但还是被卷入了回应的漩涡。希望我能做到公正平衡。 - zxq9
3
任何Java线程中未捕获的异常都将导致整个虚拟机崩溃。大多数线程池实现会捕获工作队列中任务抛出的异常。 - Kevin Krumwiede
显示剩余3条评论
7个回答

111

请跟我重复:“这些是不同的范例”

大声地重复20次左右——这是我们目前的口头禅。

如果我们非要把苹果和橘子进行比较,那么至少要考虑它们作为“水果”的共性在哪里相交。

Java的“对象”是Java程序员计算的基本单位。也就是说,一个“对象”(基本上是一个有手有脚的结构体,具有比C++中更严格的封装实施)是你建模世界的主要工具。你会想:“这个对象知道/有Data {X,Y,Z},并执行Functions {A(),B(),C()},随着它走到哪里就带着Data,并通过调用作为公共接口一部分定义的函数/方法与其他对象通信。它是一个名词,这个名词一些事情。” 也就是说,你的思维过程围绕这些计算单元展开。默认情况下,发生在对象之间的事情按顺序发生,崩溃会打断这个顺序。它们被称为“对象”,因此(如果我们忽略Alan Kay最初的意义),我们得到了“面向对象”。
Erlang的“进程”是Erlang程序员的基本计算单元。一个进程(基本上是在自己的时间和空间中运行的自包含顺序程序)是Erlanger模拟世界的主要工具(1)。与Java对象定义封装级别类似,Erlang进程也定义了封装级别,但在Erlang的情况下,计算单位是完全相互隔离的。您不能在另一个进程上调用方法或函数,也无法访问驻留在其中的任何数据,也没有一个进程在与其他进程相同的时间上下文中运行,也没有关于接收消息顺序的任何保证相对于可能正在发送消息的其他进程。它们可以像完全不同的星球一样(事实上,这实际上是可能的)。它们可以独立于彼此崩溃,只有在它们已经选择受到影响时才会影响其他进程(即使这也涉及消息传递:从死亡进程注册接收自杀通知,这本身并不保证按照整个系统的任何顺序到达,您可能会选择或不选择做出反应)。
Java处理复杂性直接采用复合算法:对象如何协同解决问题。它旨在在单个执行上下文中执行此操作,并且Java的默认情况是顺序执行。Java中的多个线程表示多个运行上下文,这是一个非常复杂的主题,因为不同时间上下文中的活动对彼此(以及整个系统)的影响很大,因此需要进行防御编程、异常方案等。在Java中说“多线程”意味着与Erlang中的说法不同,事实上,在Erlang中甚至从未这样说过,因为它始终是基本情况。请注意,Java线程意味着与时间相关的隔离,而不是内存或可见引用——在Java中,可见性通过手动选择私有和公共部分来控制;系统中普遍可访问的元素必须设计为“线程安全”和可重入,通过排队机制进行序列化,或者采用锁定机制。简而言之:调度是线程/并发Java程序中手动管理的问题。
Erlang将每个进程的运行上下文按照执行时间(调度)、内存访问和引用可见性进行分离,从而通过完全隔离简化算法的每个组成部分。这不仅是默认情况,也是该计算模型下唯一可用的情况。这样做的代价是,在处理序列穿过消息障碍后,永远无法确切地知道任何给定操作的顺序——因为消息本质上都是网络协议,没有方法调用可以保证在给定上下文中执行。这相当于为每个对象创建一个JVM实例,并只允许它们通过套接字进行通信——在Java中这将非常麻烦,但这是Erlang设计工作方式的基础(顺便说一句,如果去除与Web相关的包袱,编写“Java微服务”的概念也是如此——Erlang程序默认情况下是大量微服务的集合)。这一切都是关于权衡的。
这些是不同的范式。我们能找到的最接近共性是从程序员的角度来看,Erlang进程类似于Java对象。如果我们必须找到与Java线程相似的东西......那么,在Erlang中我们根本找不到这样的可比概念,因为在Erlang中没有这样的概念。再强调一遍:这些是不同的范式。如果您在Erlang中编写一些非平凡的程序,这将很快变得明显。
请注意,我说“这些是不同的范式”,但甚至没有触及面向对象编程(OOP)和函数式编程(FP)的主题。在“以Java思考”和“以Erlang思考”之间的区别比OOP与FP更为基本。(事实上,可以为Erlang VM编写一个像Java一样工作的OOP语言--例如:Erlang中OOP对象的实现。)
尽管真实情况是,Erlang的"并发导向"或"进程导向"基础更接近Alan Kay在创造"面向对象"这个术语时所想表达的(2),但这并非重点。Kay的意图是通过将计算单元切割成离散块来降低系统的认知复杂度,并且隔离是必须的。Java通过一种特殊的语法结构围绕称为"class definitions"的高阶分派闭包来实现这一点,但仍然在本质上保持过程化。Erlang通过按对象拆分运行上下文来实现这一点。这意味着Erlang的东西不能相互调用方法,但Java的东西可以。这意味着Erlang的东西可能会孤立崩溃,但Java的东西则不会。这种基本差异带来了大量的影响——因此称之为"不同的范式"。权衡取舍。

注释:

  1. 顺便提一下,Erlang实现了一个版本的 "Actor模型",但我们不使用这个术语,因为Erlang的设计早于该模型的普及。Joe在设计Erlang和写他的论文时并不知道它。
  2. Alan Kay曾经详细阐述过他创造"面向对象"这个术语的含义,其中最有趣的是他对消息传递的看法(从具有其自身时序和内存的独立进程向另一个进程发出单向通知)与调用(在具有共享内存的顺序执行上下文中进行函数或方法调用)-以及编程语言呈现的编程接口和底层实现之间如何有些模糊。

9
@WandMaker 很高兴听到这个!奇怪的是,你对某种编程范式的思考越多,你就会越了解其他范式。如果你有时间(提示:制造需求),尝试Prolog!尝试Forth!在Guile/Scheme/任何语言中构建OOP! 然后再回顾汇编语言!随着你不断深入,这些东西就变得非常有趣 - 直到它们都混为一体,你开始厌恶每一种语言,这就是我现在所处的地方。但问题解决仍然很有趣! ^.^ - zxq9
3
不错的文章,所以我说了21遍。 - Buzz Moschetti

16

绝对不行。Java中的所有线程共享相同的地址空间,因此一个线程可能会破坏另一个线程拥有的东西。在Erlang虚拟机中,这是不可能的,因为每个进程都与其他进程隔离。这就是它们的全部含义。每当您想让一个进程使用来自另一个进程的数据时,您的代码必须向另一个进程发送消息。在进程之间唯一共享的是大型二进制对象,而这些对象是不可变的。


1
在低级别(在语言级别,“地址空间”无论如何都是无意义的),Erlang进程也共享地址空间,这实际上是真的吗?这是实现廉价绿色线程的唯一方法。通过确保由进程可观察的指针不能解引用到另一个进程的内存块来实现隔离。 - Marko Topolnik
1
Java没有地址空间的概念。Java线程可以访问彼此的对象和静态字段。 - user253751
1
@immibis 更恰当的说法是Java没有线程对对象的所有权概念。因此,对象在线程之间是共享的。 - Marko Topolnik

14

Java线程实际上可以共享内存。例如,您可以将同一实例传递给两个单独的线程,并且两个线程都可以操作其状态,导致潜在的问题,例如死锁

另一方面,Elixir / Erlang通过不可变性的概念来解决这个问题,因此当您将某个东西传递给进程时,它将是原始值的副本。


1
它要么是不可变的,要么是复制的。在 Erlang 中,我想这意味着它不需要进行防御性复制。 - Marko Topolnik
没错,只有在必要的情况下才会进行复制。例如,如果一个列表只写入一次,那么只需要读取,据我所知,BEAM 将重用相同的内存作为一种优化。但是,效果与每次复制所有内容的效果相同。 - Patrick Oscity
是的,有时它甚至以更加重要的方式被复制:序列化并发送到另一个计算节点。 - Marko Topolnik
值得注意的是,在Erlang/Elixir中仍然可能会出现死锁问题,但不会因为共享内存问题而导致。 - Patrick Oscity
当然,这是任何异步计算范式的固有特性。编写良好的进程总是使用定时等待来接收响应。 - Marko Topolnik
1
只是为了澄清,因为第一句话可能会被误解:进程与线程是完全不同的东西。是的,前者可以通信,但不像通过传递对象那样容易。 - ellimilial

5
当Java线程结束时,它不会影响其他线程。
让我反问一个问题:为什么你认为Thread.stop()已经被弃用超过十年?原因正是上面所述的否定。举两个具体例子:当你停止正在执行诸如System.out.println()或Math.random()之类看似无害的操作的线程时,结果是整个JVM中这两个特性都将被破坏。同样的情况也适用于应用程序可能执行的任何其他同步代码。
如果我们在处理请求的线程方面进行研究,理论上可以编写应用程序,使其根本不使用由锁保护的共享资源;然而,这只能帮助指出Java线程相互依赖的确切程度。而实现的“独立性”只适用于请求处理线程,而不适用于该应用程序中的所有线程。

让我重新表达“impact”为“crash”。 - Wand Maker
这样的改述使否定更加准确地成立。崩溃和无限阻塞是两种相当可能的结果。 - Marko Topolnik
如果您能详细阐述您的答案,那将会很有帮助。我只是在暗示 Erlang 进程和 Java 线程之间存在某种相似性,其他方面可能都不同。 - Wand Maker
它们之间的相似之处是表面的,而差异则是根本性的。两个独立的JVM进程是更好的比喻。 - Marko Topolnik
我理解BEAM == JVM,Process == Thread。你建议Process == JVM。也许这是一种看待它的方式。 - Wand Maker
取决于类比的目的。如果目的是比较隔离语义,那么就像我上面所说的那样。如果涉及与底层平台的交互,则是你所说的。 - Marko Topolnik

2
为了补充之前的回答,Java线程有两种类型:守护线程和非守护线程。
要改变线程的类型,您可以调用.setDaemon(boolean on)方法。不同之处在于,守护线程不会阻止JVM退出。正如Thread的Javadoc所说:
“当所有运行的线程都是守护线程时,Java虚拟机退出。”
这意味着:用户线程(那些没有明确设置为守护线程的线程)会阻止JVM终止。另一方面,守护线程可能仍在运行,当所有非守护线程完成时,JVM将退出。因此,回答您的问题:您可以启动一个不会在完成后退出JVM的线程。
至于与Erlang / Elixir的比较,请不要忘记:它们是不同的范例,如上所述。
虽然JVM模仿Erlang的行为不是不可能,但这并不是它的目的,因此需要进行大量权衡。以下项目试图实现这一目标: 这段文本是一个无序列表,其中包含两个链接。第一个链接指向维基百科上的Erjang词条,第二个链接指向维基百科上的Akka词条。

2
这对Java线程也是正确的吗?我的意思是,当Java线程崩溃时,它也不会引起其他线程崩溃。
是和否。我解释一下:
涉及共享内存: Java进程中的不同线程共享整个堆,因此线程可以以大量计划和未计划的方式交互。然而,在堆栈中的对象(例如,传递给被调用方法的上下文)或ThreadLocal是它们自己的线程(除非它们开始共享引用)。
崩溃: 如果一个线程在Java中崩溃(抛出Throwable到Thread.run(),或者某些东西被循环或阻止),那么这种意外可能不会影响其他线程(例如,服务器连接池将继续运行)。但是,由于不同的线程相互交互,如果其中一个异常结束(例如,一个线程尝试从另一个线程的空管道中读取,后者没有关闭其端口),其他线程将很容易被搁置。因此,除非开发人员高度警惕,否则很可能会产生副作用。
我怀疑任何其他范例都打算使线程完全独立地运行。它们必须共享信息并以某种方式协调。然后就有可能搞砸事情。只是它们采取了更为防御性的方法,"给你绞死自己的绳子较少"(与指针相同的习语)。

0

从表面上看,它们可能很相似。在这样一个对比的受限环境中(“一个撞到另一个”),它们可能看起来一样。但是,一旦我们深入了解它们的细节和本质,真正的区别就会浮出水面。@zxq9提供了大量对比细节,希望能够帮助理解它们确实在细节上有所不同。

-- Erlang在分布式系统方面是一项工程奇迹。它对问题域的展望真正非凡,并且它对系统资源的处理方法确实与众不同。这种方法在其他任何地方都找不到。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接