如何创建一个线程?
主要有三种方法:
第一种:继承Thread类,重写run方法;
第二种:实现Runnable接口,重写run方法。
第三种 实现Callable<>接口并重写call方法。
那么面试官一定会问这两种方法各自的优缺点在哪?
(答案:Runnable与Thread,前者的实现方式是实现其接口即可,后者的实现方式是继承其类。两者实现方式带来最明显的区别就是,由于Java不允许多继承,因此实现了Runnable接口可以再继承其他类,但是Thread明显不可以。 从Thread源码中也可以看到,当以Thread方式去实现资源共享时,实际上源码内部是将thread向下转型为了Runnable,实际上内部依然是以Runnable形式去实现的资源共享,
我们会得出一个结论,那就是使用方式二,因为面向对象提倡少继承,尽量多用组合。
Thread 类中的start() 和 run() 方法有什么区别?
这个问题经常被问到,但还是能从此区分出面试者对Java线程模型的理解程度。start()方法被用来启动新创建的线程,而且start()内部 调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启 动,start()方法才会启动新线程。
Java中Runnable和Callable有什么不同?
Runnable和Callable都代表那些要在不同的线程中执行的任务。Runnable从JDK1.0开始就有了,Callable是在 JDK1.5增加的。它们的主要区别是Callable的 call() 方法可以返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象。
线程池概念和优势
线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价
线程池的优势
(1)降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
(4)提供更强大的功能,延时定时线程池。
多线程优化及性能比较
第一种:ThreadPool(线程池)+CountDownLatch(程序计数器)
顾名思义,CountDownLatch为线程计数器,他的执行过程如下:首先,在主线程中调用await()方法,主线程阻塞,然后,将程序计数器作为参数传递给线程对象,最后,每个线程执行完任务后,调用countDown()方法表示完成任务。countDown()被执行多次后,主线程的await()会失效。
第二种:Fork/Join框架
Jdk从版本7开始,出现了Fork/join框架,从字面来理解,fork就是拆分,join就是合并,所以,该框架的思想就是。通过fork拆分任务,然后join来合并拆分后各个人物执行完毕后的结果并汇总。
比如,我们要计算连续相加的几个数,2+4+5+7=?,我们利用Fork/join框架来怎么完成呢,思想就是拆分子任务,我们可以把这个运算拆分为两个子任务,一个计算2+4,另一个计算5+7,这是Fork的过程,计算完成后,把这两个子任务计算的结果汇总,得到总和,这是join的过程。
Fork/Join框架执行思想:首先,分割任务,使用fork类将大任务分割为若干子任务,这个分割过程需要按照实际情况来定,直到分割出的任务足够小。然后,join类执行任务,分割的子任务在不同的队列里,几个线程分别从队列里获取任务并执行,执行完的结果放到一个单独的队列里,最后,启动线程,队列里拿取结果并合并结果。
使用Fork/Join框架要用到几个类,关于类的使用方式可以参考JDK的API,使用该框架,首先需要继承ForkJoinTask类,通常,只需要继承他的子类RecursiveTask或RecursiveAction即可,RecursiveTask,用于有返回结果的场景,RecursiveAction用于没有返回结果的场景。ForkJoinTask的执行需要用到ForkJoinPool来执行,该类用于维护分割出的子任务添加到不同的任务队列。
第三种JDK8并行流,下面对这几种方式的多线程处理性能做一下比较总结。
并行流是jdk8的新特性之一,思想就是将一个顺序执行的流变为一个并发的流,通过调用parallel()方法来实现。
并行流将一个流分成多个数据块,用不同的线程来处理不同的数据块的流,最后合并每个块数据流的处理结果,类似于Fork/Join框架。
并行流默认使用的是公共线程池ForkJoinPool,他的线程数是使用的默认值,根据机器的核数,我们可以适当调整线程数的大小。线程数的调整通过以下方式来实现
以单线程作为参考,耗时最长的还是原生的Fork/Join框架,这里边尽管配置了线程池的数量,但效果较精确配置了线程池数量的JDK8并行流较差。并行流实现代码简单易懂,不需要我们写多余的for循环,一个parallelStream方法全部搞定,代码量大大的减少了,其实,并行流的底层还是使用的Fork/Join框架,这就要求我们在开发的过程中灵活使用各种技术,分清各种技术的优缺点,从而能够更好的为我们服务。