一、线程创建
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创建的线程池具有此方法。