前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >(翻译)理解并发的核心概念一

(翻译)理解并发的核心概念一

作者头像
日薪月亿
修改2020-06-17 14:24:08
5730
修改2020-06-17 14:24:08
举报
文章被收录于专栏:技术探索技术探索

原文链接:https://dzone.com/asset/download/210335

pdf资源:

1 简介

从Java创建开始,Java就支持键并发线程和锁等概念。本参考将有帮助Java开发人员使用多线程程序来了解核心并发概念以及如何应用它们。

2 概念

概念

描述

Atomicity(原子性)

一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,因此部分状态是不可能的

Visibility(可见性)

一个线程看到另一线程所做的更改时的条件

表1 并发的概念

竞争条件

当多个线程在共享资源上执行一系列操作时,就会出现竞争状态,并且根据每个线程的操作顺序,存在几种可能的结果。 下面的代码不是线程安全的,并且该值可以多次初始化,因为check-then-act(检查null,然后进行初始化)表明延迟初始化的字段不是原子的:

代码语言:java
复制
class Lazy <T> {
  private volatile T value;
  T get() {
    if (value == null)
      value = initialize();
    return value;
  }
}

数据冲突

当2个或更多线程尝试在不同步的情况下访问相同的非final变量时,就会发生数据冲突。 不使用同步可能会导致所做的更改对其他线程不可见,因此读取过时的数据是可能的,这又可能导致无限循环,损坏的数据结构或计算不正确的后果。 此代码可能会导致无限循环,因为读取器线程可能永远不会观察到写入器线程所做的更改:

代码语言:java
复制
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();
  }
}

3 JMM happens-before关系

Java内存模型是根据诸如读取和写入字段以及在监视器上进行同步之类的操作定义的。 可以通过happens-before关系

来对操作进行排序,该关系可以用来推断一个线程何时看到另一个线程的操作的结果以及什么构成了正确同步的程序。

happens-before关系规则

  • Thread#start的方法在线程的所有操作之前执行
  • 在释放当前控制器之后,后序的请求才可以获取控制器。
  • 写入volatile变量的操作在所有后序读取该变量的操作之前执行。
  • 写入final型变量的操作在发布该对象的引用之前执行
  • 线程的所有操作在从Thread#join方法返回之前执行

happens-before插图
happens-before插图

上图中,Action XAction Y之前执行,因此线程1Action X以前执行的所有操作对线程2Action Y之后的所有操作可见。

4 标准同步功能

synchronized关键字

synchronized关键字用来防止不同的线程同时进入一段代码。它确保了操作的原子性,因为你只有获得了这段代码的锁才能进入这段代码,使得该锁所保护的数据可以在独占模式下操作。除此以外,它还确保了别的线程在获得了同样的锁之后,能够观察到之前线程的操作。

代码语言:java
复制
class AtomicOperation {
  private int counter0;
  private int counter1;
  void increment() {
    synchronized (this) {
      counter0++;
      counter1++;
    }
  }
}

synchronized关键字也可以在方法级。

方法的类型

用作监视器的参考

静态方法(static)

将持有该方法的类作为加锁对象

非静态方法(non-static)

加锁this指针

表格2 监视器在整个方法同步时使用

锁是可以重入的,所以,如果线程已持有该锁,则它可以再次成功获取它。

代码语言:java
复制
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

wait/notify/notifyAll方法在Object类中声明。wait方法用来将线程状态改变为WAITING或是TIMED_WAITING(如果传入了超时时间值)。如果需要唤醒一个线程,下列的操作都可以实现:

  • 另一个线程调用notify方法,唤醒在控制器上等待的任意的一个线程
  • 另一个线程调用notifyAll方法,唤醒在该控制器上等待的所有线程
  • Thread#interrupt方法被调用,在这种情况下,会抛出InterruptedException

最常用的一个模式是一个条件性循环:

代码语言:java
复制
class ConditionLoop {
  private boolean condition;
  synchronized void waitForCondition() throws InterruptedException {
    while (!condition) {
      wait();
    }
  }
  synchronized void satisfyCondition() {
    condition = true;
    notifyAll();
  }
}
  • 记住,要想使用对象上的wait/notify/notifyAll方法,你首先需要获取对象的锁。
  • 总是在一个条件性循环中等待,从而解决如果另一个线程在wait开始之前满足条件并且调用了notifyAll而导致的顺序问题。而且它还防止线程由于伪唤起继续执行。
  • 时刻确保你在调用notify/notifyAll之前已经满足了等待条件。如果不这样的话,将只会发出一个唤醒通知,但是在该等待条件上的线程永远无法跳出其等待循环。

volatile关键字

volatile关键字解决了可见性(visibility)问题,并且使值的更改原子化,因为这里存在一个happens-before关系:对volatile值的更改会在所有后续读取该值的操作之前执行。因此,它确保了后序所有的读取操作能够看到之前的更改。

代码语言:java
复制
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();
  }
}

Atomics

java.util.concurrent.atomic包中包含了一组支持在单一值上进行多种原子性操作的类,从而从加锁中解脱出来。

使用AtomicXXX类,可以实现原子性的check-then-act操作:

代码语言:java
复制
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包中AtomicIntegerAtomicLong都用increment/decrement操作:

代码语言:java
复制
class Increment {
  private final AtomicInteger state = new AtomicInteger();
  void advance() {
    int oldState = state.getAndIncrement();
    System.out.println("Advanced: '" + oldState + "' -> '" + (oldState + 1) + "'.");
  }
}

如果你想要创建一个计数器,并不需要原子性的读操作,可以考虑使用LongAdder替代AtomicLong/AtomicIntegerLongAdder在多个单元格中维护该值,并在需要时对这些值同时递增,从而在高并发的情况下性能更好。

ThreadLocal

在线程中包含数据并且不需要锁定的一种方法是使用ThreadLocal存储。从概念上将,ThreadLocal就好像是在每个线程中都有自己版本的变量。ThreadLocal常用来存储只属于线程自己的值,例如当前的事务以及其它资源。另外,它还能用来维护单个线程专有的计数器,统计或是ID生成器。

代码语言:java
复制
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方法中获取一个引用)。要确保一个对象被安全的发布(即在初始化完成之后发布),可能需要使用同步。可以通过以下方法实现安全的发布:

  • 静态初始化方法。只有一个线程能够初始化静态变量因为该类的初始化是在一个排它锁之下完成的。
代码语言:java
复制
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关键字。读线程总是能获取最近的值,因为写线程总是在后续的读取之前进行。
代码语言:java
复制
class Volatile {
  private volatile String state;
  void setState(String state) {
    this.state = state;
  }
  String getState() {
    return state; 
  }
}
  • Atomics。例如AtomicInteger将一个值存储为volatile类型,所以这里和volatile变量的规则相同
代码语言:java
复制
class Atomics {
  private final AtomicInteger state = new AtomicInteger();
  void initializeState(int state) {
    this.state.compareAndSet(0, state);
  }
  int getState() {
    return state.get();
  }
}
  • Final类型
代码语言:java
复制
class Final {
  private final String state;
  Final(String state) {
    this.state = state;
  }
  String getState() {
    return state;
  }
}

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


代码语言:java
复制
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);
 }
}
  • 正确同步的域
代码语言:java
复制
class Synchronization {
  private String state;
  synchronized String getState() {
    if (state == null)
      state = "Initial";
    return state;
  }
}

不可变对象

不变对象的一个非常棒的属性是线程安全(thread-safe),所有无需在其上进行同步。使一个对象成为不变对象的要求为:

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

不可变对象的例子:

代码语言:java
复制
// 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

终止

表4 线程状态

Thread方法

说明

start

启动一个Thread实例并且执行run()方法

join

阻塞直到线程完成

interrupt

中断线程。如果该线程在响应终端的方法中阻塞着,则会在另一个线程中抛出InterruptedException,否则将会被设置为中断状态。

stop,suspend,resume,destroy

这些方法都已经失效

表5 线程协调方法

如何处理InterruptedException?

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

未知的(Unexpected )的异常处理

线程可以指定一个UncaughtExceptionHandler,它将接收任何导致线程突然终止的未捕获异常的通知。

代码语言:java
复制

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

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 简介
  • 2 概念
    • 竞争条件
      • 数据冲突
        • synchronized关键字
        • wait/notify
        • volatile关键字
        • Atomics
        • ThreadLocal
    • 3 JMM happens-before关系
    • 4 标准同步功能
      • 安全发布
        • 如何处理InterruptedException?
        • 未知的(Unexpected )的异常处理
    • 不可变对象
    • 线程(Threads)
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档