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

更好的结合:Java 和 Thread 类。第二部分 — 同步

已在 随机的 群组中发布

介绍

所以,我们知道Java有线程。您可以在标题为Better together:Java 和 Thread 类的评论中阅读相关内容。第一部分 — 执行线程。线程是并行执行工作所必需的。这使得线程很可能以某种方式相互交互。让我们看看这是如何发生的以及我们拥有哪些基本工具。 更好的结合:Java 和 Thread 类。 第二部分 — 同步 - 1

屈服

Thread.yield()令人费解且很少使用。它在 Internet 上以多种不同的方式描述。包括一些人写道,有一些线程队列,其中一个线程将根据线程优先级下降。其他人写道,线程会将其状态从“Running”更改为“Runnable”(尽管这些状态之间没有区别,即 Java 不区分它们)。现实情况是,它的知名度要低得多,但在某种意义上却更简单。 更好的结合:Java 和 Thread 类。 第二部分 — 同步 - 2该方法的文档记录了一个错误 ( JDK-6416721: (spec thread) Fix Thread.yield() javadoc ) 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更好的结合:Java 和 Thread 类。 第二部分 — 同步 - 3可以看到,我们的线程现在处于“Sleeping”状态。其实还有一个更优雅的方法可以帮助我们的线程有美梦:

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 中查看线程的状态,那么它将看起来像这样: 更好的结合:Java 和 Thread 类。 第二部分 — 同步 - 4感谢监视工具,您可以看到线程正在发生什么。该join方法非常简单,因为它只是一个包含 Java 代码的方法,wait()只要调用它的线程处于活动状态,它就会执行。一旦线程死亡(当它完成它的工作时),等待就会被中断。这就是该方法的全部魔力join()。那么,让我们继续最有趣的事情。

监视器

多线程包括监视器的概念。监视器一词通过 16 世纪的拉丁语进入英语,意思是“用于观察、检查或连续记录过程的仪器或设备”。在本文的上下文中,我们将尝试涵盖基础知识。对于任何想要详细信息的人,请深入研究链接的材料。我们从 Java 语言规范 (JLS):17.1 开始我们的旅程。同步。它说了以下内容: 更好的结合:Java 和 Thread 类。 第二部分 — 同步 - 5事实证明,Java 使用“监视”机制来实现线程之间的同步。监视器与每个对象相关联,线程可以使用 获取它lock()或释放它unlock()。接下来,我们将在 Oracle 网站上找到教程:Intrinsic Locks and Synchronization. 本教程说 Java 的同步是围绕称为内部锁监视器锁的内部实体构建的。这种锁通常简称为“监视器”。我们还再次看到,Java 中的每个对象都有一个与之关联的内部锁。您可以阅读Java - Intrinsic Locks and Synchronization。接下来,了解 Java 中的对象如何与监视器相关联将很重要。在 Java 中,每个对象都有一个标头,用于存储程序员无法从代码中获得的内部元数据,但虚拟机需要这些元数据才能正确处理对象。对象头包含一个“标记词”,如下所示: 更好的结合:Java 和 Thread 类。 第二部分 — 同步 - 6

https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf

这里有一篇 JavaWorld 的文章非常有用:Java 虚拟机如何执行线程同步。本文应结合 JDK 错误跟踪系统中以下问题的“摘要”部分的描述:JDK-8183909。您可以在这里阅读同样的内容:JEP-8183909。因此,在 Java 中,监视器与对象相关联,并用于在线程试图获取(或获取)锁时阻塞线程。这是最简单的例子:

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 在该机制的多个实现之间切换。因此,为简单起见,在谈论监视器时,我们实际上是在谈论锁。 更好的结合:Java 和 Thread 类。 第二部分 — 同步 - 7

同步(等待锁)

正如我们之前看到的,“同步块”(或“临界区”)的概念与监视器的概念密切相关。看一个例子:

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中,是这样的: 更好的结合:Java 和 Thread 类。 第二部分 — 同步 - 8在JVisualVM中可以看到,状态为“Monitor”,意思是线程被阻塞,不能拿monitor。您也可以使用代码来确定线程的状态,但是以这种方式确定的状态名称与 JVisualVM 中使用的名称不匹配,尽管它们是相似的。在这种情况下,th1.getState()for 循环中的语句会返回BLOCKED,因为只要循环在运行,lock对象的监视器就被线程占用mainth1线程被阻塞,直到锁被释放后才能继续执行。除了同步块之外,还可以同步整个方法。例如,这是该类的一个方法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 中,它看起来像这样: 更好的结合:Java 和 Thread 类。 第二部分 — 同步 - 10要理解它是如何工作的,请记住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 。更好的结合:Java 和 Thread 类。 第二部分 — 同步 - 11

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——等待另一个线程满足某些条件。
如果满足条件,则线程调度程序启动线程。如果线程正在等待指定时间,则其状态为 TIMED_WAITING。如果线程不再运行(结束或抛出异常),则进入 TERMINATED 状态。要找出线程的状态,请使用getState()方法。线程也有一个isAlive()方法,如果线程未终止,则返回 true。

LockSupport 和线程停放

从 Java 1.6 开始,出现了一种名为LockSupport的有趣机制。 更好的结合:Java 和 Thread 类。 第二部分 — 同步 - 12此类将“许可”与使用它的每个线程相关联。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”。 更好的结合:Java 和 Thread 类。 第二部分 — 同步 - 13让我们看另一个例子:

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,但 JVisualVMwaitsynchronized关键字和park类中区分LockSupport。为什么这LockSupport如此重要?我们再次转向 Java 文档并查看WAITING线程状态。如您所见,只有三种方法可以进入它。其中两种方式是wait()join()。第三个是LockSupport。在 Java 中,锁也可以构建在LockSupport 上并提供更高级别的工具。让我们尝试使用一个。例如,看看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 类。第 I 部分 — 执行的线程 更好地结合:Java 和 Thread 类。第 III 部分 — 更好地交互: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