자바 스레드의 덜 알려진 기술과 용도는 무엇입니까?
사람마다 무와 배추를 좋아하는 취향이 있습니다. 내가 자바를 좋아하는 것처럼. 배움에는 끝이 없습니다. 이것이 제가 배움을 좋아하는 이유 중 하나입니다. 일상 업무에서 사용하는 도구에는 일반적으로 특정 방법이나 흥미로운 용도 등 이전에 이해하지 못했던 것이 있습니다. 예를 들어 스레드. 맞습니다. 스레드입니다. 또는 오히려 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");
스레드 이름 설정 방법:
wow.setName(“Just another thread 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上都有详细的说明。