目录
一.创建线程的4种方式
二.创建线程方式有什么区别?
一.创建线程的4种方式
(1)写一个类继承Thread,覆盖重写run方法
(2)创建一个Runnable类型的对象,实现run()方法,传入Thread的构造方法中
(3)实现Callable接口,实现call()方法;
(4)通过Executor的工具类创建线程池,通过线程池获取线程
(一)继承Thread,覆盖重写run方法
继承java.lang包下的Thread类,覆盖重写thread类的run()方法,在run方法中实现运行在线程上的代码;如果线程类直接继承 Thread 类,其代码结构大致如下:
创建一个子线程,就是创建一个Thread类型的对象,并且启动。
Thread类:
* 所属包:java.lang;
成员变量:
* private Runnable target;
* 构造方法:
* public Thread();
public Thread(Runnable r);
* public Thread(Runnable r,String name);
静态方法:
* static Thread currentThread();
* 成员方法:
* void start();//该方法不能执行多次
* void run();
* String getName();
* void setName(String name);
先看一段单线程序
package com.apesource.demo02.多线程;
public class Example01 {public static void main(String[] args) {// TODO Auto-generated method stubMyThread myThread=new MyThread();//创建MyThread实例对象myThread.run();//调用MyThread类的run()方法while(true) {System.out.println("Main方法在运行");}}
}
class MyThread{public void run() {while(true) { //该循环是一个死循环System.out.println("MyThread类的run()方法在运行");}}
}
运行结果:
可以看出程序一直打印"MyThread"类的run()方法在运行,这是因为该程序是一个单线程程序,当调用MyThread类的run()方法时,遇到死循环,循环会一直进行.因此MyThread类的打印语句将永远执行,而main()方法中的打印语句无法得到执行。
如果希望文件中的两个while循环中的打印语句能够并发执行,就需要实现多线程。为此JDK中提供了一个线程类Thread,通过继承Thread类,并重写Thread类中的run()方法可实现多线程.在Thread类中,提供了一个start()方法,用于启动新线程.线程启动后,虚拟机会自动调用run()方法,如果子类重写了,该方法便会执行子类中的方法.通过修改上面的代码来演示如何通过继承Thread类的方式来实现多线程。
package com.apesource.demo02.多线程;
public class Example01 {public static void main(String[] args) {// TODO Auto-generated method stubMyThread myThread=new MyThread();//创建MyThread实例对象myThread.start();//开启线程while(true) {System.out.println("Main方法在运行");}}
}
class MyThread extends Thread{public void run() {while(true) { //该循环是一个死循环System.out.println("MyThread类的run()方法在运行");}}
}
运行结果:
运行结果可以看到,两个while循环中的打印语句轮流执行了,说明实现了多线程.为了更好的理解单线程和多线程的执行过程,通过一个图例分析一下:
单线程的程序在运行时,会按照代码的调用顺序执行.而在多线程中,main()方法和MyThread类的run()方法可以同时运行,互不影响,这正是单线程和多线程的区别.
为什么要使用多线程?
本计算机底层来说:线程是程序执行的最小单位,域程间的对接和调度的成本远小于进程。另外,多核CPU时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,只会有一个CPU核心被利用到,而创建多个线程就可以让多个CPU核心被利用到,这样就提高了CPU的利用率。
从互联网发展趋势来说:现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础, 利用好多线程机制可以大大提高系统整体的并发能力以及性能;
练习:
请使用继承Thread类的方式开启两个线程
(1)第一个线程的名字设置为:a 第二个线程的名字设置为:b
(2)第一个线程里面实现计算1+2+3+4+....+100的和
(3)第二个线程里面实现计算1+2+3+4+....+200的和
程序最终打印结果:
代码:
public class Test02 {public static void main(String[] args) {Thread t1 = new SubThread1();t1.setName("a");t1.start();Thread t2 = new SubThread2();t2.setName("b");t2.start();}
}class SubThread1 extends Thread{@Overridepublic void run() {//获取线程的名称String name = super.getName();int count = 0;for (int i = 1; i <= 100; i++) {count+=i;}System.out.println(name+":"+count);}
}class SubThread2 extends Thread{@Overridepublic void run() {//获取线程的名称String name = super.getName();int count = 0;for (int i = 1; i <= 200; i++) {count+=i;}System.out.println(name+":"+count);}
}
运行结果:
(二)实现Runnable接口创建多线程
在第二个例子中通过继承Thread类实现了多线程,但是这种方式有一定的局限性.因为java中只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类,例如学生类Student继承Person类,就无法通过继承Thread类创建线程。
为了克服这种弊端,Thread类提供了另外一个构造方法Thread(Runnable target)构造方法创建线程对象时,只需为该方法传递一个实现了Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口的类中的run()方法作为运行代码,而不需要调用Thread类中的run()方法。
通过案例来演示:
public class Test03 {public static void main(String[] args) {//创建Runnable类型的对象Runnable r = new MyRunnable();//Thread t = new Thread(r,"线程1");//创建线程对象t.start();Thread t2 = new Thread(r,"线程2");t2.start();// //通过匿名内部类的形式创建Runnable类型的对象
// Runnable r = new Runnable() {
// @Override
// public void run() {
// System.out.println(Thread.currentThread().getName());
// }
// };
// Thread t = new Thread(r,"线程1");
// t.start();
// Thread t2 = new Thread(r,"线程2");
// t2.start();}
}class MyRunnable implements Runnable{@Overridepublic void run() {//获取当前线程Thread currentThread = Thread.currentThread();//获取线程的名称String name = currentThread.getName();System.out.println(name);}
}
MyRunnable类实现了 Runnable接口,并重写了Runnable接口中的run()方法,通过Thread类的构造方法将MyRunnable类的实例对象作为参数传入.从运行结果可以看出,main()方法和run()方法都执行了,说明实现了多线程。
(三)通过线程池获取线程
线程池的作用:
线程池作用就是限制系统中执行线程的数量。根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
线程池好处:
1.降低资源消耗。使用了线程池后线程执行完任务不销毁,再来任务还可以使用该线程执行。
2.提高响应速度。创建线程池,可以提前启动线程,等待任务的到来。
3.提高线程的可管理性。提前设置线程的数量
线程池执行流程:
- 提交一个任务,线程池里存活的核心线程数小于线程数corePoolSize时,线程池会创建一个核心线程去处理提交的任务。
- 如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。
- 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务。
- 如果当前的线程数达到了maximumPoolSize ,还有新的任务过来的话,直接采用拒绝策略处理。
线程池如何创建?
通过Executors创建
java.util.concurrent.ExecutorService; 是java线程池框架的主要接口,用Future保存任务的运行状态。
Executors:
静态方法:
static ExecutorService newFixedThreadPool(int nThread);核心线程数
ExecutorService接口:
(1) void execute(Runnable r);提交任务到线程池
(2) void shutdown();
核心参数的作用:
线程池可以通过ThreadPoolExecutor来创建
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime,TimeUnit unit,
BlockingQueue <Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
核心参数的作用:
- corePoolSize :线程池核心线程数最大值
- maximumPoolSize :线程池最大线程数大小
- keepAliveTime :线程池中非核心线程空闲的存活时间大小
- unit :线程空闲存活时间单位
- workQueue :存放任务的阻塞队列
- threadFactory :用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
- handler :线程池的饱和策略事件。
线程池都有那几种工作队列:
- AraylockingQueue (有界队列) :用数组实现的有界阻籍队列,按FIFO排序。
- LinkedBlockingQueue (可设置容量队列) : 基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是个无边界的阻塞队列,最大长度为Integer.MAX _VALUE,吞吐最通常要高于ArrayBlockingQuene,newFixedThreadPool线程池使用了这个队列;
- DelayedWorkQueue (延迟队列) :任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
- PriorityBlockingQueue (优先级队列) :具有优先级的无界阻塞队列;
- SynchronousQueue (同步队列) : 不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高LinkedBlockingQuene,newCachedThreadPool线程地使用这个队列。
线程池的四种拒绝策略是什么?
- AbortPolicy :默认策略,丢弃任务共抛出ijetedExctionExcetion异常;
- DiscardPolicy :丢弃任务,不抛出异常;
- DiscardOldestPolicy :丢弃队列中的末尾任务( 最旧的任务,也就是最早进入队列的任务)后,继续将当前任务提交给线程池;
- CallerRunsPolicy :交给调用线程池的线程进行处理(谁调用, 谁处理)
常见的线程池及使用场景:
newFixedThreadPool (固定数目线程的线程池)
线程池特点:
- 核心线程数和最大线程数大小一样
- 没有所谓的非空闲时间,即keepAliveTime为0
- 阻塞队列为无界队列LinkedBlockingQueue
工作机制:
- 提交任务
- 如果线程数少于核心线程,创建核心线程执行任务
- 如果线程数等于核心线程,把任务添加到LinkedBlockingQueue阻塞队列
- 如果线程执行完任务,去阻塞队列取任务,继续执行
使用场景:
- FixedThreadPool适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。
newCachedThreadPool (可缓存线程的线程池)
线程池特点:
- 核心线程数为0
- 最大线程数为Integer.MAX _VALUE
- 阻塞队列是SynchronousQueue
- 非核心线程空闲存活时间为60秒
工作机制:- 提交任务
- 因为没有核心线程,所以任务直接加到SynchronousQueue队列。
- 判断是否有空闲线程,如果有,就去取出任务执行。
- 如果没有空闲线程,就新建一个线程执行。
- 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续存活下去;否则,被销毁。
使用场景:
- 用于并发执行大量短期的小任务。
newSingleThreadExecutor (单线程的线程池)
线程池特点:
- 核心线程数为1
- 最大线程数也为1
- 阻塞队列是LinkedBlockingQueue
- keepAliveTime为0
工作机制:
- 提交任务
- 线程池是否有一条线程在,如果没有,新建线程执行任务
- 如果有,将任务加到阻塞队列
- 当前的唯线程,从队列取任务,执行完一个,再继续取,一个线程夜以继日地干活。
使用场景:
- 适用于串行执行任务的场景, -个任务个任务地执行。
newScheduledThreadPool (定时及周期执行的线程池)
线程池特点:
- 最大线程数为Integer.MAX VALUE
- 阻塞队列是DelayedWorkQueue
- keepAliveTime为0
- scheduleAtFixedRate():按某种速率周期执行
- scheduleWithFixedDelay() :在某个延迟后执行
工作机制:
- 添加一个任务
- 线程池中的线程从DelayQueue中取任务
- 线程从DelayQueue中获取time大于等于当前时间的task
- 执行完后修改这个task的time为下次被执行的时间
- 这个task放回DelayQueue队列中
使用场景:
周期性执行任务的场景,需要限制线程数量的场景。
通过代码演示:
public class Test02 {public static void main(String[] args) {//创建Runnable类型的对象Runnable r = new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName());}};//创建固定线程池ExecutorService e = Executors.newFixedThreadPool(2);//执行任务e.execute(r);//关闭线程池e.shutdown();}
}
运行结果:
打印出来的线程名不是main,说明是通过线程池来创建的
第二个案例:sleep的目的
1.为了暂停当前线程,设置暂停时间时间结束系统继续调用该线程,减缓当前线程的执行。
2.如果有调用其他接口进行操作,可避免被误认为是恶意攻击
public class Test03 {public static void main(String[] args) {//创建Runnable类型的对象Runnable r = new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName());try {Thread.sleep(10000); //暂停10秒} catch (InterruptedException e) {e.printStackTrace();}}};Runnable r2 = new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName());}};//创建固定线程池ExecutorService e = Executors.newFixedThreadPool(2);//执行任务e.execute(r);e.execute(r);e.execute(r2);//关闭线程池e.shutdown();}
}
(3)ExecutorService接口:
Future submit(Runnable)提交任务到线程池并返回Future
Future<?> submit(Runnable r);
代码演示:
public class Test04 {public static void main(String[] args) {Runnable r = new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName());}};//创建固定线程池ExecutorService e = Executors.newFixedThreadPool(2);//执行任务e.submit(r);//关闭线程池e.shutdown();}
}
(4)ExecutorService接口:
Future submit(Callable)提交任务到线程池并返回Future,call方法的计算结果会封装到Future
<T> Future<T> submit(Callable<T> r);
代码演示:
public class Test05 {public static void main(String[] args) throws InterruptedException, ExecutionException {/** 通过匿名内部类的方式创建Callable类型的对象* * 线程执行完任务后,希望获取某个结果。*/Callable<String> c = new Callable<String>() {@Overridepublic String call() throws Exception {System.out.println(Thread.currentThread().getName());return "test callable";}};//创建固定线程池ExecutorService e = Executors.newFixedThreadPool(2);//执行任务Future<String> f = e.submit(c);//获取call方法的返回结果String result = f.get();System.out.println(result);//关闭线程池e.shutdown();}
}
二.创建线程方式有什么区别?
- 实现Runnable和实现Callable接口的方式基本相同,区别在于Callable接口线程执行call()方法后可以有返回值。
- 线程类实现Runnable接口或Callable接口,还可以继承其他类,但由于JAVA的单继承性,继承Thread类后,不能在继承其他类.推荐使用接口方式实现线程,系统扩展更灵活。
- 线程类实现Runnable接口或Callable接口,比较容易实现多个线程共享同一个target对象,非常适合实现使用多线程处理同一份资源的业务场景,从而可以将CPU,代码和数据分开,形成清晰的代码模型,体现面向对象的思想。
- 线程类实现Runnable接口或Callable接口如果需要访问当前线程,必须调用Thread.currentThread()方法。但继承Thread类后,只需要通过this即可;
- Thread/Runnable/Callable这三种线程创建方式,如果创建关闭频繁会消耗系统资源,影响性能,而使用线程池可以不用线程的时候放回线程池,用的时候再从线程池取,目前在项目开发中主要使用线程池。