Java concurrency basics:修订间差异

来自WHY42
imported>Riguz
线程是操作系统中进行运算调度的最小单位,它是一个单一顺序的控制流,不论是对于单核还是多核的CPU,都能比较有效的提高程序的吞吐率。在Java中,创建一个线程的唯一方法是创建一个`Thread`类的实例,并调用`start()`方法以启动该线程。然而当多个线程同时执行时,如何保证线程之间是按照我们期待的方式在运行呢?Java提供了多种机制来保证多个线程之间的交互。
 
Riguz留言 | 贡献
Riguz移动页面Blog:理解Java并发(1):基本机制Java concurrency basics,不留重定向
 
(未显示同一用户的17个中间版本)
第1行: 第1行:
线程是操作系统中进行运算调度的最小单位,它是一个单一顺序的控制流,不论是对于单核还是多核的CPU,都能比较有效的提高程序的吞吐率。在Java中,创建一个线程的唯一方法是创建一个`Thread`类的实例,并调用`start()`方法以启动该线程。然而当多个线程同时执行时,如何保证线程之间是按照我们期待的方式在运行呢?Java提供了多种机制来保证多个线程之间的交互。
线程是操作系统中进行运算调度的最小单位,它是一个单一顺序的控制流,不论是对于单核还是多核的CPU,都能比较有效的提高程序的吞吐率。在Java中,创建一个线程的唯一方法是创建一个<syntaxhighlight lang="java" inline>Thread</syntaxhighlight>类的实例,并调用<syntaxhighlight lang="java" inline>start()</syntaxhighlight>方法以启动该线程。然而当多个线程同时执行时,如何保证线程之间是按照我们期待的方式在运行呢?Java提供了多种机制来保证多个线程之间的交互。


= 同步(Synchronization)与监视器(Monitor)机制=
= 同步(Synchronization)与监视器(Monitor)机制=
显而易见最基本最常见的和多线程有关的就是同步`synchronized`关键字了,它底层是使用Monitor实现的。那么究竟什么是`Monitor`呢?根据JavaSE Specification的描述,在Java中,每一个对象都有一个与之关联的monitor,允许线程可以去`lock`或者`unlock`这个monitor。实际上:
显而易见最基本最常见的和多线程有关的就是同步<syntaxhighlight lang="java" inline>synchronized</syntaxhighlight>关键字了,它底层是使用Monitor实现的。那么究竟什么是<syntaxhighlight lang="java" inline>Monitor</syntaxhighlight>呢?根据JavaSE Specification的描述,在Java中,每一个对象都有一个与之关联的monitor,允许线程可以去<syntaxhighlight lang="java" inline>lock</syntaxhighlight>或者<syntaxhighlight lang="java" inline>unlock</syntaxhighlight>这个<span class="article-label">monitor</span>。实际上:


* `monitor`是独立于Java语言之上的一个概念(没想到还有另外一个名字`管程`),保证在运行线程之前获取互斥锁
* <syntaxhighlight lang="java" inline>monitor</syntaxhighlight>是独立于Java语言之上的一个概念(没想到还有另外一个名字管程),保证在运行线程之前获取互斥锁
* 在Java中,任何对象(`java.lang.Object`)都可以允许作为一个monitor,所以会有`wait``notify`之类的方法
* 在Java中,任何对象(<syntaxhighlight lang="java" inline>java.lang.Object</syntaxhighlight>)都可以允许作为一个<span class="article-label">monitor</span>,所以会有<syntaxhighlight lang="java" inline>wait</syntaxhighlight><syntaxhighlight lang="java" inline>notify</syntaxhighlight>之类的方法


`synchronized`可以作用于代码块或者方法上。如果作用在代码块上,它会尝试去lock这个对象的monitor,如果不成功将会等待直到lock成功。而当执行完毕后,无论是否出现异常,都将会释放这个锁。
<syntaxhighlight lang="java" inline>synchronized</syntaxhighlight>可以作用于代码块或者方法上。如果作用在代码块上,它会尝试去lock这个对象的<span class="article-label">monitor</span>,如果不成功将会等待直到lock成功。而当执行完毕后,无论是否出现异常,都将会释放这个锁。


如果作用在方法上,唯一的区别在于,如果是实例方法,那么将使用这个实例作为monitor,也就是`this`;如果是静态方法,那么使用的是所在类的`Class`对象。
如果作用在方法上,唯一的区别在于,如果是实例方法,那么将使用这个实例作为<span class="article-label">monitor</span>,也就是<syntaxhighlight lang="java" inline>this</syntaxhighlight>;如果是静态方法,那么使用的是所在类的<syntaxhighlight lang="java" inline>Class</syntaxhighlight>对象。


= Wait/Notify=
= Wait/Notify=
每一个Object都包含一个等待线程的集合(Wait set)。当对象创建的时候,这个队列是空的,当调用`Object.wait()``Object.nofity()`以及`Object.nofityAll()`方法的时候,会自动添加或者移除队列中的线程。或者当线程的中断状态发生改变的时候,也会引起变化。
每一个Object都包含一个等待线程的集合(Wait set)。当对象创建的时候,这个队列是空的,当调用<syntaxhighlight lang="java" inline>Object.wait()</syntaxhighlight><syntaxhighlight lang="java" inline>Object.nofity()</syntaxhighlight>以及<syntaxhighlight lang="java" inline>Object.nofityAll()</syntaxhighlight>方法的时候,会自动添加或者移除队列中的线程。或者当线程的中断状态发生改变的时候,也会引起变化。


注意,wait和notify都需要<span class="article-label">获得当前的锁</span>。
== Wait==
== Wait==
调用`wait`方法将使当前线程休眠直到另一个线程通过`notify`或者`notifyAll`来唤醒。当前线程必须持有该对象的锁,调用`wait`后即释放锁。当线程被唤醒时,需要重新取得锁并继续执行。然而,线程被唤醒有可能是因为“虚假唤醒”(spurious wakeups)导致,所以通常都需要将`wait`检测的逻辑包括在一个loop中:
调用<syntaxhighlight lang="java" inline>wait()</syntaxhighlight>方法后,线程进入等待状态,<span class="article-label">wait()</span>方法不会返回,直到将来某个时刻,线程从等待状态被其他线程唤醒后,<span class="article-label">wait()</span>方法才会返回,然后,继续执行下一条语句。
 
调用<syntaxhighlight lang="java" inline>wait</syntaxhighlight>方法将使当前线程休眠直到另一个线程通过<syntaxhighlight lang="java" inline>notify</syntaxhighlight>或者<syntaxhighlight lang="java" inline>notifyAll</syntaxhighlight>来唤醒。当前线程必须持有该对象的锁,调用<syntaxhighlight lang="java" inline>wait</syntaxhighlight>后即释放锁。当线程被唤醒时,需要重新取得锁并继续执行。然而,线程被唤醒有可能是因为“虚假唤醒”(spurious wakeups)导致,所以通常都需要将<syntaxhighlight lang="java" inline>wait</syntaxhighlight>检测的逻辑包括在一个loop中:


<syntaxhighlight lang="java">
<syntaxhighlight lang="java">
第26行: 第29行:
所谓虚假唤醒就是说,本来不该唤醒的时候唤醒了。究其原因是在操作系统层面就性能和正确性做出了权衡,放弃了正确性而选择让程序自己去处理。
所谓虚假唤醒就是说,本来不该唤醒的时候唤醒了。究其原因是在操作系统层面就性能和正确性做出了权衡,放弃了正确性而选择让程序自己去处理。


> Spurious wakeups may sound strange, but on some multiprocessor systems, making condition wakeup completely predictable might substantially slow all condition variable operations.
 
<q>Spurious wakeups may sound strange, but on some multiprocessor systems, making condition wakeup completely predictable might substantially slow all condition variable operations.
</q>
 
另外一个原因是,wait之后的逻辑的条件可能会不成立,考虑:
 
<syntaxhighlight lang="java">
public synchronized String getTask() {
    // 如果是if,那么当addTask里面notifyAll唤醒所有线程的情况下,后面的线程运行的时候queue已经是empty了,会出现问题
    while (queue.isEmpty()) {
        this.wait();
    }
    return queue.remove();
}
 
public synchronized void addTask(String s) {
    this.queue.add(s);
    this.notify(); // 唤醒在this锁等待的线程
}
</syntaxhighlight>


== Notify==
== Notify==
调用`notify`将唤醒一个正在等待持有该对象锁的线程,如果有多个对象在等待的话,将会随机唤醒其中的一个。
调用<syntaxhighlight lang="java" inline>notify</syntaxhighlight>将唤醒一个正在等待持有该对象锁的线程,如果有多个对象在等待的话,将会随机唤醒其中的一个。


被唤醒的线程必须等到当前线程释放锁之后,才能开始执行;也就是说`notify`执行完之后,并不会立即释放锁,而是需要等到同步块执行完。
被唤醒的线程必须等到当前线程释放锁之后,才能开始执行;也就是说<syntaxhighlight lang="java" inline>notify</syntaxhighlight>执行完之后,并不会立即释放锁,而是需要等到同步块执行完。


如果调用`notifyAll`的话,所有等待的线程将被唤醒,但同一时间有且仅有一个线程能取到锁并继续执行。
如果调用<syntaxhighlight lang="java" inline>notifyAll</syntaxhighlight>的话,所有等待的线程将被唤醒,但同一时间有且仅有一个线程能取到锁并继续执行。


== Interruption==
== Interruption==
当调用`Thread.interrupt`时,线程的中断状态呗设置为true。如果该线程在某个对象的waitSet中,则将会被从等待队列中移除,并在取得锁之后抛出`InterruptedException`。实际上,如果线程正在执行的是一些底层的blocking函数例如`Thread.sleep()`, `Thread.join()`, 或者 `Object.wait()`的时候,那么线程将抛出`InterruptedException`,并且`interrupted`状态会被清除;否则只会将`interrupted`状态设置为`true`
当调用<syntaxhighlight lang="java" inline>Thread.interrupt</syntaxhighlight>时,线程的中断状态呗设置为true。如果该线程在某个对象的waitSet中,则将会被从等待队列中移除,并在取得锁之后抛出<syntaxhighlight lang="java" inline>InterruptedException</syntaxhighlight>。实际上,如果线程正在执行的是一些底层的blocking函数例如<syntaxhighlight lang="java" inline>Thread.sleep()</syntaxhighlight>, <syntaxhighlight lang="java" inline>Thread.join()</syntaxhighlight>, 或者 <syntaxhighlight lang="java" inline>Object.wait()</syntaxhighlight>的时候,那么线程将抛出<syntaxhighlight lang="java" inline>InterruptedException</syntaxhighlight>,并且<syntaxhighlight lang="java" inline>interrupted</syntaxhighlight>状态会被清除;否则只会将<syntaxhighlight lang="java" inline>interrupted</syntaxhighlight>状态设置为<syntaxhighlight lang="java" inline>true</syntaxhighlight>


如果一个处于等待队列中的线程同时收到中断和通知,那么可能的行为是:
如果一个处于等待队列中的线程同时收到中断和通知,那么可能的行为是:


* 先收到通知,正常唤醒。这时候,`Thread.interrupted`将为`true`
* 先收到通知,正常唤醒。这时候,<syntaxhighlight lang="java" inline>Thread.interrupted</syntaxhighlight>将为<syntaxhighlight lang="java" inline>true</syntaxhighlight>
* 抛出`InterruptedException`并退出
* 抛出<syntaxhighlight lang="java" inline>InterruptedException</syntaxhighlight>并退出


同样,如果有多个线程处于对象m的等待队列中,然后另一个线程执行`m.notify`,那么可能:
同样,如果有多个线程处于对象m的等待队列中,然后另一个线程执行<syntaxhighlight lang="java" inline>m.notify</syntaxhighlight>,那么可能:


* 至少有一个线程正常退出wait
* 至少有一个线程正常退出wait
* 所有处于等待队列中的线程抛出`InterruptedException`而退出
* 所有处于等待队列中的线程抛出<syntaxhighlight lang="java" inline>InterruptedException</syntaxhighlight>而退出


需要注意的是,当一个线程中断了另一个线程的时候,被中断的线程并不是需要立即停止执行,程序可以选择在停止之前做一些清理工作之类的。通常如果捕获了`InterruptedException`只需要重新抛出即可,有些时候不能重新抛出的时候,需要将当前线程标记为`interrupted`使得上层堆栈的程序可以选择处理,
需要注意的是,当一个线程中断了另一个线程的时候,被中断的线程并不是需要立即停止执行,程序可以选择在停止之前做一些清理工作之类的。通常如果捕获了<syntaxhighlight lang="java" inline>InterruptedException</syntaxhighlight>只需要重新抛出即可,有些时候不能重新抛出的时候,需要将当前线程标记为<syntaxhighlight lang="java" inline>interrupted</syntaxhighlight>使得上层堆栈的程序可以选择处理,


<syntaxhighlight lang="java">
<syntaxhighlight lang="java">
第71行: 第93行:
* Timed Waiting: 有超时的等待
* Timed Waiting: 有超时的等待
* Terminated: 线程已被退出
* Terminated: 线程已被退出
[[File:https://www.baeldung.com/wp-content/uploads/2018/02/Life_cycle_of_a_Thread_in_Java.jpg|600px|Life cycle of a thread]]


= Sleep / Yield=
= Sleep / Yield=


当调用线程的`sleep`方法将导致线程暂时停止执行,值得注意的是并不会释放锁。而当线程的`yield`方法被调用时,意味着通知CPU当前线程可以“暂缓”执行的,实际很少使用。
当调用线程的<syntaxhighlight lang="java" inline>sleep</syntaxhighlight>方法将导致线程暂时停止执行,值得注意的是并不会释放锁。而当线程的<syntaxhighlight lang="java" inline>yield</syntaxhighlight>方法被调用时,意味着通知CPU当前线程可以“暂缓”执行的,实际很少使用。


>It is rarely appropriate to use this method. It may be useful
<q>It is rarely appropriate to use this method. It may be useful
      for debugging or testing purposes, where it may help to reproduce
      for debugging or testing purposes, where it may help to reproduce
      bugs due to race conditions.  
      bugs due to race conditions.</q>


= Context switching=
= Context switching=
第106行: 第126行:
* [https://en.wikipedia.org/wiki/Context_switch Context switch]
* [https://en.wikipedia.org/wiki/Context_switch Context switch]
* [https://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html How long does it take to make a context switch?]
* [https://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html How long does it take to make a context switch?]
[[Category:Concurrency]]

2023年12月19日 (二) 06:48的最新版本

线程是操作系统中进行运算调度的最小单位,它是一个单一顺序的控制流,不论是对于单核还是多核的CPU,都能比较有效的提高程序的吞吐率。在Java中,创建一个线程的唯一方法是创建一个Thread类的实例,并调用start()方法以启动该线程。然而当多个线程同时执行时,如何保证线程之间是按照我们期待的方式在运行呢?Java提供了多种机制来保证多个线程之间的交互。

同步(Synchronization)与监视器(Monitor)机制

显而易见最基本最常见的和多线程有关的就是同步synchronized关键字了,它底层是使用Monitor实现的。那么究竟什么是Monitor呢?根据JavaSE Specification的描述,在Java中,每一个对象都有一个与之关联的monitor,允许线程可以去lock或者unlock这个。实际上:

  • monitor是独立于Java语言之上的一个概念(没想到还有另外一个名字管程),保证在运行线程之前获取互斥锁
  • 在Java中,任何对象(java.lang.Object)都可以允许作为一个,所以会有waitnotify之类的方法

synchronized可以作用于代码块或者方法上。如果作用在代码块上,它会尝试去lock这个对象的,如果不成功将会等待直到lock成功。而当执行完毕后,无论是否出现异常,都将会释放这个锁。

如果作用在方法上,唯一的区别在于,如果是实例方法,那么将使用这个实例作为,也就是this;如果是静态方法,那么使用的是所在类的Class对象。

Wait/Notify

每一个Object都包含一个等待线程的集合(Wait set)。当对象创建的时候,这个队列是空的,当调用Object.wait()Object.nofity()以及Object.nofityAll()方法的时候,会自动添加或者移除队列中的线程。或者当线程的中断状态发生改变的时候,也会引起变化。

注意,wait和notify都需要

Wait

调用wait()方法后,线程进入等待状态,方法不会返回,直到将来某个时刻,线程从等待状态被其他线程唤醒后,方法才会返回,然后,继续执行下一条语句。

调用wait方法将使当前线程休眠直到另一个线程通过notify或者notifyAll来唤醒。当前线程必须持有该对象的锁,调用wait后即释放锁。当线程被唤醒时,需要重新取得锁并继续执行。然而,线程被唤醒有可能是因为“虚假唤醒”(spurious wakeups)导致,所以通常都需要将wait检测的逻辑包括在一个loop中:

synchronized (obj) {
    while (<condition does not hold>)
        obj.wait();
    // Perform action appropriate to condition
}

所谓虚假唤醒就是说,本来不该唤醒的时候唤醒了。究其原因是在操作系统层面就性能和正确性做出了权衡,放弃了正确性而选择让程序自己去处理。


Spurious wakeups may sound strange, but on some multiprocessor systems, making condition wakeup completely predictable might substantially slow all condition variable operations.

另外一个原因是,wait之后的逻辑的条件可能会不成立,考虑:

public synchronized String getTask() {
    // 如果是if,那么当addTask里面notifyAll唤醒所有线程的情况下,后面的线程运行的时候queue已经是empty了,会出现问题
    while (queue.isEmpty()) {
        this.wait();
    }
    return queue.remove();
}

public synchronized void addTask(String s) {
    this.queue.add(s);
    this.notify(); // 唤醒在this锁等待的线程
}

Notify

调用notify将唤醒一个正在等待持有该对象锁的线程,如果有多个对象在等待的话,将会随机唤醒其中的一个。

被唤醒的线程必须等到当前线程释放锁之后,才能开始执行;也就是说notify执行完之后,并不会立即释放锁,而是需要等到同步块执行完。

如果调用notifyAll的话,所有等待的线程将被唤醒,但同一时间有且仅有一个线程能取到锁并继续执行。

Interruption

当调用Thread.interrupt时,线程的中断状态呗设置为true。如果该线程在某个对象的waitSet中,则将会被从等待队列中移除,并在取得锁之后抛出InterruptedException。实际上,如果线程正在执行的是一些底层的blocking函数例如Thread.sleep(), Thread.join(), 或者 Object.wait()的时候,那么线程将抛出InterruptedException,并且interrupted状态会被清除;否则只会将interrupted状态设置为true

如果一个处于等待队列中的线程同时收到中断和通知,那么可能的行为是:

  • 先收到通知,正常唤醒。这时候,Thread.interrupted将为true
  • 抛出InterruptedException并退出

同样,如果有多个线程处于对象m的等待队列中,然后另一个线程执行m.notify,那么可能:

  • 至少有一个线程正常退出wait
  • 所有处于等待队列中的线程抛出InterruptedException而退出

需要注意的是,当一个线程中断了另一个线程的时候,被中断的线程并不是需要立即停止执行,程序可以选择在停止之前做一些清理工作之类的。通常如果捕获了InterruptedException只需要重新抛出即可,有些时候不能重新抛出的时候,需要将当前线程标记为interrupted使得上层堆栈的程序可以选择处理,

try {
    while (true) {
        Task task = queue.take(10, TimeUnit.SECONDS);
        task.execute();
    }
}catch (InterruptedException e) { 
    Thread.currentThread().interrupt();
}

线程的生命周期

每一个线程有一个生命周期,包含多个状态:

  • New: 线程创建还未开始执行,线程创建完之后即为此状态
  • Runnable: 在JVM中正在执行的状态。当线程start之后,即变为runnable状态
  • Blocked: 线程等待获取锁而被阻塞
  • Waiting: 线程等待其他线程
  • Timed Waiting: 有超时的等待
  • Terminated: 线程已被退出

Sleep / Yield

当调用线程的sleep方法将导致线程暂时停止执行,值得注意的是并不会释放锁。而当线程的yield方法被调用时,意味着通知CPU当前线程可以“暂缓”执行的,实际很少使用。

It is rarely appropriate to use this method. It may be useful for debugging or testing purposes, where it may help to reproduce bugs due to race conditions.

Context switching

在多线程中,CPU会为每个线程分配时间片区执行,即执行当前线程的一部分操作之后,操作系统需要从当前线程切换到其他线程中去。通常在下列的情况下会出现context switching:

  • 多任务处理(即多个线程正常执行)
  • 中断,


那么在这个切换的过程中,会发生一些什么事情呢?


参考: