16

I think the answer to the first part of my question is, "yes" -- no point in making objects immutable in a single-threaded application (or I guess in a multi-threaded application if that particular object will not be shared among threads). If I am wrong, please explain why immutable objects are useful in a single-threaded app.

The second part is, if you are sharing an immutable object, what is it useful for if it can't be changed? Why is it even being shared? If it is "changed" so that a new and changed version of the object is created, say a shared counter, how is this shared object used if a change makes a new object -- how does another thread access the new version? More broadly, how are immutable objects handled in a multi-threaded application -- I guess how a shared but immutable counter would be incremented between multiple threads. I have searched extensively and I think all I read were discussions that involved some kind of locking or atomic operations which, I think, could be performed on a mutable object just as well.

And finally, if immutable shared counter is useful, are the machinations any simpler, is debugging any simpler, than just locking access to a mutable shared counter?

10
  • 1
    You might also be interested in how immutable structures work. For example, you should be able to find a lot on how they work in Clojure.
    – Theraot
    Sep 20, 2020 at 4:12
  • 28
    Are you aware that Java's String class is an immutable class? Do you really think it was only made immutable because of multithreading in mind? Do you think the String class is useful despite the fact String objects cannot be changed?
    – Doc Brown
    Sep 20, 2020 at 5:10
  • 3
    A great thing about immutable objects is that you don't need to make a defensive copy every time you want to return a value to a caller. With mutable objects, making defensive copies is widely done.
    – Polygnome
    Sep 20, 2020 at 17:43
  • 2
    With immutable objects, you don't have to make defensive copies all the time. Defensive copying is exhausting...
    – Polygnome
    Sep 21, 2020 at 12:55
  • 2
    @ThomasWeller: see softwareengineering.stackexchange.com/a/416093. And such languages exist, for example the C programming language in editions before ANSI C / C89, without any const keyword. C has no String class, of course, but the equivalent are arrays of characters, and before C89, there was no way to declare such an array as "immutable". Java (and C#) have no notion of "const" parameters, in those languages it is common to use immutable classes instead.
    – Doc Brown
    Sep 21, 2020 at 13:05

10 Answers 10

45

No, immutable objects are quite useful in general.

The first and most basic reason is that concurrency in a system doesn't require a multi-threaded application. Making say... a row in a database immutable provides a lot of benefit for change tracking, collision avoidance, syncing, and backups.

And while less valuable than in concurrent scenarios, immutable objects tend to be easier to use and debug because you know the state of the object across the lifetime of your app and you know that some function isn't misbehaving and mutating it on you. Also, any sort of exceptional state will show up immediately on object creation rather than during some mutation later on during processing. Those tend to be easier to identify, and happen in places where it is easier to recover or abort cleanly.

The second part is, if you are sharing an immutable object, what is it useful for if it can't be changed? Why is it even being shared?

The most obvious example is a configuration. You don't want to change it at runtime, but it's often needed by different parts of your code. Something like the current user. You don't want to change it, but will want to share it with different modules.

If it is "changed" so that a new and changed version of the object is created, say a shared counter, how is this shared object used if a change makes a new object -- how does another thread access the new version?

So the biggest thing with immutable objects (in most languages) is that writes of the object are atomic. Uninterruptable.

Say you want to change a few fields on a mutable object. The thread changes one, then another, then another. Any other thread can read the object in-between each of those steps. It'll see a half-changed object.

But if you want to change a few fields on an immutable object it's different. The thread makes a new object, changing the three fields it wants to change. Then it overwrites the shared reference in one, uninterruptable step. Any other thread can grab a reference to the object and know that it won't change. If it grabs the reference before the other thread does its write, it might get the old object but it can never get a half-changed object.

For a counter it doesn't much matter. Incrementing an int will be just as uninterruptable as assigning a reference to a new int (though that might not apply if you need counters bigger than an int, depending on your language, compiler, target CPU, etc.). Locks though are very costly in most languages/platforms so programmers will avoid them when it is safe to do so.

(For more info consider this question, which is adjacent to this one)

13
  • 4
    “If it grabs the reference before the other thread does its write, it might get the old object but it can never get a half-changed object.” I don't think that's always true; in the absence of a memory barrier or other control, I think it's possible on some systems to see the new reference before the object it points to is fully published (depending e.g. which cache lines get written back). See e.g. this question.
    – gidds
    Sep 20, 2020 at 13:32
  • 1
    Is it actually a good idea to do this with counter? Because if two threads grab an old state, increment it, and write references one after another, you'll end up with counter incremented once and not twice. Sep 21, 2020 at 4:44
  • 2
    @Telastyn "creating an object and assigning it to a variable are not independent reorderable operations". That's only true for final fields. Non-final fields can be written to after the half-created object has already been published. But yes as long as you make all fields final (a reasonable assumption) it'll work.
    – Voo
    Sep 21, 2020 at 7:28
  • 1
    I don't consider incrementing a counter thread-safe. It has several operations: reading the old value, incrementing, writing the new value. In C#, we need Interlocked.Increment(i) instead of i++. C++ 11 has std::atomic_int for that Sep 21, 2020 at 12:49
  • 2
    @Mark i++ is not atomic on any modern cpu that I know of, including x86. The existance of cpu cache already prevents it.
    – freakish
    Sep 22, 2020 at 6:24
17

Immutable objects are useful independently of multithreading:

  • They prevent very nasty bugs with reference types (e.g. classes in Java and C#), when these are used in the implementation of value semantics: This situation create a mental trap that makes you forget about the potential object sharing, which is inherent to reference types, and might lead to side effects where you don’t expect them. Immutability then helps to enforce the value semantic principle that if you want to change value of an object you need a new object.
  • Immutability can also help to avoid bugs in the code, by reducing the risk of accidental changes (especially in languages that allow assignment operators in conditional expressions).

When used in multithreading, they give you the benefit of absence of race conditions. You may therefore use them without having to enclose their access within a critical section.

4
  • Second one doesn't really make sense IMO.
    – user253751
    Sep 21, 2020 at 22:13
  • @user253751 the second is about avoiding the risk of side effects. Even if technically speaking it looks the same, and if in functional programming one gets the second by the means of the first, it is nevertheless a different intent.
    – Christophe
    Sep 22, 2020 at 6:25
  • The second one is talking about final variables, not immutable objects.
    – user253751
    Sep 22, 2020 at 8:18
  • @user253751 if (obj.name = "Steve" ) will fail with an immutable object, but will succeed with a mutable one and change the property name.
    – Falco
    Sep 22, 2020 at 15:30
12

There are things that will naturally be immutable (read-only), because it does not make sense to allow to modify them. However, I will be talking about the other cases...

A lot of things pushed in software engineering are not there to allow to you do more. Instead they constraint how and when you can do things. And that is the case with immutable. As the name suggest, their value cannot be changed. And that makes it easier to reason about them.

With immutable objects, you can call a method/function/routine passing it as an argument, and know that it won't be changed after the call. This means, that when analyzing the code - perhaps for debugging purposes - you would have less to worry.

That, of course, is much more significant when talking about a multithreaded environment. With immutable objects, you don't have to worry if another thread mutated them, while this thread was using it between such and such lines of code. That makes threading code - again - much easier to reason about.

Of course, you could do all you can do with immutable objects by using thread-safe mutable equivalents. Remember, that we are talking of a tradeoff, a constraint in exchange of making reasoning about the code easier.

In a multithreaded environment, with immutable , it is often useful to have a mutable reference to an immutable object. So that you can swap that reference to a reference to the most up to date version. And that operation would be atomic, at no point a thread would see an invalid reference, they either see it updated or not. Then any thread that needs it can copy the reference. While writing the code that needs it, you would also know that if there is another thread creating an updated version, it will not mutate the one you are referencing... instead it will create a new object, and post it on the shared mutable reference.

3
  • By curiosity, and without disrespect, could you give examples of things that are “naturally immutable”? Because looking around, I have never found anything naturally immutable so far, except dates (probably due to the absence of time traveling to retroactively change them) ;-)
    – Christophe
    Sep 20, 2020 at 20:48
  • 1
    @Christophe perhaps naturally is not the best adjetive. I was thinking of wrapping entities from another system, that we cannot modify. Sometimes we can copy values from them, or we can duplicate them, but not really modify the original. Of course, somebody did that design decisión for that system (so, not really natural), yet it is the given for the system that interact with it.
    – Theraot
    Sep 20, 2020 at 23:09
  • Actually, I've always found the atomic swap to configuration to be problematic. If a single thread reads the configuration at two points, then it may read N and N + 1 and you get into issues. Of course it shouldn't do that, but if you have it a reference to an atomic, you can't enforce that it only reads it once per "transaction". In this case, I prefer modelling the update of configuration as a message to the thread: here is the new configuration, use it for the following transactions. Sep 21, 2020 at 15:53
12

I think the answer to the first part of my question is, "yes"

I think you could not be more wrong than this. The answer is IMHO clearly no, since the biggest benefit of immutable objects is that they are side-effect free. And unwanted side-effects are one of the biggest sources of bugs in programs since decades, long before the term "multi-threading" was even invented.

Take for example, Java's String class, which is a perfect example of an immutable class:

  • passing around references to String objects and holding references to strings in containers instead of copying them is clearly beneficial for many programs in terms of memory usage and CPU usage

  • only the guarantee of immutability makes it possible to write and extend such programs without any fear of introducing unwanted bugs by side-effects through mutations in some lower-layer methods 20 levels down the call stack.

Sure, there are sometimes cases where a mutable string class is better suited, that is why Java also has a StringBuilder class. However, as it is obvious from the naming, the immutable String seems to be the "default" string type, which everybody uses as long as it is "good enough", whilst the mutable StringBuilder is usually only used when really required (for example, for performance reasons).

2
  • 1
    Immutable doesn't necessarily mean side effect free. It would be bad architecture, but even a read function can have a side effect. And that effect may not mutate the object. They're related but concepts but not the same. In fact it may not even be bad architecture- for example a read on an immutable object may access a cache class and change its state by reading in data, even though the object itself is immutable. Sep 21, 2020 at 3:12
  • 3
    @GabeSechan: I was talking about side-effects on the immutable object itself. I think that's pretty obvious and does not invalidate my example nor my line of argumentation why immutability is useful apart from multi-threading.
    – Doc Brown
    Sep 21, 2020 at 5:49
10

In addition to the other great answers, a few further benefits that I don't think anyone's mentioned yet:

  • Documentation.  If a language supports immutability at some level, or if a class guarantees it, then anyone writing code that passes an immutable object around can see that it can't be changed; they don't need to look carefully into the documentation for everything that uses it (and everything that calls) to see what might be changed.  It makes code easier to read and to reason about.

  • Optimisation.  If a compiler knows that a value or object or whatever cannot possibly change, there are many more optimisations it can use to make the resulting code faster/shorter/simpler.  (Perhaps it can avoid some synchronisation or other memory protection; perhaps it can reuse a local copy instead of fetching from memory; perhaps it can reuse the results of checks or calculations that were made earlier.)

    And at another level, it allows programmers to add optimisations, such as caching/memo-ising the results of calculations.

  • Hash safety.  For example, an object isn't safe to store in a hash table if its hashcode can change.  (This can lead to very subtle bugs.)  And since the hash code is often computed from the object's state, changing state often changes the hash code.  Immutable objects don't suffer from this problem, so are safe to store in hash tables/sets/maps without any extra precautions.

1
  • "There are only two hard things in Computer Science: cache invalidation and naming things" and immutable objects solve cache invalidation.
    – Schwern
    Sep 22, 2020 at 0:26
3

Immutable is just another method of encapsulation. Asking if immutability is only useful for multi-threading is thus asking if not using global variables for everything is only for multi-threading.

People get encapsulation wrong all the time, they think it is security (totally wrong) or to keep others from putting a class/application in an invalid state (partly right, but really for the wrong reason).

Encapsulation exists to reduce the need to think about anything except the current task as much as possible. State is something else to think about. Encapsulation is used to eliminate it when possible.

If you have a class and it has a method that operates on just it’s parameters, it should be made static whether it is private or public or whatever. Why? Because when it is static, only things that are used inside that method exists. It makes it easier to think about what that method does.

Obviously this isn’t perfect, a parameter may be mutable, and so you may have to worry about making a call inside the static method that changes the parameter in non obvious ways.

Which is where immutability comes into play. If the reference is to an immutable object, you know that it hasn’t been changed, because it can’t. Consider a person class, being used in a culture where there is a tradition that women over 50 should have their first names by “Doña” and so the FirstName property does that. It’s easy to take that mutating class and develop a scenario where the output refers to her at different places as first one and then the other and back again multiple times without the method obviously doing something wrong.

2

Immutable objects don't change, but references to immutable objects change all the time, but only when the scope changes. In other words, the changes only happen at precisely-defined points, such as when a message is passed or a function is called. This sort of forces you to collect the changes in a centralized place, then distribute them back out where it is needed, rather than just changing things wherever and whenever and expecting other users of data to notice the change.

It feels like a limitation when you are wanting to write to data, but it's much less chaotic when you are wanting to read data, and reading usually happens much more often.

1

As others have pointed out, immutable objects are useful even for single-threaded applications - though realistically, most complex programs are going to spawn multiple threads in some library if not explicitly in the application code.

Many of their benefits are already covered in other answers, I'd like to mention a few more (in Java lang):

  • Save memory by allowing multiple references to the same object which is guaranteed to be valid: see String.intern() or the caching of smaller Integer values
  • Predictable input state by enforcing methods have to create a new object for output. At a glance, you can tell that name.toUpperCase() can't change name which may be used elsewhere as well, because of String immutability
  • Hash issues were already mentioned, to add to this: most object equals / hashCode implementations use immutable properties of the object as well
  • Memory allocation references are easier for manage for immutable objects as they won't grow in size later on
  • In some cases, immutability should simply GC operations or specific algorithm implementations (Radix sort?) though I don't have specific examples for these

Note that in Java, GC is a separate thread so its already a multithreaded app even if the developer doesn't use any of the threads/concurrent api

0

Another advantage to immutable objects, which other answers didn't call out explicitly, is that parts of an immutable datastructure can be re-used across several values. For example, consider an array:

a1 = [a, b, c, d, e, f]

Arrays are a very common datastructure. Some languages (e.g. C) let us represent arrays using just a pointer to their start address, others also store their length. Let's do the latter, in pseudocode:

type Array = Pair(Address start, Length length)

If we want to "slice" an array, we just need to adjust the start and length, e.g.

function slice(Array a, Length offset, Length length) {
  return Array(start = a.start + offset, length = length);
}

a2 = slice(a1, 3, 2)

Now a2 is the array [d, e]. Crucially, both a1 and a2 are using the same part of memory: the elements d and e aren't copied. This has a few consequences:

  1. Slicing takes constant time, since we only need to make a pair of ints (we don't need to copy any of the elements).
  2. Slicing takes constant memory, for the same reason.
  3. We cannot freeing this memory until all slices have finished using it.
  4. Mutating the contents of this memory will affect all slices using it.

If we make our arrays mutable then we have to be very careful about point (4): values in one part of the program might be affected by other parts of the program which seem unrelated, if they happen to be sharing the same pieces of memory. Note that this isn't just an issue for concurrent applications; such "aliasing" can undermine a lot of assumptions made by our algorithms, causing them to misbehave. The possibility of aliasing also prevents some optimisations being performed by the compiler.

We can avoid this problem, and problem (3), by having some of our functions make copies of the memory contents, and return pointers to those copies. This is slow and memory-intensive compared to slicing. Many libraries will copy "defensively", i.e. more often than strictly needed by the application, in order to provide a simpler API or to avoid edge-cases.

If we make our arrays immutable then point (4) doesn't matter. Point (3) is still a problem, which can also be solved by copying, but in practice this is much less of a concern: having lots of large arrays sharing the same memory is an advantage; the problem is when we're finished using a large array, but we still need a small slice of it. In this case we must copy that slice in order to free up the large array; however, that only requires copying a small amount of data (by definition), so the cost is usually small.

Immutable data can hence make it easy to write fast, safe, low-memory programs. On the other hand, with mutable data we may be forced to sacrifice one of these (e.g. defensive copying is slow; explicit copying is hard to do safely, or easy to do unsafely). Of course, constructing and garbage-collecting lots of tiny immutable values can also slow things down, so there's a balance to be struck.

Also note that this isn't limited to arrays; there's a whole family of "functional datastructures" which are designed so that common operations can share large parts of the memory without defensive copying; the most common is a singly-linked list, where prepending an element can share the entire "tail".

0

Take any immutable concept even as simple pi. Let's say pi = 3.141592653589793238.

Threads can share the definition of pi now and read it knowing its value will not change, that data races are not possible. I don't know why there is a mental block here when we introduce aggregates like whole structs or objects or arrays. The same concept applies. If it don't change, it's safe to read in parallel with the assumption that it won't change. Immutables are safe for concurrent read access because simultaneous write access is not possible.

That's really all there is to it. const absent const_casts in C++ terms will not change. You are safe to read it in parallel without the assumption that it could change at any time.

As for atomic reference counters that are shared, if you can get away from any sort of shared data between threads, I would always err on that side when possible. If you can't get away from it, it's worth noting that simply ensuring that increments and decrements to your reference counter are atomic may not be atomic enough. You might need a broader operation to complete or fail completely in an atomic way than just incrementing or decrementing the counter.

Not the answer you're looking for? Browse other questions tagged or ask your own question.