1 多线程的优势及风险
1.1 线程的优势
- 发挥多处理器的强大功能。
- 建模的简单性。通过使用线程,可以将复杂并且异步的工作流进一步分解为简单并且同步的工作流,每个工作流在一个单独的线程中运行,并在特定的同步位置进行交互。我们可以通过现有的框架来实现上述目标,例如Servlet。该框架负责一些细节问题,将请求管理、线程创建和负载平衡等在正确的时刻将请求分发给正确的应用程序组件,开发人员并不需要了解这些细节,这样可以简化组件的开发,提高开发效率。
- 异步事件的简化处理。单线程在处理多任务时,在执行一个任务的过程中,其他任务将阻塞。为了避免这个问题,单线程服务器应用程序必须使用非阻塞I/O,这种I/O的复杂性要远远高于同步I/O,并且容易出错。如果为每个任务分配其各自的线程并且使用同步I/O,将降低这类程序的开发难度。
- 响应更灵敏的用户界面。
1.2 线程带来的风险
- 安全性问题。在没有充足同步的情况下,多个线程中的操作顺序是不可预测的,甚至产生莫名其妙的结果。
- 活跃性问题。当某个操作无法继续执行下去时,就会产生活跃性问题。在串行程序中,活跃性问题的形式之一就是无意中造成的无限循环,导致之后的代码无法执行。而多线程带来的活跃性问题有死锁、饥饿和活锁等,与大多数并发性错误一样,这种活跃性问题是难以分析的。
- 性能问题。多线程能提升程序的性能,但总会带来某种程度的运行时开销。
2 什么是线程安全性
当多个线程访问某个类时,不管运行环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
无状态对象一定是线程安全的。什么是无状态对象那?就是一个对象中即不包含任何域,也不包含对其他类中域的引用。计算中的临时状态仅存在于线程栈上的局部变量中,并且只能由正在执行的线程访问。大部分Servlet都是无状态的,只有其在处理请求时保存一些信息,线程安全性才会成为一个问题。
下面是涉及到线程安全的几个相关概念:
2.1 原子性
当一个操作是不可分割时(这个操作是一个以原子方式执行的操作),我们就称该操作是原子操作。比如,int i = 0;就具有原子性。
2.2 复合操作
将原子操作组合起来即为复合操作。
2.3 竞态条件
在并发编程中,由于不恰当的执行时序而出现的不正确的结果称为竞态条件(Race Condition)。最常见的竞态条件类型就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能失效的观测结果来决定下一步动作。
2.4 怎么保证线程安全性
只要操作是原子的,就一定是线程安全的。通常有以下的方式来实现线程安全:
- 使用原子变量类,该类处于java.util.concurrent.atomic包中,用它们可以实现在数值和对象引用上的原子状态转换,确保线程安全。如下面例子所示:
public class CountBean {
private final AtomicLong count = new AtomicLong(0);
public long getCount() {
return count.get();
}
public void add() {
count.incrementAndGet();
}
}
- Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized
Block)。同步代码块包含两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。我们在方法前面加synchronized关键字或者用synchronized包装局部代码块,如下:
synchronized(lock) {
//访问或修改由锁保护的共享状态
}
静态的synchronized方法以java对象作为锁。每个java对象都可以作为一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。
线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,即使由于抛出异常而退出。获取内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
由于java内置锁是一种互斥锁,这意味着最多只有一个线程能持有这种锁。当线程A尝试去获取一个由线程B持有的锁时,线程A必须等待或阻塞,直到线程B释放这个锁。若B永远不释放锁,A将永远等下去。
由于每次只能由一个线程执行内置锁保护的代码块,因此,由这个锁保护的同步代码块会以原子方式执行,这样就保证了线程安全。但是一味的使用同步方法,将导致性能问题。
参考:
Java并发编程实战(Java Concurrency In Practice)-机械工业出版社 第一、二章