Java并发编程实战读书笔记(一)

线程安全性

线程安全性是指在多线程环境中,当多个线程同时访问某个类或资源时,该类或资源能够保持其正确性和一致性。为了确保线程安全,需要考虑以下几个方面:

  1. 不变性条件(Invariant):在类的设计中,应该定义一些约束对象状态的不变性条件。这些条件确保了对象在任何时候都满足预期的状态。

  2. 后验条件(Postcondition):在类的设计中,应该定义一些描述对象操作结果的后验条件。这些条件确保了操作完成后对象的状态是正确的。

  3. 原子性(Atomicity):在多线程环境下,操作应该是原子性的,即要么全部完成,要么全部不做。这样可以防止其他线程在操作进行过程中干扰,导致不一致的状态。

  4. 可见性(Visibility):在多线程环境下,一个线程对共享资源的修改应该立即对其他线程可见。这可以通过使用适当的同步机制来实现,如锁、volatile关键字等。

  5. 有序性(Ordering):在多线程环境下,操作的顺序应该得到保证。可以使用同步机制来确保操作按照预期的顺序执行,避免竞态条件和数据竞争。

  6. 互斥(Mutual Exclusion):在多线程环境下,对共享资源的访问应该被限制为互斥的方式,即一次只有一个线程可以访问。这可以通过使用锁、synchronized关键字等实现。

对象的共享

可见性

在多线程编程中,可见性是一个关键的概念。可见性问题涉及到当一个线程修改了共享变量的值时,其他线程是否能立即看到这个变化。在单线程环境中,一旦一个变量被写入,后续的读取操作总能获取到最新的值。然而,在多线程环境中,由于编译器优化、处理器缓存等因素,这种直观的期望可能会被违背。

可见性的挑战

在没有适当的同步措施的情况下,编译器和处理器可能会对内存操作的顺序进行重排序,这可能导致一个线程看到的变量值不是最新的。例如,Java内存模型允许在缺乏同步的情况下对操作进行重排序,这可能导致不同线程看到的操作顺序不一致。

最低安全性

尽管存在可见性问题,但在多线程程序中,读取到的值至少是由某个线程之前设置的值,而不是随机值。这种保证被称为最低安全性。然而,对于非volatile类型的64位数值变量(如double和long),JVM可能将64位的读或写操作拆分为两个32位的操作,这可能导致读取到一个值的高32位和另一个值的低32位,从而违反了最低安全性。

使用volatile关键字

在Java中,可以使用volatile关键字来声明变量,以确保变量的更新能被所有线程立即看到。volatile关键字告诉编译器和运行时系统不要对volatile变量的操作进行重排序,并且确保这些操作直接读写主存,不会被缓存在寄存器或处理器特定的缓存中。

加锁与可见性

加锁不仅保证了互斥,还保证了内存可见性。当多个线程需要访问共享变量时,使用同一个锁进行同步可以确保所有线程都能看到共享变量的最新值。

使用volatile的条件

尽管volatile变量提供了一种轻量级的同步机制,但它的使用应该谨慎。以下是使用volatile变量的一些条件:

  • 对变量的写入操作不依赖于变量的当前值,或者只有一个线程会更新变量的值。
  • 变量不会与其他状态变量一起用于不变性条件中。
  • 访问变量时不需要加锁。

发布与逸出

在讨论线程安全性和对象发布时,"发布"和"逸出"是两个关键概念。下面详细解释这两个概念及其与线程安全性的关系。

发布(Publish)

发布一个对象意味着使对象能够在其创建作用域之外的代码中使用。这通常涉及以下几种情况:

  1. 返回内部对象的引用:当一个方法返回它的内部对象的引用时,这个对象就被发布了。
  2. 将引用存储到公共数据结构中:将对象的引用存储到其他类可以访问的数据结构(如静态字段、实例字段等)中。
  3. 传递引用给其他线程或方法:通过将对象的引用传递给其他线程或方法,使得该对象可以在多个作用域中被使用。

发布对象时,需要确保以下几点:

  • 对象的完整性:在对象被发布之前,应确保其构造过程已完整完成,对象的状态是有效的。
  • 线程安全性:如果对象在多线程环境中被共享,需要确保发布操作是线程安全的,避免数据竞争和不一致性。
  • 封装性维护:避免发布对象内部的可变状态,以免破坏封装性。如果需要发布,考虑提供不可变的视图或者拷贝。

逸出(Escape)

逸出是指对象在不应该被发布的情况下被发布了。逸出可能导致的问题包括:

  1. 未完成的构造过程:如果对象在构造过程中就被发布,其他线程可能会看到对象处于不一致的状态。
  2. 意外的共享:对象被发布到一个公共作用域,而开发者原本不打算共享这个对象。
  3. 封装性破坏:对象的内部状态被意外地暴露给外部代码,导致封装性被破坏。

为了防止逸出,可以采取以下措施:

  • 限制访问:使用访问控制(如私有访问级别),确保对象不会被不当发布。
  • 局部使用:尽量在局部作用域内使用对象,避免将其引用传递到外部。
  • 线程局部存储:使用线程局部变量来存储只在当前线程中使用的对象,避免跨线程共享。

线程封闭

线程封闭是一种线程安全性策略,其核心思想是不共享数据,只在单个线程内部访问数据。这样可以避免多线程中的同步问题,因为不存在多个线程同时访问和修改同一份数据的情况。下面详细解释线程封闭的概念及其实现方式:

线程封闭的概念

线程封闭是指在设计程序时,将数据和处理这些数据的操作限制在同一个线程内部。这样,由于没有跨线程的数据共享,就自然避免了多线程并发访问的问题。线程封闭是实现线程安全性的一种简单而有效的方法。

线程封闭的实现方式

  1. Ad-hoc 线程封闭

    • Ad-hoc线程封闭是指由程序的具体实现来保证线程封闭性。这种方式的线程安全责任完全由开发者承担,因此它比较脆弱。
    • 在Ad-hoc线程封闭中,对象可能会被保存在公有变量中,但实际使用时需要确保只有一个线程能访问到这个对象。
  2. 栈封闭

    • 栈封闭是线程封闭的一种特例,通过使用局部变量来实现。局部变量存储在执行线程的栈中,其他线程无法访问这个栈,因此局部变量天然就是线程封闭的。
    • 栈封闭比Ad-hoc线程封闭更易于维护,也更加健壮,因为局部变量的线程封闭性由语言运行时自动保证。
  3. ThreadLocal

    • ThreadLocal是Java提供的一个特殊类,用于实现线程封闭。它为每个使用该变量的线程都存有一份独立的副本,使得每个线程都可以独立地读取和写入自己的副本。
    • ThreadLocal通常用于防止对可变的单实例变量或全局变量进行共享。例如,在多线程应用程序中,可以使用ThreadLocal来为每个线程维护一个独立的数据库连接,避免数据库连接对象的共享。

线程封闭的优缺点

  • 优点

    • 简单易用,不需要复杂的同步机制。
    • 减少了锁的竞争,提高了程序的性能。
    • 降低了死锁的风险。
  • 缺点

    • 线程封闭的对象不能被多个线程共享,这在某些情况下可能不符合程序的需求。
    • 对于大型程序,过度依赖线程封闭可能导致资源浪费和管理复杂性增加。

不变性

在多线程编程中,不变性是一种重要的设计原则,用于确保线程安全性。不变性意味着一旦一个对象被创建,其状态就不能被修改。这种特性使得不可变对象成为多线程环境中的理想选择,因为它们天然就是线程安全的。下面详细解释不变性的概念及其在多线程环境中的应用。

不变性的概念

不变性意味着对象一旦被创建,其状态就不能再被修改。这里的“状态”包括对象的所有属性和数据。不可变对象具有以下特点:

  1. 构造后状态不变更:对象创建后,其内部状态不能被修改。
  2. 所有域都是final:对象的内部状态由final类型的字段组成,这些字段在对象创建时被初始化,之后不能被修改。
  3. 正确创建:在对象创建过程中,this引用没有逸出,确保了对象在创建过程中的完整性。

不变性的实现

在Java中,可以通过以下方式实现不可变对象:

  • 使用final关键字:将所有字段声明为final,确保它们只能在对象创建时被初始化一次。
  • 不提供修改方法:不在类中提供任何修改对象状态的方法(如setter方法)。
  • 防御性复制:在返回可变内部状态时,进行防御性复制,避免外部对内部状态的修改。

不变性与线程安全性

不变性对于线程安全性至关重要,因为它消除了多线程环境下的多种常见问题:

  • 原子性:不可变对象的状态改变只能通过创建新的对象来实现,这保证了操作的原子性。
  • 可见性:由于状态不改变,不存在缓存一致性问题,因此不需要额外的同步措施来保证可见性。
  • 无需同步:不可变对象本身就是线程安全的,因此在访问和传递不可变对象时不需要额外的同步。

不变性的优点

  • 简化并发编程:不需要担心多线程环境下的数据一致性问题,降低了并发编程的复杂性。
  • 提高性能:由于不需要同步,可以减少锁的竞争和上下文切换,提高程序性能。
  • 增强可预测性:不可变对象的行为更加可预测,易于维护和理解。

不变性的限制

  • 性能开销:每次状态更新都需要创建新的对象,可能会增加垃圾收集的负担。
  • 内存占用:不可变对象可能会导致更高的内存消耗,特别是在有大量相似状态的情况下。

安全发布

不可变对象是一种线程安全的编程模式,它们的状态在创建后不能被修改。这种特性使得不可变对象在多线程环境中非常安全,因为它们不需要额外的同步措施来保证线程安全性。然而,对于可变对象,我们需要采取额外的措施来确保它们的线程安全性。

要安全地发布一个不可变对象,可以遵循以下原则:

  1. 对象的引用和状态必须同时对其他线程可见。
  2. 对象必须是正确构造的,即满足不可变性的所有条件。

以下是一些常见的安全发布不可变对象的方法:

  • 在静态初始化函数中初始化一个对象引用。
  • 将对象的引用保存到volatile类型的域或者AtomicReference对象中。
  • 将对象的引用保存到某个正确构造对象的final类型域中。
  • 将对象的引用保存到一个由锁保护的域中。

对于可变对象,安全发布只能确保“发布当时”状态的可见性。为了确保后续修改操作的可见性,需要在每次访问时使用同步。要安全地共享可变对象,这些对象必须被安全地发布,并且必须是线程安全的或者由某个锁保护起来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

庄隐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值