【问题标题】:I don't understand why immutable objects in java are inherrently always threadsafe我不明白为什么java中的不可变对象本质上总是线程安全的
【发布时间】:2014-05-16 02:34:28
【问题描述】:

我正在阅读 Effective Java 书和关于最小化可变性的章节第 15 条。 也许我无法理解线程安全的概念,因为我在并发方面没有太多经验。我能否举个例子来说明不可变对象如何始终是线程安全的?

提前谢谢你!

【问题讨论】:

    标签: java thread-safety java.util.concurrent


    【解决方案1】:

    不可变对象是线程安全的,因为它们不能被修改。

    如果一百万个线程同时访问同一个对象并不重要,因为没有一个线程可以改变对象。

    【讨论】:

    • 更准确地说 - 不可变对象无法修改,每个人(即所有线程)将始终读取相同的值,因此,不可变对象是线程安全的。
    【解决方案2】:

    线程安全意味着更改所述对象不会对使用该对象的其他线程产生不利影响。不可变对象无法更改。因此,根据设计,不可变对象是线程安全的,因为它们一开始就不会发生任何变化。

    请记住,线程可能会共享引用。如果更改引用指向的对象(不更改对象本身,而是将引用与 = 符号一起重新分配给另一个对象),则会危及线程安全。

    【讨论】:

    • 两个 Java 线程“共享引用”的唯一方法是通过两个线程都知道的某个对象的字段。如果他们可以“改变引用指向的对象”,那就意味着通用对象是一个可变对象。
    • 不,它没有。更改参考点的位置是创建一个新对象并将其地址分配给所述参考。如果您谈论操纵对象,则所述对象是可变的。请记住,字符串是不可变的,您可以轻松地让它们引用另一个对象。
    • 您将对象与变量混为一谈。当我们说“字符串是不可变的”时,我们谈论的是 String 类的 instances,但是当您说“您可以让它们引用另一个对象”时,您谈论的是 String 变量。我的观点是,两个线程可以使用相同变量的唯一方法是,如果它是某个对象的字段,并且如果线程可以更新该字段,那么该对象是可变的,并且它可能(取决于您的设计)需要同步举止得体。
    • 我没有将对象与变量混为一谈……您完全误解了我的意思。对象是类的一个实例。说一个字符串是不可变的意味着你不能改变内存中的实例(是对象)。重新阅读我的解释或(更好)Java 文档和 Oracle 教程。因此,您所说的“如果两个线程可以更新该字段,那么它是可变的”是不正确的。你倒着想。如果对象是不可变的,那么两个线程一开始就不能更新它。这就是不可变的定义。
    【解决方案3】:

    假设你有一个柜台:

    class Counter {
        private int counter = 0;
    
        public void increment() {
            counter++;
        }
    
        public int getCounter() {
            return counter;
        }
    

    }

    说这是你的主要方法:

    public static void main(String[] args) {
        final Counter counter = new Counter();
        final CountDownLatch startLatch = new CountDownLatch(1);
        final CountDownLatch endLatch = new CountDownLatch(4);
        final Runnable r = () -> {
            try {
                startLatch.await();
            } catch (final InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 100; i++) {
                counter.increment();
                if (counter.getCounter() % 10 == 0) {
                    System.out.println(counter.getCounter());
                }
            }
            endLatch.countDown();
        };
        new Thread(r).start();
        new Thread(r).start();
        new Thread(r).start();
        new Thread(r).start();
        startLatch.countDown();
        try {
            endLatch.await();
        } catch (final InterruptedException e) {
            e.printStackTrace();
        }
    }
    

    它很长,但基本上它所做的只是创建一个 Counter,然后创建 4 个线程,每个线程将计数器递增一百次,如果该值是 20 的倍数,则打印计数器的值。什么输出你明白了吗?

    20
    40
    60
    80
    100
    141 // <-- Huh? Not a multiple of 20?
    120 // <-- What's up with the order here?
    180
    220
    240
    160 // <-- This is way out of place...
    280
    300
    260
    200
    320
    340
    360
    380
        // <-- missing 400?
    

    嗯,这是一个惊喜。错误的值,不合适的值等等......

    问题是共享像Counter 这样具有可变状态的对象会带来很多困难。您必须处理锁、同步等,才能使可变对象正常运行。在这种情况下,同步相对容易,但要使复杂的对象正确同步却很难。如果需要示例,请查看 java.util.concurrent 中的类。

    不可变对象的好处在于它们避免了这个问题,因为它们不能被修改。所以不管有多少线程对一个不可变对象做某事,你可以绝对确定它不会改变,所以你不会处理这样奇怪的结果。不可变的Counter 将相当无用,但像String 这样的不可变且可以跨线程共享而无需担心跨线程同步更改的东西在并发世界中非常有用。

    【讨论】:

      【解决方案4】:

      您可以通过删除所有设置器和任何更改对象状态的方法来使对象不可变。

      字符串是不可变对象的例子。 无论有多少线程访问一个字符串,它们都无法更改它。每当您修改一个字符串时,都会创建一个新对象。

      所以多个线程可以读取状态但永远不能更新状态。

      【讨论】:

      • 如果一个对象的所有字段都是private,并且它的任何方法都没有更新它的任何字段,那么它实际上是不可变的。这与字段都是final.真正 不可变对象略有不同,不同之处在于,有效 不可变对象只有在安全发布时才是线程安全的。谷歌搜索“Java”和“安全发布”以了解更多信息。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-12-31
      • 1970-01-01
      • 2021-07-15
      • 2019-04-21
      • 2011-10-14
      • 1970-01-01
      相关资源
      最近更新 更多