资讯专栏INFORMATION COLUMN

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

Pink / 832人阅读

摘要:前言上一篇文章请参考猫头鹰的深夜翻译核心并发一安全发布发布一个对象是指该对象的引用对当前的域之外也可见比如,从方法中获取一个引用。任务的功能性接口表示一个没有返回值的任务表示一个包含返回值的计算。

前言

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

安全发布

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

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

class StaticInitializer {
  // Publishing an immutable object without additional initialization
  public static final Year year = Year.of(2017); 
  public static final Set keywords;
  // Using static initializer to construct a complex object
  static {
    // Creating mutable set
    Set keywordsSet = new HashSet<>(); 
    // Initializing state
    keywordsSet.add("java");
    keywordsSet.add("concurrency");
    // Making set unmodifiable 
    keywords = Collections.unmodifiableSet(keywordsSet); 
  }
}

volatile关键字。读者线程总是能获取最近的值,因为写线程总是在后续的读取之前进行

class Volatile {
  private volatile String state;
  void setState(String state) {
    this.state = state;
  }
  String getState() {
    return state; 
  }
}

Atomics。比如,AtomicInteger将一个值存储为volatile类型,所以这里和volatile变量的规则相同

class Atomics {
  private final AtomicInteger state = new AtomicInteger();
  void initializeState(int state) {
    this.state.compareAndSet(0, state);
  }
  int getState() {
    return state.get();
  }
}

Final类型

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 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;
  }
}
不变的对象

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

所有的字段为final类型

所有字段可以是可变对象或不可变对象,但不能越过对象的范围,从而对象的状态在构建后不能更改。

this引用在初始化期间不会泄露

该类为final类型,所以无法在子类中修改其行为

不变对象的例子:

// 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 tracks; 
  public Artist(String name, List tracks) {
    this.name = name;
    // Defensive copy
    List 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,它会在程序突然中断的时候收到通知。

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

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

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);
      }
    }
  }
}

死锁可能会这样产生:

一个线程正视图从第一个账户向第二个账户转账,并且已经获得了第一个账户的锁

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

避免死锁的方法有:

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

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);
      }
    }
  }
}

会超时的锁 - 不要无限的占有锁,应当释放所有的锁并重新尝试获取

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的状态以及阻塞当前读取操作直至结果可以被读取等方法。

ExecutorService executorService = Executors.newSingleThreadExecutor();
Future 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实例的例子:

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实现),它被定义为一组读写锁,支持多个同步读者和单一写者。

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://www.ucloud.cn/yun/70993.html

相关文章

  • 头鹰深夜翻译核心JAVA并发(一)

    摘要:简介从创建以来,就支持核心的并发概念如线程和锁。这篇文章会帮助从事多线程编程的开发人员理解核心的并发概念以及如何使用它们。请求操作系统互斥,并让操作系统调度程序处理线程停放和唤醒。 简介 从创建以来,JAVA就支持核心的并发概念如线程和锁。这篇文章会帮助从事多线程编程的JAVA开发人员理解核心的并发概念以及如何使用它们。 (博主将在其中加上自己的理解以及自己想出的例子作为补充) 概念 ...

    Richard_Gao 评论0 收藏0
  • 头鹰深夜翻译:Volatile原子性, 可见性和有序性

    摘要:有可能一个线程中的动作相对于另一个线程出现乱序。当实际输出取决于线程交错的结果时,这种情况被称为竞争条件。这里的问题在于代码块不是原子性的,而且实例的变化对别的线程不可见。这种不能同时在多个线程上执行的部分被称为关键部分。 为什么要额外写一篇文章来研究volatile呢?是因为这可能是并发中最令人困惑以及最被误解的结构。我看过不少解释volatile的博客,但是大多数要么不完整,要么难...

    Lionad-Morotar 评论0 收藏0
  • 头鹰深夜翻译:为何需要缓存以及如何实现缓存

    摘要:由于需要跨进程访问网络上的高速缓存,因此延迟,故障和对象序列化会导致性能下降。应用程序高速缓存会自动清除条目以保持其内存占用。缓存统计高速缓存统计信息可帮助识别高速缓存的运行状况并提供有关高速缓存行为和性能的信息。 前言 这篇文章探索了现有的各种JAVA缓存基数,它们对各种场景下提高应用的性能起着重要的作用。 近十年来,信息技术极高的提升了业务流程,它已经成为了全球企业的战略性方案。它...

    FuisonDesign 评论0 收藏0
  • 头鹰深夜翻译:JDK Vs. JRE Vs. JVM之间区别

    摘要:什么是为执行字节码提供一个运行环境。它的实现主要包含三个部分,描述实现规格的文档,具体实现和满足要求的计算机程序以及实例具体执行字节码。该类先被转化为一组字节码并放入文件中。字节码校验器通过字节码校验器检查格式并找出非法代码。 什么是Java Development Kit (JDK)? JDK通常用来开发Java应用和插件。基本上可以认为是一个软件开发环境。JDK包含Java Run...

    blair 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<