借助这题复习一下 Java 并发类和工具。

题目描述

两个不同的线程将会共用一个 FooBar 实例。其中一个线程将会调用 foo () 方法,另一个线程将会调用 bar () 方法。

请设计修改程序,以确保 “foobar” 被输出 n 次。

我们提供一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class FooBar {
private int n;

public FooBar(int n) {
this.n = n;
}

public void foo(Runnable printFoo) throws InterruptedException {

for (int i = 0; i < n; i++) {

//printFoo.run () outputs "foo". Do not change or remove this line.
printFoo.run ();
}
}

public void bar(Runnable printBar) throws InterruptedException {

for (int i = 0; i < n; i++) {

//printBar.run () outputs "bar". Do not change or remove this line.
printBar.run ();
}
}
}
  • 示例 1:

    输入: n = 1
    输出: “foobar”
    解释:这里有两个线程被异步启动。其中一个调用 foo () 方法,另一个调用 bar () 方法,”foobar” 将被输出一次。

  • 示例 2:

    输入: n = 2
    输出: “foobarfoobar”
    解释: “foobar” 将被输出两次。

思路

这题考的是线程同步,那么显而易见,信号量、互斥锁、共享内存这些方案都是可行的。

题解

信号量 Semaphore

Semaphore 翻译成字面意思为 信号量,Semaphore 可以控同时访问的线程个数,通过 acquire () 获取一个许可,如果没有就等待,而 release () 释放一个许可。

Semaphore 类位于 java.util.concurrent 包下,它提供了 2 个构造器:

1
2
3
4
5
6
public Semaphore(int permits) {          // 参数 permits 表示许可数目,即同时可以允许多少线程进行访问 
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) { // 这个多了一个参数 fair 表示是否是公平的,即等待时间越久的越先获取许可
sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
}

下面说一下 Semaphore 类中比较重要的几个方法,首先是 acquire ()、release () 方法:

1
2
3
4
public void acquire() throws InterruptedException {  }     // 获取一个许可 
public void acquire(int permits) throws InterruptedException { } // 获取 permits 个许可
public void release() { } // 释放一个许可
public void release(int permits) { } // 释放 permits 个许可

acquire () 用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。

release () 用来释放许可。注意,在释放许可之前,必须先获获得许可。

这 4 个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:

1
2
3
4
5
6
7
public boolean tryAcquire() { };    
// 尝试获取一个许可,若获取成功,则立即返回 true,若获取失败,则立即返回 false
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { };
// 尝试获取一个许可,若在指定的时间内获取成功,则立即返回 true,否则则立即返回 false
public boolean tryAcquire(int permits) { };
// 尝试获取 permits 个许可,若获取成功,则立即返回 true,若获取失败,则立即返回 false
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; // 尝试获取 permits 个许可,若在指定的时间内获取成功,则立即返回 true,否则则立即返回 false

另外还可以通过 availablePermits () 方法得到可用的许可数目。

此题可设置两个信号量分别标示 FooBar 的可用状态。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class FooBar {
private int n;

public FooBar(int n) {
this.n = n;
}

Semaphore foo = new Semaphore(1);
Semaphore bar = new Semaphore(0);

public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
foo.acquire ();
printFoo.run ();
bar.release ();
}
}

public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
bar.acquire ();
printBar.run ();
foo.release ();
}
}
}

公平锁

锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。 但是,一些锁可能允许并发访问共享资源,如 ReadWriteLock 的读写锁。在 Lock 接口出现之前,Java 程序是靠 synchronized 关键字实现锁功能的。JDK1.5 之后并发包中新增了 Lock 接口以及相关实现类来实现锁功能。

Lock 接口的实现类: ReentrantLockReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock

Lock 接口提供的 synchronized 关键字不具备的主要特性:

特性 描述
尝试非阻塞地获取锁 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
能被中断地获取锁 获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁 在指定的截止时间之前获取锁, 超过截止时间后仍旧无法获取则返回

Lock 接口基本的方法:

方法名称 描述
void lock () 获得锁。如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到获取锁。
void lockInterruptibly () 获取锁,如果可用并立即返回。如果锁不可用,那么当前线程将被禁用以进行线程调度,并且处于休眠状态,和 lock () 方法不同的是在锁的获取中可以中断当前线程(相应中断)。
Condition newCondition () 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的 wait () 方法,而调用后,当前线程将释放锁。
boolean tryLock () 只有在调用时才可以获得锁。如果可用,则获取锁定,并立即返回值为 true;如果锁不可用,则此方法将立即返回值为 false 。
boolean tryLock (long time, TimeUnit unit) 超时获取锁,当前线程在一下三种情况下会返回: 1. 当前线程在超时时间内获得了锁;2. 当前线程在超时时间内被中断;3. 超时时间结束,返回 false.
void unlock () 释放锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class FooBar {
private int n;

public FooBar(int n) {
this.n = n;
}

Lock lock = new ReentrantLock(true);
volatile boolean permitFoo = true;

public void foo(Runnable printFoo) throws InterruptedException {

for (int i = 0; i < n; ) {
lock.lock ();
try {
if(permitFoo) {
printFoo.run ();
i++;
permitFoo = false;
}
}finally {
lock.unlock ();
}
}
}

public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; ) {
lock.lock ();
try {
if(!permitFoo) {
printBar.run ();
i++;
permitFoo = true;
}
}finally {
lock.unlock ();
}
}
}
}

无锁

以上的公平锁方案完全可以改造成无锁方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class FooBar {
private int n;

public FooBar(int n) {
this.n = n;
}

volatile boolean permitFoo = true;

public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; ) {
if(permitFoo) {
printFoo.run ();
i++;
permitFoo = false;
}
}
}

public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; ) {
if(!permitFoo) {
printBar.run ();
i++;
permitFoo = true;
}
}
}
}

CyclicBarrier 类

循环场景中常用 CyclicBarrier

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public FooBar(int n) {
this.n = n;
}

CyclicBarrier cb = new CyclicBarrier(2);
volatile boolean fin = true;

public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
while(!fin);
printFoo.run ();
fin = false;
try {
cb.await ();
} catch (BrokenBarrierException e) {
}
}
}

public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
try {
cb.await ();
} catch (BrokenBarrierException e) {
}
printBar.run ();
fin = true;
}
}
}