haleonj
haleonj

Reputation: 1518

What are alternatives to functional programming for handling shared mutability?

After watching some videos on the Rust language, I'm increasingly interested in examining my coding decisions based on mitigating the complexity of shared mutable state. Functional programming/Lambda Calculus seems to be the most popular standard to overcome the problem of shared mutable state. Are there alternatives though? Is there a consensus now that functional programming is a reasonable default approach to solve the problem?

Upvotes: 0

Views: 398

Answers (1)

mmirwaldt
mmirwaldt

Reputation: 883

Disclaimer:
I am aware that this post might not directly answer your question. However, many programmers still overlook they can sometimes avoid shared mutability. I want to show you how here with an example and hope, it helps you though.

TL;DR: Ask yourself whether unshared mutability or shared immutability can also be options.

What about doubting whether you really need shared mutability?
If you turn one of both terms into the opposite, then you gain two useful alternatives:

  • unshared mutability
  • shared immutability

Let's have an example in Java 8 to illustrate what I mean. This example of shared mutability uses synchronize to avoid visibility issues and race conditions:

public class MutablePoint {
    private int x, y;

    void move(int dx, int dy) {
        x += dx;
        y += dy;
    }

    @Override
    public String toString() {
        return "MutablePoint{x=" + x + ", y=" + y + '}';
    }
}
   
public class SharedMutability {
    public static void main(String[] args) {
        final MutablePoint mutablePoint = new MutablePoint();
        final Thread moveRightThread = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (mutablePoint) {
                    mutablePoint.move(1, 0);
                }
                Thread.yield();
            }
        }, "moveRight");
        final Thread moveDownThread = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (mutablePoint) {
                    mutablePoint.move(0, 1);
                }
                Thread.yield();
            }
        }, "moveDown");

        final Thread displayThread = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (mutablePoint) {
                    System.out.println(mutablePoint);
                }
                Thread.yield();
            }
        }, "display");

        moveRightThread.start();
        moveDownThread.start();
        displayThread.start();
    }
}

Explanation:
We have got 3 threads. While the two threads moveRight and moveDown write on the mutable point, the one thread display reads from it. All 3 threads must synchronize on the mutable point to avoid visibility issues and race conditions.

How can you apply unshared mutability?

Unshared means "only one thread reading and writing on a mutable object". You don't need much for that. It's quite easy. You always only access one mutable object from the same ONE thread. Therefore you don't need the keyword synchronize nor any locks nor the keyword volatile. Moreover, this one thread can be very fast without locks and broken memory barriers if it only focuses on reading and writing values in the mutable object. However you are limited to that one thread. That's usually no problem unless you block that one thread with tasks like I/O (don't do that!). Furthermore, you must ensure that the mutable object doesn't "escape" somehow by being assigned to a variable or field outside the one thread and accessed from there.

If you apply unshared mutability to the example, it could look like that:

public class UnsharedMutability {
    private static final ExecutorService accessorService = Executors.newSingleThreadExecutor(); // only ONE thread!
    private static final MutablePoint mutablePoint = new MutablePoint();

    public static void main(String[] args) {
        final Thread moveRightThread = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                accessorService.submit(() -> {
                    mutablePoint.move(1, 0);
                });
                Thread.yield();
            }
        }, "moveRight");
        final Thread moveDownThread = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                accessorService.submit(() -> {
                    mutablePoint.move(0, 1);
                });
                Thread.yield();
            }
        }, "moveDown");
        final Thread displayThread = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                accessorService.submit(() -> {
                    System.out.println(mutablePoint);
                });
                Thread.yield();
            }
        }, "display");

        moveRightThread.start();
        moveDownThread.start();
        displayThread.start();
    }
}

Explanation:
We have got all 3 threads again. However, all 3 threads don't need to synchronize on the mutable point because they only access the mutable point in the same one thread which runs in the single threaded ExecutorService accessorService.

How can you apply shared immutability?

Immutability means "no ability to change the state of the object after its creation". Immutable objects always have only one state. Therefore they are always threadsafe. Immutable objects can create new immutable objects when you want to change them though. However, creating too many objects too fast can cause a high memory consumption and lead to a higher GC activity. Sometimes you can deduplicate immutable objects if you have many duplicates of them.

If you apply shared immutability to the example, it could look like that:

public class ImmutablePoint {
    private final int x;
    private final int y;

    public ImmutablePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }

    ImmutablePoint move(int dx, int dy) {
        return new ImmutablePoint(x+dx, y+dy);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ImmutablePoint that = (ImmutablePoint) o;
        return x == that.x && y == that.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "ImmutablePoint{x=" + x + ", y=" + y + '}';
    }
}

public class SharedImmutability {
    private static AtomicReference<ImmutablePoint> pointReference = new AtomicReference<>(new ImmutablePoint(0, 0));

    public static void main(String[] args) {
        final Thread moveRightThread = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                pointReference.updateAndGet(point -> point.move(1, 0));
                Thread.yield();
            }
        }, "moveRight");
        final Thread moveDownThread = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                pointReference.updateAndGet(point -> point.move(0, 1));
                Thread.yield();
            }
        }, "moveDown");
        final Thread displayThread = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                System.out.println(pointReference.get());
                Thread.yield();
            }
        }, "display");

        moveRightThread.start();
        moveDownThread.start();
        displayThread.start();
    }
}

Explanation:
We have got all 3 threads again. However, we use an immutable point instead of a mutable point. While the two threads moveRight and moveDown replace the older instance of the immutable point by a newer one in the atomic reference pointReference, the thread display can get the current instance from pointReference and display it (whenever this thread wants because the instance of immutable point is independent of older and newer ones).

Remark:
The calls to yield() should force thread switches because a loop with only 1000 iterations is just too small. Most CPUs execute such a loop in one time slice.

Upvotes: 1

Related Questions