Java并发编程实践 - 读书笔记(1)

线程安全

编写线程安全的代码,本质上就是管理对状态的访问,而且通常都是共享的、可变的状态。

  • 共享:一个对象可以被多个线程访问
  • 可变:变量的值在它的生命周期内可以改变

通俗地说,一个对象的状态就是它的数据,存储在状态变量(state variable)中,比如实例成员或静态成员。

无论何时,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,就必须使用同步来协调线程对对象的访问。

Java中首要的同步机制是synchronized关键字,它提供了独占锁。此外,术语“同步”还包括了volatile变量、显式锁和原子变量的使用。

没有正确同步的情况下,如果有多个线程访问了同一个变量,你的程序就存在隐患,需要修复:

  • 不要跨线程共享变量
  • 使状态变量为不可变的
  • 在任何访问状态变量的时候,使用同步

当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步和调用方代码不用做其他协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。
对于线程安全类的实例进行顺序或并发的一系列操作,都不会导致实例处于无效状态。
线程安全的类封装了任何必要的同步,因此客户不需要自己提供。

无状态对象(不包含成员也不引用其他类的成员)永远是线程安全的。一次特定计算的瞬时状态,会唯一地存在本地变量中,这些本地变量存储在线程的栈中,只有执行线程才能访问。

竞争条件:当计算的正确性依赖运行时相关的时序或多线程的交替时,会产生竞争条件。换句话说,想得到正确的答案,要依赖幸运的时序。
最常见的一种竞争条件是检查再运行(check-then-act),使用一个潜在的过期值作为下一步操作的依据。

数据竞争(data race)出现于没有使用同步来协调那些共享的非final成员访问的情况。一个线程写入一个变量,可以被另一个线程读取。一个线程读取刚刚被另一个线程写入的变量,如果两个线程都没有使用同步,就会处于数据竞争的风险中。

像大多数并发错误一样,竞争条件并不总是导致失败。

为了确保线程安全,“检查再运行”操作(比如惰性初始化)和“读-改-写”操作(比如自增),必须是原子操作。

为了保证状态的一致性,要在单一的原子操作中,更新相互关联的状态变量。

Java提供了强制原子性的内置锁机制,synchronized块。一个synchronized块有两部分,锁对象的引用,以及锁保护的代码块。它扮演了互斥锁的角色。

当一个线程请求其他线程已经占有的锁时,请求线程将被阻塞。然而,内部锁是可重入的,即锁的请求是基于“per-thread”-锁关联了请求计数和占有它的线程。当计数为0,未被占有。

因为锁使得线程能够串行地访问它所保护的代码路径,所以我们可以用锁来创建相关的协议,以保证线程对共享状态的独占访问。只要始终如一地遵循这些协议,就能确保状态的一致性。
操作共享状态的复合操作必须是原子的,以避免竞争条件。
用锁来协调访问变量时,每次访问变量都需要用同一个锁。
对于每个可被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,这种情况下,我们称这个变量是由这个锁保护的。

一种常见的锁规则是在对象内部封装所有的可变状态,通过对象的内部锁来同步任何访问可变对象的代码路径,保护他们在并发访问中的安全。比如同步容器类就是这样实现的。

对于每一个涉及多个变量的不变约束,需要同一个锁保护其所有的变量。

共享对象

我们不仅希望能够避免一个线程修改其他线程正在使用的对象的状态,而且希望确保当一个线程修改了一个对象的状态后,其他线程能够真正看到改变。
为了确保跨线程写入内存的可见性,你必须使用同步机制。

在单个线程中,只要重排序不会对结果产生影响,那么就不能保证其中的操作一定按照程序写定的顺序执行。
在没有同步的情况下,编译器、处理器,运行时安排操作的执行顺序可能完全出人意料。在没有进行适当同步的多线程程序中,试尝推断那些“必然”发生在内存中的动作时,你总是会判断错误。

锁不仅是关于同步和互斥的,也是关于内存可见的。为了保证所有线程都能看到共享的、可变变量的最新值,读取和写入线程必须使用公共的锁进行同步。

volatile是同步的弱形式。它确保对一个对象的更新以可预见的方式通知其他线程。当一个成员声明为volatile时,编译器和运行时会监视这个变量:它是共享的,而且对它的操作不会与其他的内存操作一起重排序。所以,读一个volatile类型的变量时,总会返回由某个线程写入的最新值。

只有当volatile能够简化实现和同步策略的验证时,才使用他们。当验证正确性必须推断可见性问题时,应该避免使用volatile变量。正确使用volatile变量的方式包括:

  • 用于确保他们引用的对象状态的可见性
  • 用于标识重要的生命周期事件(比如初始化和关闭)的发生

volatile变量固然方便,但是存在局限性。他们通常被当作标识完成、中断、状态的标识使用。
加锁可以保证可见性和原子性,而volatile只能保证可见性。

满足了下面所有标准,才能使用volatile变量:

  • 写入变量时不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
  • 变量不需要与其他状态变量共同参与不变约束
  • 而且,访问变量时,没有其他原因需要加锁

不要让this引用在构造期间逸出。比如,不要在构造函数中启动线程,否则this引用会被新线程共享。

只有满足如下状态,一个对象才是不可变的:

  • 它的状态不能在创建后修改
  • 所有成员都是final类型,并且
  • 它被正确创建(创建期间没有发生this引用的逸出)

final成员使得确保初始化安全性(initialization safety)成为可能。初始化安全性让不可变对象不需要同步就能自由地被访问和共享。

为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确创建的对象可以通过下列条件安全地发布:

  • 通过静态初始化器初始化对象的引用
  • 将它的引用存储到volatile成员或者AtomicReference
  • 将它的引用存储到正确创建的对象的final成员中
  • 或者将它的引用存储到由锁正确保护的成员中
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值