第二章:线程的安全性
1.什么是线程安全?当多个线程访问某个类的时候,这个类始终都能表现出正确的行为,这个类就是线程安全的。
2.原子性
1)竞态条件
当某个计算的正确性取决于多个线程的简体执行时序时,就会发生竞态条件。最常见的竞态条件类型就是“先检查后执行”,即通过一个可能失效的观测结果来决定下一步的动作。“读取-修改-写入”也是一种竞态条件(例如递增一个计数器),基于对象之前的状态来定义对象状态的转换。将“先检查后执行”以及“读取-修改-写入”等操作统称为符合操作。
原子方式执行(或者说不可分割)的操作:假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说就是原子的。原子操作是指,对于访问同一状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。
2)内置锁
Java提供了一种内置的锁机制来支持原子性:同步代码块。同步代码块包括两部分:一个作为锁的对象引用;一个作为这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁。
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁。获得内置锁的唯一途径就是进入由这个保护的同步代码块或方法。Java的内置锁相当于一种互斥体,这意味着最多只有一个线程能持有这种锁,所以这个锁保护的同步代码块会以原子的方式执行,多个进程在执行该代码块的时候也不会互相干扰。
3)重入
当某个线程请求由其他线程持有的锁时,发出请求的线程就会阻塞。然而,内置锁是可重入的,因此如果某个线程试图获取一个已经由他持有的锁,那么这个请求就会成功。重入的一种实现方式是,为每个锁关联一个获取计数值和一个所有者线程。
3.用锁来保护状态
由于锁能使其保护的代码路径以串行形式(对象的串行访问与对象的序列化(即将对象转化为字节流)操作毫不相干。串行访问意味着多个线程依次以独占的方式来访问对象,而不是并发的访问)来访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。
第三章、对象的共享
同步还有一个重要的方面:内存可见性,我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态 之后,其他线程也能看到发生的状态变化。
1.可见性
多线程中,读操作和写操作可能在不同的线程中执行,为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
1)加锁与可见性
内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享数据的最新值,所有执行读操作或写操作的线程都必须在同一个锁上同步。
2)Volatile变量
Java提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型时,编译器与运行时都会注意到这个变量是共享的,因此不会将变量上的操作与其他内存操作一起重排序。然而,在访问volatile变量时不会执行加锁操作,因此也不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。
仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式包括:确保他们自身状态的可见性;确保他们所引用对象的状态的可见性;以及标识一些重要的程序生命周期事件的发生(例如初始化或关闭)。
加锁机制既能保证可见性又可以确保原子性,而volatile变量只能保证可见性。
当且仅当满足以下所有条件时,才应该使用volatile变量:对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值;该变量不会与其他状态变量一起纳入不变性条件中;在访问变量时不需要加锁。
2.发布与逸出
发布一个对象是指是对象能够在当前作用域之外的代码中使用。当某个类不应该发布的对象被发布时,这种情况就是逸出。
3.线程封闭
如果仅在单线程中访问数据,就不需要同步。这称为线程封闭,是实现线程安全性的最简单方式之一。当某个对象封闭在一个线程中,这种用法将自动实现线程安全性,即使这个对象本身不是线程安全的。
2)栈封闭:是线程封闭的一种特例,在栈封闭中,只有通过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。
3)ThreadLocal类
维持线程封闭性的一种更规范的方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。
4.不变性
满足同步需求的另一种方法是使用不可变对象。当目前介绍了许多与原子性和可见性相关的问题,例如得到失效数据,丢失更新操作或者观察到某个对象处于不一致的状态等待,都与多线程访问同一个可变的状态有关。如果这个对象的状态不可变,那么这个对象就是不可变对象。线程安全性是不可变对象的固有属性。
不可变性并不等于将对象中所有的域都声明为final类型,即使对象中所有域都被声明为final类型,这个对象也仍然是可变的,因为在final类型的域中也可以保存对可变对象的引用。
1)Final域
final类型的域是不能修改的(但如果final域所引用的对象是可变的,那么这些被引用的对象是可以修改的)。final域能够确保初始化过程的安全性,从而可以不受限制的访问不可变对象,并在共享这些对象的时候可以不用同步。
5.安全发布
第四章:对象的组合
介绍一些组合模式,使一个类更容易成为线程安全的,并且在维护这些类时不会无意中破坏类的安全性保证。
1.设计线程安全的类
1)收集同步需求
第五章:基础构建模块
Java平台类库包含了丰富的并发基础构建模块,例如线程安全的容器类以及各种用于协调多个互相协作的线程控制流的同步工具类。
1.同步容器类:包括Vector和HashTable。这些类实现线程安全的方式是:将它们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器状态。
2.并发容器:同步容器将所有对容器状态的访问都串行化,以实现线程安全。这种方法的代价是严重降低并发性,当多个线程竞争容器的锁时,吞吐量将严重降低。Java5.0增加两种新容器类型:Queue和BlockingQueue
1)ConcurrentHashMap
同步容器类在执行每个操作期间都持有一个锁。与HashMap一样,ConcurrentHashMap也是一个基于散列的Map,但是使用一种完全不同的加锁策略来提高更高的并发性和伸缩性。
3)CopyOnWriteArrayList
CopyOnWriteArrayList用于代替同步List,在迭代期间不需要对容器进行加锁或复制。类似的CopyOnWriteArraySet代替同步Set
3.阻塞队列和生产者-消费者模式