《Java并发编程实战》读记——基础知识

多线程并发应用程序可能存在的性能问题:(1)频繁的上下文切换开销,局部性丢失;(2)多线程共享数据需要同步,同步机制会导致CPU缓存失效。

Java中的同步机制包括:synchronized关键字、显式锁、volatile变量和原子变量。

先检查后执行(Check-Then-Act)是非常常见的一种竞态条件,它会导致某些线程基于一种可能失效的状态来做出判断或执行某个计算。

每一个Java对象都有一个互斥锁,称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock),synchronized关键字即使用内置锁进行同步。内置锁是可重入的。

使用内置锁可能导致性能问题,因为内置锁以对象为粒度,这在某些场景中可能是不适合的。同一个对象的多个synchronized代码块是不能并行的,哪怕它们在逻辑上毫无关系,因为它们使用同一个内置锁。

同步有两个层面的作用:(1)用于实现操作的原子性;(2)用于实现内存可见性(Memory Visibility)。

在没有同步的情况下,Java内存模型允许编译器对操作顺序进行重排,并将数值缓存在寄存器中。此外,它还允许CPU对操作顺序进行重排,并将数值缓存在处理器特定的缓存中。

Java的volatile变量与C/C++的volatile变量不同,不仅会阻止此变量被缓存在寄存器或对其他处理器不可见的地方,还会阻止编译器和CPU对该变量上的操作与其他内存操作进行重排,从而实现内存可见性。Java中volatile变量的内存可见性是非常重要的,C/C++的volatile变量没有这个特性。

加锁既可以确保内存可见性又可以确保原子性,volatile则只能确保内存可见性。

最低安全性(out-of-thin-air safety):当线程在没有同步的情况下读取变量时,可能会得到一个失效的值,这个值是由之前的某个线程设置的值,而不是一个随机值。但是最低安全性对于非volatile类型的64位数值变量(double和long)无效,JVM允许将64位读写操作分解成两个32位的操作。为了使得64位变量的安全性得到保证,需要声明为volatile或者加锁。

当且仅当对象的构造函数返回时,对象才处于一个确定的状态。因此,在构造函数中要注意不要让对象的this引用逸出。

ThreadLocal变量为每个使用该变量的线程都存储一份独立的副本,这些特定于线程的值保存在Thread对象中,当线程终止后这些值会作为垃圾回收。要谨慎使用ThreadLocal,它类似于全局变量,可能使得代码难以理解和维护。

Final域能确保初始化过程的安全性。Java内存模型为不可变对象的共享提供了特殊的初始化安全性保证:如果一个不可变对象的所有域都是final域并且被正确构造(在对象构造期间,this引用没有逸出),那么即使在发布该对象的引用时没有使用同步机制,也可以安全地访问该对象。

Object的构造函数会在子类构造函数运行之前先将默认值写入所有的域。

要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。

同步容器类通过使用内置锁将所有对容器状态的访问都串行化,以实现线程安全性,代价是严重降低并发性。典型的同步容器类包括Vector和Hashtable,以及由Collections.synchronizedXxx等工厂方法创建的同步封装器类。

并发容器类使用细粒度的加锁机制,在实现线程安全性的同时兼顾并发性能。通常情况下可以用并发容器替代同步容器。然而,并发容器类也使得客户端代码不可能再通过持有容器的内置锁来独占容器。

普通容器和同步容器的迭代器在迭代过程中如果发现容器被修改,会立即抛出ConcurrentModificationException异常。迭代器通过使用一个计数器来跟踪容器的修改,从而检测到容器在迭代过程中发生的变化。

并发容器和写时复制容器在迭代时不会抛出ConcurrentModificationException异常。

写时复制容器在每次修改时都会生成一个新的容器副本,这可能需要较大的开销,因此写时复制容器只应该应用于读多写少的场景。

在设计程序时应该总是使用有界队列来进行资源管理,以应对负荷过载的情况。

双端队列和工作密取可以实现更好的并发性和负载均衡:每个工作线程都有自己的双端队列作为任务队列,平时每个工作线程只是访问自己的双端队列,从而极大地减少竞争;当自己的双端队列为空时,从其他工作线程的双端队列尾部秘密获取任务。

同步工具类根据自身的状态来协调多个线程的控制流,典型的同步工具类包括阻塞队列(BlockingQueue)、信号量(Semaphore)、栅栏(Barrier)以及闭锁(Latch)。

闭锁在到达结束状态之前,线程不能通过,当到达结束状态之后,线程可以通过。闭锁到达结束状态之后,将不会再改变状态。

栅栏与闭锁相似,可以阻塞一组线程直到某个事件发生。两者的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。栅栏可以用于在并行迭代算法中在某些点实施操作同步。

Exchanger是一种两方(Two-Party)栅栏,用于两方在栅栏位置上交换数据。

计数信号量用于控制同时访问某个特定资源的操作数量,可用于实现资源池,与操作系统的信号量类似。二值信号量是初始值为1的计数信号量,此时计数信号量退化为互斥量。

Read More