CodeGym /Java 博客 /随机的 /更好的结合:Java 和 Thread 类。第三部分——互动
John Squirrels
第 41 级
San Francisco

更好的结合:Java 和 Thread 类。第三部分——互动

已在 随机的 群组中发布
简要概述线程如何交互的细节。之前,我们研究了线程是如何相互同步的。这次我们将深入探讨线程交互时可能出现的问题,并讨论如何避免这些问题。我们还将提供一些有用的链接以供更深入的研究。 更好的结合:Java 和 Thread 类。 第三部分——互动 - 1

介绍

所以,我们知道Java有线程。您可以在标题为Better together:Java 和 Thread 类的评论中阅读相关内容。第一部分 — 执行线程我们在标题为Better together:Java 和 Thread 类的评论中探讨了线程可以相互同步这一事实。第二部分 — 同步。是时候讨论线程如何相互交互了。他们如何共享共享资源?这里可能会出现什么问题? 更好的结合:Java 和 Thread 类。 第三部分——互动 - 2

僵局

最可怕的问题是死锁。死锁是指两个或多个线程永远在等待另一个线程。我们将从描述死锁的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更好的结合:Java 和 Thread 类。 第三部分——互动 - 3安装了 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
...
从示例中可以看出,两个线程依次尝试获取两个锁,但都失败了。但是,他们并没有陷入僵局。从表面上看,一切都很好,他们正在做自己的工作。 更好的结合:Java 和 Thread 类。 第三部分——互动 - 4根据 JVisualVM,我们看到了休眠期和停放期(这是当线程试图获取锁时——它进入停放状态,正如我们之前讨论线程同步时所讨论的。您可以在此处查看活锁示例:Java - 线程活锁

饥饿

除了死锁和活锁,多线程还有一个问题:饥饿。这种现象不同于以前的阻塞形式,因为线程没有被阻塞——它们只是没有足够的资源。结果,虽然一些线程占用了所有执行时间,但其他线程无法运行: 更好的结合:Java 和 Thread 类。 第三部分——互动 - 5

https://www.logicbig.com/

你可以在这里看到一个超级例子:Java - Thread Starvation and Fairness。此示例显示线程在饥饿期间会发生什么,以及从Thread.sleep()到 的一个小更改如何Thread.wait()让您平均分配负载。 更好的结合:Java 和 Thread 类。 第三部分——互动 - 6

竞争条件

在多线程中,存在“竞争条件”这样的东西。当线程共享资源时会发生这种现象,但代码的编写方式无法确保正确共享。看一个例子:

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 中看到一个工具提示: 更好的结合:Java 和 Thread 类。 第三部分——互动 - 7此检查已作为IDEA-61117问题的一部分添加到 IntelliJ IDEA 中,该问题在2010 年的 发行说明中列出。

原子性

原子操作是不可分割的操作。例如,给变量赋值的操作必须是原子的。不幸的是,递增操作不是原子的,因为递增需要多达三个 CPU 操作:获取旧值,对其加一,然后保存该值。为什么原子性很重要?随着增量操作,如果出现竞争条件,那么共享资源(即共享值)可能随时突然改变。此外,涉及 64 位结构的操作(例如longdouble)不是原子操作。可以在此处阅读更多详细信息:读写 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或维基百科上的比较和交换文章中阅读更多相关信息。 更好的结合:Java 和 Thread 类。 第三部分——互动 - 9

http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html

发生在之前

有一个有趣而神秘的概念叫做“发生在之前”。作为学习线程的一部分,您应该阅读它。happens-before 关系显示线程之间的操作将被看到的顺序。有许多解释和评论。这是关于此主题的最新演示文稿之一:Java“之前发生”的关系

概括

在这篇评论中,我们探讨了线程如何交互的一些细节。我们讨论了可能出现的问题,以及识别和消除这些问题的方法。有关该主题的其他材料清单: 更好的结合:Java 和 Thread 类。第 I 部分 — 执行的线程 更好地结合:Java 和 Thread 类。第二部分 — 同步 更好地结合:Java 和 Thread 类。第 IV 部分 — Callable、Future 和朋友 更好地结合在一起:Java 和 Thread 类。第五部分 — Executor、ThreadPool、Fork/Join 更好地结合在一起:Java 和 Thread 类。第六部分——开火!
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION