多线程的一个接口和一个类
//Runnable接口: @FunctionalInterface public interface Runnable { public abstract void run(); } //Thread类,它其实也是实现了Runnable接口 public class Thread implements Runnable { /* Make sure registerNatives is the first thing <clinit> does. */ private static native void registerNatives(); static { registerNatives(); } private volatile char name[]; private int priority; private Thread threadQ; private long eetop; /* Whether or not to single_step this thread. */ private boolean single_step; /* Whether or not the thread is a daemon thread. */ private boolean daemon = false; /* JVM state */ private boolean stillborn = false; /* What will be run. */ private Runnable target; ...... }
创建新的线程并运行
//方法1: public class WelcomeApp2 { public static void main(String[] args) { // 创建线程 Thread welcomeThread = new Thread(new Runnable() { @Override public void run() { System.out.printf("2.Welcome! I'm %s.%n", Thread.currentThread() .getName()); } }); // 启动线程 welcomeThread.start(); // 这里直接调用线程的run方法,严重错误,将运行在main线程中,仅是出于演示的目的 welcomeThread.run(); System.out.printf("1.Welcome! I'm %s.%n", Thread.currentThread().getName()); } } //方法2: public class WelcomeApp1 { public static void main(String[] args) { // 创建线程 Thread welcomeThread = new Thread(new WelcomeTask()); // 启动线程 welcomeThread.start(); // 输出“当前线程”的线程名称 System.out.printf("1.Welcome! I'm %s.%n", Thread.currentThread().getName()); } } class WelcomeTask implements Runnable { // 在该方法中实现线程的任务处理逻辑 @Override public void run() { // 输出“当前线程”的线程名称 System.out.printf("2.Welcome! I'm %s.%n", Thread.currentThread().getName()); } }
创建新线程如果用Thread子类方式是基于继承的技术,如果用Runable接口实现是基于组合的技术,需要看情况来运用,一般来说基于接口更灵活。
以下直接运行接口实现类的run方法,使用的是什么线程?
public class JavaThreadAnywhere { public static void main(String[] args) { // 获取当前线程 Thread currentThread = Thread.currentThread(); // 获取当前线程的线程名称 String currentThreadName = currentThread.getName(); System.out.printf("The main method was executed by thread:%s.%n", currentThreadName); Helper helper = new Helper("Java Thread AnyWhere."); helper.run(); } static class Helper implements Runnable { private final String message; public Helper(String message) { this.message = message; } private void doSomething(String message) { // 获取当前线程 Thread currentThread = Thread.currentThread(); // 获取当前线程的线程名称 String currentThreadName = currentThread.getName(); System.out.printf("The doSomething method was executed by thread:%s.%n", currentThreadName); System.out.println("Do something with: " + message); } @Override public void run() { doSomething(message); } } }
很明显是main线程,和之前welcomeThread.run()是一样的
线程的父子关系,如果一个父线程是守护线程,那子线程也是守护线程,如果父是普通线程,子也是普通线程,优先级也是如此。
多线程程序可以提升JVM进程的并发,提高响应性(避免UI卡死),充分利用多核。但需要注意线程安全问题,共享数据需要注意一致性,避免死锁和活锁。
竞态和原子性
如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。线程之间共享堆空间,在编程的时候就要格外注意避免竞态条件。危险在于多个线程同时访问相同的资源并进行读写操作。当其中一个线程需要根据某个变量的状态来相应执行某个操作的之前,该变量很可能已经被其它线程修改。
比如以下代码,index发生了竞态错误
public class MyThread extends Thread { //public static AtomicInteger index; public static int index; public void run() { int index1 = 0; //index = new AtomicInteger(); for (int i = 0; i < 10; i++) { //System.out.println(getName() + ":" + index.getAndAdd(1)); //System.out.println(getName() + ":" + index1++); System.out.println(getName() + ":" + index++); } } } public class Test { public static void main(String[] args) { new MyThread().start(); new MyThread().start(); } } 结果: Thread-0:0 Thread-1:1 Thread-0:2 Thread-1:3 Thread-0:4 Thread-1:5 Thread-0:6 Thread-1:7 Thread-1:9 Thread-1:10 Thread-1:11 Thread-1:12 Thread-1:13 Thread-1:14 Thread-0:8 Thread-0:15 Thread-0:16 Thread-0:17 Thread-0:18 Thread-0:19
解决竞态问题的办法1:
使用局部变量,但逻辑变为分别执行了,实现分别累加(每个线程从0到9)
public class MyThread extends Thread { //public static AtomicInteger index; //public static int index; public void run() { int index1 = 0; //index = new AtomicInteger(); for (int i = 0; i < 10; i++) { //System.out.println(getName() + ":" + index.getAndAdd(1)); System.out.println(getName() + ":" + index1++); //System.out.println(getName() + ":" + index++); } } } 结果: Thread-0:0 Thread-1:0 Thread-0:1 Thread-1:1 Thread-0:2 Thread-1:2 Thread-1:3 Thread-1:4 Thread-0:3 Thread-1:5 Thread-0:4 Thread-1:6 Thread-0:5 Thread-1:7 Thread-0:6 Thread-0:7 Thread-0:8 Thread-0:9 Thread-1:8 Thread-1:9
解决竞态和原子性问题的办法2:
使用同步synchronized,或者原子类。
import java.util.concurrent.atomic.AtomicInteger; public class IncrementTestDemo { public static int count = 0; public static Counter counter = new Counter(); public static AtomicInteger atomicInteger = new AtomicInteger(0); volatile public static int countVolatile = 0; volatile public static long countLonVolatile = 0; public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread() { public void run() { for (int j = 0; j < 1000; j++) { count++; counter.increment(); atomicInteger.getAndIncrement(); countVolatile++; countLonVolatile++; } } }.start(); } //主线程休息会,让其他10个线程完成任务 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("static count: " + count); System.out.println("Counter: " + counter.getValue()); System.out.println("AtomicInteger: " + atomicInteger.intValue()); System.out.println("countVolatile: " + countVolatile); System.out.println("countLonVolatile: " + countLonVolatile); } } class Counter { private int value; public synchronized int getValue() { return value; } public synchronized int increment() { return ++value; } public synchronized int decrement() { return --value; } } 结果: static count: 9974 Counter: 10000 AtomicInteger: 10000 countVolatile: 9974 countLonVolatile: 9990
静态字段和Volatile并不解决原子安全性(这和某些教程说的Volatile可解决long、double的原子性有很大问题)。
如何保证原子性?可以使用内部锁(同步块)、原子类(上面已经展示了)、锁(显式的可重入锁和他们的子类,比如读写锁)
采用synchronized:
public class Test { public int inc = 0; public synchronized void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
采用Lock:
public class Test { public int inc = 0; Lock lock = new ReentrantLock(); public void increase() { lock.lock(); try { inc++; } finally{ lock.unlock(); } } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
可见性
在多线程中,一个线程对一个变量进行了更新,后续的线程可能不能读到这个更新的结果,对于程序来说结果将非常糟糕。
可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每使用前立即从内存刷新。因为我们可以说volatile保证了线程操作时变量的可见性,而普通变量则不能保证这一点。
例子:程序将一直运行,没办法在5秒后停止,主线程修改的cancel变量,并不能让子线程获知。
public class VisibilityDemo { public static void main(String[] args) throws InterruptedException { TimeConsumingTask timeConsumingTask = new TimeConsumingTask(); Thread thread = new Thread(new TimeConsumingTask()); //thread.setDaemon(true); thread.start(); // 指定的时间内任务没有执行结束的话,就将其取消 Thread.sleep(5000); timeConsumingTask.cancel(); } } class TimeConsumingTask implements Runnable { private boolean toCancel = false; @Override public void run() { while (!toCancel) { doExecute(); } if (toCancel) { System.out.println("Task was canceled."); } else { System.out.println("Task done."); } } private void doExecute() { System.out.println("executing..."); // 模拟实际操作的时间消耗 Tools.randomPause(1000); // 省略其他代码 } public void cancel() { toCancel = true; System.out.println(this + " canceled."); } }
这段代码while (!toCancel)被认为一直是true,造成了死循环。
也不能正常,解决的方法是使用守护线程。
Thread thread = new Thread(new TimeConsumingTask()); thread.setDaemon(true);
也可以使用volatile:
volatile static boolean toCancel = false;
第二个避免可见性异常的例子
public class VisibilityDemo { private static volatile boolean flag = true; private static int i = 0; public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { while (flag) { i++; } System.out.printf("**********test1 跳出成功, i=%d ********** ", i); }); thread.start(); Thread.sleep(100); flag = false; System.out.printf("**********test1 main thread 结束, i=%d ********** ", i); } }
上下文切换
即使是单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)。
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换。
线程从消耗完时间片保存当前执行的状态再到获取下一次时间片加载这个状态的过程,我们称之为上下文切换,这种切换会影响多线程的执行速度.
多线程控制不当,比如锁竞争激烈就会不停的导致上下文切换.
减少上下文切换的方法:
- CAS算法:JAVA的Atomic包使用CAS算法,无需加锁.可以阅读源码参考.
- 无锁并发编程:使用别的办法避免共享对象从而产生锁.比如ThreadLocal.
- 使用少的线程:避免创建不需要的线程,空闲的线程会进入等待状态.
- 协程:单线程中实现多任务的调度,在单线程里维持多个人物之间的切换.
Volatile
volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile 变量而不是锁。当使用 volatile 变量而非锁时,某些习惯用法(idiom)更加易于编码和阅读。此外,volatile 变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于锁的性能优势。
您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使 x 的值在操作期间保持不变,而 volatile 变量无法实现这点。(然而,如果将值调整为只从单个线程写入,那么可以忽略第一个条件。)
大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile 变量不能像 synchronized 那样普遍适用于实现线程安全。清单 1 显示了一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是小于或等于上界。
什么时候单单使用 volatile 就够了?
就如之前提到的,如果两个线程都对共享变量进行读写,那么只使用关键字 volatile 就不能满足要求了。这种情况你需要用 synchronized 来保证读写变量的原子性。对 volatile 变量的读写操作并不会阻塞其他线程的读写。如果需要阻塞,你就必须在临界区周围使用关键字 synchronized 。如果不想用 synchronized 代码块,你可以从包java.util.concurrent中找到很多有用的原子数据类型,如 AtomicLong,AtomicReference 或者其他。假设只有一个线程同时读写 volatile 变量,其他线程只读取,那么只读线程一定能看到最新写入到 volatile 变量的值。
使用 volatile 的模式
模式 #1:状态标志
也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
很多应用程序包含了一种控制结构,形式为 “在还没有准备好停止程序时再执行一些工作”,如清单 2 所示:
volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } }
很可能会从循环外部调用 shutdown() 方法 —— 即在另一个线程中 —— 因此,需要执行某种同步来确保正确实现 shutdownRequested 变量的可见性。(可能会从 JMX 侦听程序、GUI 事件线程中的操作侦听程序、通过 RMI 、通过一个 Web 服务等调用)。然而,使用 synchronized 块编写循环要比使用清单 2 所示的 volatile 状态标志编写麻烦很多。由于 volatile 简化了编码,并且状态标志并不依赖于程序内任何其他状态,因此此处非常适合使用 volatile。
这种类型的状态标记的一个公共特性是:通常只有一种状态转换;shutdownRequested 标志从 false 转换为 true,然后程序停止。
模式 #2:一次性安全发布(one-time safe publication)
缺乏同步会导致无法实现可见性,这使得确定何时写入对象引用而不是原语值变得更加困难。在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。(这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象)。
实现安全发布对象的一种技术就是将对象引用定义为 volatile 类型。清单 3 展示了一个示例,其中后台线程在启动阶段从数据库加载一些数据。其他代码在能够利用这些数据时,在使用之前将检查这些数据是否曾经发布过。
public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // do lots of stuff theFlooble = new Flooble(); // this is the only write to theFlooble } } public class SomeOtherClass { public void doWork() { while (true) { // do some stuff... // use the Flooble, but only if it is ready if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } } }
如果 theFlooble 引用不是 volatile 类型,doWork() 中的代码在解除对 theFlooble 的引用时,将会得到一个不完全构造的 Flooble。
该模式的一个必要条件是:被发布的对象必须是线程安全的,或者是有效的不可变对象(有效不可变意味着对象的状态在发布之后永远不会被修改)。volatile 类型的引用可以确保对象的发布形式的可见性,但是如果对象的状态在发布后将发生更改,那么就需要额外的同步。
模式 #3:独立观察(independent observation)
安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。
使用该模式的另一种应用程序就是收集程序的统计信息。清单 4 展示了身份验证机制如何记忆最近一次登录的用户的名字。将反复使用 lastUser 引用来发布值,以供程序的其他部分使用。
public class UserManager { public volatile String lastUser; public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); lastUser = user; } return valid; } }
模式 #4:开销较低的读-写锁策略
目前为止,您应该了解了 volatile 的功能还不足以实现计数器。因为 ++x 实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值有可能会丢失。
然而,如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。清单 6 中显示的线程安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。
@ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; } }
非阻塞同步算法与CAS(Compare and Swap)无锁算法
锁是用来做并发最简单的方式,当然其代价也是最高的。内核态的锁的时候需要操作系统进行一次上下文切换,加锁、释放锁会导致比较多的上下文切换和调度延时,等待锁的线程会被挂起直至锁释放。在上下文切换的时候,cpu之前缓存的指令和数据都将失效,对性能有很大的损失。操作系统对多线程的锁进行判断就像两姐妹在为一个玩具在争吵,然后操作系统就是能决定他们谁能拿到玩具的父母,这是很慢的。用户态的锁虽然避免了这些问题,但是其实它们只是在没有真实的竞争时才有效。
Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有守护变量的锁,都采用独占的方式来访问这些变量,如果出现多个线程同时访问锁,那第一些线线程将被挂起,当线程恢复执行时,必须等待其它线程执行完他们的时间片以后才能被调度执行,在挂起和恢复执行过程中存在着很大的开销。锁还存在着其它一些缺点,当一个线程正在等待锁时,它不能做任何事。如果一个线程在持有锁的情况下被延迟执行,那么所有需要这个锁的线程都无法执行下去。
独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
CAS(乐观锁)比较与交换可以表示为:
do{
备份旧数据;
基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))
调用了sun.misc.Unsafe类库里面的 CAS算法,用CPU指令来实现无锁自增。所以,AtomicLong.incrementAndGet的自增比用synchronized的锁效率倍增。
以下是效率对比:
package io.github.viscent.mtia.ext; import java.util.concurrent.atomic.AtomicLong; public class main { /** * @param args */ public static void main(String[] args) { System.out.println("START -- "); calc(); calcSynchro(); calcAtomic(); testThreadsSync(); testThreadsAtomic(); testThreadsSync2(); testThreadsAtomic2(); System.out.println("-- FINISHED "); } private static void calc() { stopwatch sw = new stopwatch(); sw.start(); long val = 0; while (val < 10000000L) { val++; } sw.stop(); long milSecds = sw.getElapsedTime(); System.out.println(" calc() elapsed (ms): " + milSecds); } private static void calcSynchro() { stopwatch sw = new stopwatch(); sw.start(); long val = 0; while (val < 10000000L) { synchronized (main.class) { val++; } } sw.stop(); long milSecds = sw.getElapsedTime(); System.out.println(" calcSynchro() elapsed (ms): " + milSecds); } private static void calcAtomic() { stopwatch sw = new stopwatch(); sw.start(); AtomicLong val = new AtomicLong(0); while (val.incrementAndGet() < 10000000L) { } sw.stop(); long milSecds = sw.getElapsedTime(); System.out.println(" calcAtomic() elapsed (ms): " + milSecds); } private static void testThreadsSync(){ stopwatch sw = new stopwatch(); sw.start(); Thread t1 = new Thread(new LoopSync()); t1.start(); Thread t2 = new Thread(new LoopSync()); t2.start(); while (t1.isAlive() || t2.isAlive()) { } sw.stop(); long milSecds = sw.getElapsedTime(); System.out.println(" testThreadsSync() 1 thread elapsed (ms): " + milSecds); } private static void testThreadsAtomic(){ stopwatch sw = new stopwatch(); sw.start(); Thread t1 = new Thread(new LoopAtomic()); t1.start(); Thread t2 = new Thread(new LoopAtomic()); t2.start(); while (t1.isAlive() || t2.isAlive()) { } sw.stop(); long milSecds = sw.getElapsedTime(); System.out.println(" testThreadsAtomic() 1 thread elapsed (ms): " + milSecds); } private static void testThreadsSync2(){ stopwatch sw = new stopwatch(); sw.start(); Thread t1 = new Thread(new LoopSync()); t1.start(); Thread t2 = new Thread(new LoopSync()); t2.start(); while (t1.isAlive() || t2.isAlive()) { } sw.stop(); long milSecds = sw.getElapsedTime(); System.out.println(" testThreadsSync() 2 threads elapsed (ms): " + milSecds); } private static void testThreadsAtomic2(){ stopwatch sw = new stopwatch(); sw.start(); Thread t1 = new Thread(new LoopAtomic()); t1.start(); Thread t2 = new Thread(new LoopAtomic()); t2.start(); while (t1.isAlive() || t2.isAlive()) { } sw.stop(); long milSecds = sw.getElapsedTime(); System.out.println(" testThreadsAtomic() 2 threads elapsed (ms): " + milSecds); } private static class LoopAtomic implements Runnable { public void run() { AtomicLong val = new AtomicLong(0); while (val.incrementAndGet() < 10000000L) { } } } private static class LoopSync implements Runnable { public void run() { long val = 0; while (val < 10000000L) { synchronized (main.class) { val++; } } } } } class stopwatch { private long startTime = 0; private long stopTime = 0; private boolean running = false; public void start() { this.startTime = System.currentTimeMillis(); this.running = true; } public void stop() { this.stopTime = System.currentTimeMillis(); this.running = false; } public long getElapsedTime() { long elapsed; if (running) { elapsed = (System.currentTimeMillis() - startTime); } else { elapsed = (stopTime - startTime); } return elapsed; } public long getElapsedTimeSecs() { long elapsed; if (running) { elapsed = ((System.currentTimeMillis() - startTime) / 1000); } else { elapsed = ((stopTime - startTime) / 1000); } return elapsed; } // sample usage // public static void main(String[] args) { // StopWatch s = new StopWatch(); // s.start(); // //code you want to time goes here // s.stop(); // System.out.println("elapsed time in milliseconds: " + // s.getElapsedTime()); // } } 结果: calc() elapsed (ms): 7 calcSynchro() elapsed (ms): 324 calcAtomic() elapsed (ms): 88 testThreadsSync() 1 thread elapsed (ms): 1187 testThreadsAtomic() 1 thread elapsed (ms): 117 testThreadsSync() 2 threads elapsed (ms): 1196 testThreadsAtomic() 2 threads elapsed (ms): 108
差别很大。