介绍
所以,我们知道Java有线程。您可以在标题为Better together:Java 和 Thread 类的评论中阅读相关内容。第一部分 — 执行线程。线程是并行执行工作所必需的。这使得线程很可能以某种方式相互交互。让我们看看这是如何发生的以及我们拥有哪些基本工具。
屈服
Thread.yield()令人费解且很少使用。它在 Internet 上以多种不同的方式描述。包括一些人写道,有一些线程队列,其中一个线程将根据线程优先级下降。其他人写道,线程会将其状态从“Running”更改为“Runnable”(尽管这些状态之间没有区别,即 Java 不区分它们)。现实情况是,它的知名度要低得多,但在某种意义上却更简单。
yield()
。如果你阅读它,很明显yield()
方法实际上只是向 Java 线程调度器提供了一些建议,可以给这个线程更少的执行时间。但是实际发生了什么,即调度程序是否根据建议采取行动以及它通常做什么,取决于 JVM 的实现和操作系统。它也可能取决于其他一些因素。所有的混淆很可能是由于随着 Java 语言的发展而重新考虑多线程这一事实。在此处的概述中阅读更多内容:Java Thread.yield() 简介。
睡觉
线程可以在其执行期间进入休眠状态。这是与其他线程交互的最简单类型。运行我们Java代码的Java虚拟机的操作系统有自己的线程调度器。它决定启动哪个线程以及何时启动。程序员不能直接从 Java 代码与这个调度程序交互,只能通过 JVM。他或她可以要求调度程序暂停线程一段时间,即让它休眠。您可以在这些文章中阅读更多内容:Thread.sleep()和多线程的工作原理。您还可以查看线程在 Windows 操作系统中的工作方式:Internals of Windows Thread。现在让我们亲眼看看。将以下代码保存在名为的文件中HelloWorldApp.java
:
class HelloWorldApp {
public static void main(String []args) {
Runnable task = () -> {
try {
int secToWait = 1000 * 60;
Thread.currentThread().sleep(secToWait);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(task);
thread.start();
}
}
如您所见,我们有一些任务等待 60 秒,然后程序结束。我们使用命令“ javac HelloWorldApp.java
”进行编译,然后使用“”运行程序java HelloWorldApp
。最好在单独的窗口中启动该程序。例如,在 Windows 上,它是这样的:start java HelloWorldApp
。我们使用jps命令获取PID(进程ID),我们用"打开线程列表jvisualvm --openpid pid
: 
try {
TimeUnit.SECONDS.sleep(60);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
你有没有注意到我们InterruptedException
到处都在处理?让我们理解为什么。
线程中断()
问题是当一个线程正在等待/休眠时,有人可能想要中断。在这种情况下,我们处理一个InterruptedException
. 这个机制是在Thread.stop()
方法被声明为 Deprecated 之后创建的,即过时的和不可取的。原因是当stop()
方法被调用时,线程被简单地“杀死”了,这是非常不可预测的。我们无法知道线程何时停止,也无法保证数据的一致性。想象一下,当线程被终止时,您正在将数据写入文件。Java 的创建者没有杀死线程,而是决定告诉它应该被中断会更合乎逻辑。如何响应这些信息是线程自己决定的事情。有关详细信息,请阅读为什么不推荐使用 Thread.stop?在甲骨文的网站上。让我们看一个例子:
public static void main(String []args) {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
在这个例子中,我们不会等待 60 秒。相反,我们会立即显示“Interrupted”。这是因为我们调用了interrupt()
线程上的方法。此方法设置一个称为“中断状态”的内部标志。也就是说,每个线程都有一个不可直接访问的内部标志。但是我们有与这个标志交互的本地方法。但这不是唯一的方法。一个线程可能正在运行,而不是等待什么,只是执行操作。但它可能预料到其他人会希望在特定时间结束其工作。例如:
public static void main(String []args) {
Runnable task = () -> {
while(!Thread.currentThread().isInterrupted()) {
// Do some work
}
System.out.println("Finished");
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
在上面的例子中,while
循环会一直执行到线程被外部中断为止。至于isInterrupted
标志,重要的是要知道,如果我们捕获到InterruptedException
, isInterrupted 标志将被重置,然后isInterrupted()
返回 false。Thread 类还有一个静态的Thread.interrupted()方法,它只适用于当前线程,但是这个方法会将标志重置为 false!在标题为“线程中断”的这一章中阅读更多内容。
加入(等待另一个线程完成)
最简单的等待类型是等待另一个线程完成。
public static void main(String []args) throws InterruptedException {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.join();
System.out.println("Finished");
}
在本例中,新线程将休眠 5 秒。同时,主线程会一直等待,直到休眠线程被唤醒并完成它的工作。如果您在 JVisualVM 中查看线程的状态,那么它将看起来像这样: 
join
方法非常简单,因为它只是一个包含 Java 代码的方法,wait()
只要调用它的线程处于活动状态,它就会执行。一旦线程死亡(当它完成它的工作时),等待就会被中断。这就是该方法的全部魔力join()
。那么,让我们继续最有趣的事情。
监视器
多线程包括监视器的概念。监视器一词通过 16 世纪的拉丁语进入英语,意思是“用于观察、检查或连续记录过程的仪器或设备”。在本文的上下文中,我们将尝试涵盖基础知识。对于任何想要详细信息的人,请深入研究链接的材料。我们从 Java 语言规范 (JLS):17.1 开始我们的旅程。同步。它说了以下内容:
lock()
或释放它unlock()
。接下来,我们将在 Oracle 网站上找到教程:Intrinsic Locks and Synchronization. 本教程说 Java 的同步是围绕称为内部锁或监视器锁的内部实体构建的。这种锁通常简称为“监视器”。我们还再次看到,Java 中的每个对象都有一个与之关联的内部锁。您可以阅读Java - Intrinsic Locks and Synchronization。接下来,了解 Java 中的对象如何与监视器相关联将很重要。在 Java 中,每个对象都有一个标头,用于存储程序员无法从代码中获得的内部元数据,但虚拟机需要这些元数据才能正确处理对象。对象头包含一个“标记词”,如下所示: 
https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf
public class HelloWorld{
public static void main(String []args){
Object object = new Object();
synchronized(object) {
System.out.println("Hello World");
}
}
}
在这里,当前线程(执行这些代码行的线程)使用关键字synchronized
尝试使用与object"\
获取/获取锁的变量。如果没有其他人在争夺监视器(即没有其他人在使用同一对象运行同步代码),那么 Java 可能会尝试执行称为“偏向锁定”的优化。在对象头中的mark word中添加相关的tag和关于哪个线程拥有monitor的锁的记录。这减少了锁定监视器所需的开销。如果监视器以前由另一个线程拥有,那么这样的锁定是不够的。JVM 切换到下一种锁定类型:“基本锁定”。它使用比较和交换 (CAS) 操作。更重要的是,对象头的标记词本身不再存储标记词,而是存储它的位置的引用,并且标记发生变化,以便 JVM 了解我们使用的是基本锁定。如果多个线程竞争(contend)一个monitor(一个已经获取了锁,第二个在等待锁释放),那么mark word中的tag就变了,mark word现在存储了一个对monitor的引用作为一个对象——JVM 的一些内部实体。正如 JDK Enchancement Proposal (JEP) 中所述,这种情况需要内存的 Native Heap 区域中的空间来存储此实体。对该内部实体内存位置的引用将存储在对象头的标记字中。因此,监视器实际上是一种在多个线程之间同步访问共享资源的机制。JVM 在该机制的多个实现之间切换。因此,为简单起见,在谈论监视器时,我们实际上是在谈论锁。第二个正在等待锁被释放),然后标记词中的标记发生变化,标记词现在将对监视器的引用存储为对象——JVM 的某个内部实体。正如 JDK Enchancement Proposal (JEP) 中所述,这种情况需要内存的 Native Heap 区域中的空间来存储此实体。对该内部实体内存位置的引用将存储在对象头的标记字中。因此,监视器实际上是一种在多个线程之间同步访问共享资源的机制。JVM 在该机制的多个实现之间切换。因此,为简单起见,在谈论监视器时,我们实际上是在谈论锁。第二个正在等待锁被释放),然后标记词中的标记发生变化,标记词现在将对监视器的引用存储为对象——JVM 的某个内部实体。正如 JDK Enchancement Proposal (JEP) 中所述,这种情况需要内存的 Native Heap 区域中的空间来存储此实体。对该内部实体内存位置的引用将存储在对象头的标记字中。因此,监视器实际上是一种在多个线程之间同步访问共享资源的机制。JVM 在该机制的多个实现之间切换。因此,为简单起见,在谈论监视器时,我们实际上是在谈论锁。mark word 现在将对监视器的引用存储为对象——JVM 的某个内部实体。正如 JDK Enchancement Proposal (JEP) 中所述,这种情况需要内存的 Native Heap 区域中的空间来存储此实体。对该内部实体内存位置的引用将存储在对象头的标记字中。因此,监视器实际上是一种在多个线程之间同步访问共享资源的机制。JVM 在该机制的多个实现之间切换。因此,为简单起见,在谈论监视器时,我们实际上是在谈论锁。mark word 现在将对监视器的引用存储为对象——JVM 的某个内部实体。正如 JDK Enchancement Proposal (JEP) 中所述,这种情况需要内存的 Native Heap 区域中的空间来存储此实体。对该内部实体内存位置的引用将存储在对象头的标记字中。因此,监视器实际上是一种在多个线程之间同步访问共享资源的机制。JVM 在该机制的多个实现之间切换。因此,为简单起见,在谈论监视器时,我们实际上是在谈论锁。对该内部实体内存位置的引用将存储在对象头的标记字中。因此,监视器实际上是一种在多个线程之间同步访问共享资源的机制。JVM 在该机制的多个实现之间切换。因此,为简单起见,在谈论监视器时,我们实际上是在谈论锁。对该内部实体内存位置的引用将存储在对象头的标记字中。因此,监视器实际上是一种在多个线程之间同步访问共享资源的机制。JVM 在该机制的多个实现之间切换。因此,为简单起见,在谈论监视器时,我们实际上是在谈论锁。 
同步(等待锁)
正如我们之前看到的,“同步块”(或“临界区”)的概念与监视器的概念密切相关。看一个例子:
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Runnable task = () -> {
synchronized(lock) {
System.out.println("thread");
}
};
Thread th1 = new Thread(task);
th1.start();
synchronized(lock) {
for (int i = 0; i < 8; i++) {
Thread.currentThread().sleep(1000);
System.out.print(" " + i);
}
System.out.println(" ...");
}
}
在这里,主线程首先将任务对象传递给新线程,然后立即获取锁并与之进行长时间操作(8 秒)。一直以来,任务无法继续,因为它无法进入块synchronized
,因为已经获取了锁。如果线程拿不到锁,就会等待监听。一旦获得锁,它将继续执行。当线程退出监视器时,它会释放锁。在JVisualVM中,是这样的: 
th1.getState()
for 循环中的语句会返回BLOCKED,因为只要循环在运行,lock
对象的监视器就被线程占用main
,th1
线程被阻塞,直到锁被释放后才能继续执行。除了同步块之外,还可以同步整个方法。例如,这是该类的一个方法HashTable
:
public synchronized int size() {
return count;
}
在任何给定时间,此方法将仅由一个线程执行。我们真的需要锁吗?是的,我们需要它。在实例方法的情况下,“this”对象(当前对象)充当锁。这里有一个关于这个主题的有趣讨论:Is there an advantage to using a Synchronized Method instead of a Synchronized Block? . 如果该方法是静态的,那么锁定的将不是“this”对象(因为静态方法没有“this”对象),而是一个 Class 对象(例如,)Integer.class
。
等待(等待监视器)。notify() 和 notifyAll() 方法
Thread 类有另一个与监视器关联的等待方法。sleep()
与and不同join()
,不能简单地调用此方法。它的名字是wait()
。wait
在与我们要等待的监视器关联的对象上调用该方法。让我们看一个例子:
public static void main(String []args) throws InterruptedException {
Object lock = new Object();
// The task object will wait until it is notified via lock
Runnable task = () -> {
synchronized(lock) {
try {
lock.wait();
} catch(InterruptedException e) {
System.out.println("interrupted");
}
}
// After we are notified, we will wait until we can acquire the lock
System.out.println("thread");
};
Thread taskThread = new Thread(task);
taskThread.start();
// We sleep. Then we acquire the lock, notify, and release the lock
Thread.currentThread().sleep(3000);
System.out.println("main");
synchronized(lock) {
lock.notify();
}
}
在 JVisualVM 中,它看起来像这样: 
wait()
和notify()
方法与 相关联java.lang.Object
。与线程相关的方法在类中可能看起来很奇怪Object
。但其原因现在正在展开。您会记得 Java 中的每个对象都有一个标头。标头包含各种内部管理信息,包括有关监视器的信息,即锁的状态。请记住,每个对象或类的实例都与 JVM 中的一个内部实体相关联,称为内部锁或监视器。在上面的示例中,任务对象的代码表明我们进入了与该对象关联的监视器的同步块lock
。如果我们成功获得了这个监视器的锁,那么wait()
叫做。执行任务的线程会释放对象的监视器,但会进入等待对象监视器lock
通知的线程队列。lock
这个线程队列称为 WAIT SET,它更恰当地反映了它的用途。也就是说,它更像是一个集合而不是一个队列。该main
线程使用任务对象创建一个新线程,启动它,并等待 3 秒。这使得新线程很有可能在线程之前获得锁main
,并进入监视器的队列。之后main
线程自己进入lock
对象的synchronized块,使用monitor进行线程通知。发送通知后,main
线程释放lock
对象的监视器,之前等待lock
对象监视器被释放的新线程继续执行。可以只向一个线程 ( notify()
) 发送通知,也可以同时向队列中的所有线程 ( notifyAll()
) 发送通知。在此处阅读更多信息:Java 中的 notify() 和 notifyAll() 之间的区别。请务必注意,通知顺序取决于 JVM 的实现方式。在此处阅读更多内容:如何使用 notify 和 notifyAll 解决饥饿问题?. 可以在不指定对象的情况下执行同步。当同步整个方法而不是单个代码块时,您可以执行此操作。例如,对于静态方法,锁将是一个 Class 对象(通过 获得.class
):
public static synchronized void printA() {
System.out.println("A");
}
public static void printB() {
synchronized(HelloWorld.class) {
System.out.println("B");
}
}
在使用锁方面,这两种方法是一样的。如果一个方法不是静态的,那么同步将使用当前的instance
,即使用this
。顺便说一下,我们之前说过您可以使用该getState()
方法来获取线程的状态。wait()
例如,对于队列中等待监视器的线程,如果方法指定超时 ,状态将为 WAITING 或 TIMED_WAITING 。
https://stackoverflow.com/questions/36425942/what-is-the-lifecycle-of-thread-in-java
线程生命周期
在线程的生命周期中,线程的状态会发生变化。事实上,这些变化构成了线程的生命周期。一旦线程被创建,它的状态就是 NEW。在这种状态下,新线程还没有运行,Java 线程调度器还不知道任何关于它的信息。为了让线程调度程序了解线程,您必须调用该thread.start()
方法。然后线程将转换为 RUNNABLE 状态。Internet 上有很多区分“Runnable”和“Running”状态的错误图表。但这是一个错误,因为 Java 不区分“准备工作”(runnable)和“工作中”(running)。当线程处于活动状态但不活动(不是 Runnable)时,它处于两种状态之一:
- BLOCKED — 等待进入临界区,即块
synchronized
。 - WAITING——等待另一个线程满足某些条件。
getState()
方法。线程也有一个isAlive()
方法,如果线程未终止,则返回 true。
LockSupport 和线程停放
从 Java 1.6 开始,出现了一种名为LockSupport的有趣机制。
park()
如果许可可用,则对该方法的调用会立即返回,并在此过程中消耗许可。否则,它会阻塞。unpark
如果许可尚不可用,则调用该方法可使许可可用。只有 1 个许可证。的 Java 文档LockSupport
引用了该类Semaphore
。让我们看一个简单的例子:
import java.util.concurrent.Semaphore;
public class HelloWorldApp{
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(0);
try {
semaphore.acquire();
} catch (InterruptedException e) {
// Request the permit and wait until we get it
e.printStackTrace();
}
System.out.println("Hello, World!");
}
}
这段代码将一直等待,因为现在信号量有 0 个许可。当acquire()
在代码中调用时(即请求许可),线程将等待直到它收到许可。由于我们在等待,所以我们必须处理InterruptedException
. 有趣的是,信号量获得了一个单独的线程状态。如果我们查看 JVisualVM,我们会看到状态不是“Wait”,而是“Park”。 
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
// Park the current thread
System.err.println("Will be Parked");
LockSupport.park();
// As soon as we are unparked, we will start to act
System.err.println("Unparked");
};
Thread th = new Thread(task);
th.start();
Thread.currentThread().sleep(2000);
System.err.println("Thread state: " + th.getState());
LockSupport.unpark(th);
Thread.currentThread().sleep(2000);
}
线程的状态将是 WAITING,但 JVisualVMwait
从synchronized
关键字和park
类中区分LockSupport
。为什么这LockSupport
如此重要?我们再次转向 Java 文档并查看WAITING线程状态。如您所见,只有三种方法可以进入它。其中两种方式是wait()
和join()
。第三个是LockSupport
。在 Java 中,锁也可以构建在LockSuppor
t 上并提供更高级别的工具。让我们尝试使用一个。例如,看看ReentrantLock
:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HelloWorld{
public static void main(String []args) throws InterruptedException {
Lock lock = new ReentrantLock();
Runnable task = () -> {
lock.lock();
System.out.println("Thread");
lock.unlock();
};
lock.lock();
Thread th = new Thread(task);
th.start();
System.out.println("main");
Thread.currentThread().sleep(2000);
lock.unlock();
}
}
就像前面的示例一样,这里的一切都很简单。该lock
对象等待某人释放共享资源。如果我们查看 JVisualVM,我们会看到新线程将被暂停,直到线程main
释放对它的锁定。你可以在这里阅读更多关于锁的信息:Java 8 StampedLocks vs. ReadWriteLocks and Synchronized and Lock API in Java。为了更好地理解锁是如何实现的,阅读这篇文章中有关 Phaser 的内容会很有帮助:Java Phaser 指南。谈到各种同步器,您必须阅读有关 The Java Synchronizers 的 DZone文章。
结论
在这篇评论中,我们研究了 Java 中线程交互的主要方式。附加材料:- 更好的结合:Java 和 Thread 类。第一部分——执行线程
- https://dzone.com/articles/the-java-synchronizers
- https://www.javatpoint.com/java-multithreading-interview-questions
GO TO FULL VERSION