Java并发编程-线程的创建方式
总的来说Java中线程的创建方式包含以下四种:
- 继承Thread类,重写run方法
- 实现Runable接口,实现run方法
- 实现Callable接口,实现call方法,借助FutureTask执行任务并通过get方法获取最终结果
- 通过线程池创建
一、继承Thread类
public class MyThread extends Thread { private String name; public MyThread(String name){ this.name = name; } @Override public void run(){ // 要执行的内容 for(int i = 0 ; i < 10 ; i ++){ System.out.println("Thread start : " + this.name + ",i= " + i); } } }
使用:
public class MyThreadTest{ public static void main(String[] args){ MyThread mt1 = new MyThread("Thread-1"); MyThread mt2 = new MyThread("Thread-2"); mt1.start(); mt2.start(); } }
注意点:该方式最简单,在访问当前线程时不需要调用Thread.currentThread()方法,用this即可,但由于Java的单继承性质,该类继承了Thread后就不可再继承其他的类
二、继承Runnable接口
public class MyRunnable implements Runnable{ @Override public void run(){ for(int i = 0; i < 10; i++){ System.out.println("Thread start:" + Thread.currentThread().getName() + ".i = " + i); } }
}
使用:
public class MyRunnableTest{ public static void main(String[] args){ MyRunnable mr1 = new MyRunnable(); MyRunnable mr2 = new MyRunnable(); Thread t1 = new Thread(mr1); Thread t2 = new Thread(mr2); t1.start(); t2.start(); } }
注意点:相比较继承Thread,该方式常被用于线程的创建,因为Java支持类实现多个接口,且多个线程可以实现资源共享
资源共享:
public class MyRunnable implements Runnable{ private int i = 0; @Override public void run(){ while(i < 20){ synchronized (this){ // 线程同步,保证共享数据的一致性 if(i < 20){ // 保证累加结果不超出范围 System.out.println("Thread start : " + Thread.currentThread().getName() + ",i= " + i); i++; } } } } }
public class MyRunableTest{ public static void main(String[] args){ MyRunnable mr = new MyRunnable(); Thread t1 = new Thread(mr); Thread t2 = new Thread(mr); t1.start(); t2.start(); } }
运行结果:
Thread start : Thread-1,i= 0 Thread start : Thread-1,i= 1 Thread start : Thread-1,i= 2 Thread start : Thread-1,i= 3 Thread start : Thread-1,i= 4 Thread start : Thread-1,i= 5 Thread start : Thread-1,i= 6 Thread start : Thread-1,i= 7 Thread start : Thread-1,i= 8 Thread start : Thread-1,i= 9 Thread start : Thread-1,i= 10 Thread start : Thread-0,i= 11 Thread start : Thread-0,i= 12 Thread start : Thread-0,i= 13 Thread start : Thread-0,i= 14 Thread start : Thread-0,i= 15 Thread start : Thread-0,i= 16 Thread start : Thread-0,i= 17 Thread start : Thread-0,i= 18 Thread start : Thread-0,i= 19 Process finished with exit code 0
三、继承Callable接口
import java.util.concurrent.Callable; public class MyCallable implements Callable<String> { @Override public String call() throws Exception{ String value="test"; // 要返回的数据 System.out.println("Ready to work"); Thread.currentThread().sleep(5000); System.out.println("task done"); return value; } }
使用:
public class MyCallableTest{ public static void main(String[] args) throws ExecutionException, InterruptedException { MyCallable mc = new MyCallable(); FutureTask<String> task = new FutureTask(mc); new Thread(task).start(); // 启动线程 if(!task.isDone()){ System.out.println("Task has not finished, please wait!"); } System.out.println("Task return is " + task.get()); } }
注意点:该方式是JDK5.0之后新增的创建线程的方式,相比较前两种,该方式的最大特点就是可以获取线程执行结果的返回值,但要借助FutureTask类实现
FutureTask类介绍:
该类是Future接口的唯一实现类
Future接口可以对具体的Runable或Callable任务的执行进行取消、查询是否完成、获取执行结果等操作,具体过程见下图:
该类同时实现了Runable接口与Future接口,既可以作为Runable被线程执行,也可以作为Future得到Callable的返回值
四、线程池
该方式也是JDK5.0中新增的线程创建方式,由于一下优点在开发中常用该方式创建线程
- 提高了响应速度,线程池初始化时会创建一定数量的线程,当需要执行任务时可以直接使用创建好的线程,减少了创建线程的时间,且任务执行完毕后线程不立即销毁,继续执行其他任务,减少了销毁线程的时间
- 通过线程池可以对所有的线程进行统一管理
- 降低了资源消耗,线程池中创建好的线程可以重复使用,降低了创建线程和销毁线程时的资源消耗
- 隔离线程环境
几个重要概念:
核心线程数corePoolSize:线程池中已创建的线程,默认情况下这些这些线程不会被销毁,只有设置了allowCoreThreadTimeOut=true后核心线程可在限制指定的一段时间后销毁
最大线程数maximumPoolSize:线程池中允许创建的最大线程数目
保持活跃时间keepAliveTime:当线程池中的线程数目大于核心线程数时,线程可以空闲的最大时间(线程空闲时间超过该时间时,该线程会被回收直至线程池中线程数目等于corePoolSize,若设置了allowCoreThreadTimeOut=true则也回收核心线程);该时间的单位由unit(TimeUnit类的实例对象)指定
任务队列workQueue:用于缓存还未执行的任务
拒绝策略RejectedExecutionHandler:当任务队列已满并且线程池中线程数目已达到最大值时对新提交任务的处理方式
工作原理:
当某一任务需要执行时:
- 若线程池中线程数目未达到核心线程数目时,创建新线程执行任务
- 若已达到核心线程数目时,
- 若工作队列未满,将任务添加到工作队列中
- 否则创建新线程执行任务
- 若工作队列已满,并且线程池中线程数目达到了最大线程数,则执行拒绝策略
主要API:
(一)工具类Executors:线程池的工厂类,用于创建并返回不同类型的线程池
注意点:线程池在初始化时只对参数进行了设置,并不会创建线程,只有当调用exectue()方法后才会创建线程
- Executors.newFixedThreadPool(n):创建一个具有固定线程数目的线程池,但任务队列无界(任务队列长度被设置为Integer.MAX_VALUE,相当于队列无界)
- 该线程池核心线程数目与最大线程数目均为n
- keepAliveTime初始设置为0
- 任务队列为LinkedBlockingQueue,且队列长度默认为Integer.MAX_VALUE,因此加入大量任务可能会发生OOM(Out of memory)内存溢出问题
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池,用于处理大量短期异步任务
- 该线程池核心线程数目为0,最大线程数目为Integer.MAX_VALUE,因此可能会因为创建大量线程而造成OOM问题
- keepAliveTime初始设置为60,单位为TimeUnit.SECONDS(秒)
- 任务队列为SynchronousQueue,该队列没有存储任务的能力,因此执行任务时要不断地创建线程,该队列详细内容可参考【Java并发之SynchronousQueue实现原理】
- 该线程池核心线程数目为0,最大线程数目为Integer.MAX_VALUE,因此可能会因为创建大量线程而造成OOM问题
- Executors.newScheduledThreadPool(n):创建一个有n个核心线程的可周期性执行任务的线程池
- 该线程核心线程数目为n,最大线程数目为Integer.MAX_VALUE
- keepAliveTime初始设置为0,单位为NANOSECONDS(纳妙,微妙的千分之一)
- 任务队列为DelayWorkQueue,
- Executors.newSingleExecutor():创建只有一个线程的线程池,保证任务的串行执行
- 该线程核心线程数目为1,最大线程数目为1
- keepAliveTime初始设置为0,单位为TimeUnit.MILLSECONDS(毫秒)
- 任务队列为LinkedBlockingQueue,队列长度默认为Integer.MAX_VALUE
综上所述, 以上四种方式对最大线程数目或工作队列长度均默认设置为Integer.MAX_VALUE(没有进行控制),存在OMM问题,因此以上创建线程池的方式在实际使用中不推荐,这些实现方式可以看做是一些模板,在实际使用时用另一种方式创建线程池
(二)ThreadPoolExecutor类:通过该类,传入核心线程数corePoolSize、最大线程数maximumPoolSize、空闲时间keepAliveTime、时间单位unit、工作队列workQueue、拒绝策略RejectedExecutionHandler、线程创建工厂ThreadFactory创建符合实际需求的线程池,Executors工具类中的四种线程池实现方式实际上也是通过创建ThreadPoolExecutor的对象实现的
ThreadPoolExector类的继承结构:
上层接口ExectorService中有三个主要方法:
- void execute(Runnable command):该方法没有返回值,一般用来执行Runnable
- <T> Future<T> submit(Callable<T> task):该方法有可以获取返回值,一般用于执行Callable
- void shutdown():关闭连接池
AbstractExecutorService虚拟类中实现submit()
ThreadPoolExecutor实现了execute()和shutdown()