funnycoding / blog

A Github Issue Blog
22 stars 0 forks source link

《Java并发编程实战》3.对象的共享 #18

Open funnycoding opened 4 years ago

funnycoding commented 4 years ago

编写正确并发程序关键在于:访问共享可变状态时需要进行正确的管理【也就是需要使用同步来管理对共享可变状态的访问。】第2章 介绍了如何通过同步避免多个线程在同一时刻访问相同的数据,本章的主题是介绍如何「共享」(sharing)和「发布」(publishing)对象,从而使它们能够「安全」地被多个线程同时访问。 <----- 【其实就是指的是如何让别的类安全的访问修饰符为 public 的类变量】

同步代码块同步方法可以确保以原子的方式执行操作,但是 syncrhonized 不仅用于实现原子性操作或者确定临界区(Critical Section)<----【也就是被锁保护的代码块】,同步还有另一个重要的方面内存可见性(Memory Visibility)。 我们不仅希望防止某个线程正在使用对象的状态时,该字段同时被其他线程修改,而且希望确保当一个线程修改了对象的状态后,其他线程能看到状态发生了变化

如果没有同步,这种可见性就无法实现。 你可以通过显式的同步或者类库中内置的同步来保证对象安全的发布

3.1 可见性

可见性是一种复杂的属性,因为可见性中的错误总会违背我们的直觉。 在 「单线程环境」 中,如果向某个变量先写入值,在没有其他操作的情况下读取这个变量,那么总能得到相同的值。

这是一件看起来自然而然的事情,但是当 「读操作」「写操作」「不同的线程」 中执行时,情况却并非如此,我们无法确保执行「读」操作的线程能看到其他线程修改的最新的变量状态有时甚至是根本无法做到

为了确保多个线程之间对内存写入操作的可见性,必须使用「同步机制」

程序清单 3-1 中的 NoVisibility 说明了当多个线程在没有同步的情况下「共享」 数据时出现的错误。 在代码中,主线程读线程都将访问共享变量 readynumber

「主线程」 启动 「读线程」,然后将 number 设置为 42, 并将 ready 设置为 true

读线程一直循环判断,直到发现 ready 的值变为 true,然后输出 number 的值。

虽然 Novisibility 看起来会输出 「42」 ,但事实上很可能输出0或者根本无法终止循环【看不到 ready 的值为 true】。

这是因为在代码中没有使用足够的 「同步机制」,因此无法保证主线程写入的 ready 值 和 number 值 对于读线程可见的。

程序清单 3-1 在没有同步的情况下共享变量(不要这么做)

// NoVisibility.java
public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        @Override
        public void run() {
            System.out.println(ready);
            System.out.println(number);
            // 当读线程无法访问到主线程给 ready 的赋值的时候,会一直进入这个循环
            while (!ready) {
                System.out.println("ready != true");
                Thread.yield();
            }
            // 输出的值可能是42 也可能是0 因为读线程获取到的 number 值 可能在主线程给 number 赋值之前获取到
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}
/**
但是在这个例子中这里基本上不会看到 输出 number =0 的情况,因为现在的 CPU 速度太快了。只能说这种情况理论存在
*/

当读线程无法获取到主线程给 numberready 的赋值时,就会发生错误的情况,而读线程并不一定能看到主线程对变量的赋值,造成这种情形的机制是 重排序(Recordering)【这个机制非常关键】

只要在某个线程中无法检测到重排序情况(即使在其他线程中可以很明显地看到该线程中的重排序),那么就无法确保线程中的操作按照程序中指定的顺序来执行。

在没有同步的情况下,编译器处理器,以及运行时(JVM)等都可能对操作执行的顺序选择一些意向不到的调整。【重排序的来源:编译器,处理器(CPU)和 JVM】

在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得到正确结论。

重排序出现的原因JVM为了充分利用现代多核处理器的强大性能。 【提升JVM 在多核处理器运行环境下的效率】

缺乏同步的情况下 :Java 内存模型允许编译器对操作顺序进行重排序,并将数值缓存寄存器 中。

此外它还允许 CPU 对操作顺序进行重排序,并将数值缓存在处理器特定的缓存中。更多细节在第16章。

【也就是重排序是为了提高对多核处理器的利用,提高程序执行的效率】

要想避免 NoVisibility 程序中出现的问题,就要保证: 只要数据在多个线程之间共享,就使用正确的同步

3.1.1 失效数据

Novisibility 展示了缺乏同步程序中可能产生错误的一种情况: 失效数据。

例子中的读线程查看 ready 变量的时候,可能得到的是一个已经失效的值,除非在每次访问变量时都使用同步,否则很可能获得一个失效的变量值。更糟糕的是,失效值可能不会同时出现一个线程可能获得某个变量最新值,另一个线程获得变量的失效值。

【随机产生的错误最可怕】

失效值 可能导致严重的安全错误活跃性问题。在Novisibility失效值可能导致输出错误的值(读线程获取到的 number 是 主线程赋值之前的值,也就是0或者使程序无法结束(获取到的 ready的值 是 false)。

如果是对 对象引用的失效(例如链表中的指针) 则情况会更复杂失效数据还可能导致令人困惑的故障 例如:意料之外的异常被破坏的数据结构不精确的计算以及无限循环等。

程序清单 3-2 中的 MutableInteger非线程安全的,因为 getset 都是在没有同步的情况下访问value 的。 与其他问题相比,失效值问题更容易出现:如果某个线程调用了 set,那么另一个正在调用 get 的线程不一定获取到的是最新的值

程序清单 3-2 非线程安全的可变整数类:

@NotThreadSafe
public class MutableInteger {
    private int value;

    public int get() {
        return value;
    }

    public void set(int value) {
        this.value = value;
    }
}

【这其实就是一个非常普通的 Bean,但是因为 get 和 set 方法都是非同步方法,所以可能导致线程不安全的情况发生】

程序清单 3-3SynchronizedInteger 中,通过对 getset 等方法进行同步,可以使 MutableInteger 成为一个线程安全的类, 但是仅对 set 方法进行同步是不够的,调用 get 时 仍然可能看见失效的值。【要保证互斥,就必须都加锁,这样才能保证获取值的时候同时没有别的线程正在修改值】

如果某个线程调用了 set, 那么另一个正在调用 get 的线程可能会看到更新后的 value也可能无法看到(这就引发了问题)。

程序清单 3-3 线程安全的可变整数类:

// 使用内置锁同步 get 和 set 方法。保证 共享变量 value 的 可见性。
@ThreadSafe
public class SynchronizedInteger {
   @GuardedBy("this") private int value;

    public synchronized int get() {
        return value;
    }

    public synchronized void set(int value) {
        this.value = value;
    }
}

3.1.2 非原子的64位操作

线程没有同步的情况下读取变量时,可能会得到一个 失效值,但至少这个值是由之前 某个线程 设置的值,而不是一个随机值。这种安全性也被称为 最低安全性(out-of-thin-air-safety)。【也就是最低安全性需要保证该值是一个被之前线程设置的值,而不是一个没有逻辑的随机值】

最低安全性适用于绝大多数变量,除了 非volatile 的 64位数值变量(double 和 long)

Java 内存模型要求:变量的读取和写入操作都必须是 原子操作。 但对于volatile 类型的 longdouble 变量, JVM 允许将 64位的读操作或写操作 分解为 两个 32位的操作。当读取一个非 volatile 类型的 long 变量时,如果对该变量的 读操作 和 写操作在不同的线程中执行,那么很可能会读取到某个值的高32位 和 另一个值的 低32位。【组合起来就变成了一个莫名其妙的随机数?】

因此,即使不考虑数据失效问题,在多线程中使用共享且可变longdouble 等类型的变量也是不安全的,除非使用同步机制 — volatile 关键字 或者使用锁 syncrhonized 将变量保护起来。

R大对于 JVM 对于 原子性的 long 和 double 的规范的回答

3.1.3 加锁与可见性

内置锁 :可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果

如下图所示,当线程A执行某个 同步代码块时,线程B 随后进入由同一个锁保护的同步代码块

在这种情况下可以保证,在锁被释放之前, A 看到的变量值 在B 获得锁后同样可以由 B 看到。也就是当线程B执行由锁保护的同步代码块时,可以看到线程A之前在同一个同步代码块中的所有操作

【Happens-Before规则所保证的可见性】

如果没有同步,上述操作无法保证。

现在,我们可以进一步理解为什么在访问某个共享可变的变量时,要求所有线程在同一个锁上进行同步,就是为了确保某个线程写入该变量的值,对于其他线程都是可见的。否则,如果一个线程在未持有「正确锁」的情况下读取某个变量,那么可能读取到的是一个「失效值」

加锁的含义不仅仅局限于互斥行为,还包括内存可见性。

为了确保所有线程都能看到共享变量的最新值,所有执行 读操作 或者 写操作线程都必须在同一个锁上同步。

3.1.4 Volatile变量

Java 语言提供了一种稍弱的同步机制volatile 变量,用来确保将变量的更新操作通知到其他线程。

当变量被声明为 volatile 类型后,编译器和JVM 都会注意到这个变量是共享的,因此不会将这个变量上的操作与其他内存操作一起进行重排序

volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此读取 volatile 变量返回的总是其最新的值【保证了变量的内存可见性】

为了更好的理解 volatile 关键字,可以将 volatile 成想象 程序清单 3-3 中的 SynchronizedInteger 的类似行为,将 字段 valuevalatile 关键字修饰,就可以获得 类似在其 get/set 方法上加锁的效果。 **①**然而因为 volatile 没有实际的产生加锁行为,所以不会使执行线程阻塞,因此 volatile 是一种比内置锁 synchronized 更轻量级的同步机制。

volatile 变量对可见性的影响 比 volatile 变量本身更重要。

但是 volatile 对变量提供的同步性比使用锁的同步代码块更脆弱,同时如果大量滥用也会造成难以理解的代码的出现

仅当 volatile 变量能简化代码的实现以及对同步策略的验证,才应该使用它们,如果验证 「正确性」 时需要对可见性进行复杂的判断,就不适合使用 volatile 变量。

volatile 正确的使用方式:

程序清单 3-4 给出了 volatile 变量的典型用法检查某个状态标记以判断是否退出循环。在这个例子中,线程试图通过类似数绵羊的传统方法进入休眠状态。为了使代码能正确执行,asleep 必须是 volatile 变量。否则当另一个线程修改 asleep 状态时,执行判断的线程可能无法获取 asleep 的最新状态。 也可以使用 锁 来保证更新操作的可见性,但是使用锁有点大材小用,并使代码更加复杂。

【不过说实话,第一遍看,感觉还是有点抽象了,如果能有更加具体的例子就好了】

程序清单 3-4 数绵羊

// 保证 asleep 在并发环境下的可见性
volatile boolean asleep;
...
while(!asleep) {
        countSomeSheep();
}

将volatile 变量类比为对 get/set 方法加锁并不准确, synchronizedInteger 在内存上的可见性比 volatile 更强。详细参见第16章

: 在当前大多数处理器架构上,读取 volatile 变量的开销只比读取非 volatile 变量高一点。【也就是开销忽略不记】

对于服务器应用程序,无论是开发还是调试阶段,都应该使用 JVM 中的 -server 模式, server 模式的JVM 比 client 模式的 JVM 会进行更多的优化。 例如将循环中未被修改的变量提升到循环外部,因此在开发环境(client) 中能运行的代码可能会在部署环境(server)模式中运行失败。【但是不那么讲究的话,可能开发和部署环境都是 client模式...】

例如上例中,如果不将 asleep 声明为 volatile 类型,则server 模式的 JVM 会将 asleep 的判断条件移动到循环体外部,这将导致一个死循环。「JVM优化导致的并发问题」

【所以使用 -server 模式最大的价值还是在于开发环境与部署环境的统一,不过现在都用 docker,这种问题少了很多了】

虽然 volatile 变量很方便,但也存在一些局限性。 volatile 变量通常用做某个操作完成,发生中断或者状态的标志,例如程序清单 3-4 中的 asleep 标志。 尽管 volatile 变量可以用于表示其他的状态信息,但在使用时要非常小心。因为 volatile语义不足以确保递增操作 count++ 的原子性,除非你能确保只有一个线程对变量执行写操作

「加锁机制既能确保可见性又确保原子性,而 volatile 只确保可见性。」

【有得必有失, synchronized 是一种重型的同步机制,当「只」需要确保可见性的时候,使用 volatile 更好。】

当且仅当满足以下 所有 条件时,才应该使用 volatile 变量

说实话对以上三点,能有更具体的例子就好了。

3.2发布与逸出

【其实就是本来应该被封装在类的内部的可变性变量,封装被破坏之后怎样保证线程安全性】

发布Publish)一个对象的定义: 使对象能够在当前作用域之外的代码中使用。

例如

【上面的都是破坏封装的情形 ↑

这里的发布其实就是指 私有字段/方法 经过代理/中转 可以被外部类访问的,其实典型代表就是 beanget/set 方法?【我的理解】

【之前的这个理解还是狭隘了,指的是类中封装的一切,字段也好,对象也好,暴露给了外界,就是发布】

【也就是某种意义上对封装的一种破坏。】

许多情况下我们需要确保对象及其内部状态不被发布(也就是对封装性的维持),而在某些情况下,我们又需要发布某个对象

但是如果在发布时需要确保线程的安全性,则可能需要使用同步。【多线程环境下保证对被发布状态的访问的安全性】

发布类的内部状态可能会破坏封装性,并使程序难以维持不变性条件。例如如果在对象构造完成之前就发布该对象,就会破坏线程的安全性

当某个不该发布的对象被发布时,这种情况被称为`逸出(Escape)`。3.5 节 介绍了如何安全发布对象的一些方法。

发布对象的最简单的方法是:将对象的引用保存到一个 公有静态变量中,以便任何类和线程 都能看见该对象。如程序清单 3-5 所示。 在 initilaize 方法中实例化一个新的 HashSet 对象,并将对象的引用保存到 knowSecrets 中以「发布」 该对象。

程序清单 3-5 发布一个对象

// 其他类可以直接使用 knowSecrets,并获取其中保存的 Secret 的状态
public static Set<Secret> knowSecretes;

public void initialize() {
        knowSecretes = new HashSet<Secret>();
}

「发布」某个对象时,可能会间接发布其他对象。如果将一个 Secret 对象添加到集合 knowSecrets中,那么同样这个 Secret 会被间接发布,因为任何代码都可以遍历这个集合,并获得对这个新 Secret 对象的引用。 同样,如果从非私有方法中返回一个引用,那么同样会发布返回的对象

程序清单 3-6 中的 UnsafeStates 发布了本应是私有状态的数组

程序清单 3-6 使内部的可变状态 「逸出」(不要这么做)

// UnsafeState.java
public class UnsafeState {
    private String[] states = new String[]{
            "AK", "AL" /* ... */
    };
    // 这个方法返回了私有数组的引用,导致其他类可以访问并修改这个私有数组
    public String[] getStatus() {
        return states;
    }
}

如果按照上面例子中的方式来 「发布」 states , 就会出现问题。因为任何调用者都能修改这个数组的内容。 在这个示例中,数组 states 已经「逸出」了它所在的作用域,因为这个本应私有的变量已经被发布了。

当发布一个对象时,该对象中的非私有域引用的所有对象同样会被发布。一般来说,如果一个已经发布的对象能够通过非私有的变量引用和方法调用到达「其他对象」,那么这些对象也会被发布

假设有一个类C,对于 C 来说,外部(Alien)方法 是指行为并不完全由 C 来规定的方法,包括其他类中定义的方法以及 C 中可以被 override 的方法(既不是 private 也不是 final 的方法)。当把一个对象传递给某个外部方法时,就相当于发布了这个这个对象。

你无法知道哪些代码会执行,也无法知道在外部方法中究竟会发布这个对象,还是保留对象的引用并在随后由另一个线程使用。

无论其他线程会对已发布的引用执行哪种操作,其实都不重要,因为误用引用的风险会始终存在。

【就像你的密码被泄漏,无论账号是否被人使用,都已经不安全了】这正是需要使用封装的最主要的原因:封装能够使得对程序的正确性进行分析变得可能,并使得无意中破坏设计约束条件变得更难。

最后一种发布对象或其内部状态的机制就是发布一个内部的类的实例,如程序清单 3-7ThisEscape 所示。当 ThisEscape 发布 EventListener 时,也隐含地发布了一个 ThisEscape实例本身,因为在这个内部类的实例中包含了对 ThisEscape 实例的引用(Java 中的内部类持有外部类的隐式指针

程序清单 3-7 隐式地使 this 引用逸出(不要这么做)

// ThisEscape.java
public class ThisEscape {
    // 这里构造 ThisEscape 对象实际上是为了构造一个实现了 EventListener 的实例
    // 但还是因为这个类是 ThisEscape 的内部类,所以匿名内部类隐式持有外部类的引用
    // 相当于 ThisEscape 实例本身也被发布了。
    public ThisEscape(EventSource source) {
        source.registerListener(new EventListener() {
            @Override
            public void onEvent(Event e) {
                doSomething(e);
            }
        });
    }

    void doSomething(Event e) {
    }

    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
    }
}

安全对象的构造过程

ThisEscape 中给出了一个特殊的逸出示例:this 引用在构造函数中逸出。当内部的 EventListener 实例发布时,在外部封装的ThisEscape 实例也逸出了。并且仅当对象构造函数返回时,对象才处于可预测和一致的状态。因此当从对象的构造函数中发布对象时,只是发布了一个尚未构造完成的对象。即使发布对象的语句位于构造函数的最后一行也是如此。如果 this 引用在构造过程中逸出,那么这种对象就被认为是不正确构造

不要在构造过程中使 this 逸出。

在构造过程中使 this 引用逸出的一个常见错误是,在构造函数中启动一个线程。【为啥要在构造函数中启动一个线程?】

当对象在其构造函数中创建一个线程时,无论是 显示创建(通过将它传给构造函数)还是隐式创建(由于 ThreadRunnable 是该对象的一个内部类), this 引用都会被新创建的线程共享。

在对象尚未完全构造之前,新的线程就可以看见它。【也就是对象的可见性与构造函数是否完成无关】

在构造函数中创建线程并没有错误,但是最好不要立即启动它,而是通过一个 startinitialize 方法来启动(详情在第7章 更多服务生命周期的内容)。

在构造函数中调用一个可改写的实例方法时(也就是非 privatefinal 的方法,可以被子类覆写的方法),同样会导致 this 引用在构造过程中逸出

疑问:

【构造函数中调用 非 privatefinal 方法是怎样导致 this 引用在构造过程中逸出的?】

如果想在构造函数中注册一个事件监听器 或者启动线程,那么可以使用 私有的构造函数和一个公共的工厂方法(Factory Method),从而避免不正确的构造过程

程序清单3-8 使用工厂方法来防止 this 引用在构造函数中逸出

// 使用私有构造函数 + 公共工厂方法防止 this 逸出
// SafeListener.java
public class SafeListener {
    private final EventListener listener;

    // 私有的构造函数
    private SafeListener() {
        listener = new EventListener() {
            @Override
            public void onEvent(Event e) {
                doSomething(e);
            }
        };
    }

    // 公共工厂方法用来获取 SafeListener 类实例

    public static SafeListener newInstance(EventSource source) {
        SafeListener safeListener = new SafeListener();
        source.registerListener(safeListener.listener);
        return safeListener;
    }

    void doSomething(Event e) {
    }

    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
    }
}

与上面例子中的区别:

3.3 线程封闭

需要使用同步的情景:

于是我们有了一个不需要使用同步也能让线程安全的方法:不共享数据

如果仅在单线程内访问数据,就不需要同步,这句技术被称为 「线程封闭」(Thread Confinement),它是实现线程安全的最简单的方式之一。<---【也就是将变量的访问区域确定在单个线程中,使变量无法同时被多个线程同时访问】

当某个对象被封闭在一个线程中时,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。

使用线程封闭的两个典型的例子:

Java 语言中并没有强制规定某个变量必须由锁来保护。 同样 Java 语言也无法强制将对象封闭在某个线程中。

↑【也就是线程封闭是一种从设计上来保证线程安全的手段,而不是 Java 语言的一种强行的安全机制,所以只能依靠程序员自己来实现并保证。】

线程封闭是「程序设计」中的一个考虑因素,必须在程序中实现。 Java 语言及其核心库提供了一些机制来帮助维持线程的封闭性例如:局部变量<---【将变量封闭在方法栈上】 和 ThreadLocal 类。

但是即便如此,线程封闭仍然需要程序员来小心地实践与确定其封闭对象不会逸出。

3.3.1 Ad-hoc 线程封闭

Ad-hoc 线程封闭:维护线程封闭性的职责完全由程序实现来承担。【也就是完全靠程序员对类的设计,没有任何强制的手段和机制】

Ad-hoc 线程封闭是 非常`脆弱`的,因为没有任何一种语言特性来强制保证对变量的封闭,例如可见性修饰符或局部变量,能将对象封闭到目标线程上。 事实上,对线程封闭对象的引用(例如 GUI 应用程序中的可视化组件或数据模型等)通常保存公有变量中。

当决定使用线程封闭技术时,通常是因为要将某个特定的子系统实现为一个单线程子系统。在某些情况下单线程子系统提供的简便性胜过 Ad-hoc 线程封闭技术的 脆弱性。【使用单线程子系统的另一个原因是为了避免 死锁,这也是大多数 GUI 都是单线程的原因。「第9章」将 进一步介绍 「单线程子系统」。】

volatile 变量上存在一种特殊的线程封闭: 只要你能确保只有 单个线程共享volatile 变量执行写入操作,那么就可以安全地在这些共享的 volatile 变量上执行 "读取 — 修改 — 写入" 的操作。<---【volatile 保证了变量的可见性,再加上只有单个线程对变量访问,这两个条件结合在一起确保了对变量操作的原子性】

在这种情况下,相当于将修改操作 封闭在 单个线程中以防止发生竞态条件,并且 volatile 变量的 可见性保证还确保了其他线程可以看到最新的值。

总结: Ad-hoc 线程封闭技术「太脆弱」,因此在程序中应该尽量少用,在可能的情况下使用更强的 栈封闭 或者 ThreadLocal 类 来实现 线程封闭。

3.3.2 栈封闭

栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。

【其实也就是使用方法内的局部变量来替代类变量,因为方法中的变量都存在于虚拟机栈中,是线程的私有变量,所以不存在线程安全问题。】

「栈封闭」中,只能通过「局部变量」才能访问对象。正如 「封装」 能使得代码更容易维持「不变性条件」那样,「同步变量」也能使对象更易于封闭在线程中。 局部变量的固有属性之一就是 封闭执行线程

它们位于执行线程的中,其他线程无法访问这个栈<---【因为 Java内存模型中方法栈是线程私有的部分,每个线程都有自己的方法栈。】

「栈封闭」 也被称为线程内部使用或者线程局部使用,比 Ad-hoc 更容易维护,也更加健壮

对于 基本类型局部变量,例如 3-9 中的 loadTheArk 方法的 numParis 无论如何使用,都不会破坏栈封闭性。由于任何方法都无法获得对基本类型的引用,因此 Java 语言的这种语义就确保了基本类型的局部变量始终封闭在线程内

程序清单 3-9 基本类型的局部变量引用变量的线程封闭性:

// 一个动物类,封装了具体的物种,性别,以及一个 动物的容器 ark
// Animal.java
public class Animals {
    Ark ark;
    Species species;
    Gender gender;

    // 把传入集合中的种族相同 性别不同的动物存入 ark 中 并统计其数量
    public int loadTheArk(Collection<Animal> candidates) {
        SortedSet<Animal> animals;
        int numPairs = 0;
        Animal candidate = null;

        // animal confined to method, don't let them escape! 不要被封闭在方法中的动物们给跑了
        animals = new TreeSet<>(new SpeciesGenerComparator());
        animals.addAll(candidates);
        for (Animal a : animals) {
            if (candidate == null || !candidate.isPotentiaMate(a)) {
                candidate = a;
            } else {
                ark.load(new AnimalPair(candidate, a));
                ++numPairs;
                candidate = null;
            }
        }
        return numPairs;
    }

    class Animal {
        Species species;
        Gender gender;

        // 判断 Animal 是否种族相同且性别不同
        public boolean isPotentiaMate(Animal other) {
            return species == other.species && gender != other.gender;
        }
    }

    // 动物的种类
    enum Species {
        AARDVARK, BENGAL_TIGER, CARIBOU, DINGO, ELEPHANT, FROG, GNU, HYENA,
        IGUANA, JAGUAR, KIWI, LEOPARD, MASTADON, NEWT, OCTOPUS,
        PIRANHA, QUETZAL, RHINOCEROS, SALAMANDER, THREE_TOED_SLOTH,
        UNICORN, VIPER, WEREWOLF, XANTHUS_HUMMINBIRD, YAK, ZEBRA
    }

    // 动物的性别
    enum Gender {
        MALE, FEMALE
    }

    class AnimalPair {
        private final Animal one, two;

        public AnimalPair(Animal one, Animal two) {
            this.one = one;
            this.two = two;
        }
    }

    // 实现一个比较器 用来比较2个 AnimalPair 是否相同
    class SpeciesGenerComparator implements Comparator<Animal> {
        @Override
        public int compare(Animal one, Animal two) {
            // 如果是0则说明物种相同
            int speciesCompare = one.species.compareTo(two.species);
            // 如果不等于0 说明这俩物种不同 则直接返回,如果等于0 则返回 性别的比较结果
            return (speciesCompare != 0) ? speciesCompare : one.gender.compareTo(two.gender);
        }
    }

    class Ark {
        private final Set<AnimalPair> loadedAnimals = new HashSet<>();

        public void load(AnimalPair pair) {
            loadedAnimals.add(pair);
        }
    }
}

书上只截取了 loadTheArk 方法中的代码。

维持对象引用的栈封闭性时,程序员需要多做一些工作以确保被引用对象不会被逸出。在 loadTheArk 中实例化了一个 TreSet 对象,并将指向该对象的一个引用保存到 animals 中。

// 先声明了一个 animal 引用
SortedSet<Animal> animals; 
//然后将这个引用 指向 TreeSet 对象
animals = new TreeSet<>(new SpeciesGenerComparator());

此时,只有一个引用指向集合 animals这个引用被封闭在局部变量中,因此也被封闭在执行线程中。

然而如果发布了对集合 animals (或者该对象中的任何内部数据的引用),封闭性将被破坏,并导致 对象 animals 「逸出」

疑问:

// 直接使用 TreeSet 来保存 candidates
TreeSet<Animal> animals = new TreeSet<>(new SpeciesGenerComparator());
animals.addAll(candidates); 

// 和上面的 先声明一个 SortedSet 然后 再将这个 SortedSet 的引用 赋值给一个新的 TreeSet 然后再将 candidates 存入 TreeSet 
SortedSet<Animal> animals; 
animals = new TreeSet<>(new SpeciesGenerComparator());
animals.addAll(candidates);

【这两者之间,后者就将 animals 封闭在了方法中吗? 为什么?我个人认为 没有区别

如果在线程内部(Within-Thread)上下文中使用非线程安全的对象,那么该对象仍然是线程安全的【线程封闭可以的特性可以让非线程安全的对象变为线程安全的】

然而,要小心的是,只有编写代码的开发人员才知道哪些对象需要备封闭到执行线程中,以及被封闭的对象是否是线程安全的。如果没有明确地说明这些需求,后续的维护中很可能错误地将对象逸出。【也就是如果没有明确的文档,指出需要维护线程封闭性,单靠编程很容易破坏这种封闭】

3.3.3 ThreadLocal 类

维持线程封闭性的一种更规范的方法是使用 ThreadLocal,这个类能使线程中的某个值保存对象关联起来

ThreadLocal 中有 getset 等方法,这些方法为每个使用该变量的线程都独立的保存了一个副本因此 get 总是返回当前执行线程在调用 set 时设置的最新值。

// ThreadLocal.java 源码 中的 get方法,可以看到 注释中说的很清楚,返回当前线程中 thread-local 值的 copy 
// 其实内部就是用一个 Map 来将 线程名称 与 value 进行映射
/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

ThreadLocal 对象通常用于防止对可变的单例变量(Singleton) 或 全局变量进行共享。

具体应用:

​ 例如,在单线程应用程序中程序可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都需要传递一个 Connection 对象。

​ 由于 JDBC 的连接对象不一定是 线程安全的,因此当多线程应用程序在 没有协同 的情况下使用全局变量时,就不是线程安全的。 通过将 JDBC 的连接保存到 ThreadLocal 对象中,每个线程就拥有了属于自己的连接。

程序清单 3-10 使用 ThreadLocal 来维持线程封闭性:

// 使用 ThreadLocal 保证线程的封闭性
// ConnectionDispenser.java
public class ConnectionDispenser {
    static String DB_URL = "jdbc:mysql://localhost/mydatabase";

    // 将当前数据库的链接地址生成的 Connection 存入 ThreadLocal,当需要使用时 调用 getConnection() 获取这个连接
    private ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
        public Connection initialValue() {
            try {
                return DriverManager.getConnection(DB_URL);
            } catch (SQLException e) {
                throw new RuntimeException("Unable to acquire Connection", e);
            }
        }
    };

    public Connection getConnection() {
        return connectionHolder.get();
    }
}

当某个频繁执行的操作需要一个临时对象例如一个缓冲区,而又希望避免每次执行时重新分配该临时对象,就可以使用这项技术。

当某个线程初次调用 ThreadLocal.get() 方法时,就会调用 initialValue 来获取初始值。

从概念上看,可以将 ThreadLocal<T> 视为包含了 Map<Thread,T> 的对象,其中保存了特定于该现成的 值,但 ThreadLocal 的实现并非如此这些特定于线程的值保存在 Thread 对象中当线程终止后,这些值会作为垃圾回收

Java5.0 之前, Integer.toString() 方法使用 ThreadLocal 对象来保存一个 12个字节大小的缓冲区,用于对结果进行格式化,而不是使用共享的静态缓冲区(因为这需要使用锁机制)或者在每次调用时都分配一个新的缓冲区。

:除非这个操作执行频率非常高,或者分配操作的开销非常高,否则这项技术不可能带来性能提升。在 Java 5.0 中,这项技术被一种更直接的方式替代—— 每次调用时分配一个新的缓冲区,对于像临时缓冲区这种简单的对象,该技术没有什么性能优势

ThreadLocal的应用场景:

假设你需要将一个单线程应用移植到多线程环境中,通过将 共享的全局变量 转换为 ThreadLocal 对象(如果全局变量的语义允许),就可以维持线程的安全性

然而,如果将应用程序范围内的 缓存 转换为 线程局部的缓存,就不会有太大作用

【疑问:】

【为什么 将应用程序范围内的 缓存 转换为 线程局部的缓存,就不会有太大作用。】

在实现应用程序框架时,大量使用了 ThreadLocal。

例如在 EJB 调用期间, J2EE 容器需要将一个事务上下文(Transaction Context) 与某个执行中的 线程关联起来。 通过将事务上下文保存在一个 静态的 ThreadLocal 对象中,可以很容易地实现这个功能:当框架代码需要判断当前运行的是哪个事务时,只需要从这个 ThreadLocal 中读取事务上下文(通过get获取)。

这种机制很方便,因为它避免了在调用每个方法时都传递上下文信息,然而这也将使用该机制的代码与框架代码 耦合 在了一起。【缺点】

开发人员经常性的 滥用 ThreadLocal,例如将所有全局变量都作为 ThreadLocal 对象,或者作为一种"隐藏" 方法参数的手段。 ThreadLocal 变量类似于全局变量,它能降低代码的可重用性,并在类之间引入 隐含的耦合性,因此在使用时要格外小心

3.4 不变性

满足「同步」需求的另一种方法是使用不可变对象(Immutable Object)

目前为止介绍了许多与 原子性可见性 相关的问题:

如果对象的状态不会被改变,那么这些问题与复杂性也就消失了。如果某个对象在被创建后不能被修改,那么这个对象就被称为 「不可变对象」。「线程安全性」 是 不可变对象的 固有属性之一,它们的不变性条件是由 「构造函数」 创建的,只要它们的状态不改变,那么这些不变性条件就能得以维持

不可变对象一定是「线程安全」的。

不可变对象简单,因为它们只有一种状态,并且在创建的时候由构造函数来控制。在程序设计中,一个最困难的地方就是判断「复杂对象」的可能状态。但是「不可变对象」将这种困难消除了。

同样,「不可变对象」也更加安全。如果将一个「可变对象」传递给不可信的代码,或者将该对象发布到了不可信代码可以访问到的地方,那么就很危险——不可信代码会改变可变对象的状态。更糟糕的是,在代码中将保留一个对该对象的引用,并稍后在其他线程中修改对象的状态。

另一方面,不可变对象不会被「不可信代码」或者「恶意代码」破坏状态,因此可以安全地「共享」和「发布」这些对象,而无须创建保护性的副本

如果将一个 可变对象 传递不可信的代码 或者将该对象发布不可信代码可以访问到的地方,那么就很危险,因为不可信代码会修改可变对象的状态,更糟糕的是代码中将保留一个对该对象的引用并稍后在其他线程中修改可变对象的状态

Java 语言规范」和 「内存模型」 中都没有给出对 「不可变性」 的正式定义,但是不可变性并不等于将对象中的所有域都声明为 final 类型,即使对象中的所有域都是 final 类型的,这个对象也仍然是可变的,因为在 final 类型的域中可以保存对「可变对象」 的 引用

满足以下条件,对象才是不可变

注解①:从技术来看,不可变对象并不需要将其所有域都声明为 final 类型,例如 String 就是这种情况。

这就要对垒的「良性竞争数据 Benign Data Race)情况做「精确分析」,因此需要深入理解 「Java 内存模型」。

(注意:String 会将 散列值计算推迟到第一次调用 hash Code 时进行,并将计算得到的散列值缓存到 final 类型的 域中,但这种方法之所以是可行的,因为这个域有一个 非默认的值,并且在每次计算中都得到相同的结果(因为基于一个不可变的状态),而自己在编码时不要这么做)

在 「不可变对象」的内部仍然可以使用可变对象」来管理它们的状态,如 程序清单 3-11 中的 ThreeStooges 所示。 尽管保存姓名的 Set 是「可变」的,但从 ThreeStooges 的设计中可以看到,在 Set 对象构造完成后无法对其进行「修改」。 stooges 是一个 final 类型的「引用变量」,因此所有其所有对象的状态都通过一个 final 域 来访问

对象不可变的最后一个要求是「正确地构造」,这很容易满足,因为构造函数能使该引用由除了构造函数及其调用者之外的代码来访问

程序清单 3-11 在「可变对象」基础上「构建不可变类」:

// 使用可变的基础对象构建不可变的类
// ThreeStooges.java
@Immutable
public class ThreeStooges {
    // 用来保存名字的Set,虽然用final修饰,但是其保存的内容是可变的。
    private final Set<String> stooges = new HashSet<>();

    // 在构造函数中,修改 stooges 的状态
    public ThreeStooges() {
        stooges.add("傀儡1");
        stooges.add("傀儡2");
        stooges.add("傀儡3");
    }

    public boolean isStooge(String name) {
        return stooges.contains(name);
    }

    // 这里相当于是要跟初始化类时放入的傀儡名称做一个对应
    public String getStoogeName() {
        List<String> stooges = new Vector<>();
        stooges.add("傀儡1");
        stooges.add("傀儡2");
        stooges.add("傀儡3");
        // 其实这个 Set 是可变的,只要你手动多添加一个,就破坏了不可变性
        this.stooges.add("傀儡4");

        return stooges.toString();
    }

    public static void main(String[] args) {
        ThreeStooges ts = new ThreeStooges();
        System.out.println("List: " + ts.getStoogeName());
        System.out.println("Set: " + ts.stooges);
    }
}
/**
输出
List: [傀儡1, 傀儡2, 傀儡3]
Set: [傀儡1, 傀儡2, 傀儡3, 傀儡4]
*/

由于程序的「状态」总在不断地变化,你可能会认为需要使用「不可变对象」的地方不多,但实际情况并非如此。

在 「不可变对象」与「不可变对象引用」 之间存在着差异。保存在不可变对象中的程序状态仍然可以更新——通过将一个保存新状态的实例来替换原有的不可变对象。下一节将给出使用这项技术的实例。

注解①:许多开发人员担心这种方法会带来性能问题但是这是没有必要的。 内存分配的开销比你想象的还要,并且不可变对象还会带来其他的性能优势减少了对「加锁」或者「保护性副本」的需求,以及降低 对基于 "代" 的垃圾收集机制的影响

3.4.1 Final 域

关键字 final 可以视为 C++ 中的 const 机制的一种「受限版本」,用于构造不可变性对象final 类型的不能修改的,但是如果 final所引用的对象可变的,那么这些被引用的对象是可以「修改」的。 【对象的引用不能修改,但是对象中的字段的值是可以修改的,比如一条狗链,这个狗链本身不能被替换,但是这个狗链所栓的狗是可以修改的。

Java 内存模型中,final 域有着 特殊的语义final 域能确保 初始化过程中的「安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须使用同步机制

即使对象是可变的,通过将对象的某些声明为 final 类型,仍然可以简化对状态的判断,因此限制对象的可变性,也就相当于限制了该对象的可能状态集合。仅包含一两个可变状态的"基本不可变" 对象要比包含多个可变状态的对象简单。通过将域声明为 final 也会明确告诉维护人员 这个域是不会发生变化的。

除非需要更高的可见性,所有域都应该是 private 的。

除非需要某个域是可变的,否则域应声明为 final 域。

以上2点是良好的编程习惯。

3.4.2 示例:使用 Volatile 类型来发布不可变对象

第2章中的 UnsafeCachingFactorizer中尝试使用了两个 AtomicReference 来保存最新的数值以及因式分解结果,但是这种方式并非线程安全。因为无法以原子的方式来同时 更新或读取 这两个相关的值。同样, 用 volatile 修饰的变量来保存这2个值也不是线程安全的。然而在某些情况下,不可变对象能提供弱形式的原子性

因式分解 Servlet 需要执行2个原子操作:

每当需要对一组相关数据原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据。例如程序清单 3-12 中的 OneValueCache

注解①:如果在 OneValueCache构造函数没有调用 copyOf,那么 OneValueCache不是 不可变的。 Arrays.copyOf 是在 Java6 中才引入, 同样可以调用 clone。【也就是在构造函数中创建副本】

程序清单 3-12 对数值以及因数分解结果进行缓存不可变容器类:

// 使用不可变类缓存因式分解的数值和结果
// OneValueCache.java
@Immutable
public class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastfactors;

    public OneValueCache(BigInteger lastNumber, BigInteger[] factors) {
        this.lastNumber = lastNumber;
        // 这是一个关键操作,如果不使用 Arrays.copyOf创建一个副本,则该类不是 不可变类
        this.lastfactors = Arrays.copyOf(factors, factors.length);
    }

    public BigInteger[] getFactors(BigInteger i) {
        if (lastNumber == null || !lastNumber.equals(i)) {
            return null;
        } else {
            // 返回的都是副本,而不是直接返回对象引用
            return Arrays.copyOf(lastfactors, lastfactors.length);
        }
    }
}

对于在访问和更新多个相关变量时出现的「竞态条件」问题,可以通过将这些变量全部保存在一个「不可变对象」中消除。 如果是一个「可变对象」,那么就必须使用 来确保原子性。如果是一个「不可变对象」,那么当线程获得了该对象的引用后,不必担心「另一个线程」会修改对象的状态。 如果要更新这些变量,那么可以创建一个新的「容器」对象,但其他使用原有对象的「线程」仍然会看到对象处于一致状态

程序清单 3-13 中的 VolatileCachedFactorizer.java 使用了 3-12中构建的 OneValueCache 来缓存数值以及因数分解的结果。当一个线程将 volatile 类型的 cache 设置为引用一个新的 OneValueCache 时,其他线程会立即看到新缓存的数据。【volatile 保证了内存可见性

当一个线程将 volatile 类型的 cache 设置为引用一个新的 OneValueCache 时,其他线程会立即看到新的缓存数据【 volatile 的内存可见性 特性】

程序清单 3-13 使用指向不可变容器volatile 类型引用来缓存最新结果

// Caching the last result using a volatile reference to an immutable holder object
// 使用 volatile + 不可变对象容器封装缓存数据
// VolatileCachedFactorizer.java

@ThreadSafe
public class VolatileCachedFactorizer extends GenericServlet implements Servlet {
        // 不可变容器 OneValueCache
    private volatile OneValueCache cache = new OneValueCache(null, null);

    @Override
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = cache.getFactors(i);
        if (factors == null) {
            factors = factor(i);
            // 通过给不可变容器重新赋值一个对象来修改不可变类的状态,volatile 保证其他线程能第一时间看到容器状态的变化
            cache = new OneValueCache(i, factors);
        }
        encodeIntoResponse(resp, factors);
    }

    void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[]{i};
    }
}

cache 相关的操作不会互相干扰,因为 OneValueCache不可变的,并且在每条相应的代码路径中只会访问它一次。通过使用包含多个状态变量的容器对象来维持「不变性条件」,并使用 volatile 类型的引用来确保可见性。使得 VolatileCachedFactorizer 在没有显示的使用的情况下就保证了线程的安全性

3.5 安全发布

目前为止,讨论的重点是如何确保对象不被发布,例如让对象封闭在线程另一个对象的内部。在某些情况下我们希望在多个线程之间共享对象,此时必须确保安全地进行共享。然而,如果只是像 程序清单 3-14 那样将对象引用保存到 「公有域」 中,那么还不足以保证安全地发布这个对象:

程序清单 3-14没有足够同步的情况下发布对象 (不要这么做):

// 不安全的发布 将要发布的对象的引用保存在 类 public 字段中
public class StuffIntoPublic {
    public Holder holder;

    public void initialize() {
        holder = new Holder(42);
    }
}

这个看似没有问题的示例为什么会失败?在单线程中这个类毫无疑问是可用的,但是在多线程环境下,由于存在「可见性」问题,其他线程看到的 Holder 对象可能处于不一致的状态,即便在该对象的构造函数中已经正确地构建了 「不变性条件」。 这种不正确的发布导致其他线程看到尚未创建完成的对象

3.5.1 不正确的发布:正确的对象被破坏

一个尚未完全创建的对象并不拥有「完整性」。 某个观察该对象的线程将看到对象处于不一致状态,然后看到对象的状态突然发生变化,即使线程在对象「发布」后没有修改过它。 事实上,如果程序清单 3-15 中的 Holder 使用 清单 3-14 中的不安全发布方式,那么另一个线程在调用 assertSanity 时 将抛出 AssertionError①。

注解①:问题不在于 Holder 类本身,而在于 Holder 类未被正确地发布。然而,如果将 n 声明为 final 类型,那么 Holder 将成为不可变类,从而避免出现非正确发布的问题。

程序清单 3-15 由于未被正确发布,这个类可能出现故障

// Class at risk of failure if not properly published
// 不安全的发布,在多线程环境下可能会出现异常与状态不一致
// Holder.java
public class Holder {
    private int n;

    public Holder(int n) {
        this.n = n;
    }

    public void assertSanity() {
        if (n != n) {
            throw new AssertionError("This statement is false");
        }
    }
}

由于没有使用同步机制来保证 Holder 对象,因此将 Holder 称为 「未被正确发布」的对象。 在这种对象中存在两个问题

注解②:尽管在构造函数中设置的值似乎是第一次向这些域中写入的值,因此不会有"更旧的值" 这种失效情况,但是 Object构造函数会在子类构造函数 运行之前先将默认值写入所有的域。因此,某个域的默认值可能被视为是 失效值

如果没有足够的同步,在多个线程之间共享数据会发生一些非常奇怪的事情。

3.5.2 不可变对象与初始化安全性

不可变对象是一种非常重要的对象。 「Java内存模型」为「不可变对象」提供了特殊的初始化安全性保证。我们之前已经知道,即使某个对象的引用对其他线程是可见的,也并不意味着对象状态 对于使用该对象的线程来说一定是可见的。【缓存带来了可见性问题】为了确保「对象状态」能呈现出「一致的视图」,就必须使用同步

另一方面,即使在「发布不可变对象的引用时没有使用同步,也可以安全地访问该对象。为了维持这种初始化安全性的保证,必须满足不可变性的所有需求:

任何线程都可以在不需要额外同步的情况下安全地访问 「不可变对象」,即使在发布这些对象时没有使用同步

这种保证还将延伸到被正确创建对象中所有 final 类型的。 在没有额外同步的情况下,也可以安全地访问 final 类型的域。然而,如果 final 类型的域指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步

3.5.3 安全发布的常用模式

「可变对象」必须通过 「安全的方式」来发布,这意味着 「发布」和「使用」 该对象的线程都必须使用「同步」

下面是如何确保使用对象的线程能够看到该对象处于已发布状态,并稍后介绍如何在发布后对其可见性进行修改

安全地发布一个对象「对象的引用 以及 「对象的状态 必须 同时 对其他线程可见,一个正确构造的对象可以通过以下方式来安全地发布:

「线程安全容器」内部进行同步意味着,将对象放入到某个容器 如 VectorsynchronizedList 时,将满足上述最后一条需求。如果 线程A对象X 放入一个线程安全的容器,随后线程B 读取这个对象,那么可以确保 线程B 看到 线程A 设置的 X的状态,即便这段 读/写 X 的应用程序代码中没有显示的包含显式的同步

尽管 Javadoc 在这个主题上没有给出很清晰的说明,但是线程安全库中的容器类提供了以下的安全发布保证

上面就是三类线程安全的容器。

类库中的其他数据传递机制 例如 FutureExchanger 同样能实现安全发布,在介绍这些机制时将要讨论它们的安全发布功能。

通常,要发布一个 静态构造对象, 最简单和安全的方式是使用静态的初始化器

// 将 new 关键字创建的对象用静态变量保存就是 使用 静态的初始化器?
public static Holder holder = new Holder(42)

「静态的初始化器」JVM 在类的「初始化」阶段执行(static 的特性)。由于 在「JVM 内部」存在着「同步机制」,因此通过这种方式初始化的任何对象都可以被安全地发布[JLS 12.4.2]。

【↑也就是利用虚拟机的同步机制来安全发布对象。

3.5.4 事实不可变对象(Effectively Immutable Object)

【这里的事实不可变跟 《OnJava8》中讲函数式编程中的闭包特性事实不可变是一致的概念:虽然这个变量是非final 修饰的,但是在这个作用域内它确实没有改变 就是事实不可变对象

如果对象在发布后没有被修改,那么对于其他没有在额外同步的情况下安全地访问这些对象的线程来说,「安全发布」是足够的。所有的「安全发布机制」都能确保,当「对象的引用」对所有访问该对象的线程可见时,对象发布时的状态对于所有线程也将是可见的。并且如果对象状态不会再改变,那么就足以确保任何线程访问都是安全的。

如果对象从技术上来说是可变的,但是事实上其状态在发布后不会再改变,那么将这种对象称为事实不可变对象(Effectively Immutable Object)。通过使用「事实不可变对象」可以保证安全性,简化开发过程,同时不会因为使用同步而减少性能。

在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的 事实不可变对象

例如 Date 本身是可变的①,但是如果将它作为不可变对象来使用,那么在多个线程之间共享 Date 对象时就省去了对的使用。

注解①:这或许是类库设计中的一个失误。

假设需要维护一个Map对象,其中保存了每位用户最近的登录时间:

public Map<String,Date> lastLogin = Collections.synchronizedMap(new HashMap<String,Date>());

如果 Date 值被放入 Map 中之后就不会再改变,那么 synchronizedMap 中的同步机制足以使 Date

被安全地发布,并且在访问这些 Date 时不需要额外的同步

3.5.5 可变对象

如果对象在构造后可以修改,那么「安全发布」只能确保 "发布当时" 状态的可见性。 对于「可变对象」,不仅在发布对象时需要使用同步,而且在每次对象访问时同样需要使用同步来确保后续操作的「可见性」。要安全地共享可变对象」,这些对象就必须被安全地发布并且必须是线程安全的,或者由某个保护起来

对象的发布需求取决于它的可变性

3.5.6 安全地共享对象

当获取一个对象的引用时,你首先需要知道在这个引用上可以执行哪些操作。在使用这个引用之前是否需要获得?是否可以修改它的状态,或者只能读取它。许多并发错误都是没有理解共享对象的这些 "既定规则" 而导致的。当发布一个对象时,必须明确地说明对象的访问方式

【也就是需要有文档性的说明来指导怎样使用对象的引用】

并发程序使用共享对象时,可以使用一些实用的策略

思维导图

3.对象共享