Heim  >  Artikel  >  Java  >  5 Tipps zur Verwendung von Java-Threads

5 Tipps zur Verwendung von Java-Threads

伊谢尔伦
伊谢尔伦Original
2016-11-26 11:16:501166Durchsuche

Was sind die weniger bekannten Techniken und Verwendungsmöglichkeiten von Java-Threads?

Jeder hat seine eigenen Vorlieben für Radieschen und Kohl. Als ob ich Java mag. Das Lernen hat kein Ende, und das ist einer der Gründe, warum ich es liebe. Die Werkzeuge, die Sie in Ihrer täglichen Arbeit verwenden, haben normalerweise etwas, das Sie noch nie zuvor verstanden haben, beispielsweise eine bestimmte Methode oder einige interessante Verwendungsmöglichkeiten. Zum Beispiel Threads. Stimmt, es ist ein Thread. Oder besser gesagt die Thread-Klasse. Wenn wir hochskalierbare Systeme bauen, stehen wir normalerweise vor einer Vielzahl gleichzeitiger Programmierprobleme, aber das, worüber wir jetzt sprechen werden, kann etwas anders sein.

In diesem Artikel sehen Sie einige weniger häufig verwendete Methoden und Techniken, die von Threads bereitgestellt werden. Egal, ob Sie Anfänger, fortgeschrittener Benutzer oder Java-Experte sind, ich hoffe, Sie können einen Blick auf das werfen, was Sie bereits wissen und worüber Sie gerade lernen. Wenn Sie der Meinung sind, dass es in diesem Thread noch etwas gibt, das es wert ist, mitgeteilt zu werden, hoffe ich, dass Sie unten positiv antworten können. Also fangen wir an.

Anfänger

1. Thread-Name

Jeder Thread im Programm hat einen Namen. Beim Erstellen eines Threads wird ihm ein einfacher Java-String als Thread-Name zugewiesen . Die Standardnamen sind „Thread-0“, „Thread-1“, „Thread-2“ usw. Hier kommt nun das Interessante: Thread bietet zwei Möglichkeiten zum Festlegen des Thread-Namens:

Thread-Konstruktor, das Folgende ist die einfachste Implementierung:

class SuchThread extends Thread {
 
    Public void run() {
        System.out.println ("Hi Mom! " + getName());
    }
 
}
 
SuchThread wow = new SuchThread("much-name");

Methode zum Festlegen des Thread-Namens:

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

Ja, der Threadname ist variabel. Daher können wir seinen Namen zur Laufzeit ändern, anstatt ihn während der Initialisierung anzugeben. Das Namensfeld ist eigentlich ein einfaches String-Objekt. Mit anderen Worten, es kann bis zu 2³¹-1 Zeichen lang sein (Integer.MAX_VALUE). Das ist genug. Beachten Sie, dass dieser Name keine eindeutige Kennung ist, sodass verschiedene Threads denselben Thread-Namen haben können. Ein weiterer Punkt ist, dass Sie nicht null als Thread-Namen verwenden sollten, da sonst eine Ausnahme ausgelöst wird (natürlich ist „null“ immer noch in Ordnung).

Verwenden Sie Thread-Namen zum Debuggen von Problemen

Da Sie Thread-Namen festlegen können und bestimmte Benennungsregeln befolgen, ist es einfacher, Probleme zu beheben, wenn sie auftreten. Ein Name wie „Thread-6“ wirkt so herzlos, dass es einen besseren Namen geben muss. Bei der Verarbeitung von Benutzeranfragen können Sie die Transaktions-ID an den Thread-Namen anhängen, was Ihre Zeit zur Fehlerbehebung erheblich verkürzen kann.

“pool-1-thread-1″ #17 prio=5 os_prio=31 tid=0x00007f9d620c9800
nid=0x6d03 in Object.wait() [0x000000013ebcc000]

„pool-1-thread-1“, das ist zu ernst. Werfen wir einen Blick auf das, was vor sich geht, und geben wir ihm einen besseren Namen:

Thread.currentThread().setName(Context + TID + Params + current Time, ...);

Lassen Sie uns nun jstack erneut ausführen, und die Situation wird plötzlich klarer:

”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]

Wenn wir wissen, was der Thread tut, können wir, wenn etwas schief geht, zumindest die Transaktions-ID erhalten, um mit der Fehlerbehebung zu beginnen. Sie können das Problem zurückverfolgen, reproduzieren, lokalisieren und beheben. Wenn Sie wissen möchten, welche leistungsstarken Funktionen Jstack bietet, können Sie diesen Artikel lesen.

 2. Thread-Priorität

Ein weiteres interessantes Attribut eines Threads ist seine Priorität. Die Priorität des Threads reicht von 1 (MINPRIORITY) bis 10 (MAXPRIORITY), und der Hauptthread ist standardmäßig auf 5 (NORM_PRIORITY) eingestellt. Jeder neue Thread erbt standardmäßig die Priorität des übergeordneten Threads. Wenn Sie sie also nicht festgelegt haben, haben alle Threads die Priorität 5. Dies ist eine Eigenschaft, die normalerweise ignoriert wird. Wir können ihren Wert über die Methoden getPriority() und setPriority() abrufen und ändern. Im Thread-Konstruktor gibt es keine solche Funktion.

Wo wird Priorität verwendet?

Natürlich sind nicht alle Threads gleich. Einige Threads erfordern sofortige Aufmerksamkeit der CPU, während andere Threads nur Hintergrundaufgaben sind. Mithilfe der Priorität werden diese dem Thread-Scheduler des Betriebssystems mitgeteilt. In Takipi, einem von uns entwickelten Fehlerverfolgungs- und Fehlerbehebungstool, ist die Priorität des Threads, der für die Behandlung von Benutzerausnahmen verantwortlich ist, MAX_PRIORITY, und die Threads, die nur neue Bereitstellungssituationen melden, haben eine niedrigere Priorität. Sie denken vielleicht, dass Threads mit höherer Priorität mehr Zeit vom Thread-Scheduler der JVM erhalten. Dies ist jedoch nicht immer der Fall.

  在操作系统层面,每一个新线程都会对应一个本地线程,你所设置的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上都有详细的说明。

   

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn