猫头鹰的深夜翻译:核心JAVA并发二

前言

上一篇文章请参考猫头鹰的深夜翻译:核心JAVA并发(一)

安全发布

发布一个对象是指该对象的引用对当前的域之外也可见(比如,从getter方法中获取一个引用)。要确保一个对象被安全的发布(即在初始化完成之后发布),可能需要使用同步。可以通过以下方法实现安全的发布:

  • 静态初始化方法。只有一个线程能够初始化静态变量因为该类的初始化是在一个排它锁之下完成的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class StaticInitializer {
    // Publishing an immutable object without additional initialization
    public static final Year year = Year.of(2017);
    public static final Set<String> keywords;
    // Using static initializer to construct a complex object
    static {
    // Creating mutable set
    Set<String> keywordsSet = new HashSet<>();
    // Initializing state
    keywordsSet.add("java");
    keywordsSet.add("concurrency");
    // Making set unmodifiable
    keywords = Collections.unmodifiableSet(keywordsSet);
    }
    }
  • volatile关键字。读者线程总是能获取最近的值,因为写线程总是在后续的读取之前进行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Volatile {
    private volatile String state;
    void setState(String state) {
    this.state = state;
    }
    String getState() {
    return state;
    }
    }
  • Atomics。比如,AtomicInteger将一个值存储为volatile类型,所以这里和volatile变量的规则相同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Atomics {
    private final AtomicInteger state = new AtomicInteger();
    void initializeState(int state) {
    this.state.compareAndSet(0, state);
    }
    int getState() {
    return state.get();
    }
    }
  • Final类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Final {
    private final String state;
    Final(String state) {
    this.state = state;
    }
    String getState() {
    return state;
    }
    }

确保this引用不会再初始化过程中泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ThisEscapes {
private final String name;
ThisEscapes(String name) {
Cache.putIntoCache(this);
this.name = name;
}
String getName() { return name; }
}
class Cache {
private static final Map<String, ThisEscapes> CACHE = new ConcurrentHashMap<>();
static void putIntoCache(ThisEscapes thisEscapes) {
// 'this' reference escaped before the object is fully constructed.
CACHE.putIfAbsent(thisEscapes.getName(), thisEscapes);
}
}
  • 正确同步的域
    1
    2
    3
    4
    5
    6
    7
    8
    class Synchronization {
    private String state;
    synchronized String getState() {
    if (state == null)
    state = "Initial";
    return state;
    }
    }

不变的对象

不变对象的一个非常棒的属性时,他们是现成安全的,所有无需在其上进行同步。是一个对象成为不变对象的要求为:

  • 所有的字段为final类型
  • 所有字段可以是可变对象或不可变对象,但不能越过对象的范围,从而对象的状态在构建后不能更改。
  • this引用在初始化期间不会泄露
  • 该类为final类型,所以无法在子类中修改其行为

不变对象的例子:

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
// Marked as final - subclassing is forbidden
public final class Artist {
// Immutable object, field is final
private final String name;
// Collection of immutable objects, field is final
private final List<Track> tracks;
public Artist(String name, List<Track> tracks) {
this.name = name;
// Defensive copy
List<Track> copy = new ArrayList<>(tracks);
// Making mutable collection unmodifiable
this.tracks = Collections.unmodifiableList(copy);
// 'this' is not passed to anywhere during construction
}
// Getters, equals, hashCode, toString
}
// Marked as final - subclassing is forbidden
public final class Track {
// Immutable object, field is final
private final String title;
public Track(String title) {
this.title = title;
}
// Getters, equals, hashCode, toString
}

Threads

java.lang.Thread类用来表示一个应用或是一个JVM现场。其代码通常在某个进程类的上下文中执行。(使用Thread#currentThread来获取当前线程本身)

线程的状态和相应的描述:

NEW: 还未启动
RUNNABLE: 启动并运行
BLOCKED: 在控制器上等待 - 该线程正视图获取锁并进入关键区域
WAITING: 等待另一个线程执行特殊操作(notify/notifyAll,LockSupport#unpark)
TIMED_WAITING: 和WAITING类似,但是有超时设置
TERMINATED: 停止

Thread的方法和相应的描述:

start: 启动一个Thread实例并且执行run()方法
join: 阻塞直到线程完成
interrupt: 中断线程。如果该线程在响应终端的方法中阻塞着,则会在另一个线程中抛出InterruptedException,否则将会被设置为中断状态。
stop,suspend,resume,destroy: 这些方法都已经失效了。

如何处理InterruptedException

  • 如果可能的话,清理资源并终止线程的运行
  • 声明当前的方法会抛出InterruptedException
  • 如果一个方法并没有被声明抛出InterruptedException,应该使用Thread.currentThread().interrupt()将中断标识回复为true,然后在该层抛出异常。将中断标识设为true很重要,它使得异常在可以在更高的层次上进行处。

意料之外的异常处理

Threads可以设置UncaughtExceptionHandler,它会在程序突然中断的时候收到通知。

1
2
3
4
5
6
Thread thread = new Thread(runnable);
thread.setUncaughtExceptionHandler((failedThread, exception) -> {
logger.error("Caught unexpected exception in thread '{}'.",
failedThread.getName(), exception);
});
thread.start();

生命力

死锁

当多个线程在等待彼此释放持有的资源,从而形成了资源占有和等待的循环时,就产生了死锁。可能产生死锁的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Account {
private long amount;
void plus(long amount) { this.amount += amount; }
void minus(long amount) {
if (this.amount < amount)
throw new IllegalArgumentException();
else
this.amount -= amount;
}
static void transferWithDeadlock(long amount, Account first, Account second){
synchronized (first) {
synchronized (second) {
first.minus(amount);
second.plus(amount);
}
}
}
}

死锁可能会这样产生:

  • 一个线程正视图从第一个账户向第二个账户转账,并且已经获得了第一个账户的锁
  • 与此同时,另一个线程正视图从第二个线程像第一个线程转账,并且已经获得了第二个账户的锁

避免死锁的方法有:

  • 顺序加锁 - 总是按相同的顺序获得锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Account {
    private long id;
    private long amount;
    // Some methods are omitted
    static void transferWithLockOrdering(long amount, Account first, Account second){
    boolean lockOnFirstAccountFirst = first.id < second.id;
    Account firstLock = lockOnFirstAccountFirst ? first : second;
    Account secondLock = lockOnFirstAccountFirst ? second : first;
    synchronized (firstLock) {
    synchronized (secondLock) {
    first.minus(amount);
    second.plus(amount);
    }
    }
    }
    }
  • 会超时的锁 - 不要无限的占有锁,应当释放所有的锁并重新尝试获取

    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 Account {
    private long amount;
    // Some methods are omitted
    static void transferWithTimeout(
    long amount, Account first, Account second, int retries, long timeoutMillis
    ) throws InterruptedException {
    for (int attempt = 0; attempt < retries; attempt++) {
    if (first.lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS))
    {
    try {
    if (second.lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS))
    {
    try {
    first.minus(amount);
    second.plus(amount);
    }
    finally {
    second.lock.unlock();
    }
    }
    }
    finally {
    first.lock.unlock();
    }
    }
    }
    }
    }

活锁和线程饥饿

当所有的线程都在协商对资源的访问,或是预防死锁,从而导致没有一个线程真正在运行时,会产生活锁。当一个线程长时间占据一个锁导致别的线程无法进展时,会产生线程饥饿现象。

java.util.concurrent

线程池

线程池的核心接口是ExecutorServicejava.util.concurrent还提供了一个静态工厂Executors,它包含创建具有最常见配置的线程池的工厂方法。

工厂方法如下:

newSingleThreadExecutor: 返回一个只有一个线程的ExecutorService
newFixedThreadPool: 返回一个具有固定数目线程的ExecutorService
newCachedThreadPool: 返回一个可变大小的线程池ExecutorService
newSingleThreadScheduledExecutor: 返回只有一个线程的ScheduledExecutorService
newScheduledThreadPool: 返回包含一组线程的ScheduledExecutorService
newWorkStealingPool: 返回一个带有并行级别的ExecutorService

当调整线程池大小时,最好基于机器运行该应用时分配的逻辑内核数。可以通过调用Runtime.getRuntime().availableProcessors()来获得该值。

线程池的实现类

任务通过ExecutorService#submitExecutorService#invokeAllExecutorService#invokeAny提交,它们对不同类型的任务有多种重载。

任务的功能性接口:

Runnable: 表示一个没有返回值的任务
Callable: 表示一个包含返回值的计算。它还声明可以抛出原始异常,所以不需要对检查异常进行包装

### Future ###

Future是对所有的异步计算的抽象。它表示这些计算的结果,在某些时候可用。大多数的ExecutorService方法都是用Future作为返回值。它包含检查当前future的状态以及阻塞当前读取操作直至结果可以被读取等方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(() -> "result");
try {
String result = future.get(1L, TimeUnit.SECONDS);
System.out.println("Result is '" + result + "'.");
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
catch (ExecutionException e) {
throw new RuntimeException(e.getCause());
}
catch (TimeoutException e) {
throw new RuntimeException(e);
}
assert future.isDone();

Locks

Lock
java.util.concurrent.locks包中有一个标准的Lock接口,ReentrantLock实现复制了synchronized关键字的功能,同时提供了一些额外的功能,比如获取当前锁状态的信息,非阻塞的tryBlock()方法,以及可中断的锁。下面是使用具体的ReentrantLock实例的例子:

1
2
3
4
5
6
7
8
9
10
11
12
class Counter {
private final Lock lock = new ReentrantLock();
private int value;
int increment() {
lock.lock();
try {
return ++value;
} finally {
lock.unlock();
}
}
}

ReadWriteLock
java.util.concurrent.locks包还包含了ReadWriteLock接口(以及ReentrantReadWriteLock实现),它被定义为一组读写锁,支持多个同步读者和单一写者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Statistic {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int value;
void increment() {
lock.writeLock().lock();
try {
value++;
} finally {
lock.writeLock().unlock();
}
}
int current() {
lock.readLock().lock();
try {
return value;
} finally {
lock.readLock().unlock();
}
}
}

CountDownLatch
CountDownLatch通过一个数值初始化。线程会调用await()方法阻塞自己,等待计数值为0后再继续运行。其它的线程(或是同一个线程)调用countDown()来减少计数。一旦计数为0后,该倒计时器便不可以重复使用。用来在达到某个条件后,启动一组未知数量的线程

CompletableFuture
CompletableFuture是异步计算的一个抽象。不同于Future,只能通过阻塞获取结果,该类鼓励注册回调函数来创建一组任务,从而在得到返回值或是出现异常时执行该任务。

原文链接: https://dzone.com/refcardz/core-java-concurrency?chapter=5