java谜题--Thread.join()方法容易忽略的地方

ORB-SLAM3编程实践指南 它基于特征点的立体视觉和视觉惯性里程计技术,能够在没有先验知识的情况下,从单目、双目或RGB-D摄像头的输入中实时构建稠密三维地图,并实现相机的定位。通过正确配置环境、准备数据集和运行ORB-SLAM3,您可以实现高效的视觉SLAM系统,并获取相机在三维空间中的位姿。其中,[path_to_vocabulary]是ORB-SLAM3的词汇文件路径,[path_to_settings]是相机参数配置文件路径,[path_to_sequence]是输入图像的路径。运行ORB-SLAM3。 阅读详情

谜题77:搞乱锁的妖怪

下面的这段程序模拟了一个小车间。程序首先启动了一个工人线程,该线程在停止时间到来之前会一直工作(至少是假装在工作),然后程序安排了一个定时器任务(timer task)用来模拟一个恶毒的老板,他会试图阻止停止时间的到来。最后,主线程作为一个善良的老板会告诉工人停止时间到了,并且等待工人停止工作。那么这个程序会打印什么呢?
import java.util.*;
public class Worker extends Thread {
    private volatile boolean quittingTime = false;
    public void run() {
        while (!quittingTime)
            pretendToWork();
        System.out.println("Beer is good");
    }
    
    private void pretendToWork() {
        try {
            Thread.sleep(300); // Sleeping on the job?
        } catch (InterruptedException ex) { }
    }
    // It's quitting time, wait for worker - Called by good boss
    synchronized void quit() throws InterruptedException {
        quittingTime = true;
        join();
    }
    // Rescind quitting time - Called by evil boss
    synchronized void keepWorking() {
        quittingTime = false;
    }
    
    public static void main(String[] args)
            throws InterruptedException {
        final Worker worker = new Worker();
        worker.start();
        Timer t = new Timer(true); // Daemon thread
        t.schedule(new TimerTask() {
            public void run() { worker.keepWorking(); }
        }, 500);
        Thread.sleep(400);
        worker.quit();
    }
}

想要探究这个程序到底做了什么的最好方法就是手动地模拟一下它的执行过程。下面是一个近似的时间轴,这些时间点的数值是相对于程序的开始时刻进行计算的:

  • 300 ms:工人线程去检查易变的quittingTime 域,看看停止时间是否已经到了。这个时候并没有到停止时间,所以工人线程会回去继续“工作”。
  • 400ms:作为善良的老板的主线程会去调用工人线程的quit方法。主线程会获得工人线程实例上的锁(因为quit是一个同步化的方法),将quittingTime的值设为true,并且调用工人线程上的join方法。这个对join方法的调用并不会马上返回,而是会等待工人线程执行完毕。
  • 500m:作为恶毒的老板定时器任务开始执行。它将试图调用工人线程的keepWorking方法,但是这个调用将会被阻塞,因为keepWorking是一个同步化的方法,而主线程当时正在执行工人线程上的另一个同步化方法(quit方法)。
  • 600ms:工人线程会再次检查停止时间是否已经到来。由于quittingTime域是易变的,那么工人线程肯定会看到新的值true,所以它会打印 Beer is good 并结束运行。这会让主线程对join方法的调用执行返回,随后主线程也结束了运行。而定时器线程是后台的,所以它也会随之结束运行,整个程序也就结束了。
所以,我们会认为程序将运行不到1秒钟,打印 Beer is good ,然后正常的结束。但是当你尝试运行这个程序的时候,你会发现它没有打印任何东西,而是一直处于挂起状态(没有结束)。我们的分析哪里出错了呢?

其实,并没有什么可以保证上述几个交叉的事件会按照上面的时间轴发生。无论是Timer类还是Thread.sleep方法,都不能保证具有实时(real-time)性。这就是说,由于这里计时的粒度太粗,所以上述几个事件很有可能会在时间轴上互有重叠地交替发生。100毫秒对于计算机来说是一段很长的时间。此外,这个程序被重复地挂起;看起来好像有什么其他的东西在工作着,事实上,确实是有这种东西。

我们的分析存在着一个基本的错误。在500ms时,当作为恶毒老板的定时器任务运行时,根据时间轴的显示,它对keepWorking方法的调用会被阻塞,因为keepWorking是一个同步化的方法并且主线程正在同一个对象上执行着同步化方法quit(在Thread.join中等待着)。这些都是对的,keepWorking确实是一个同步化的方法,并且主线程确实正在同一个对象上执行着同步化的quit方法。即使如此,定时器线程仍然可以获得这个对象上的锁,并且执行keepWorking方法。这是如何发生的呢?

问题的答案涉及到了Thread.join的实现。这部分内容在关于该方法的文档中(JDK文档)是找不到的,至少在迄今为止发布的文档中如此,也包括5.0版。在内部,Thread.join方法在表示正在被连接(join)的那个Thread实例上调用Object.wait方法。(调用了原线程对象的wait()方法,释放了获取的对象锁)这样就在等待期间释放了该对象上的锁。在我们的程序中,这就使得作为恶毒老板的定时器线程能够堂而皇之的将quittingTime重新设置成false,尽管此时主线程正在执行同步化的quit方法。这样的结果是,工人线程永远不会看到停止时间的到来,它会永远运行下去。作为善良的老板的主线程也就永远不会从join方法中返回了。

使这个程序产生了预料之外的行为的根本原因就是WorkerThread类的作者使用了实例上的锁来确保quit方法和keepWorking方法的互斥,但是这种用法与超类(Thread)内部对该锁的用法发生了冲突。这里的教训是:除非有关于某个类的详细说明作为保证,否则千万不要假设库中的这个类对它的实例或类上的锁会做(或者不会做)某些事情。对于库的任何调用都可能会产生对wait、notify、notifyAll方法或者某个同步化方法的调用。所有这些,都可能对应用级的代码产生影响。

如果你需要获得某个锁的完全控制权,那么就要确定没有任何其他人能够访问到它。如果你的类扩展了库中的某个类,而这个库中的类可能使用了它的锁,或者如果某些不可信的人可能会获得对你的类的实例的访问权,那么请不要使用与这个类或它的实例自动关联的那些锁。取而代之的,你应该在一个私有的域中创建一个单独的锁对象。在5.0版本发布之前,用于这种锁对象的正确类型只有Object或者它的某个普通的子类。从5.0版本开始,java.util.concurrent.locks提供了2种可选方案:ReentrantLock和ReentrantReadWriteLock。相对于Object类,这2个类提供了更好的机动性,但是它们使用起来也要更麻烦一点。它们不能被用在同步化的语句块(synchronized block)中,而且必须辅以try-finally语句对其进行显式的获取和释放。

订正这个程序最直接的方法是添加一个Object类型的私有域作为锁,并且在quit和keepWorking方法中对这个锁对象进行同步。通过上述修改之后,该程序就会打印出我们所期望的Beer is good。可以看出,该程序能够产生正确行为并不依赖于它必须遵从我们前面分析的时间轴:

private final Object lock = new Object();
// It's quitting time, wait for worker - Called by good boss
void quit() throws InterruptedException{
    synchronized (lock){
        quittingTime = true;
        join();
    }
}
// Rescind quitting time - Called by evil boss
void keepWorking(){
    synchronized(lock){
        quittingTime = false;
    }
}

另外一种可以修复这个程序的方法是让Worker类实现Runnable而不是扩展Thread,然后在创建每个工人线程的时候都使用Thread(Runnable)构造器。这样可以将每个Worker实例上的锁与其线程上的锁进行解耦。这是一个规模稍大一些的重构。

正如库类对锁的使用会干扰应用程序一样,应用程序中对锁的使用也会干扰库类。例如,在迄今为止发布的所有版本的JDK(包括5.0版本)中,为了创建一个新的Thread实例,系统都会去获取Thread类上的锁。而执行下面的代码就可以阻止任何新线程的创建:

synchronized(Thread.class){
    Thread.sleep(Long.MAX_VALUE);
}

总之,永远不要假设库类会(或者不会)对它的锁做某些事情。为了隔离你自己的程序与库类对锁的使用,除了那些专门设计用来被继承的库类之外,请避免继承其它库类 [EJ Item 15]。为了确保你的锁不会遭受外部的干扰,可以将它们设为私有以阻止其他人对它们的访问。

对于语言设计者来说,需要考虑的是为每个对象都关联一个锁是否是合适的。如果你决定这么做了,就需要考虑限制对这些锁的访问。在Java中,锁实际上是对象的公共属性,或许它们变为私有的会更有意义。同时请记住在Java语言中,一个对象实际上就是一个锁:你在对象本身之上进行同步。如果每个对象都有一个锁,而且你可以通过调用一个访问器方法来获得它,这样或许会更有意义。


【Android Performance】Android CPU调度模式详解:从performance到schedutil的Governor调优指南 本文系统讲解Android/Linux系统中的CPU频率调节策略(Governor),涵盖六大调度模式的底层工作原理:performance(性能优先)、powersave(省电优先)、conservative(保守调频)、ondemand(按需调频)、userspace(手动控制)以及schedutil(调度器驱动)。文中深入拆解每种Governor的调频决策逻辑、上升/下降曲线的差异、适用场景与选型建议,并配以实操命令演示如何查看可用Governor列表、动态切换调度模式以及多Cluster架构下的统一 阅读详情

相关推荐

通俗解析:霍尔编码器、光电编码器、电位计、磁编码器和旋转变压器的实战选型指南

本文深入解析了霍尔编码器、光电编码器、电位计、磁编码器和旋转变压器五大主流编码器的工作原理与核心特性。通过对比精度、环境耐受性、成本及寿命等关键维度,并结合消费电子、工业自动化、汽车电子及极端环境等典型应用场景,提供了一份清晰的实战选型指南,旨在帮助工程师根据具体需求,在性能、可靠性与成本间做出最优决策。

weixin_29216353的博客 253

Java核心故障诊断:从NoClassDefFoundError到OOM的四层能力验证

Java核心机制是理解JVM运行时行为的基础,其本质涵盖类加载、内存模型、字节码执行与运行时契约四大原理。掌握这些原理,不仅能精准定位NoClassDefFoundError背后的类加载器隔离失效,还能预判OutOfMemoryError中堆外内存泄漏等深层风险。技术价值在于将语法知识升维为系统级问题推演能力,支撑高可用架构设计与线上故障秒级响应。典型应用场景包括Spring Boot启动失败分析、微服务间JWT时间戳校验偏差排查、容器化环境元空间配置优化等。本套Core Java Quiz以真实故障为驱动

cong84596496的博客 333

实验六 存储器实验

只读存储器(ROM)是一种在正常工作时其存储的数据固定不变,其中的数据只能读出,不能写入,即使断电也能够保留数据,要想在只读存储器中存入或改变数据,必须具备特定的条件。当地址为00000101时,会分别在数据位宽为8,16,32的只读寄存器中找到地址为00000101所对应的十进制地址位置,也就是地址为5的数据,并将其分别以8位,16位,32位输出。因此,输出保持为0。因此,当异步清零信号为1时,无论其他输入信号是什么,触发器的输出都会被强制清零,输出为0。1)寄存器输入0011,使能端1,异步清零0。

wenmou1111的博客 3001

java 线程 thread.join_Java Thread.join() 方法详解

有些知识点还是要一个一个来,一文秒懂 系列还是来的有点肤浅。这几天开始尝试深入 API 层写一些东西,看看效果如何?本章节我尝试详细的介绍下 Thread.join() 方法。然后过几天回来看看这篇文章的质量如何?Thread.join() 方法有许多重载,可用于各种场景,本文也会针对一些较常见的场景写一些使用 Thread.join() 的示例。Thread.join() 方法Thread.jo...

weixin_42297562的博客 577

面试官连夜整理:103道AI Agent面试题,答对一半直接加薪50%

维度AutoGPTLangGraphAutoGen控制流LLM 驱动的隐式循环代码层定义的显式状态图对话驱动的自动流转状态存储Prompt 上下文中(易丢失)结构化 State 对象(可持久化)对话历史(自动累积)分支能力依赖 LLM 的文本输出(不稳定)条件边 + 节点路由(确定性)多 Agent 对话自然分支(涌现式)回溯/回滚无原生支持,只能重新开始原生 Checkpoint 支持时间旅行无全局回滚,单 Agent 可重启人机协同仅人工在关键点介入(需要自定义)原生支持。

weixin_35774598的博客 620

Autosar诊断实战系列23-NRC优先级解析

NRC分类,优先级判断逻辑及测试注意事项介绍。

initiallizer的博客 2617

Thread->sleep、wait、join使用

Thread sleep、wait、join使用这里先介绍join,然后把两个有关联的sleep和wait一起介绍.join()这个方法比较好理解,当前线程等待指定线程终止后在执行,将两个交替执行的线程合并为顺序执行的线程.比如在B线程中调用A线程的join()方法,直到A线程执行完毕,B线程才会继续执行.api有两个 void join()当前线程等待调用这个方法的线程终止后再执行. void j

朱利源的博客 3674

LVGL core group焦点分组管理(lv_group.c)

lv_group_t是用于焦点管理的,将需要获取焦点的对像加入到同一个分组内,再将其通过挂到指定的事件驱动中,就可以控制焦点了。而被加入的对象又可以通过焦点状态事件和事件来控制对象的焦点逻辑了。创建好的分组通过接口挂事件驱动中,这样就可以指定一个自定义的焦点分组了。;) {if(!} }友情提示:无论是接口还是都是单例的,即被设置后前一个被设置的值就失效了;此方案可以保证焦点的唯一 性,但也一定要记住要在适合的位置设置回来,以防丢失焦点。

Bigan(安)的博客 3906

JAVA中的合并线程join()方法

JAVA中的合并线程join()方法 void join() thread.Join把指定的线程加入到当前线程, 可以将两个交替执行的线程合并为顺序执行的线程。 比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。 t.join(); //调用join方法,等待线程t执行完毕 t.join(1000); //等待 t 线程,等待时间是1000毫秒。 public class ThreadScheduleTest03 { public static

qq_42843172的博客 590

57. SAP UI5 应用基于 OData 注解的 Smart Field 使用方法学习

所谓 SAP UI5 Smart Field,顾名思义,是一类比较`智能`的控件,这类控件同 SAP UI5 传统的控件不同,会根据 SAP UI5 应用程序连接的 OData 服务中元数据里提供的注解(`annotations`),在运行时动态生成底层的 SAP UI5 控件。...

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 2769

面向元数据的 SAP UI 中枢:namespace sap.ui.mdc 全景解析

如果把应用前端比作一条数据到交互的装配线,sap.ui.mdc 就是那条线的标准化工装:以 PropertyInfo 描述原料,以 Delegate 定义装配方法,以 p13n 与 VariantManagement 交付面向个体的最终产物;而 Condition Model、Operator 与 ValueHelp 则让输入与筛选的语义落地。与 OData V4 服务与 Fiori Elements 宏构件组合使用时,它能在保持一致体验的同时,给自由式开发者留下足够的可编排空间。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 238

关于thread.join()的一次错误理解

public static void main(String[] args) throws InterruptedException { Thread prevThread = Thread.currentThread(); for(int i = 0;i<10;i++){ Thread t = new Thread(new ThreadJoin(prevThread,i)); t.start(); prevThread = t; .

qq_36115368的博客 1075

GPU硬件架构与微架构深度解析28-稀疏计算:稀疏矩阵计算的硬件支持

本文介绍了GPU加速稀疏计算的关键技术。主要内容包括:1)稀疏数据的存储格式(COO、CSR、CSC)及其特点;2)现代GPU的稀疏计算硬件优化,如NVIDIA的稀疏张量核心和内存访问优化;3)稀疏计算在推荐系统、NLP和科学计算等领域的应用案例;4)性能优化技巧,包括存储格式选择、硬件特性利用和混合精度计算等。文章指出,专用稀疏计算技术可显著提升AI和大规模科学计算的效率,相比传统方法可获得5-10倍的性能提升。

2301_79143213的博客 111

ScheduledThreadPool中的schedule方法

延迟指定时间执行任务的记录/** * for     测试scheduled方法  延迟执行任务的方法 * get    先执行任务之后的代码,指定时间后执行任务代码(遇见新的线程,只要时间到就执行任务代码) *  * @author fangguanqgiang * * 2018年6月7日 */public class TestScheduledThreadPool { public stati...

qw92578的博客 3967

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

Java线程join()方法的用处

run() 和start() 是大家都很熟悉的两个方法。把希望并行处理的代码都放在run() 中;stat() 用于自动调用run(),这是JAVA的内在机制规定的。并且run() 的访问控制符必须是public,返回值必须是void(这种说法不准确,run() 没有返回值),run()不带参数。闲话少说,我们要讲的是join(),我们首先来看个例子:代码: /*** @author QingHe

左心室 2万+

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

hread的join()方法

本文介绍两种主线程等待子线程的实现方式,以5个子线程来说明:   1、使用Thread的join()方法,join()方法会阻塞主线程继续向下执行。   2、使用java.util.concurrent中的CountDownLatch,是一个倒数计数器。初始化时先设置一个倒数计数初始值,每调用一次countDown()方法,倒数值减一,他的await()方法会阻塞当前进程,直到倒数至0.  

u012402926的专栏 1542

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

java多线程—Thread.Join()和Thread.Sleep()

java多线程—Thread.Join()和Thread.Sleep()

阿飞云漫步 1628

java方法中 thread_java Thread方法中join()的用法

Java Thread中, join() 方法主要是让调用改方法的thread完成run方法里面的东西后, 在执行join()方法后面的代码。示例:public class ThreadTester {public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(new Thre...

weixin_33564753的博客 182

Thread.join()方法

转自https://blog.csdn.net/chenkaibsw/article/details/80912878 1.synchronized中的对象锁是线程的实例 我们可以使用同步语句块的方式对需要同步的代码进行包裹。 Object obj = new Object(); synchronized(obj){    obj.wait();    //线程在这里等待 } 此时线程会...

zx_ssln的博客 1640

java谜题--java运行时修改引用类的静态常量

谜题93:类的战争 下面这个谜题测试了你关于二进制兼容性(binary compatibility)的知识:当你改变了某个类所依赖的另外一个类时,第一个类的行为会发生什么改变呢?更特殊的是,假设你编译的是如下的2个类。第一个作为一个客户端,第二个作为一个库类,会怎么样呢: public class PrintWords { public static void main(Strin

想飞的鱼 2352

java谜题-库谜题(复写hashcode与equal方法)

谜题57:名字里有什么? 下面的程序包含了一个简单的不可变类,它表示一个名字,其main方法将一个名字置于一个集合中,并检查该集合是否确实包含了该名字。那么,这个程序到底会打印出什么呢? import java.util.*; public class Name { private String first, last; public Name(String first,

想飞的鱼 2121

java谜题--类初始化时容易死锁情况

谜题85:惰性初始化 下面这个可怜的小类实在是太懒了,甚至于都不愿意用通常的方法进行初始化,所以它求助于后台线程。这个程序会打印什么呢?每次你运行它的时候都会打印出相同的东西吗? public class Lazy { private static boolean initialized = false; static { Thread t = new T

想飞的鱼 1003

java谜题--类谜题(父类的static方法)

谜题48:我所得到的都是静态的 下面的程序对巴辛吉小鬣狗和其它狗之间的行为差异进行了建模。如果你不知道什么是巴辛吉小鬣狗,那么我告诉你,这是一种产自非洲的小型卷尾狗,它们从来都不叫唤。那么,这个程序将打印出什么呢? class Dog { public static void bark() { System.out.print("woof "); } }

想飞的鱼 948

Java 谜题 ——异常谜题

谜题40:不情愿的构造器 尽管在一个方法声明中看到一个throws子句是很常见的,但是在构造器的声明中看到一个throws子句就很少见了。下面的程序就有这样的一个声明。那么,它将打印出什么呢? public class Reluctant { private Reluctant internalInstance = new Reluctant(); public Reluc

想飞的鱼 672

java谜题--final变量

谜题72:终极危难 本谜题旨在检验当你试图隐藏一个final域时将要发生的事情。下面的程序将做些什么呢? class Jeopardy { public static final String PRIZE = "$64,000"; } public class DoubleJeopardy extends Jeopardy { public static final Str

想飞的鱼 627

InnoDB 内存结构总览:Change Buffer、AHI 与 Log Buffer

本文系统梳理InnoDB内存结构中的ChangeBuffer、Adaptive Hash Index(AHI)与LogBuffer。ChangeBuffer缓存二级索引页不在内存时的变更,减少随机IO,仅在页加载时合并;聚簇索引修改立即执行,因数据本身不可延后。AHI基于高频等值查询自动构建哈希索引,加速定位,仅支持等值操作。LogBuffer为redo log内存缓冲,配合磁盘日志实现崩溃恢复。三者均服务于性能优化,其中ChangeBuffer与AHI共享BufferPool空间,而LogBuffer独立

bksczm的博客 42
上一篇: java谜题--final变量
下一篇: ibatis 实现一对多代码
feiyu8607
博客等级 码龄18年 26粉丝 51原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值