四. Java并发基础知识(二)

Java自诞生开始就明智地选择了内置对多线程的支持,这使得Java语言相比同一时期的其他语言具有明显的优势。线程作为操作系统调度的最小单元,多个线程能够同时执行,这将显著提升程序性能,在多核环境中表现得更加明显。但是,过多的创建线程和对线程的不当管理也容易造成问题。本章将着重介绍Java并发编程的基础知识,从启动一个线程到线程间不同的通信方式,最后通过简单的线程池示例以及应用来串联本章介绍的内容。

线程简介

什么是线程

现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统为它创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程中可以创建多个线程,这些线程拥有自己的计数器,堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉这些线程在同时执行。

一个Java程序从main()方法开始执行,然后按照既定的逻辑代码执行,看似没有其他线程参与,但实际上Java程序天生就是多线程程序,因为执行main()方法的是一个名称为main的线程。下面使用JMX来查看一个普通的Java程序包含哪些线程。

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class MultiThread {
    public static void main(String[] args) {
        //获取Jav线程管理MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        //不需要获取同步的monitor和synchronizer信息,仅获取线程和线程堆栈信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        for(ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
        }
    }
}

输出如下所示(输出内容可能不同):

可以看出,一个Java程序的运行并不仅仅是main()方法的运行,而是main线程和多个其他线程的同时运行。

线程优先级

现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完就会发生线程调度,并等待下次分配。线程分配到的时间片多少也决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。

在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1-10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级为5,优先级高的线程分配时间片的数量要多于优先级低的线程。在设置优先级时,针对频繁阻塞(休眠或IO操作)的线程需要设置较高的优先级,而偏重计算的线程则设置较低的优先级,确保处理器不会被独占。在不同JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class Priority {
    private static volatile boolean notStart = true;
    private static volatile boolean notEnd = true;

    public static void main(String[] args) throws Exception {
        List<Job> jobs = new ArrayList<>(16);
        for(int i = 0; i < 10; ++i) {
            int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
            Job job = new Job(priority);
            jobs.add(job);
            Thread thread = new Thread(job, "Thread:" + i);
            thread.setPriority(priority);
            thread.start();
        }
        notStart = false;
        TimeUnit.SECONDS.sleep(10);
        notEnd = false;

        for(Job job : jobs) {
            System.out.println("Job Priority: " + job.priority + ", Count: " + job.jobCount);
        }
    }

    private static class Job implements Runnable {
        private int priority;
        private long jobCount;

        Job(int priority) {
            this.priority = priority;
        }

        @Override
        public void run() {
            while (notStart) {
                Thread.yield();
            }
            while (notEnd) {
                Thread.yield();
                jobCount++;
            }
        }
    }
}

运行该实例,输出如下:

Job Priority: 1, Count: 328139
Job Priority: 1, Count: 328468
Job Priority: 1, Count: 328122
Job Priority: 1, Count: 328002
Job Priority: 1, Count: 328111
Job Priority: 10, Count: 2763533
Job Priority: 10, Count: 2776199
Job Priority: 10, Count: 2770687
Job Priority: 10, Count: 2776199
Job Priority: 10, Count: 2765352

此结果是在win10, jdk10下的输出,可以看出优先级生效了。
Job Priority: 1, Count: 3976833
Job Priority: 1, Count: 4058807
Job Priority: 1, Count: 5225471
Job Priority: 1, Count: 4941778
Job Priority: 1, Count: 4190309
Job Priority: 10, Count: 4257979
Job Priority: 10, Count: 3897556
Job Priority: 10, Count: 4107731
Job Priority: 10, Count: 4068744
Job Priority: 10, Count: 4164109

此结果是在debian9, jdk8下的输出,可以看出优先级没有生效。

从上面两个输出来看,程序正确性不能依赖线程的优先级高低。

线程的状态

Java线程在运行的生命周期中可能处于如下所示的6种不同状态,在给定的一个时刻,线程只能处理其中的一个状态:

状态名称 说明
NEW 初始状态,线程被构建,但还没有调用start()方法
RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做一些特定动作(通知或中断)
TIME_WAITING 超时等待状态,不同于WAITING,他可以在指定时间自行返回
TERMINATED 终止状态,表示当前线程已经执行完毕

下面我们使用jstack(位于JDK安装目录的bin目录下)工具,查看示例代码运行时的线程信息,更加深入了解线程状态。

import util.SleepUtils;

public class ThreadState {
    public static void main(String[] args) {
        new Thread(new TimeWaiting(), "TimeWaitingThread").start();
        new Thread(new Waiting(), "WaitingThread").start();
        new Thread(new Blocked(), "BlockedThread-1").start();
        new Thread(new Blocked(), "BlockedThread-2").start();
    }

    private static class TimeWaiting implements Runnable {
        @Override
        public void run() {
            //该线程不断进行睡眠
            while (true) {
                SleepUtils.sleep(100);
            }
        }
    }

    private static class Waiting implements Runnable {
        @Override
        public void run() {
            while (true) {
                //该线程在Waiting.class上等待
                synchronized (Waiting.class) {
                    try {
                        Waiting.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    private static class Blocked implements Runnable {
        @Override
        public void run() {
            //该线程在Blocked.class上加锁后,不会释放锁
            synchronized (Blocked.class) {
                while (true) {
                    SleepUtils.sleep(100);
                }
            }
        }
    }
}

------------------------------------------------------------------------

package util;

import java.util.concurrent.TimeUnit;

public class SleepUtils {
    public static void sleep(long seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行该实例,打开终端,输入jps,输出如下:

PS C:\Program Files\Java\jdk-10.0.2\bin> jps
29120 Launcher
42148
49112 Jps
50220 ThreadState

可以看到运行实例对应的进程ID为50220,接着键入 jstack 50220(此处进程ID需要和你自己输入jps得到的ID一致),部分输出如下:

"TimeWaitingThread" #13 prio=5 os_prio=0 tid=0x00000242c87c9000 nid=0x27d4 waiting on condition [0x000000e411efe000]
java.lang.Thread.State: TIMED_WAITING (sleeping)

"WaitingThread" #14 prio=5 os_prio=0 tid=0x00000242c87cb800 nid=0x7d34 in Object.wait() [0x000000e411fff000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(java.base@10.0.2/Native Method)
- waiting on <0x00000006d11a0a88> (a java.lang.Class for p4.ThreadState$Waiting)

"BlockedThread-1" #15 prio=5 os_prio=0 tid=0x00000242c87cc000 nid=0xc398 waiting on condition [0x000000e4120fe000]
java.lang.Thread.State: TIMED_WAITING (sleeping)

"BlockedThread-2" #16 prio=5 os_prio=0 tid=0x00000242c87cd000 nid=0xa124 waiting for monitor entry [0x000000e4121ff000]
java.lang.Thread.State: BLOCKED (on object monitor)
- waiting to lock <0x00000006d11a1db0> (a java.lang.Class for p4.ThreadState$Blocked)

从上个例子中我们可以了解到,线程在自身的生命周期中并不是固定的处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java线程状态变迁如下所示:

Daemon线程

Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工具。这意味着,当Java虚拟机不存在非Daemon线程时,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。

Daemon属性需要在启动线程前设置,不能在启动后设置。

Daemon线程被用作完成支持性工作,但在Java虚拟机退出时Daemon线程中的finally块不一定会执行。

public class DaemonTest {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            try {
                SleepUtils.sleep(5);
            } finally {
                System.out.println("DaemonThread finally run.");
            }
        }, "DaemonThread");
        thread.setDaemon(true);
        thread.start();
    }
}

运行此程序,可以看到没有任何输出。main线程(非Daemon线程)在启动了DaemonRunner线程后main方法执行完毕,main线程终止,因此Java虚拟机中已没有非Daemon线程,虚拟机退出。Java虚拟机中所有Daemon线程都需要立即终止,因此DaemonRunner立即停止,finally块并未执行。

在构建Daemon线程时,不能靠finally块中的内容来确保执行关闭或清理资源的逻辑。

线程间通信

线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步执行,直到终止。但是,每个运行中的线程,如果仅仅孤立地运行,那么没有什么太大的价值,如果多个线程能够相互配合完成工作,这将带来巨大的价值。

volatile和synchronized关键字

Java支持多个线程同时访问一个对象或对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及其成员变量在共享内存中分配内存,但是每个执行的线程还是可以拥有一份拷贝,加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。

关键词volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对他的改变必须同步刷新回共享内存,他能保证所有线程对变量访问的可见性。但是,过多的使用volatille是不必要的,因为它会降低程序执行的效率。

关键词synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或同步块中,他保证了线程对变量访问的排他性和可见性。

如下的代码使用了同步块和同步方法,通过javap工具查看生成的class文件信息来分析synchronized关键字的实现细节。

public class Synchronized {
    public static void main(String[] args) {
        synchronized (Synchronized.class) {
        }
        m();
    }

    private static synchronized void m() {

    }
}

键入javap -v Synchronized.class

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class p4/Synchronized
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: invokestatic #3 // Method m:()V
18: return

在上面的class信息中,对于同步块实现使用了monitorentermonitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED(上面未出现)来完成的。无论采取哪种方式,其本质是对一个对象的监视器进行获取,这个获取的过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。

任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器的线程将会阻塞在同步块和同步方法入口处,进入BLOCKED状态。

下图描述了对象,监视器,同步队列和执行线程之间的关系。

从图中可以看出,任意线程对Object(Object由synchronized保护)的访问,首先要获取Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱线程(获得了锁的线程)释放了锁,则该操作会唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

等待/通知机制

一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模式隔离了“做什么”和“怎么做”,在功能层面实现了解耦,体系结构上具备了良好了伸缩性,但在Java语言中如何实现类似的功能呢?

简单的方法是让消费者线程不断循环检查变量是否符合预期,如下面代码所示,在while循环中设置不满足的条件,如果条件满足则退出while循环,从而完成消费者的工作。

while (value != desire) {
    Thread.sleep(1000);
}
doSomething();

上面这段伪代码在条件不满足时就睡眠一段时间,这样做的目的是防止过快地“无效”尝试,这种方式看似能够实现所需的功能,但是却存在以下问题:

  1. 难以确保及时性。在睡眠时,基本不消耗处理器资源,但是如果睡的太久,就不能及时发现条件已经变化,也就是及时性难以保证。
  2. 难以降低开销。如果降低睡眠的时间,比如休眠1ms,这样消费者就能更快速的发现条件变化,但也消耗了更多的处理器资源,造成了无端的浪费。

以上两个问题看似矛盾难以调和,但Java通过内置的等待/通知机制能够很好的解决这个矛盾并实现所需的功能。等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上。

  • notify()
  • notifyAll()
  • wait()
  • wait(long)
  • wait(long, int)

等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()方法或者notifyAll()方法,线程A收到通知后从对象的wait()方法返回,进而执行后续操作。上面两个线程通过对象O实现交互,而对象上的wait()和notify()/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

下面代码创建了两个线程——WaitThread和NotifyThread,前者检查flag是否为false,如果是就进行后续操作,否则在LOCK上等待,后者睡眠一段时间后对LOCK进行通知。

public class WaitNotifyTest {
    private static boolean flag = true;
    private static final Object LOCK = new Object();

    @Test
    public void test() {
        Thread waitThread = new Thread(new Wait(), "WaitThread");
        waitThread.start();

        SleepUtils.sleep(1);
        Thread notifyThread = new Thread(new Notify(), "NotifyThread");
        notifyThread.start();

        //使用JUnit测试时,如果test线程结束,其他线程也会结束,所以等待waitThread和notifyThread执行
        SleepUtils.sleep(10);
    }

    private static class Wait implements Runnable {
        @Override
        public void run() {
            synchronized (LOCK) {
                while (flag) {
                    try {
                        System.out.println(Thread.currentThread() + " flag is true. wait @ " + new Date());
                        LOCK.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread() + " flag is false. running @ " + new Date());
            }
        }
    }

    private static class Notify implements Runnable {
        @Override
        public void run() {
            synchronized (LOCK) {
                System.out.println(Thread.currentThread() + " hold lock. notify @ " + new Date());
                flag = false;
                LOCK.notifyAll();
                SleepUtils.sleep(5);
            }
            synchronized (LOCK) {
                System.out.println(Thread.currentThread() + " hold lock again. sleep @ " + new Date());
                SleepUtils.sleep(5);
            }
        }
    }
}

输出如下:

Thread[WaitThread,5,main] flag is true. wait @ Wed Dec 26 16:02:03 CST 2018
Thread[NotifyThread,5,main] hold lock. notify @ Wed Dec 26 16:02:04 CST 2018
Thread[WaitThread,5,main] flag is false. running @ Wed Dec 26 16:02:09 CST 2018
Thread[NotifyThread,5,main] hold lock again. sleep @ Wed Dec 26 16:02:09 CST 2018

第三行和第四行的输出可能会互换,上述例子主要说明了调用wait()和notify()/notifyAll()时需要注意的细节:

  1. 使用wait(), notify()以及notifyAll()时需要先对调用对象加锁
  2. 调用wait()方法后,线程状态从RUNNING变为WAITING,并将线程放置到对象的等待队列
  3. notify()或notifyAll()方法调用后,等待线程依然不会从wait()返回,需要调用notify()或notifyAll()的线程释放锁之后,等待线程才有机会从wait()返回
  4. notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则将等待队列中所有的线程都移到同步队列,被移动的线程状态从WAITING变为BLOCKED
  5. 从wait()方法返回的前提是获得了调用对象的锁

从上面细节可以看出,等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改。

下图描述了上述示例的过程:

WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃锁进入对象的等待队列中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread便能获取对象的锁,然后调用对象的notify()方法,将WaitThread从等待队列转移到同步队列。NotifyThread释放锁后,WaitThread再次获取锁并从wait()方法返回继续执行。

等待/通知的经典范式

从前面的示例中可以提炼出等待/通知的经典范式,分为两部分,分别针对等待方(消费者)和通知方(生产者)。

等待方:

  1. 获取对象的锁
  2. 如果条件不满足,调用对象的wait()方法,被通知后仍要检查条件
  3. 条件满足执行对应逻辑
synchronized(对象) {
    while(条件不满足) {
        对象.wait();
    }
    对应逻辑
}

通知方:

  1. 获取对象的锁
  2. 改变条件
  3. 通知所有等待在对象上的线程
synchronized(对象) {
    改变条件
    对象.notifyAll();
}

管道输入/输出流

管道输入/输出流和普通文件的输入/输出流或者网络输入/输出流的不同之处在于,它主要用于线程之间的数据传输,传输媒介为内存。

管道输入/输出流主要包括了4种具体实现:PipedOutputStreamPipedInputStreamPipedReaderPipedWriter,前两种面向字节,后两种面向字符。下面是一个简单的例子:

/**
 * 管道输入主要用于线程之间的数据传输
 */
public class Piped {
    public static void main(String[] args) throws Exception {
        PipedWriter writer = new PipedWriter();
        PipedReader reader = new PipedReader();
        //将输入输出流进行连接,否则使用时会抛出IOException
        writer.connect(reader);

        Thread printThread = new Thread(new Print(reader), "PrintThread");
        printThread.start();
        int receive;
        try {
            while ((receive = System.in.read()) != -1) {
                writer.write(receive);
            }
        } finally {
            writer.close();
        }
    }


    private static class Print implements Runnable {
        private PipedReader reader;

        Print(PipedReader reader) {
            this.reader = reader;
        }

        @Override
        public void run() {
            int receive;
            try {
                while ((receive = reader.read()) != -1) {
                    System.out.print((char) receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

结果如下,输入一组字符串,被原样输出:

13465
13465

Thread.join()的使用

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法外,还提供了join(long mills)和join(long mills, int nanos)两个具备超时性质的方法。这两个方法表示,如果线程thread在给定的时间内没有终止,那么将会从该超时方法中返回。

下面这个例子在前面已经提及过:

import util.SleepUtils;

public class Join {
    public static void main(String[] args) throws Exception {
        Thread previous = Thread.currentThread();
        for(int i=0;i<10;++i) {
            Thread thread = new Thread(new Domino(previous), String.valueOf(i));
            thread.start();
            previous=thread;
        }
        SleepUtils.sleep(5);
        System.out.println(Thread.currentThread().getName() + " terminate.");
    }

    private static class Domino implements Runnable {
        private Thread thread;
        Domino(Thread thread) {
            this.thread=thread;
        }

        @Override
        public void run() {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " terminate.");
        }
    }
}

输出如下:

main terminate.
0 terminate.
1 terminate.
2 terminate.
3 terminate.
4 terminate.
5 terminate.
6 terminate.
7 terminate.
8 terminate.
9 terminate.

从上述输出可以看出,每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从join()方法返回,这里其实也涉及到了等待/通知机制。

ThreadLocal的使用

ThreadLocal即线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构。这个结构被附带在线程上,也就是一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

Java中的ThreadLocal类使您能够创建只能由同一线程读取和写入的变量。因此,即使两个线程正在执行相同的代码,并且代码具有对ThreadLocal变量的引用 ,那么这两个线程也看不到彼此的ThreadLocal变量。

创建ThreadLocal

这是一个代码示例,演示如何创建 ThreadLocal 变量:

private ThreadLocal myThreadLocal = new ThreadLocal();

如你所见,你实例化了一个新 ThreadLocal 对象,这只需要每个线程完成一次。即使不同的线程执行相同获取 ThreadLocal 的代码,每个线程也只能看到自己的 ThreadLocal 实例。即使两个不同的线程在同一个 ThreadLocal 对象上设置不同的值,它们也看不到彼此的值。

访问ThreadLocal

一旦 ThreadLocal 被创建,你可以像这样设置值:

myThreadLocal.set("一个线程本地值");

你可以这样读取存储在 ThreadLocal 内容中的值:

String threadLocalValue = (String) myThreadLocal.get();

get() 方法返回一个Object,该 set() 方法采用Object作为参数。

通用ThreadLocal

您可以创建一个泛型 ThreadLocal 这样就不必对 get() 返回的值进行类型转换 。这是一个通用的 ThreadLocal 例子:

private ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();

现在,你只能在该 ThreadLocal 实例中存储字符串。此外,你也不需要对从该 ThreadLocal 获得的值进行类型转换:

myThreadLocal.set(“Hello ThreadLocal”);
String threadLocalValue = myThreadLocal.get();

初始ThreadLocal值

由于在 ThreadLocal 对象上设置的值仅对设置值的线程可见,因此没有线程可以在 ThreadLocal 上使用 set() 设置初始值,使其对所有线程可见。

相反,您可以通过子类化 ThreadLocal 和重写 initialValue() 方法为 ThreadLocal 对象指定初始值。这是看起来如何使用的:

private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
    @Override protected String initialValue() {
        return "This is the initial value";
    }
}; 

现在所有线程在调用set()之前调用get()都会看到相同的初始值。

InheritableThreadLocal

InheritableThreadLocal 类是 ThreadLocal 的子类。比起每个线程在 ThreadLocal 中都有自己的值, InheritableThreadLocal 的值对创建它的线程以及该线程创建的所有子线程来说都能访问。

下面是一个ThreadLocal完整的例子:

public class Profiler {
    private static final ThreadLocal<Long> TIME_THREAD_LOCAL = ThreadLocal.withInitial(System::currentTimeMillis);

    public static void begin() {
        TIME_THREAD_LOCAL.set(System.currentTimeMillis());
    }

    public static long end() {
        return System.currentTimeMillis()-TIME_THREAD_LOCAL.get();
    }

    public static void main(String[] args) throws Exception {
        Profiler.begin();
        SleepUtils.sleep(1);
        System.out.println("Cost: " + Profiler.end() + " mills");

        //result:1546423458054
        //       main: 1546423457053
        //可以看出ThreadLocal变量是依附于一个线程的,不像普通变量被共享
        begin();
        Thread thread1 = new Thread(() -> System.out.println(TIME_THREAD_LOCAL.get()));
        thread1.start();
        System.out.println("main: " + TIME_THREAD_LOCAL.get());
    }
}

输出如下:

Cost: 1002 mills
1546423458054
main: 1546423457053

此类可以被复用在耗时统计的功能上,在方法入口执行begin()方法,方法调用后执行end()方法,好处是这两个方法的调用不需要在同一个类或方法中,比如在AOP编程中,可以在方法调用前的切入点执行begin()方法,而在方法调用后的切入点执行end()方法,这样依旧可以获得方法的执行耗时。

下面是ThreadLocal几个常用方法的源码:

/**
     * Creates a thread local variable. The initial value of the variable is
     * determined by invoking the {@code get} method on the {@code Supplier}.
     *
     * @param <S> the type of the thread local's value
     * @param supplier the supplier to be used to determine the initial value
     * @return a new thread local variable
     * @throws NullPointerException if the specified supplier is null
     * @since 1.8
     */
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,176评论 5 469
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,190评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,232评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,953评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,879评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,177评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,626评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,295评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,436评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,365评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,414评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,096评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,685评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,771评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,987评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,438评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,032评论 2 341

推荐阅读更多精彩内容

  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    胜浩_ae28阅读 5,084评论 0 23
  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    小徐andorid阅读 2,796评论 3 53
  • 1. cpu通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一任务。但是,再切换之前会保存上一...
    冰与河豚鱼阅读 651评论 0 0
  • 一、基本写法 a.上面定义了一个类,包含着一个构造函数和一个say方法。构造函数内的this指向实例对象,且该函数...
    www_ye阅读 194评论 0 0
  • 据科学研究,养成一个习惯的平均时间是66天,而不同的习惯养成的时间相差很大,从18天到254天不等。 很多人都喜欢...
    纤陌颜阅读 463评论 0 3