TL;DR
- JavaScript is single-threaded within a realm (realm ~= the global environment, variables, etc.).
- JavaScript uses a job queue to run code in a realm, and each job must run to completion before any other job in the realm can run.
- JavaScript programs can have multiple threads working in parallel, by having multiple realms ("workers").
- Realms (and therefore threads) can communicate, transfer objects to each other, and even share memory.
- Usually, the single-threaded rule applies only within a single realm, but sometimes realms are grouped together and only one of them can be serviced by a thread at any given time.
The above was only specified at the language level after this question was asked. It was introduced in the ES2017 spec because it was necessary in order to fully describe the semantics and memory model for shared memory. Prior to that, the word "thread" doesn't appear in any JavaScript specification (for instance, not in ES2016).
Details
When you asked your question in 2016, JavaScript the language was silent on the topic of threading and your interpretation that it was really host environments (browsers, Node.js, etc.) that defined whether the code was run single- or multi-threaded was correct. However, even then, all major JavaScript implementations were single-threaded within a realm.¹
But things have moved on. ES2017 defined the language as allowing a single active thread within a realm at any given time, in order to define the semantics and memory model for shared memory.
Let's look at those five bullet points above in more detail, starting with the first two:
JavaScript is single-threaded within a realm (realm ~= the global environment, variables, etc.).
JavaScript uses a job queue to run code in a realm, and each job must run to completion before any other job in the realm can run.
(1) means that when a thread is running code in a realm, no other thread can be running other code in that realm.
(2) means that once a "job" is started (for instance, the initial job running the script, or a job for running an event handler), that job must be run to completion before any other job can be started. (Without this rule, it would be possible for a single thread to start a job, suspend it in the middle, and run another job before finishing the first, all without violating the single-active-thread rule.)
These two things work together to make reasoning about JavaScript code fairly straightforward compared to shared-memory multi-threaded environments. Let's look at an example.
Suppose we have this code as part of a larger JavaScript program:
// Assume nothing assigns to `counter` except `increment` below
let counter = 0;
function increment() {
if (counter < 10) {
++counter;
}
}
// ...various places `increment` is called...
In a spec-compliant JavaScript environment, only one thread can be running code that can access counter
at any given time (that is, only a single thread can be running code in the realm counter
belongs to). And once the job that called increment
starts, it has to run to completion before any other job can be started in the realm. Those two things together mean we know that counter
will never reach 11. It will reach 10 if increment
is called ten times, but it will never reach 11, no matter how many times increment
is called.
That wouldn't be reliably true in a multi-threaded shared-memory environment (like the Java JVM, or multi-threaded C/C++ programs), for several reasons. One is that it would have a race condition between the counter < 10
check and the ++counter
operation. Two different threads could each get past the counter < 10
check when counter
was 9, then both execute the ++counter
operation.² That isn't possible in JavaScript, though, because of its single-active-thread-per-realm specification and run-to-completion semantics. No other code can be running in the realm between the counter < 10
check and the ++counter
operation. This makes reasoning about code in JavaScript much simpler than it is in shared-memory multi-threaded environments.
("But what about if counter
is in shared memory?" I hear you ask. Good question! Indeed, then you have to worry about race conditions and all kinds of other dragons. This is one reason we have Atomics.compareExchange
. See Chapter 16 of my book JavaScript: The New Toys for details.)
- JavaScript programs can have multiple threads working in parallel, by having multiple realms ("workers").
The browser environment, Node.js, Deno, etc. all have the concept of workers (web, Node.js, Deno). The main realm can create a worker, and since the main realm and worker realms are distinct, code in those realms can be running in parallel on different threads. That's fine, because there's still only a single thread active within a realm at any given time.
- Realms (and therefore threads) can communicate, transfer objects to each other, and even share memory.
Realms and threads can communicate via postMessage
and similar, can transfer some kinds of objects to each other, and can use shared memory so that they literally share a block of memory. Note that shared memory comes with all kinds of dragons! Race conditions, out-of-order writes, all sorts of things. (Again, more details in that chapter on shared memory I mentioned earlier.)
- Usually, the single-threaded rule applies only within a single realm, but sometimes realms are grouped together and only one of them can be serviced by a thread at any given time.
One example where realms are grouped together in this way is when, in a browser, a window opens another same-origin child window:
const child = window.open("something.html");
Now, the parent and child windows have direct access to each other's realms via child
(in the parent) and opener
or parent
(in the child). They can even call each other's global functions.
If different threads could be running the code in each of those realms in parallel, it would potentially violate the single-active-thread-per-realm rule and/or the run-to-completion rule if (say) the child called a method in the parent. To prevent that, the realms are grouped so that only one thread can be running code in any of them at any given time. This means only one realm can make forward progress at any given moment.
¹ Aside from some experimental projects, the only exception to that I'm aware of is Java's support for running "scripting" code in JavaScript using Rhino. The resulting code, running on the JVM, could be running multi-threaded.
² In practice, that specific example would be unlikely even in a shared-memory multi-threaded environment, but it's a reasonable example of the kind of thing that happens, without being too complicated.