jlearning.cn

《Java并发编程实战》读书笔记(一)——并发简介与线程安全性

并发编程简介

线程允许在同一个进程中同时存在多个程序控制流。线程共享进程范围内的资源。

线程的优势

  1. 线程可以降低程序的开发和维护成本、提升复杂应用程序的性能。
  2. 将大部分的异步工作流转换成串行工作流,更好的模拟人类的工作方式。
  3. 降低代码的复杂度。
  4. 在GUI应用中,提高用户界面的响应灵敏度。
  5. 在服务器应用中,提升资源利用率以及系统吞吐量。
  6. 简化JVM的实现,垃圾收集器通常在一个或多个专门的线程中运行。
发挥多处理器的强大能力
  1. 多核处理器:多线程发挥多核的优势。
  2. 单核处理器:多线程可以使程序再IO阻塞期间继续运行。
建模的简单性

如果程序只包含一种类型的任务,比包含多种不同类型任务的程序要易于编写。

为每种类型的任务分配一个专门的线程,将程序的执行逻辑与调度机制的细节,交替执行的操作,异步IO以及资源等待等问题分离开来。

异步事件的简化处理

服务器程序为每一个客户端分配一个线程。

响应更灵敏的用户界面

线程带来的风险

安全性问题
跳跃性问题

死锁、饥饿。

性能问题

上下文切换、同步机制会抑制某些编译器优化,使内存缓存区中的数据无效,增加共享内存总线的同步流量。


线程安全性

要编写线程安全的代码,核心在于要对状态访问操作进行管理。特别是对共享的(Shared)和可变的(Mutable)状态的访问。

Informally,对象的状态是指存储在状态变量(实例或者静态域)中的数据。对象的状态可能包括其他依赖对象的域。

共享表示变量可以由多线程同时访问。可变意味着其值在生命周期内可以发生变化。

Java中同步机制关键字是synchronized,以独占加锁的方式。

“同步”还包括:volatile类型的变量、显式锁(Explicit Lock)、原子变量。

访问某个变量的代码越少,越容易确保对变量的所有访问都实现正确同步。

Java没有要求强制要求将状态都封装在类中,开发人员可以将状态保存在某个公开的域,甚至公开的静态域中,或者提供一个对内部对象的公开引用。然而,封装有利于线程安全。

在任何情况下,只有当类中仅包含自己的状态时,线程安全类才是有意义的。

什么是线程安全性

正确性:某个类的行为与其规范完全一致。

良好的规范中通常会定义各种不变性条件(Invariant)来约束对象的状态,以及定义各种后验条件(postcondition

)来描述对象操作的结果。

线程安全性:当多个线程访问某个类时,这个类式中都能表现出正确的行为,那么就称这个类是线程安全的。

当多个线程访问某个类时,不管运行时环境采用何种调度方式,或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类就能表现出正确的行为,那么久称这个类是线程安全的。

无状态对象一定是线程安全的。

原子性

由于不恰当的执行时序而出现不正确的结果:竞态条件(Race Condition)

竞态条件

最常见的竞态条件类型就是“Check-Then-Act”

延迟初始化中的竞态条件

延迟初始化:

1
2
3
4
5
6
7
8
9
10
//NotThreadSate
public class LazyinitRace{
private ExpensiveObject instance = null;
public ExpensiveObject getInstance(){
if(instance == null)
instance = new Expensiveobject();
return instance;
}
}

如果将UnsafeSequence用于某个持久化框架中生成对象的标识,那么两个不同的对象最终将获得相同的标识,这就违反了标识的完整性约束条件

复合操作

使用一个现有的线程安全类:

1
2
3
4
5
6
7
8
9
10
11
12
@ThreadSafe
public class CountingFactorizer implements Servlet(){
private final AtomicLong count = new AtomicLong(0);
public long getCount(){ return count.get();}
public void service(ServletRequest req,ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp,factors);
}
}

当在无状态的类中添加一个状态时,如果改状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的。

加锁机制

当在不变形条件中涉及多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。因此更新一个变量时,需要在同一个原子操作中对其他变量同时进行更新。

要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。

内置锁

Java提供了一种内置的锁机制来支持原子性,Synchronized Block(同步代码块)。

同步代码块包括两个部分:

  • 一个作为锁的对象引用
  • 一个作为由这个锁保护的代码块
重入

“重入”意味着获取锁的操作的粒度是“线程”,而不是“调用”。

实现方法:为每个锁关联一个计数值和一个所有者线程

重入避免了,子类改写父类的synchronized方法,然后再这个方法里调用父类的方法。每个方法在执行前都会获取父类上的锁,如果没有重入,那么会永远停留在子类调用父类方法中。

用锁来保护状态

通过锁来实现对共享状态的独占访问——>确保状态的一致性。

如果用同步来协调对某个变量的访问,那么在访问这个变量的所有位置上都需要使用同步。而且在访问变量的所有位置上都要使用同一个锁。称这个状态是由这个锁保护的。

@GuardedBy内置锁

之所以每个对象都有一个内置锁,是为了免去显式地创建锁对象。你需要自行构造加锁协议或者同步策略来实现对共享状态的安全访问,并且在陈旭中自始至终的使用它们。

一种常见的加锁约定:

将所有的可变状态都封装在对象内部,通过对象的内置锁对所有访问可变状态的代码路径进行同步。

e.g. Vector和其他同步集合类

缺点:如果添加新的方法或者代码路径时,忘记使用了同步,那么这种加锁协议会很容易被破坏。

对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

活跃性和性能

不良并发(Poor Concurrency):

可同时调用的数量,不仅受到可用处理资源的限制,还收到应用程序本身结构的限制。

在获取和释放锁等操作都需要一定的开销,因此如果将同步代码块分解得过细,并不好。

需要在安全性(必须满足),简单性和性能上权衡。一定不要为了性能牺牲简单性,可能会破坏安全性。