简要概述线程如何交互的细节。之前,我们研究了线程是如何相互同步的。这次我们将深入探讨线程交互时可能出现的问题,并讨论如何避免这些问题。我们还将提供一些有用的链接以供更深入的研究。
安装了 JVisualVM 插件(通过工具 -> 插件)后,我们可以看到死锁发生的位置:
根据 JVisualVM,我们看到了休眠期和停放期(这是当线程试图获取锁时——它进入停放状态,正如我们之前讨论线程同步时所讨论的)。您可以在此处查看活锁示例:Java - 线程活锁。
你可以在这里看到一个超级例子:Java - Thread Starvation and Fairness。此示例显示线程在饥饿期间会发生什么,以及从
此检查已作为IDEA-61117问题的一部分添加到 IntelliJ IDEA 中,该问题在2010 年的 发行说明中列出。

介绍
所以,我们知道Java有线程。您可以在标题为Better together:Java 和 Thread 类的评论中阅读相关内容。第一部分 — 执行线程。我们在标题为Better together:Java 和 Thread 类的评论中探讨了线程可以相互同步这一事实。第二部分 — 同步。是时候讨论线程如何相互交互了。他们如何共享共享资源?这里可能会出现什么问题?
僵局
最可怕的问题是死锁。死锁是指两个或多个线程永远在等待另一个线程。我们将从描述死锁的Oracle 网页中获取示例:
public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse = new Friend("Alphonse");
final Friend gaston = new Friend("Gaston");
new Thread(() -> alphonse.bow(gaston)).start();
new Thread(() -> gaston.bow(alphonse)).start();
}
}
第一次可能不会在这里发生死锁,但是如果您的程序确实挂起,那么就该运行了jvisualvm
: 
"Thread-1" - Thread t@12
java.lang.Thread.State: BLOCKED
at Deadlock$Friend.bowBack(Deadlock.java:16)
- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
线程 1 正在等待线程 0 的锁。为什么会发生这种情况?Thread-1
开始运行并执行该Friend#bow
方法。它被标记为关键字,这意味着我们正在为(当前对象)synchronized
获取监视器。this
该方法的输入是对其他对象的引用Friend
。现在,Thread-1
想要在另一个上执行该方法Friend
,并且必须获取它的锁才能这样做。但是如果另一个线程(在本例中Thread-0
)设法进入该bow()
方法,那么锁已经被获取并Thread-1
等待Thread-0
,反之亦然。这是无法解决的僵局,我们称之为僵局。死锁就像一个无法松开的死神之握,是一种无法打破的相互阻碍。关于死锁的另一种解释,可以看这个视频:死锁和活锁详解。
活锁
如果有死锁,是否也有活锁?是的,有 :) 活锁发生在线程表面上看起来还活着,但它们无法做任何事情时,因为它们继续工作所需的条件无法满足。基本上,活锁类似于死锁,但线程不会“挂起”等待监视器。相反,他们永远在做某事。例如:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class App {
public static final String ANSI_BLUE = "\u001B[34m";
public static final String ANSI_PURPLE = "\u001B[35m";
public static void log(String text) {
String name = Thread.currentThread().getName(); // Like "Thread-1" or "Thread-0"
String color = ANSI_BLUE;
int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
if (val != 0) {
color = ANSI_PURPLE;
}
System.out.println(color + name + ": " + text + color);
try {
System.out.println(color + name + ": wait for " + val + " sec" + color);
Thread.currentThread().sleep(val * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Lock first = new ReentrantLock();
Lock second = new ReentrantLock();
Runnable locker = () -> {
boolean firstLocked = false;
boolean secondLocked = false;
try {
while (!firstLocked || !secondLocked) {
firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
log("First Locked: " + firstLocked);
secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
log("Second Locked: " + secondLocked);
}
first.unlock();
second.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(locker).start();
new Thread(locker).start();
}
}
此代码的成功取决于 Java 线程调度程序启动线程的顺序。如果Thead-1
先开始,那么我们会得到活锁:
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
从示例中可以看出,两个线程依次尝试获取两个锁,但都失败了。但是,他们并没有陷入僵局。从表面上看,一切都很好,他们正在做自己的工作。 
饥饿
除了死锁和活锁,多线程还有一个问题:饥饿。这种现象不同于以前的阻塞形式,因为线程没有被阻塞——它们只是没有足够的资源。结果,虽然一些线程占用了所有执行时间,但其他线程无法运行:
https://www.logicbig.com/
Thread.sleep()
到 的一个小更改如何Thread.wait()
让您平均分配负载。 
竞争条件
在多线程中,存在“竞争条件”这样的东西。当线程共享资源时会发生这种现象,但代码的编写方式无法确保正确共享。看一个例子:
public class App {
public static int value = 0;
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
int oldValue = value;
int newValue = ++value;
if (oldValue + 1 != newValue) {
throw new IllegalStateException(oldValue + " + 1 = " + newValue);
}
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
此代码第一次可能不会产生错误。当它出现时,它可能看起来像这样:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
at App.lambda$main$0(App.java:13)
at java.lang.Thread.run(Thread.java:745)
如您所见,在newValue
分配一个值时出了点问题。newValue
太大了。value
由于竞争条件,其中一个线程设法更改了两个语句之间的变量。事实证明,线程之间存在竞争。现在想想不在货币交易中犯类似的错误是多么重要......示例和图表也可以在这里看到:Code to simulate race condition in Java thread。
易挥发的
说到线程的交互,这个volatile
关键词值得一提。让我们看一个简单的例子:
public class App {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Runnable whileFlagFalse = () -> {
while(!flag) {
}
System.out.println("Flag is now TRUE");
};
new Thread(whileFlagFalse).start();
Thread.sleep(1000);
flag = true;
}
}
最有趣的是,这很可能不起作用。新线程将看不到字段中的更改flag
。要为该flag
字段修复此问题,我们需要使用volatile
关键字。如何以及为什么?处理器执行所有操作。但是计算结果必须存储在某个地方。为此,有主内存和处理器的缓存。处理器的高速缓存就像一小块内存,用于比访问主内存更快地访问数据。但是任何事情都有一个缺点:缓存中的数据可能不是最新的(如上例中,标志字段的值未更新时)。所以volatile
关键字告诉 JVM 我们不想缓存我们的变量。这允许在所有线程上看到最新的结果。这是一个高度简化的解释。至于volatile
关键字,我强烈建议您阅读这篇文章。有关更多信息,我还建议您阅读Java 内存模型和Java Volatile 关键字。此外,重要的是要记住这volatile
是关于可见性,而不是关于更改的原子性。查看“竞争条件”部分中的代码,我们将在 IntelliJ IDEA 中看到一个工具提示: 
原子性
原子操作是不可分割的操作。例如,给变量赋值的操作必须是原子的。不幸的是,递增操作不是原子的,因为递增需要多达三个 CPU 操作:获取旧值,对其加一,然后保存该值。为什么原子性很重要?随着增量操作,如果出现竞争条件,那么共享资源(即共享值)可能随时突然改变。此外,涉及 64 位结构的操作(例如long
和double
)不是原子操作。可以在此处阅读更多详细信息:读写 64 位值时确保原子性。与原子性相关的问题可以在这个例子中看到:
public class App {
public static int value = 0;
public static AtomicInteger atomic = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
value++;
atomic.incrementAndGet();
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
Thread.sleep(300);
System.out.println(value);
System.out.println(atomic.get());
}
}
特殊AtomicInteger
班总是给我们30000,但是value
会时不时的变化。本主题有一个简短的概述:Introduction to Atomic Variables in Java。“比较和交换”算法是原子类的核心。您可以在无锁算法的比较 - JDK 7 和 8 示例中的 CAS 和 FAA或维基百科上的比较和交换文章中阅读更多相关信息。 
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
GO TO FULL VERSION