在学习和工作中,我们自己都需要做很多的事情,事情大多都是我们亲自一件一件去完成,每件事情都是自己动手,那在做事情的花费时间就是每件事情的耗时总和。但我们都梦想着有一天能当上领导,当有任务的时候,我们可以交给下属去做,我们直接获取结果就好,不用关注做事情的过程,这样就可以提高我们处理事情的效率。那这种情景在计算机中是如何模拟呢?
在介绍线程之前,我们先介绍一下计算机中的任务执行:首先任务需放在内存,当得到调度的时候就需要分配CPU时间,有了CPU支持,任务才可以执行。常见的执行方式有两种:当我们采用一种串行的方式去执行任务,即CPU处理好当下的任务,才会去处理下一个任务,这样的执行时间就是每个任务时间的总和。
当在性能要求较高的场景下,我们会采用一种并发的方式去执行任务,在每个线程执行任务时,会获得CPU时间片,在这一点时间内可以处理当前任务。在并发的方式中,每个任务都能通过一定的方式获取到一部分CPU时间。这样宏观下就可以看到多个任务在一起执行,而且并发也可以很好地利用CPU的性能。
/** * 堆代码 duidaima.com * 打印方法 */ public static void doPrint(){ try{ Thread.sleep(2000); // 模拟该方法处理时花费的时间 } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("--- doPrint ---"); }方法2:模拟我们敲代码的方法
/** * 编写代码方法 */ public static void doCode(){ try{ Thread.sleep(3000);// 模拟该方法处理时花费的时间 } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("--- doCode ---"); }情景一 所有的事情自己做
/** * 串行调用 * @param args */ public static void main(String[] args) { long start = System.currentTimeMillis(); doCode(); doPrint(); System.out.println(System.currentTimeMillis() - start); }如代码所示,我们首先记录开始时间,然后串行去处理方法2和方法1。
--- doCode --- --- doPrint --- 5005如结果所示,串行执行方法的时间花费是两个方法的总和,这就相当于敲代码和打印这两件事自己来做,这种串行也是我们平时常见的编程方式,虽然很方便,但是并没有很好地利用计算机的性能,可能会导致无意义的等待。
public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); // 堆代码 duidaima.com // 建立一个线程,找个人去打印 Thread threadA = new Thread(() -> { try { doPrint(); } catch (Exception e) { throw new RuntimeException(e); } }, "threadA"); // 启动一个线程 threadA.start(); doCode(); // 阻塞等待线程结果 threadA.join(); // 打印花费的时间 System.out.println(System.currentTimeMillis() - start); }如上代码所示,使用Thread建立了一个线程threadA,让线程去处理打印方法,然后使用start()方法启动线程,最后使用join()等待线程threadA执行完毕。
--- doPrint --- --- doCode --- 3032如上结果所示,使用线程后,时间是doCode多一点点,这样使用线程后main线程就只需处理一个方法,执行效率得到了极大提升。
// 找到两个人进行处理 public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); Thread threadA = new Thread(() -> { try { doPrint(); } catch (Exception e) { throw new RuntimeException(e); } }, "threadA"); Thread threadB = new Thread(() -> { try { doCode(); } catch (Exception e) { throw new RuntimeException(e); } }, "threadB"); // 启动线程 threadA.start(); threadB.start(); // 阻塞当前线程,等待两个线程运行结束 threadA.join(); threadB.join(); System.out.println(System.currentTimeMillis() - start); }如上所示,采用了两个线程去处理编码和打印两个方法。
--- doPrint --- --- doCode --- 3047大家可能有疑惑怎么时间和情景二结果一致呢?这是因为在Main方法(线程)中我们需要等待两个线程的运行结果,在代码中我们使用到了join()方法,这个方法是一种同步阻塞的方法,即他会阻塞Main线程,等待threadA和threadB两个线程运行结束。又因为是用了两个线程,所以等待的时间就是较长方法的时间。
private final static int AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors(); private final static ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(AVAILABLE_PROCESSORS, AVAILABLE_PROCESSORS * 2, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>(5), new ThreadPoolExecutor.CallerRunsPolicy());如上我们就建立了一个线程池POOL_EXECUTOR啦,其中AVAILABLE_PROCESSORS是为了获取计算机的核心数量;然后使用了ThreadPoolExector显示创建了一个线程池POOL_EXECUTOR。
public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); POOL_EXECUTOR.execute(() -> { try { doPrint(); } catch (InterruptedException e) { throw new RuntimeException(e); } }); POOL_EXECUTOR.execute(() -> { try { doCode(); } catch (InterruptedException e) { throw new RuntimeException(e); } }); System.out.println(System.currentTimeMillis() - start); // 挂起当前线程 Thread.currentThread().join(); }运行结果:
63 --- doPrint --- --- doCode ---如结果所示,使用了线程池,执行了我们的方法,并且main方法等待的时间大大缩小了,但是他的执行顺序是排到第一的。这其实是因为我们没有阻塞去等待其他的方法,所以在main线程中,先打印了时间,这也是并发的原因,线程的执行顺序是不能保证的。
//修改doPrint和doCode两个方法,添加返回值。 /** * 打印方法 */ public static String doPrint1(){ try{ Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("--- doPrint ---"); return "Print Task Done"; } /** * 编写代码方法 */ public static String doCode1(){ try{ Thread.sleep(3000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("--- doCode ---"); return "Code Task Done"; } public static void main(String[] args) throws ExecutionException, InterruptedException { long start = System.currentTimeMillis(); Future<?> doPrint = POOL_EXECUTOR.submit(() -> { try { doPrint1(); } catch (Exception e) { e.printStackTrace(); } }); Future<?> doCode = POOL_EXECUTOR.submit(() -> { try { doCode1(); } catch (Exception e) { e.printStackTrace(); } }); // 阻塞线程 doPrint.get(); doCode.get(); System.out.println(System.currentTimeMillis() - start); }运行结果:
--- doPrint --- --- doCode --- 3051如上所述,当线程使用execute()方法时,是不需要阻塞去获取结果的,这种可以使用到日志打印。使用submit()就可以使用get()方法去阻塞获取结果。