这篇文章最初发布在Takipi博客上 -Java和Scala异常分析和性能监控。
Java线程最鲜为人知的事实和用例是什么?
有些人喜欢爬山,有些人喜欢跳伞。 我,我喜欢Java。 我喜欢它的一件事是,您永不停止学习。 您每天使用的工具通常可以为您带来全新的面貌,以及您还没有机会看到的方法和有趣的用例。 例如线程。 实际线程。 或者更好的是,Thread类本身。 当我们使用高可伸缩性系统时,并发编程永远不会停止挑战,但是现在我们将讨论一些不同的东西。
在这篇文章中,您将看到线程支持的一些鲜为人知但有用的技术和方法。 无论您是初学者,高级用户还是专家Java开发人员,都请尝试查看其中哪些已为您所知,以及哪些是新产品。 关于线程,您还有其他值得一提的地方吗? 我希望在下面的评论中听到它。 让我们开始吧。
初学者
1.线程名称
应用中的每个线程都有一个名称,即构造该线程时为其生成的简单Java字符串。 默认名称值从“ Thread-0”到“ Thread-1”,“ Thread-2”,依此类推。 现在出现了更有趣的部分–线程公开了两种可用来设置其名称的方法:
线程构造函数,这是最简单的一个:
class SuchThread extends Thread {
Public void run() {
System.out.println ("Hi Mom! " + getName());
}
}
SuchThread wow = new SuchThread("much-name");
线程名称设置器:
wow.setName(“Just another thread name”);
是的,线程名是可变的。 因此,除了在实例化它们时设置自定义名称外,我们还可以在运行时更改它。 名称字段本身设置为简单的String对象。 这意味着它最多可以包含2³¹-1个字符(Integer.MAX_VALUE)。 我说的绰绰有余。 请注意,该名称不像唯一ID,因此线程可以共享相同的名称。 另外,不要尝试将null作为名称传递,除非您希望引发异常(不过“ null”是可以的,我没有判断!)。
使用线程名称进行调试
因此,既然您已经可以访问线程名,那么遵循自己的一些命名约定可以在发生问题时使您的生活变得更加轻松。 “ Thread-6”听起来有些无情,我相信您可以想到一个更好的名字。 在处理用户请求时,将其与自分配的事务ID结合在一起,将其附加到线程的名称上,您将大大减少错误解决时间。
保留在此处的一个好习惯是确保您在应用程序的每个线程的入口点生成一个UUID,并在请求在节点,进程和线程之间传递时保持一致。 让我们看一下这个示例,某个线程池中的一个工作线程挂起了太长时间。 您运行jstack仔细看一下,然后看到以下内容:
“ pool-1-thread-1” #17 prio = 5 os_prio = 31 tid = 0x00007f9d620c9800
在Object.wait()中的nid = 0x6d03 [0x000000013ebcc000]
好的,“ pool-1-thread-1”,为什么这么严重? 让我们更好地了解您,并想出一个更合适的名称:
Thread.currentThread().setName(Context + TID + Params + current Time, ...);
” 队列处理线程,MessageID:AB5CAD,键入:
AnalyzeGraph,队列:ACTIVE_PROD,交易ID:5678956,
开始时间:30/12/2014 17:37“ #17 prio = 5 os_prio = 31 tid = 0x00007f9d620c9800
在Object.wait()中的nid = 0x6d03 [0x000000013ebcc000]
我们知道线程在阻塞时正在做什么,并且还拥有启动所有线程的事务ID。 您可以追溯步骤,重现错误,隔离并解决问题。 要了解更多有关使用jstack的酷方法的信息,您可以在这里查看这篇文章。
2.线程优先级
优先级是另一个有趣的领域线程。 线程的优先级是介于1(MIN_PRIORITY)到10(MAX_PRIORITY)之间的值,主线程的默认值为5(NORM_PRIORITY)。 每个新线程都获得其父级的优先级,因此,如果您不手动使用它,则您所有线程的优先级都可能设置为5。这也是Thread类的一个经常被忽略的字段,我们可以访问和操作它。通过方法getPriority()和setPriority() 。 无法在线程构造函数中进行设置。
谁仍然需要优先考虑?
当然,并非所有线程都是平等创建的,有些线程需要您的CPU立即关注,而其他线程只是后台任务。 优先级用于向OS线程调度程序发出信号。 在Takipi,我们开发了一个错误跟踪和分析工具,为用户处理新异常的线程将获得MAX_PRIORITY,而处理诸如报告新部署等任务的线程将获得较低的优先级。 可能希望具有更高优先级的线程从与JVM一起使用的线程调度程序中获得更多时间。 好吧,并非总是如此。
每个Java线程都会在OS级别上打开一个新的本机线程,并且您为每个平台以不同的方式将您设置的Java优先级转换为本机优先级。 在Linux上,在运行应用程序时,还必须包括“ -XX:+ UseThreadPriorities”标志,以将其考虑在内。 话虽如此,线程优先级仍然只是您提供的建议。 与本机Linux优先级相比,它们甚至没有涵盖整个值范围(1..99,以及线程范围的影响范围在-20..20之间)。 主要要点是保持自己的逻辑以确保优先级在每个线程获得的CPU时间中得到反映的重要性,但不建议仅依赖优先级。
高级
3.线程本地存储
这与我们在这里谈论的其他生物有些不同。 ThreadLocal是一个从Thread类( java.lang.ThreadLocal )实现的概念,但是为每个线程存储唯一的数据。 就像上面说的那样,它为您提供了线程本地存储,这意味着您可以创建每个线程实例唯一的变量。 与您拥有线程名称或优先级的方式类似,您可以创建自定义字段,使其看起来像是Thread类的成员。 那不是很酷吗? 但是,我们不要太激动 ,前面有一些警告。
建议使用以下两种方法之一创建ThreadLocal:作为静态变量或单例的一部分,在该变量中不必是静态的。 请注意,它位于全局范围内,但对每个能够访问它的线程都作用于本地。 这是一个ThreadLocal变量的示例,该变量持有我们自己的数据结构以便于访问:
public static class CriticalData
{
public int transactionId;
public int username;
}
public static final ThreadLocal<CriticalData> globalData =
new ThreadLocal<CriticalData>();
一旦有了ThreadLocal,就可以使用globalData.set()和globalData.get()对其进行访问 。
一定是邪恶的
不必要。 ThreadLocal变量可以保留事务ID。 当您有一个未捕获的异常使您的代码冒泡时,这可以派上用场。 一个好的做法是设置一个UncaughtExceptionHandler ,我们也可以通过Thread类获得它,但必须自己实现。 一旦我们到达那个阶段,关于实际上是什么使我们到达那里的提示就不多了。 我们剩下的是Thread对象,当堆栈框架关闭时,无法访问将我们带到那里的任何变量。 在我们的UncaughtExceptionHandler中,随着线程的最后呼吸,ThreadLocal几乎是我们剩下的仅有的事情之一。
我们可以本着以下精神做一些事情:
System.err.println("Transaction ID " + globalData.get().transactionId);
就像这样,我们为错误添加了一些有价值的上下文。 使用ThreadLocal的一种更有创意的方法是通过分配指定的内存块,以供工作线程反复使用作为缓冲区。 当然,这可能会很有用,具体取决于您在内存的哪一侧与CPU开销之间的权衡。 也就是说,要注意的是滥用我们的内存空间。 只要特定的线程处于活动状态,它就存在于特定线程中,除非将其释放或线程死亡,否则不会被垃圾回收。 因此,在使用它时最好小心并保持简单。
4.用户线程和守护程序线程
回到我们的线程类。 我们应用中的每个线程都会收到“用户”或“守护程序”状态。 换句话说,前景或后台线程。 默认情况下,主线程是用户线程,每个新线程都获得创建它的线程的状态。 因此,如果将线程设置为守护程序,则它创建的所有线程也将被标记为守护程序。 当您的应用程序中剩下的仅处于运行状态的线程处于守护程序状态时,该过程将关闭。 要进行测试,检查和更改线程状态,我们有布尔值.setDaemon(true)和.isDaemon()方法。
您何时设置Daemon线程?
当线程对线程的结束不是很关键时,应将其状态更改为守护进程,以便进程可以关闭。 它消除了正确关闭线程,立即停止所有操作的麻烦,让我们Swift结束了它。 另一方面,当有一个线程运行的操作必须正确结束时,否则将发生不良情况,请确保将其设置为用户线程。 关键事务可以是例如数据库条目或完成不间断的更新。
专家
5. Java处理器亲和力
这部分使我们更接近代码与金属相遇的硬件。 处理器关联允许您将线程或进程绑定到特定的CPU内核。 这意味着无论何时执行该特定线程,它都将专门在一个特定内核上运行。 通常情况下,操作系统线程调度程序将根据其自己的逻辑担当此角色,可能会考虑我们前面提到的线程优先级。
讨价还价的筹码是CPU缓存。 如果一个线程只在一个特定的内核上运行,则更有可能享受将所有数据准备好在缓存上的乐趣。 当数据已经存在时,无需重新加载它。 您节省的微秒数可以被更好地利用,并且代码实际上将在该时间运行,从而更好地利用分配的CPU时间。 尽管确实在操作系统级别进行了一些优化,并且硬件体系结构当然也起着重要作用,但是使用亲和力可以消除线程切换内核的机会。
由于这里有许多因素在起作用,因此确定处理器亲和力将如何影响您的吞吐量的最佳方法是接受测试的习惯。 尽管可能并不总是会好得多,但是您可能会遇到的好处之一就是吞吐量稳定。 亲和力策略可以降低到手术水平,具体取决于获得的收益。 高频交易行业将是这类事情最重要的地方之一。
测试处理器亲和力
Java没有对处理器相似性的本机支持,但这当然还没有结束。 在Linux上,我们可以使用tasket命令设置进程亲和力。 假设我们正在运行一个Java进程,并且希望将其固定到特定的CPU:
tasket -c 1“ java AboutToBePinned”
或者,如果它已经在运行:
任务集-c 1 <PID>
现在,要进入线程级别,我们需要插入一些新代码。 幸运的是,有一个开源库可以帮助我们做到这一点: Java-Thread-Affinity 。 由OpenHFT的Peter Lawrey撰写,这可能是最简单的方法。 让我们看一个固定线程的简单例子,更多的信息可以在该库的GitHub存储库中找到:
AffinityLock al = AffinityLock.acquireLock();
就是这样。 GitHub上提供了用于获取锁的更高级选项,其中考虑了选择特定内核的不同策略。
结论
我们已经看到了5种查看线程的方法:线程名称,线程本地存储,优先级,守护程序线程和相似性。 希望这对您每天处理的事情有所帮助,并很高兴听到您的评论! 还有哪些其他线程处理方法可以适用?
尝试使用新的开发人员工具怎么样? Takipi会检测生产中的所有错误,并像发生错误时一样显示变量值–立即部署并获得免费的T恤 。
Java / Scala开发人员? Takipi在代码中检测到所有异常和错误,并告诉您它们为什么发生。 只需1分钟即可安装: 尝试Takipi 。
翻译自: https://jaxenter.com/magic-tricks-java-threads-114519.html