StopWatch单线程问题及多线程ConcurrentStopWatch解决方案

1. 背景介绍

相信很多人都知道,spring-core中有个非常实用的监控类StopWatch,在Spring Boot应用启动过程中也有使用,可以监控代码段执行耗时以及占整个监控过程耗时百分比,这样很容易监控出比较耗时的代码,及时发现问题,优化性能。但不足的是,StopWatch是个线程不安全的工具类,不能用于多线程环境,因此基于StopWatch的不足,自定义组件mbyx-component-concurrent-stopwatch提供一个多线程版本的ConcurrentStopWatch便于在多线程环境监控代码执行耗时。

2. 为什么线程不安全

2.1 线程安全的含义

线程安全问题发生在共享变量或静态变量的情况,多个线程想要同时对共享变量或静态变量进行修改操作,就形成线程竞争,存在竞争即线程不安全,反之,如果对共享变量或静态变量修改时,一次只允许有一个线程执行,不存在竞争,这种情况是线程安全的。

2.2 多线程环境下使用StopWatch会怎样

先多线程环境下测试一下StopWatch监控结果

package com.mbyx.component.concurrent.stopwatch;
import org.springframework.util.StopWatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author zhuxiaolong
 * @package PACKAGE_NAME
 * @description
 * @date 2019/3/27 15:35
 */
public class Main {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10000; i++) {
            service.execute(Test.getInstance());
        }

    }
}

class Test implements Runnable {
    StopWatch watch = new StopWatch();

    @Override
    public void run() {
        try {
            watch.start("111111111111");
            TimeUnit.SECONDS.sleep(1);

            watch.stop();
            watch.start("222222222222");
            TimeUnit.SECONDS.sleep(1);

            watch.stop();
            watch.start("333333333333");
            TimeUnit.SECONDS.sleep(1);

            watch.stop();
            watch.start("444444444444");
            TimeUnit.SECONDS.sleep(1);

            watch.stop();
            String out = watch.prettyPrint();
            System.out.println(out);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static class SingleHolder {
        private static Test test = new Test();
    }
    public static Test getInstance() {
        return SingleHolder.test;
    }
}
  • 想要的结果:
StopWatch '': running time (millis) = 4002
-----------------------------------------
ms     %     Task name
-----------------------------------------
01000  025%  111111111111
01001  025%  222222222222
01000  025%  333333333333
01001  025%  444444444444

StopWatch '': running time (millis) = 4018
-----------------------------------------
ms     %     Task name
-----------------------------------------
01000  025%  111111111111
01001  025%  222222222222
01000  025%  333333333333
01017  025%  444444444444

StopWatch '': running time (millis) = 4002
-----------------------------------------
ms     %     Task name
-----------------------------------------
01000  025%  111111111111
01001  025%  222222222222
01000  025%  333333333333
01001  025%  444444444444
.........
  • 实际的结果:
Connected to the target VM, address: '127.0.0.1:58560', transport: 'socket'
Exception in thread "pool-1-thread-2" java.lang.IllegalStateException: Can't start StopWatch: it's already running
	at org.springframework.util.StopWatch.start(StopWatch.java:128)
	at com.mbyx.component.concurrent.stopwatch.Test.run(Main.java:32)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "pool-1-thread-3" java.lang.IllegalStateException: Can't start StopWatch: it's already running
	at org.springframework.util.StopWatch.start(StopWatch.java:128)
	at com.mbyx.component.concurrent.stopwatch.Test.run(Main.java:32)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "pool-1-thread-4" java.lang.IllegalStateException: Can't start StopWatch: it's already running
	at org.springframework.util.StopWatch.start(StopWatch.java:128)
	at com.mbyx.component.concurrent.stopwatch.Test.run(Main.java:32)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "pool-1-thread-5" java.lang.IllegalStateException: Can't start StopWatch: it's already running
	at org.springframework.util.StopWatch.start(StopWatch.java:128)
	at com.mbyx.component.concurrent.stopwatch.Test.run(Main.java:32)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "pool-1-thread-6" java.lang.IllegalStateException: Can't start StopWatch: it's already running
	at org.springframework.util.StopWatch.start(StopWatch.java:128)
	at com.mbyx.component.concurrent.stopwatch.Test.run(Main.java:32)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
	......

2.3 为什么会出现这种结果

  • 线程不安全原因:
    watch对象是个线程共享变量,假如线程A执行了watch.start(),watch对象内部的属性被改变,线程A还未来得及执行watch.stop(),线程B就执行watch.start()。根据下面StopWatch源码可以看出,watch.start()和watch.stop()是一组操作,如果顺序被打乱,那么就会抛出异常。
    public void start(String taskName) throws IllegalStateException {
        if (this.currentTaskName != null) {
            throw new IllegalStateException("Can't start StopWatch: it's already running");
        } else {
            this.currentTaskName = taskName;
            this.startTimeMillis = System.currentTimeMillis();
        }
    }

    public void stop() throws IllegalStateException {
        if (this.currentTaskName == null) {
            throw new IllegalStateException("Can't stop StopWatch: it's not running");
        } else {
            long lastTime = System.currentTimeMillis() - this.startTimeMillis;
            this.totalTimeMillis += lastTime;
            this.lastTaskInfo = new StopWatch.TaskInfo(this.currentTaskName, lastTime);
            if (this.keepTaskList) {
                this.taskList.add(this.lastTaskInfo);
            }

            ++this.taskCount;
            this.currentTaskName = null;
        }
    }

3. 原理探索

3.1 共享变量为何不安全

假设主存中有共享变量a(初始值1),现在线程A和线程B要执行a++

  • 线程不安全的做法:

线程A和线程B同时从主存读取a到本地线程工作内存,并执行a++,然后线程A和线程B分别将结果写回主存,无论线程A和线程B谁先写回主存,执行最终结果a的值为2(与预期值3不符合),这就是线程不安全导致的错误数据

  • 线程安全的做法:

线程A从主存读取a到本地线程工作内存(线程B正在等待),并执行a++,然后线程A将结果写回主存,此时a的值为2。接着线程B从主存读取a到本地线程工作内存,并执行a++,然后线程B将结果写回主存,此时a的值为3(与预期值3符合),不存在竞争的情况即为线程安全

3.2 ThreadLocal原理

结合源码分析,ThreadLocal核心为get()和set()方法

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        // 获取当前线程的threadLocals属性
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	// threadLocals不为空,通过当前的ThreadLocal对象(this)获取存入ThreadLocal中的对象e.value
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果threadLocals 为null,执行初始化设置初始值
        return setInitialValue();
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    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;
    }

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

Thread类有个属性ThreadLocal.ThreadLocalMap threadLocals = null;,这个map键为ThreadLocal共享变量,value是ThreadLocal.ThreadLocalMap.Entry静态类对象,Entry的键为ThreadLocal共享变量,value为存入ThreadLocal中的对象。因此ThreadLocal最终是每个线程有个ThreadLocal.ThreadLocalMap对象,多线程拥有各自的ThreadLocal.ThreadLocalMap副本,不存在竞争,所以是线程安全的。

和Synchronized相比:

  • Synchronized以时间换空间,不需要额外的空间,但较耗时,性能没有ThreadLocal好
  • ThreadLocal以空间换时间,增加了额外的存储空间,但带来的是性能的提升

备注:网上有不少博文说ThreadLocal有内存泄露的可能,看过相关的文章及ThreadLocal的源码,发现ThreadLocal.ThreadLocalMap.Entry是继承自WeakReference类的,即弱引用。根据JVM垃圾收集器原理,如果是弱引用类型的,当虚拟机执行垃圾回收时,会将其列入回收集,因此笔者认为是不存在内存泄露的问题(JDK1.6+)

4. 线程安全的解决方案

4.1 线程安全的ConcurrentStopWatch

基于ThreadLocal,提供一个线程安全的ConcurrentStopWatch类

  • ConcurrentStopWatch.java
package com.mbyx.component.concurrent.stopwatch;

import org.springframework.util.StopWatch;


/**
 * @author zhuxiaolong
 * @package com.mbyx.component.stopwatch
 * @description 支持多线程的代码执行耗时监控类,以空间换时间,多线程情况下,为每个线程提供一个StopWatch对象,支持更高的并发,相比synchronized的以时间换空间方式,不需要加锁,性能更好
 * @date 2019/3/27 16:51
 */
public class ConcurrentStopWatch {

    private ThreadLocal<StopWatch> stopWatch;

    public ConcurrentStopWatch() {
        this.stopWatch = new ThreadLocal<StopWatch>() {
            @Override
            protected StopWatch initialValue() {
                return new StopWatch();
            }
        };
    }


    public ConcurrentStopWatch(final String name) {
        this.stopWatch = new ThreadLocal<StopWatch>() {
            @Override
            protected StopWatch initialValue() {
                return new StopWatch(name);
            }
        };
    }

    public void start(String taskName) {
        try {
            if (!this.stopWatch.get().isRunning()) {
                this.stopWatch.get().start(taskName);
            } else {
                this.stopWatch.get().stop();
            }
        } catch (Exception e) {
            System.err.println(String.format("开始监控任务%s失败,原因:%s", taskName, e.getCause()));
        }
    }

    public void stop() {
        try {
            if (this.stopWatch.get().isRunning()) {
                this.stopWatch.get().stop();
            }
        } catch (Exception e) {
            System.err.println(String.format("停止监控任务失败,原因:%s", e.getCause()));
        }
    }

    public String prettyPrint() {
        try {
            return this.stopWatch.get().prettyPrint();
        } catch (Exception e) {
            System.err.println(String.format("打印监控日志失败,原因:%s", e.getCause()));
        }
        return "";
    }
}

4.2 多线程环境并发测试

  • Main.java测试类
package com.mbyx.component.concurrent.stopwatch;

import org.springframework.util.StopWatch;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author zhuxiaolong
 * @package PACKAGE_NAME
 * @description
 * @date 2019/3/27 15:35
 */
public class Main {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(100);
        for (int i = 0; i < 1000; i++) {
            service.execute(Test.getInstance());
        }

    }
}


class Test implements Runnable {
    ConcurrentStopWatch watch = new ConcurrentStopWatch();

    @Override
    public void run() {
        try {
            watch.start("111111111111");
            TimeUnit.SECONDS.sleep(1);

            watch.stop();
            watch.start("222222222222");
            TimeUnit.SECONDS.sleep(1);

            watch.stop();
            watch.start("333333333333");
            TimeUnit.SECONDS.sleep(1);

            watch.stop();
            watch.start("444444444444");
            TimeUnit.SECONDS.sleep(1);

            watch.stop();
            String out = watch.prettyPrint();
            System.out.println(out);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static class SingleHolder {
        private static Test test = new Test();
    }

    public static Test getInstance() {
        return SingleHolder.test;
    }
}
  • 测试结果
Connected to the target VM, address: '127.0.0.1:60728', transport: 'socket'
StopWatch '': running time (millis) = 4056
-----------------------------------------
ms     %     Task name
-----------------------------------------
01054  026%  111111111111
01001  025%  222222222222
01000  025%  333333333333
01001  025%  444444444444

StopWatch '': running time (millis) = 4057
-----------------------------------------
ms     %     Task name
-----------------------------------------
01055  026%  111111111111
01001  025%  222222222222
01000  025%  333333333333
01001  025%  444444444444

StopWatch '': running time (millis) = 4057
-----------------------------------------
ms     %     Task name
-----------------------------------------
01055  026%  111111111111
01001  025%  222222222222
01000  025%  333333333333
01001  025%  444444444444

StopWatch '': running time (millis) = 4057
-----------------------------------------
ms     %     Task name
-----------------------------------------
01055  026%  111111111111
01001  025%  222222222222
01000  025%  333333333333
01001  025%  444444444444
......

5. 结论

在多线程环境需要访问共享变量时,可以用ThreadLocal让每个线程保持一份副本,代价是增加了存储空间,优点是性能好。
如果不追求性能,也可以使用synchronized进行多线程同步访问,代价是多线程不能并发,需要排队执行,另外synchronized在jdk1.6之后做了很大的优化,从偏向锁、自旋锁、自适应自旋锁、轻量级锁、重量级锁有个锁升级的过程,只要不膨胀成重量级锁,synchronized也不是很差。在jdk1.6之前,synchronized只有重量级锁的实现。

上述文章中如有技术性错误,请指正

  • 11
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
在Unity中使用Task来实现异步操作时,可以使用Stopwatch类来计算线程执行时间。Stopwatch类是C#中的一个计时器类,可以精确地计算代码执行时间。 在异步任务中,可以在开始执行前调用Stopwatch.StartNew方法来启动计时器,在执行完成后调用Stopwatch.Stop方法来停止计时器,并使用Stopwatch.Elapsed属性来获取线程执行时间。最后将线程执行时间显示到UI界面上,或者通过Debug.Log方法输出到控制台上。 下面是一个简单的示例,使用Task和Stopwatch来实现异步任务,并显示线程执行时间: ``` async void MyFunction() { // 启动计时器 Stopwatch sw = Stopwatch.StartNew(); // 创建异步任务 Task myTask = Task.Factory.StartNew(MyThreadFunction); // 等待任务完成 await myTask; // 停止计时器 sw.Stop(); // 显示线程执行时间 Debug.Log("线程执行时间:" + sw.ElapsedMilliseconds + "ms"); } void MyThreadFunction() { // 模拟耗时操作 Thread.Sleep(3000); } ``` 在MyFunction函数中,使用Stopwatch类启动计时器,并创建了一个异步任务myTask。在等待任务完成后,停止计时器,并使用sw.ElapsedMilliseconds属性获取线程执行时间。最后使用Debug.Log方法将线程执行时间输出到控制台上。 需要注意的是,Stopwatch类的计时器精度是纳秒级别的,可以精确地计算代码执行时间。同时,需要确保异步任务不会访问主线程对象,否则可能会导致线程死锁。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

羽轩GM

您的鼓励是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值