Java线程有哪些不太为人所知的技巧与用法?
萝卜白菜各有所爱。像我就喜欢Java。学无止境,这也是我喜欢它的一个原因。日常工作中你所用到的工具,通常都有些你从来没有了解过的东西,比方说某个方法或者是一些有趣的用法。比如说线程。没错,就是线程。或者确切说是Thread这个类。当我们在构建高可扩展性系统的时候,通常会面临各种各样的并发编程的问题,不过我们现在所要讲的可能会略有不同。
从本文中你将会看到线程提供的一些不太常用的方法及技术。不管你是初学者还是高级用户或者是Java专家,希望都能看一下哪些是你已经知道的,而哪些是刚了解的。如果你认为关于线程还有什么值得分享给大家的,希望能在下面积极回复。那我们就先开始吧。
初学者
1.线程名
程序中的每个线程都有一个名字,创建线程的时候会给它分配一个简单的Java字符串来作为线程名。默认的名字是”Thread-0″, “Thread-1″, “Thread-2″等等。现在有趣的事情来了——Thread提供了两种方式来设置线程名:
线程构造函数,下面是最简单的一个实现:
class SuchThread extends Thread {
Public void run() {
System.out.println ("Hi Mom! " + getName());
}
}
SuchThread wow = new SuchThread("much-name");
|
线程名setter方法:
wow.setName(“Just another thread name”);
没错,线程名是可变的。因此我们可以在运行时修改它的名字,而不用在初始化的时候就指定好。name字段其实就是一个简单的字符串对象。也就是说它能达到231-1个字符那么长(Integer.MAX_VALUE)。这足够用了。注意这个名字并不是一个唯一性的标识,因此不同的线程也可以拥有同样的线程名。还有一点就是,不要把null用作线程名,否则会抛出异常(当然了,”null”还是可以的)。
使用线程名来调试问题
既然可以设置线程名,那么如果遵循一定的命名规则的话,出了问题的时候排查起来就能更容易一些。“Thread-6″这样的名字看起来就太没心没肺了,肯定有比它更好的名字。在处理用户请求的时候,可以将事务ID追加到线程名后面,这样能显著减少你排查问题的时间。
“pool-1-thread-1″ #17 prio=5 os_prio=31 tid=0x00007f9d620c9800
nid=0x6d03 in Object.wait() [0x000000013ebcc000]
“pool-1-thread-1″,这也太严肃了吧。我们来看下这是什么情况,给它起一个好点的名字:
Thread.currentThread().setName(Context + TID + Params + current Time, ...);
现在我们再来运行下jstack,情况便豁然开朗了:
”Queue Processing Thread, MessageID: AB5CAD, type:
AnalyzeGraph, queue: ACTIVE_PROD, Transaction_ID: 5678956,
Start Time: 30/12/2014 17:37″ #17 prio=5 os_prio=31 tid=0x00007f9d620c9800
nid=0x6d03 in Object.wait() [0x000000013ebcc000]
如果我们能知道线程在做什么,这样当它出问题的时候,至少可以拿到事务ID来开始排查。你可以回溯这个问题,复现它,然后定位问题并搞定它。如果你想知道jstack有什么给力的用法,可以看下这篇文章。
2. 线程优先级
线程还有一个有意思的属性就是它的优先级。线程的优先级介于1 (MINPRIORITY)到10 (MAXPRIORITY)之间,主线程默认是5(NORM_PRIORITY)。每个新线程都默认继承父线程的优先级,因此如果你没有设置过的话,所有线程的优先级都是5。这个是通常被忽视的属性,我们可以通过getPriority()与setPriority()方法来获取及修改它的值。线程的构造函数里是没有这个功能的。
什么地方会用到优先级?
当然并不是所有的线程都是平等的,有的线程需要立即引起CPU的重视,而有些线程则只是后台任务而已。优先级就是用来把这些告诉给操作系统的线程调度器的。在Takipi中,这是我们开发的一错误跟踪及排查的工具,负责处理用户异常的线程的优先级是MAX_PRIORITY,而那些只是在上报新的部署情况的线程,它们的优先级就要低一些。你可能会觉得优先级高的线程从JVM的线程调度器那得到的时间会多一些。但其实并都是这样的。
在操作系统层面,每一个新线程都会对应一个本地线程,你所设置的Java线程的优先级会被转化成本地线程的优先级,这个在各个平台上是不一样的。在Linux上,你可以打开“-XX:+UseThreadPriorities”选项来启用这项功能。正如前面所说的,线程优先级只是你所提供的一个建议。和Linux本地的优先级相比,Java线程的优先级并不能覆盖全所有的级别(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类本身就支持的,但是你得自己去实现一下这个接口。一旦执行到了UncaughtExceptionHandler里,就几乎没有任何线索能够知道到底发生了什么事情了。这会儿你能获取到的就只有Thread对象,之前导致异常发生的所有变量都无法再访问了,因为那些栈帧都已经被弹出了。一旦到了UncaughtExceptionHandler里,这个线程就只剩下最后一口气了,唯一能抓住的最后一根稻草就是ThreadLocal。
我们来试下这么做:
System.err.println("Transaction ID " + globalData.get().transactionId);
我们可以将一些与错误相关的有价值的上下文信息给存储到里面添。ThreadLocal还有一个更有创意的用法,就是用它来分配一块特定的内存,这样工作线程可以把它当作缓存来不停地使用。当然了,这有没有用得看你在CPU和内存之间是怎么权衡的了。没错,ThreadLocal需要注意的就是会造成内存空间的浪费。只要线程还活着,那么它就会一直存在,除非你主动释放否则它是不会被回收的。因此如果使用它的话你最好注意一下,尽量保持简单。
4. 用户线程及守护线程
我们再回到Thread类。程序中的每个线程都会有一个状态,要么是用户状态,要么是守护状态。换句话说,要么是前台线程要么是后台线程。主线程默认是用户线程,每个新线程都会从创建它的线程中继承线程状态。因此如果你把一个线程设置成守护线程,那么它所创建的所有线程都会被标记成守护线程。如果程序中的所有线程都是守护线程的话,那么这个进程便会终止。我们可以通过Boolean .setDaemon(true)和.isDaemon()方法来查看及设置线程状态。
什么时候会用到守护线程?
如果进程不必等到某个线程结束才能终止,那么这个线程就可以设置成守护线程。这省掉了正常关闭线程的那些麻烦事,可以立即将线程结束掉。换个角度来说,如果一个正在执行某个操作的线程必须要正确地关闭掉否则就会出现不好的后果的话,那么这个线程就应该是用户线程。通常都是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。
专家级
5. 处理器亲和性(Processor Affinity)
这里要讲的会更靠近硬件,也就是说,当软件遇上了硬件。处理器亲和性使得你能够将线程或者进程绑定到特定的CPU核上。这意味着只要是某个特定的线程,它就肯定只会在某个特定的CPU核上执行。通常来讲如何绑定是由操作系统的线程调度器根据它自己的逻辑来决定的,它很可能会将我们前面提到的线程优先级也一并考虑进来。
这么做的好处在于CPU缓存。如果某个线程只会在某个核上运行,那么它的数据恰好在缓存里的概率就大大提高了。如果数据正好就在CPU缓存里,那么就没有必要重新再从内存里加载了。你所节省的这几毫秒时间就能用在刀刃上,在这段时间里代码可以马上开始执行,也就能更好地利用所分配给它的CPU时间。当然了,操作系统层面可能会存在某种优化,硬件架构当然也是个很重要的因素,但利用了处理器的亲和性至少能够减小线程切换CPU的机率。
由于这里掺杂着多种因素,处理器亲和性到底对吞吐量有多大的影响,最好还是通过测试的方式来进行证明。也许这个方法并不是总能显著地提升性能,但至少有一个好处就是吞吐量会相对稳定。亲和策略可以细化到非常细的粒度上,这取决于你具体想要什么。高频交易行业便是这一策略最能大显身手的场景之一。
处理器亲和性的测试
Java对处理器的亲和性并没有原生的支持,当然了,故事也还没有就此结束。在Linux上,我们可以通过taskset命令来设置进程的亲和性。假设我们现在有一个Java进程在运行,而我们希望将它绑定到某个特定的CPU上:
taskset -c 1 “java AboutToBePinned”
如果是一个已经在运行了的进程:
taskset -c 1 <PID>
要想深入到线程级别还得再加些代码才行。所幸的是,有一个开源库能完成这样的功能:Java-Thread-Affinity。这个库是由OpenHFT的Peter Lawrey开发的,实现这一功能最简单直接的方式应该就是使用这个库了。我们通过一个例子来快速看下如何绑定某个线程,关于该库的更多细节请参考它在Github上的文档:
AffinityLock al = AffinityLock.acquireLock();
这样就可以了。关于获取锁的一些更高级的选项——比如说根据不同的策略来选择CPU——在Github上都有详细的说明。
相关推荐
电子书相关:包含4个有关JAVA线程的电子书(几乎涵盖全部有关线程的书籍) OReilly.Java.Threads.3rd.Edition.Sep.2004.eBook-DDU Java Thread Programming (Sams) java线程第二版中英文 java线程第二版中英文 ...
通过分析并实践`threadTest`案例,我们可以深入理解Java多线程的原理和使用技巧,为编写高效并发程序打下坚实基础。同时,也要注意多线程编程中的死锁、活锁和饥饿等问题,合理设计线程间的交互,避免出现不可预期的...
### Java线程高级使用知识点详解 #### 一、线程基础概述 - **定义与特点**:线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。Java是首个在语言级别明确支持线程特性的...
在Java出现以前,似乎人人都在谈论线程,却很少有人使用它。用线程编程是技巧性很强的且不可移植。 而在Java中却完全不同。Java的线程工具易于使用,并且像Java中的其他东西一样可以在不同的平台之间移植。这是一件...
Java线程有五种基本状态:新建(New)、可运行(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Terminated)。这些状态之间的转换是通过调用特定的方法,如start()、sleep()、join()、wait()、notify()等...
在本文中,我们将介绍如何使用Java实现两个线程同时运行的案例,涉及Java多线程相关操作与使用技巧。 Java多线程编程的优点包括: 1. 提高程序的执行效率:多线程编程可以将复杂的任务分解成多个小任务,每个任务...
Java使用Callable和Future创建线程操作示例主要介绍了Java使用Callable和Future创建线程操作,结合实例形式分析了java使用Callable接口和Future类创建线程的相关操作技巧与注意事项。 首先,Java 5开始,Java提供了...
Java线程与多线程是Java开发中的核心概念,对于任何Java开发者来说,理解和掌握这部分内容至关重要。在Java中,线程是程序执行的基本单元,它允许程序同时执行多个任务,提高了程序的运行效率和响应速度。本教程将...
在Java编程中,多线程是一项关键特性,它允许程序同时执行多个任务,极大地提高了效率。本实验"java多线程之赛马程序实验8多线程练习下载进度"聚焦于如何利用多线程来模拟实际场景中的下载进度显示。在这一过程中,...
本书的新版本展示了如何利用Java线程工具的全部优势,并介绍了JDK 2线程接口中的最新变化。你将学习如何使用线程来提高效率,如何有效地使用它们,以及如何避免常见的错误。本书讨论了死锁、竞态条件以及饥饿等问题...
通过本教程的学习,您不仅能够理解Java线程的基本概念,还能掌握多线程编程的核心技巧,为进一步探索Java并发编程打下坚实的基础。随着实践经验的积累,您将能够更加自如地利用多线程来解决实际问题,提升程序性能和...
5. **线程优先级**:Java线程有10个优先级,从`Thread.MIN_PRIORITY`(1)到`Thread.MAX_PRIORITY`(10),默认优先级是`Thread.NORM_PRIORITY`(5)。但线程优先级并不保证执行顺序,具体执行顺序依赖于JVM和操作...
这些案例涵盖了从简单的并发任务到复杂的多线程设计模式,如生产者消费者模型、线程池的使用以及线程间的通信技巧。通过这些实战练习,读者能够更好地理解多线程编程中的挑战和解决方案。 本书还涉及到了Java并发...
在Java编程领域,多线程是一项重要的技术,它允许程序同时执行多个任务,极大地提高了程序的效率和响应性。在“JAVA编写的多线程小弹球测试”项目中,开发者利用Java语言创建了一个生动有趣的多线程应用,即一个模拟...
Java多线程程序设计是Java开发中的重要概念,它允许在一个程序中同时执行多个任务,提高了应用程序的效率和响应性。在Java中,有两种主要的方式来实现多线程:继承Thread类和实现Runnable接口。 首先,我们要理解多...
本文介绍了在Java多线程环境下减少内存占用量的一些关键策略,包括线程生命周期管理、对象生命周期设计、同步机制选择、线程池的使用和线程数量控制。同时,代码的异常处理和JVM参数调优也是提升多线程应用性能的...
Java线程是并发编程的核心部分,它允许程序在同一时间执行多个任务,从而提高了系统资源的利用率和程序的响应速度。本专题将深入探讨Java中的线程技术,包括线程的创建、状态管理、同步机制以及相关API的使用。 在...
### Java多线程技巧详解:join方法的精妙运用 #### 概述 在Java编程中,多线程是处理并发任务的关键技术之一,能够显著提升应用程序的性能和响应速度。然而,多线程的引入也带来了同步和数据一致性的问题。在本文...
总而言之,Java多线程的学习是一个逐步深入了解并不断实践的过程。对于初学者而言,要通过理论学习与代码实践相结合的方式,逐步掌握多线程编程的核心概念和实际操作技巧,从而在实际应用中灵活运用。在学习的过程中...