原文链接:https://dzone.com/asset/download/210335
pdf资源:
从Java创建开始,Java就支持键并发线程和锁等概念。本参考将有帮助Java开发人员使用多线程程序来了解核心并发概念以及如何应用它们。
概念 | 描述 |
---|---|
Atomicity(原子性) | 一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,因此部分状态是不可能的 |
Visibility(可见性) | 一个线程看到另一线程所做的更改时的条件 |
表1 并发的概念
当多个线程在共享资源上执行一系列操作时,就会出现竞争状态,并且根据每个线程的操作顺序,存在几种可能的结果。 下面的代码不是线程安全的,并且该值可以多次初始化,因为check-then-act(检查null,然后进行初始化)表明延迟初始化的字段不是原子的:
class Lazy <T> {
private volatile T value;
T get() {
if (value == null)
value = initialize();
return value;
}
}
当2个或更多线程尝试在不同步的情况下访问相同的非final
变量时,就会发生数据冲突。 不使用同步可能会导致所做的更改对其他线程不可见,因此读取过时的数据是可能的,这又可能导致无限循环,损坏的数据结构或计算不正确的后果。 此代码可能会导致无限循环,因为读取器线程可能永远不会观察到写入器线程所做的更改:
class Waiter implements Runnable {
private boolean shouldFinish;
void finish() { shouldFinish = true; }
public void run() {
long iteration = 0;
while (!shouldFinish) {
iteration++;
}
System.out.println("Finished after: " + iteration);
}
}
class DataRace {
public static void main(String[] args) throws InterruptedException {
Waiter waiter = new Waiter();
Thread waiterThread = new Thread(waiter);
waiterThread.start();
waiter.finish();
waiterThread.join();
}
}
Java内存模型是根据诸如读取和写入字段以及在监视器上进行同步之类的操作定义的。 可以通过happens-before关系
来对操作进行排序,该关系可以用来推断一个线程何时看到另一个线程的操作的结果以及什么构成了正确同步的程序。
happens-before关系规则
Thread#start
的方法在线程的所有操作之前执行volatile
变量的操作在所有后序读取该变量的操作之前执行。final
型变量的操作在发布该对象的引用之前执行Thread#join
方法返回之前执行上图中,Action X
在Action Y
之前执行,因此线程1
在Action X
以前执行的所有操作对线程2
在Action Y
之后的所有操作可见。
synchronized
关键字用来防止不同的线程同时进入一段代码。它确保了操作的原子性,因为你只有获得了这段代码的锁才能进入这段代码,使得该锁所保护的数据可以在独占模式下操作。除此以外,它还确保了别的线程在获得了同样的锁之后,能够观察到之前线程的操作。
class AtomicOperation {
private int counter0;
private int counter1;
void increment() {
synchronized (this) {
counter0++;
counter1++;
}
}
}
synchronized
关键字也可以在方法级。
方法的类型 | 用作监视器的参考 |
---|---|
静态方法(static) | 将持有该方法的类作为加锁对象 |
非静态方法(non-static) | 加锁this指针 |
表格2 监视器在整个方法同步时使用
锁是可以重入的,所以,如果线程已持有该锁,则它可以再次成功获取它。
class Reentrantcy {
synchronized void doAll() {
doFirst();
doSecond();
}
synchronized void doFirst() {
System.out.println("First operation is successful.");
}
synchronized void doSecond() {
System.out.println("Second operation is successful.");
}
}
不同的竞争等级会影响监视器的获取方式:
状态 | 说明 |
---|---|
init | 刚刚创建,没有被获取 |
biased | 锁下的代码只被一个线程执行,不会产生冲突 |
thin | 控制器被几个线程无冲突的获取。使用CAS(compare and swap)来管理这个锁 |
fat | 产生冲突。JVM请求操作系统互斥,并让操作系统调度程序处理线程停放和唤醒。 |
表3 监视器的状态
wait/notify/notifyAll
方法在Object
类中声明。wait
方法用来将线程状态改变为WAITING
或是TIMED_WAITING
(如果传入了超时时间值)。如果需要唤醒一个线程,下列的操作都可以实现:
notify
方法,唤醒在控制器上等待的任意的一个线程notifyAll
方法,唤醒在该控制器上等待的所有线程Thread#interrupt
方法被调用,在这种情况下,会抛出InterruptedException
最常用的一个模式是一个条件性循环:
class ConditionLoop {
private boolean condition;
synchronized void waitForCondition() throws InterruptedException {
while (!condition) {
wait();
}
}
synchronized void satisfyCondition() {
condition = true;
notifyAll();
}
}
wait/notify/notifyAll
方法,你首先需要获取对象的锁。notifyAll
而导致的顺序问题。而且它还防止线程由于伪唤起继续执行。notify/notifyAll
之前已经满足了等待条件。如果不这样的话,将只会发出一个唤醒通知,但是在该等待条件上的线程永远无法跳出其等待循环。volatile
关键字解决了可见性(visibility)问题,并且使值的更改原子化,因为这里存在一个happens-before
关系:对volatile
值的更改会在所有后续读取该值的操作之前执行。因此,它确保了后序所有的读取操作能够看到之前的更改。
class VolatileFlag implements Runnable {
private volatile boolean shouldStop;
public void run() {
while (!shouldStop) {
//do smth
}
System.out.println("Stopped.");
}
void stop() {
shouldStop = true;
}
public static void main(String[] args) throws InterruptedException {
VolatileFlag flag = new VolatileFlag();
Thread thread = new Thread(flag);
thread.start();
flag.stop();
thread.join();
}
}
java.util.concurrent.atomic
包中包含了一组支持在单一值上进行多种原子性操作的类,从而从加锁中解脱出来。
使用AtomicXXX
类,可以实现原子性的check-then-act
操作:
class CheckThenAct {
private final AtomicReference<String> value = new AtomicReference<>();
void initialize() {
if (value.compareAndSet(null, "Initialized value")) {
System.out.println("Initialized only once.");
}
}
}
java.util.concurrent.atomic包中AtomicInteger
和AtomicLong
都用increment/decrement
操作:
class Increment {
private final AtomicInteger state = new AtomicInteger();
void advance() {
int oldState = state.getAndIncrement();
System.out.println("Advanced: '" + oldState + "' -> '" + (oldState + 1) + "'.");
}
}
如果你想要创建一个计数器,并不需要原子性的读操作,可以考虑使用LongAdder
替代AtomicLong/AtomicInteger
,LongAdder
在多个单元格中维护该值,并在需要时对这些值同时递增,从而在高并发的情况下性能更好。
在线程中包含数据并且不需要锁定的一种方法是使用ThreadLocal存储。从概念上将,ThreadLocal就好像是在每个线程中都有自己版本的变量。ThreadLocal常用来存储只属于线程自己的值,例如当前的事务以及其它资源。另外,它还能用来维护单个线程专有的计数器,统计或是ID生成器。
class TransactionManager {
private final ThreadLocal<Transaction> currentTransaction
= ThreadLocal.withInitial(NullTransaction::new);
Transaction currentTransaction() {
Transaction current = currentTransaction.get();
if (current.isNull()) {
current = new TransactionImpl();
currentTransaction.set(current);
}
return current;
}
}
发布对象是指该对象的引用对当前的域之外也可见(比如,从getter方法中获取一个引用)。要确保一个对象被安全的发布(即在初始化完成之后发布),可能需要使用同步。可以通过以下方法实现安全的发布:
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);
}
}
class Volatile {
private volatile String state;
void setState(String state) {
this.state = state;
}
String getState() {
return state;
}
}
class Atomics {
private final AtomicInteger state = new AtomicInteger();
void initializeState(int state) {
this.state.compareAndSet(0, state);
}
int getState() {
return state.get();
}
}
class Final {
private final String state;
Final(String state) {
this.state = state;
}
String getState() {
return state;
}
}
确保this
引用不会再初始化过程中泄漏
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);
}
}
class Synchronization {
private String state;
synchronized String getState() {
if (state == null)
state = "Initial";
return state;
}
}
不变对象的一个非常棒的属性是线程安全(thread-safe),所有无需在其上进行同步。使一个对象成为不变对象的要求为:
final
类型this
引用在初始化期间不会泄露不可变对象的例子:
// 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
}
java.lang.Thread
类用来表示一个应用或是一个JVM现场。其代码通常在某个进程类的上下文中执行。(使用Thread#currentThread
来获取当前线程本身)
线程状态 | 说明 |
---|---|
NEW | 还未启动 |
RUNNABLE | 启动并运行 |
BLOCKED | 在控制器上等待 - 该线程正视图获取锁并进入关键区域 |
WAITING | 等待另一个线程执行特殊操作(notify/notifyAll,LockSupport#unpark) |
TIMED_WAITING | 和WAITING类似,但是有超时设置 |
TERMINATED | 终止 |
表4 线程状态
Thread方法 | 说明 |
---|---|
start | 启动一个Thread实例并且执行run()方法 |
join | 阻塞直到线程完成 |
interrupt | 中断线程。如果该线程在响应终端的方法中阻塞着,则会在另一个线程中抛出InterruptedException,否则将会被设置为中断状态。 |
stop,suspend,resume,destroy | 这些方法都已经失效 |
表5 线程协调方法
InterruptedException
InterruptedException
,应该使用Thread.currentThread().interrupt()
将中断标识回复为true,然后在该层抛出异常。将中断标识设为true很重要,它使得异常在可以在更高的层次上进行处。线程可以指定一个UncaughtExceptionHandler,它将接收任何导致线程突然终止的未捕获异常的通知。
Thread thread = new Thread(runnable);
thread.setUncaughtExceptionHandler((failedThread, exception) -> {
logger.error("Caught unexpected exception in thread '{}'.",
failedThread.getName(), exception);
});
thread.start();