实现模拟线程#sleep()

and*_*lka 5 java simulation multithreading

背景

我正在设计我的软件,以便我可以轻松地执行单元测试.我有一个IClock接口,其中包括其他方法 IClock#wait(TimeUnit timeUnit, long duration).此方法将暂停当前线程的timeUnit持续时间(即1秒).

IClock接口有两种实现方式:

  • SimulatedClock:有手动增加时钟存储时间的方法
  • RealClock:时间通过参考自动增加 System.currentTimeMillis()

这是以下的默认方法IClock#wait(...):

/**
     * Locks current thread for specified time
     *
     * @param timeUnit
     * @param dt
     */
    default void wait(TimeUnit timeUnit, long dt)
    {
        Lock lock = new ReentrantLock();
        scheduleIn(timeUnit, dt, lock::unlock);
        lock.lock();
    }
Run Code Online (Sandbox Code Playgroud)

问题

我希望模拟单元测试工作的当前方式是

  1. 启动线程
  2. 等到所有线程都完成或处于阻塞状态(我假设它们被阻止,他们已经调用IClock#wait(...))
  3. 如果所有线程都已完成,请完成.否则,将SimulatedClock时间增加一毫秒.

然而,真正发生的是:

  1. 启动线程
  2. 即使线程IClock#wait()第一次没有调用,也要开始增加时间.

所以,我需要做的是能够确定何时完成或阻止所有线程.虽然可以这样做Thread#getState(),但我宁愿使用更优雅且可以使用的解决方案ForkJoinPool.

完整代码

GitHub上

SimulatedClock

package com.team2502.ezauton.utils;

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

public class SimulatedClock implements IClock
{

    private long time = 0;

    private List<Job> jobs = new ArrayList<>();

    public SimulatedClock() {}

    public void init()
    {
        init(System.currentTimeMillis());
    }

    public void init(long time)
    {
        setTime(time);
    }

    /**
     * Add time in milliseconds
     *
     * @param dt millisecond increase
     * @return The new time
     */
    public long addTime(long dt)
    {
        setTime(getTime() + dt);
        return getTime();
    }

    /**
     * Adds time with units
     *
     * @param timeUnit
     * @param value
     */
    public void addTime(TimeUnit timeUnit, long value)
    {
        addTime(timeUnit.toMillis(value));
    }

    /**
     * Add one millisecond and returns new value
     *
     * @return The new time
     */
    public long incAndGet()
    {
        return addTime(1);
    }

    /**
     * Increment a certain amount of times
     *
     * @param times
     */
    public void incTimes(long times, long dt)
    {
        long init = getTime();
        long totalDt = times * dt;
        for(int i = 0; i < times; i++)
        {
            if(!jobs.isEmpty())
            {
                addTime(dt);
            }
            else
            {
                break;
            }
        }
        setTime(init + totalDt);
    }

    /**
     * Increment a certain amount of times
     *
     * @param times
     * @return
     */
    public void incTimes(long times)
    {
        incTimes(times, 1);
    }

    @Override
    public long getTime()
    {
        return time;
    }

    public void setTime(long time)
    {
        jobs.removeIf(job -> {
            if(job.getMillis() < time)
            {
                job.getRunnable().run();
                return true;
            }
            return false;
        });

        this.time = time;
    }

    @Override
    public void scheduleAt(long millis, Runnable runnable)
    {
        if(millis < getTime())
        {
            throw new IllegalArgumentException("You are scheduling a task for before the current time!");
        }
        jobs.add(new Job(millis, runnable));
    }

    private static class Job
    {
        private final long millis;
        private final Runnable runnable;

        public Job(long millis, Runnable runnable)
        {
            this.millis = millis;
            this.runnable = runnable;
        }

        public long getMillis()
        {
            return millis;
        }

        public Runnable getRunnable()
        {
            return runnable;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

模拟

package com.team2502.ezauton.command;

import com.team2502.ezauton.utils.SimulatedClock;

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

public class Simulation
{

    private final SimulatedClock simulatedClock;
    private List<IAction> actions = new ArrayList<>();

    public Simulation()
    {
        simulatedClock = new SimulatedClock();
    }

    public SimulatedClock getSimulatedClock()
    {
        return simulatedClock;
    }

    public Simulation add(IAction action)
    {
        actions.add(action);
        return this;
    }

    /**
     * @param timeoutMillis Max millis
     */
    public void run(long timeoutMillis)
    {
        simulatedClock.init();

        actions.forEach(action -> new ThreadBuilder(action, simulatedClock).buildAndRun());

        simulatedClock.incTimes(timeoutMillis);

        // Need to wait until all threads are finished
        if(!ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.SECONDS))
        {
            throw new RuntimeException("Simulator did not finish in a second.");
        }

    }

    public void run(TimeUnit timeUnit, long value)
    {
        run(timeUnit.toMillis(value));
    }
}
Run Code Online (Sandbox Code Playgroud)

示例单元测试

@Test
public void testSimpleAction()
{
    AtomicBoolean atomicBoolean = new AtomicBoolean(false);
    Simulation simulation = new Simulation();
    simulation.add(new DealyedAction((TimeUnit.SECONDS, 5) -> atomicBoolean.set(true)));
    simulation.run(TimeUnit.SECONDS, 100);
    Assert.assertTrue(atomicBoolean.get());
}
Run Code Online (Sandbox Code Playgroud)