JAVA创建线程的方式有三种,分别是:
- 继承Thread
- 实现Runnable
- 实现Callable
1、继承Thread
通过继承抽象类Thread,创建MyThreadExtends对象,调用其start方法。
package Thread; import java.util.concurrent.*; public class TestThread { public static void main(String[] args) throws Exception { testExtends(); } public static void testExtends() throws Exception { Thread t1 = new MyThreadExtends(); Thread t2 = new MyThreadExtends(); t1.start(); t2.start(); } } class MyThreadExtends extends Thread { @Override public void run() { System.out.println("通过继承Thread,线程号:" + currentThread().getName()); } }
2、实现Runnable
通过实现接口Runnable,创建Runnable对象r,然后将r作为参数创建Thread对象t,最后调用t的start方法。
package Thread; import java.util.concurrent.*; public class TestThread { public static void main(String[] args) throws Exception { testImplents(); } public static void testImplents() throws Exception { MyThreadImplements myThreadImplements = new MyThreadImplements(); Thread t1 = new Thread(myThreadImplements); Thread t2 = new Thread(myThreadImplements, "my thread -2"); t1.start(); t2.start(); } } class MyThreadImplements implements Runnable { @Override public void run() { System.out.println("通过实现Runable,线程号:" + Thread.currentThread().getName()); } }
3、实现Callable
通过实现接口Callable ,创建Callable 对象c,然后以c为参数创建FutureTask 对象f,再以f为参数创建Thread对象t,调用t的start方法。此方法能通过FutureTask 对象f的get方法接收返回值。
package Thread; import java.util.concurrent.*; public class TestThread { public static void main(String[] args) throws Exception { testCallable(); } public static void testCallable() throws Exception { Callable callable = new MyThreadCallable(); FutureTask task = new FutureTask(callable); new Thread(task).start(); System.out.println(task.get()); Thread.sleep(10);//等待线程执行结束 //task.get() 获取call()的返回值。若调用时call()方法未返回,则阻塞线程等待返回值 //get的传入参数为等待时间,超时抛出超时异常;传入参数为空时,则不设超时,一直等待 System.out.println(task.get(100L, TimeUnit.MILLISECONDS)); } } class MyThreadCallable implements Callable { @Override public Object call() throws Exception { System.out.println("通过实现Callable,线程号:" + Thread.currentThread().getName()); return 10; } }
4、三者对比
- 继承Thread使用继承方式,由于JAVA中使用单继承方式,故对编码局限性较高;其余两种方式其实最后都是创建了Thread对象。
- Runable使用实现接口方式,属于常用方式。
- Callable能接收返回值,不过实现相比Runable较为繁琐,再不关注返回值的情况下不使用。
在实际项目使用中,多线程都与线程池同时出现,故在此说明线程池的创建(有很多博文认为线程池是实现多线程的一种方式,我并不认可。我认为线程池只是创建线程的一种方式,并不是实现)。
在这里介绍的是使用java.util.concurrent包下的Executors创建线程池。
1、使用代码示例
public static void testExecutor() throws Exception { //创建单个线程的线程池(核心线程数与最大线程数都为1) ExecutorService executor1 = Executors.newSingleThreadExecutor(); //创建定长线程池(核心线程数与最大线程数一样) ExecutorService executor2 = Executors.newFixedThreadPool(4); //创建定长线程池(定核心线程数,最大线程数为int的最大值) ExecutorService executor3 = Executors.newScheduledThreadPool(4); //创建无限长线程池(核心线程数为0,最大线程数为int最大值) ExecutorService executor4 = Executors.newCachedThreadPool(); //创建线程池(所有参数自定义) ExecutorService executor5 = new ThreadPoolExecutor(4, 20, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy()); MyThreadExecutor myThreadImplements = new MyThreadExecutor(); MyThreadCallable myThreadCallable = new MyThreadCallable(); for (int i = 0; i < 4; i++) { executor2.execute(myThreadImplements); Future futureTask = executor1.submit(myThreadCallable); } }
2、创建线程池方法详解
根据上述得知,Executors提供了五种实现方式,其作用如上,我们接下来看其源码。
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>())); } public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } //ScheduledThreadPoolExecutor继承ThreadPoolExecutor public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue()); } public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,RejectedExecutionHandler handler) { if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ?null :AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
通过关注以上源码,最终发现最后都是调用的ThreadPoolExecutor的 public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)构造器,故重点关注此构造器。此构造器有7个参数,每个参数含义如下:
- corePoolSize 线程池核心线程数量。当新任务在方法 execute(java.lang.Runnable) 中提交时,如果运行的线程少于 corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。
- maximumPoolSize 线程池最大线程数量。如果运行的线程多于 corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程。
- keepAliveTime 当线程数大于corePoolSize 时,则这些多出的线程在空闲时间超过 keepAliveTime 时将会终止。
- unit 参数keepAliveTime 的时间单位。
- workQueue 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。
A.队列保存策略:若运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队;若运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程;若无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。
B.队列选取通常策略:
a.直接提交:直接提交队列(如SynchronousQueue),此种队列将任务直接提交给线程而不保存他们,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。如newCachedThreadPool
b.无界队列。使用无界队列(如 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize(因此,maximumPoolSize 的值也就无效了)。当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。如newFixedThreadPool和newSingleThreadExecutor
c.有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。 - threadFactory 执行程序创建新线程时使用的工厂。若参数为空,则在同一个 ThreadGroup 中使用 Executors.defaultThreadFactory() 创建线程,并且这些线程具有相同的 NORM_PRIORITY 优先级和非守护进程状态。
- handler 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。一般不会自定义,而使用默认。
3、创建线程池方法对比
通过上述描述,对创建线程池方法进行对比分析:
- 若自身对性能有很大需求,且对于机器性能、代码能力等有足够自信,使用ThreadPoolExecutor的构造方法是最合适的。
- newSingleThreadExecutor()是构造只有一个线程的线程池,保存任务的队列是无界的,可接收所有任务,但是同时只有一个线程执行任务
- newFixedThreadPool()是构造可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程,在需要时使用提供的 ThreadFactory 创建新线程。
- newScheduledThreadPool()创建一个可重用线程池(最大线程数为int最大值),它可安排在给定延迟后运行命令或者定期地执行(因为使用DelayedWorkQueue()队列)。
- newCachedThreadPool()是构造一个可根据需要创建新线程的线程池(最大线程数为int最大值),但是在以前构造的线程可用时将重用它们。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。
4、线程池提交线程方法
executor2.execute(myThreadImplements); Future futureTask = executor1.submit(myThreadCallable); ScheduledFuture scheduledFuture = executor3.schedule(myThreadImplements, 100L, TimeUnit.SECONDS);
如上,提交方式有execute,submit,schedule三种(不是所有线程都可用此三种):
- execute属于ExecutorService的父接口Executor的方法,所有线程池都具有此方法。
- submit属于ExecutorService的方法。具有返回值Future ,当内部线程使用实现Callable方式是现实时(具有返回值),可以接收返回值。上述所有线程池都具有此方法。
- schedule属于ExecutorService的子孙类ScheduledThreadPoolExecutor方法。具有返回值Future ,当内部线程使用实现Callable方式是现实时(具有返回值),可以接收返回值。能实现定时任务和延迟任务。上述线程池中只有newScheduledThreadPool创建的线程池具有此方法。