Java线程有哪些不太为人所知的技巧与用法?

转载出处:Java线程的5个使用技巧
英文原文连接html

萝卜白菜各有所爱。像我就喜欢Java。学无止境,这也是我喜欢它的一个缘由。平常工做中你所用到的工具,一般都有些你历来没有了解过的东西,比方说某个方法或者是一些有趣的用法。好比说线程。没错,就是线程。或者确切说是Thread这个类。当咱们在构建高可扩展性系统的时候,一般会面临各类各样的并发编程的问题,不过咱们如今所要讲的可能会略有不一样。java

从本文中你将会看到线程提供的一些不太经常使用的方法及技术。无论你是初学者仍是高级用户或者是Java专家,但愿都能看一下哪些是你已经知道的,而哪些是刚了解的。若是你认为关于线程还有什么值得分享给你们的,但愿能在下面积极回复。那咱们就先开始吧。linux

初学者

1.线程名

程序中的每一个线程都有一个名字,建立线程的时候会给它分配一个简单的Java字符串来做为线程名。默认的名字是"Thread-0", "Thread-1", "Thread-2"等等。如今有趣的事情来了——Thread提供了两种方式来设置线程名:git

  1. 线程构造函数,下面是最简单的一个实现:github

    class SuchThread extends Thread {
    
        Public void run() {
            System.out.println ("Hi Mom! " + getName());
        }
    
    }
    
    SuchThread wow = new SuchThread("much-name");
  2. 线程名setter方法:数据库

    wow.setName(“Just another thread name”);

没错,线程名是可变的。所以咱们能够在运行时修改它的名字,而不用在初始化的时候就指定好。name字段其实就是一个简单的字符串对象。也就是说它能达到2³¹-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 (MIN_PRIORITY)到10 (MAX_PRIORITY)之间,主线程默认是5(NORM_PRIORITY)。每一个新线程都默认继承父线程的优先级,所以若是你没有设置过的话,全部线程的优先级都是5。这个是一般被忽视的属性,咱们能够经过getPriority()与setPriority()方法来获取及修改它的值。线程的构造函数里是没有这个功能的。

什么地方会用到优先级?

固然并非全部的线程都是平等的,有的线程须要当即引发CPU的重视,而有些线程则只是后台任务而已。优先级就是用来把这些告诉给操做系统的线程调度器的。在Takipi中,这是咱们开发的一错误跟踪及排查的工具,负责处理用户异常的线程的优先级是MAX_PRIORITY,而那些只是在上报新的部署状况的线程,它们的优先级就要低一些。你可能会以为优先级高的线程从JVM的线程调度器那获得的时间会多一些。但其实并都是这样的。

在操做系统层面,每个新线程都会对应一个本地线程,你所设置的Java线程的优先级会被转化成本地线程的优先级,这个在各个平台上是不同的。在Linux上,你能够打开“-XX:+UseThreadPriorities”选项来启用这项功能。正如前面所说的,线程优先级只是你所提供的一个建议。和Linux本地的优先级相比,Java线程的优先级并不能覆盖全全部的级别(Linux共有1到99个优先级,线程的优先级在是-20到20之间)。最大的好处就是你所设定的优先级能在每一个线程得到的CPU时间上有所体现,不过彻底依赖于线程优先级的作法是不推荐的。

image

进阶篇

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上都有详细的说明。

结论

本文咱们介绍了关于线程的5点知识:线程名,线程本地存储,优先级,守护线程以及处理器亲和性。但愿这能为你平常工做中所用到的内容打开一扇新的窗户,期待大家的反馈!还有什么有关线程处理的方法能够分享给你们的吗,请不吝赐教。

相关文章
相关标签/搜索