用于处理共享可变性的函数式编程的替代方法是什么?

时间:2017-01-26 15:48:41

标签: concurrency functional-programming immutability

在观看Rust language上的一些视频后,我越来越有兴趣在减轻共享可变状态的复杂性的基础上检查我的编码决策。函数式编程/ Lambda微积分似乎是克服共享可变状态问题的最流行的标准。有没有其他选择?现在是否已达成共识,函数式编程是一种合理的默认方法来解决问题?

1 个答案:

答案 0 :(得分:1)

免责声明:
我知道这篇文章可能不会直接回答你的问题。 然而,许多程序员仍然忽略了他们有时可以避免共享可变性。我想通过一个例子向你展示如何在这里,但希望对你有所帮助。

TL;DR:问问自己非共享可变性共享不变性是否也可以作为选项。

怀疑您是否真的需要共享可变性怎么办?
如果您将两个术语中的一个变成相反的,那么您将获得两个有用的替代方案:

  • 非共享可变性
  • 共享不变性

让我们举一个 Java 8 的例子来说明我的意思。 这个共享可变性示例使用同步来避免可见性问题和竞争条件:

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();
    }
}

说明:
我们有 3 个线程。当两个线程 moveRightmoveDown 在可变点上写入时,一个线程 display 从中读取。所有 3 个线程都必须在可变点上同步,以避免可见性问题和竞争条件。

如何应用非共享可变性

Unshared 意味着“只有一个线程在一个可变对象上读写”。 你不需要太多。这很容易。您始终只能从同一个线程访问一个可变对象。因此,您不需要关键字同步,也不需要任何锁,也不需要关键字 volatile。此外,如果这个线程只专注于在可变对象中读取和写入值,那么它可以非常快,没有锁和损坏的内存屏障。 但是,您仅限于该线程。这通常没有问题,除非你用 I/O 之类的任务阻塞了那个线程(不要那样做!)。此外,您必须确保可变对象不会通过分配给一个线程外部的变量或字段并从那里访问而以某种方式“逃逸”。

如果您将非共享可变性应用到示例中,它可能如下所示:

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();
    }
}

说明:
我们再次获得了所有 3 个线程。但是,所有 3 个线程不需要在可变点上同步,因为它们只访问在单线程 ExecutorService accessorService 中运行的同一个线程中的可变点。

如何应用共享不变性

Immutability 的意思是“在对象创建后没有能力改变它的状态”。不可变对象总是只有一种状态。因此它们总是线程安全的。但是,当您想要更改不可变对象时,不可变对象可以创建新的不可变对象。 但是,过快地创建过多对象会导致高内存消耗并导致更高的 GC 活动。有时,如果不可变对象有很多重复项,您可以对其进行去重。

如果您将共享不变性应用到示例中,它可能如下所示:

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();
    }
}

说明:
我们再次获得了所有 3 个线程。但是,我们使用不可变点而不是可变点。当两个线程 moveRightmoveDown 用原子引用 pointReference 中的新实例替换不可变点的旧实例时,线程 display 可以从 pointReference 获取当前实例并显示它(只要这个线程需要,因为不可变点的实例是独立于新旧实例的)。

备注:
对 yield() 的调用应该强制线程切换,因为只有 1000 次迭代的循环太小了。大多数 CPU 在一个时间片内执行这样的循环。