资讯专栏INFORMATION COLUMN

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

Richard_Gao / 416人阅读

摘要:简介从创建以来,就支持核心的并发概念如线程和锁。这篇文章会帮助从事多线程编程的开发人员理解核心的并发概念以及如何使用它们。请求操作系统互斥,并让操作系统调度程序处理线程停放和唤醒。

简介

从创建以来,JAVA就支持核心的并发概念如线程和锁。这篇文章会帮助从事多线程编程的JAVA开发人员理解核心的并发概念以及如何使用它们。

(博主将在其中加上自己的理解以及自己想出的例子作为补充)

概念
原子性:原子操作是指该系列操作要么全部执行,要么全部不执行,因此不存在部分执行的状态。
可见性:一个线程能够看见另一个线程所带来的改变。
竞争情况

当多个线程在一个共享的资源上执行一组操作时,会产生竞争。根据各个线程执行操作的顺序可能产生多个不同结果。下面的代码不是线程安全的,value可能会被初始化多次,因为check-then-act型(先判断是否为null,然后初始化)的惰性初始化并非原子性操作

class Lazy  {
  private volatile T value;
  T get() {
    if (value == null)
      value = initialize();
    return value;
  }
}
数据冲突

当两个或多个线程在没有同步的情况下试图访问同一个非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关系

JAVA内存模型是根据读写字段等操作来定义的,并在控制器上进行同步。操作根据happens-before关联排序,这解释了一个线程何时能够看到另一个线程操作的结果,以及是什么构成了一个同步良好的程序。

happens-before关联有以下属性:

Thread#start的方法在线程的所有操作之前执行

在释放当前控制器之后,后序的请求才可以获取控制器。(Releasing a monitor happens before any subsequent acquisition of the same monitor.)

写入volatile变量的操作在所有后序读取该变量的操作之前执行。

写入final型变量的操作在发布该对象的引用之前执行

线程的所有操作在从Thread#join方法返回之前执行

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

标注的同步功能 synchronized关键字

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

class AtomicOperation {
  private int counter0;
  private int counter1;
  void increment() {
    synchronized (this) {
      counter0++;
      counter1++;
    }
  }
}

synchronized关键字也可以在方法层上声明。

静态方法:将持有该方法的类作为加锁对象
非静态方法:加锁this指针

锁是可重入的。所以如果一个线程已经持有了该锁,它可以一直访问该锁下的任何内容:

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

争用程度影响如何获得控制器:

初始化:刚刚创建,没有被获取
biased:锁下的代码只被一个线程执行,不会产生冲突
thin:控制器被几个线程无冲突的获取。使用CAS(compare and swap)来管理这个锁
fat:产生冲突。JVM请求操作系统互斥,并让操作系统调度程序处理线程停放和唤醒。
wait/notify

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方法,你首先需要获取对象的锁

总是在一个条件性循环中等待,从而解决如果另一个线程在wait开始之前满足条件并且调用了notifyAll而导致的顺序问题。而且它还防止线程由于伪唤起继续执行。

时刻确保你在调用notify/notifyAll之前已经满足了等待条件。如果不这样的话,将只会发出一个唤醒通知,但是在该等待条件上的线程永远无法跳出其等待循环。

博主备注:这里解释一下为何建议将wait放在条件性循环中、假设现在有一个线程,并没有将wait放入条件性循环中,代码如下:

class UnconditionLoop{
    private boolean condition;
    
    synchronized void waitForCondition() throws InterruptedException{
        //....
        wait();
    }
    
    synchronized void satisfyCondition(){
        condition = true;
        notifyAll();
    }
}

假设现在有两个线程分别同时调用waitForConditionsatisfyCondition(),而调用satisfyCondition的方法先调用完成,并且发出了notifyAll通知。鉴于waitForCondition方法根本没有进入wait方法,因此它就错过了这个解挂信号,从而永远无法被唤醒。

这时你可能会想,那就使用if判断一下条件呗,如果条件还没满足,就进入挂起状态,一旦接收到信号,就可以直接执行后序程序。代码如下:

class UnconditionLoop{
    private boolean condition;
    
    private boolean condition2;
    
    synchronized void waitForCondition() throws InterruptedException{
        //....
        if(!condition){
            wait();
        }
    }
    synchronized void waitForCondition2() throws InterruptedException{
        //....
        if(!condition2){
            wait();
        }
    }
    synchronized void satisfyCondition(){
        condition = true;
        notifyAll();
    }
    
    synchronized void satisfyCondition2(){
        condition2 = true;
        notifyAll();
    }
}

那让我们再假设这个 方法中还存在另一个condition,并且也有其对应的等待和唤醒方法。假设这时satisfyConsition2被满足并发出nofityAll唤醒所有等待的线程,那么waitForConditionwaitForCondition2都将会被唤醒继续执行。而waitForCondition的条件并没有被满足!

因此在条件中循环等待信号是有必要的。

volatile关键字

volatile关键字解决了可见性问题,并且使值的更改原子化,因为这里存在一个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();
  }
}
Atomics

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

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

class CheckThenAct {
  private final AtomicReference value = new AtomicReference<>();
  void initialize() {
    if (value.compareAndSet(null, "Initialized value")) {
      System.out.println("Initialized only once.");
    }
  }
}

AtomicIntegerAtomicLong都用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/AtomicIntegerLongAdder在多个单元格中维护该值,并在需要时对这些值同时递增,从而在高并发的情况下性能更好。
ThreadLocal

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

class TransactionManager {
  private final ThreadLocal currentTransaction 
      = ThreadLocal.withInitial(NullTransaction::new);
  Transaction currentTransaction() {
    Transaction current = currentTransaction.get();
    if (current.isNull()) {
      current = new TransactionImpl();
      currentTransaction.set(current);
    }
    return current;
  }
}


想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注我的微信公众号!将会不定期的发放福利哦~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70996.html

相关文章

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

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

    Pink 评论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元查看
<