介绍了Single Threaded Execution、Immutable、Guarded Suspension三个模式。

Single Threaded Execution模式

顾名思义,这个模式表示同时只有一个线程可以执行,这个模式其实模拟的是经典的临界区概念。简单来说,所有可能访问临界区的方法或一段代码,都要加锁,保证同时只有一个线程可以访问。在Java中,这种模式的实现很简单,也是其他多线程模式的基础,就是像第一次博客中描述的那样使用synchronized关键字。但显然,使用synchronized关键字可能导致程序的性能下降,因为获取锁需要时间,一旦发生冲突,在等待队列中等待也将耗费时间,因此除非必须,我们要尽量避免加锁,那么哪些情况不需要加锁呢?

  • 单线程程序。显而易见
  • 多个线程执行但互不干扰,有可能存在情况,同时运行的多个线程并不会访问共享的资源,这种情况也不需要加锁
  • 不可变对象(Immutable),我们都知道,读是不会产生冲突的,只有写(或者具体来说,改变)才会引起冲突,不可变对象不会发生变化,因此多线程对不可变对象的共享并不需要加锁

值得注意的是,对该模式的不同实现也可能导致程序性能的差异。我们在多线程程序中使用容器时,很多时候都需要选择线程安全的容器。HashTable是线程安全的容器,它使用的就是简单的Single Threaded Execution模式;而与它功能相似的concurrentHashMap也是线程安全的,但它是通过将内部数据结果分为多段,对每段的操作互不相关来实现线程安全,因此在多线程的情况下使用,性能可能会比HashTable更好。

Before/After模式

对于synchronized关键字,我们容易理解,其实是在代码块的一开始加锁,在代码块的结束部分释放锁,那么synchronized关键字能否简单地等价于下面的代码呢?

1
2
3
lock()
...
unlock()

显然是不能的,因为如果中间的代码有return语句,或者抛出异常,就会导致锁没有被释放。怎么才能保证锁一定被释放呢?我们可以利用Java的try catch finally语句。

1
2
3
4
5
6
lock()
try {
...
} finally {
unlock()
}

最多只能有n个线程执行

synchronized方法实际上是保证最多只有一个线程可以同时执行,如果我们想实现最多有n给线程执行呢?或许有人会想到操作系统中学到的信号量,其实,Java已经为我们实现了信号量,我们可以直接使用java.util.concurrent.Semaphore类。

Immutable模式

在上一个模式中,我们提到,如果一个对象是不可变的,即使它被多个线程共享,也不需要对它加锁。这样不可变的对象被称为Immutable,Java中有许多Immutable的类,如常见的String和BigInteger。事实上,使用不可变对象不仅可以保证多线程下的线程安全,在单线程模式中,不可变对象也有一席之地。

  • 频繁访问的对象,使用不可变对象提高性能。例如Java的BigInteger中提供了静态方法来获得常用的0,1等常用不可变大整数,这样我们就不必每次使用时都创建一个实例,提供了系统的性能。
  • 属性确实不会改变的对象。如只需要getter方法而不需要setter方法的类,这样的类天生就是不可变对象。设计这样的类有什么好处呢?简单来说,虽然有可能降低系统性能(因为可能会导致对象的重复创建),但不可变对象会使我们系统的复杂度降低,进而提高系统的健壮性。

Guarded Suspension模式

假想这样一个情景,有一个发送端不断发送请求,有一个接收端不断接受请求,发送和接受都通过中间的一个队列,我们如何保证这个队列的线程安全呢?仅仅使用Single Threaded Execution模式显然是不够的,因为我们并不是仅仅需要保证同时只有一个线程可以操作队列,而是需要实现当队列为空时,接收端需要被阻塞,直到发送端的线程提供请求。这里的关键是,我们有一个条件,条件满足时,线程不需要被阻塞;而条件不满足时,线程被阻塞,直到条件满足。这就需要用到Guarded Suspension模式,或者说需要用到之前提到的wait和notifyAll方法。示例代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RequestQueue {
private final Queue<Request> queue = new LinkedList<Request> ();
public synchronized Request getRequest() {
while (queue.peek() == null) {
try{
wait();
} catch (InterruptedException e) {
}
return queue.remove();
}
}

public synchronized void putRequest(Request request) {
queue.offer(request);
notifyAll();
}
}

代码中getRequest的关键代码是queue.remove(),即移除队列的第一个元素并返回。但为了安全的执行,我们需要保证队列不为空,即满足条件queue.peek() !=null,这个条件被称为守护(Guarded)条件。如果不满足该条件,线程就会调用wait方法,进入等待队列并释放锁。那线程实质上是在wait什么呢,从逻辑上讲,是等待有发送端提供Request,从代码的执行讲,实质上是在等待notify,即执行putRequest方法。notify,notifyAll和interrupt都可以使线程退出等待序列,当然退出后,还需要获得锁才能继续执行。