Java多线程笔记
Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程的概念
进程:
每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源分配的最小单位)
线程:
同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)
多进程是指操作系统能同时运行多个任务(程序)。
多线程是指在同一程序中有多个顺序流在执行。
- 每个进程至少有一个线程,若程序只有一个线程,那就是程序本身。
- 现代计算机支持多个线程的并发执行。
- 同一个进程的多个线程共享IO,内存资源。
- 线程是有状态的。
注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即一个 cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。
创建多线程的三种方式以及区别
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Java可以用三种方式来创建线程,如下所示:
1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程
继承Thread类创建线程
通过继承Thread类来创建并启动多线程的一般步骤如下
1】定义Thread类的子类。
2】重写该类的**run()**方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
3】创建Thread子类的实例,也就是创建了线程对象。
4】启动线程,即调用线程的**start()**方法。
public class MyThread extends Thread{ //继承Thread类 |
实现Runnable接口创建线程(推荐)
优点:将线程的任务从线程的子类中分离出来,进行单独的封装;面向接口编程,避免单继承局限。
步骤如下:
1】定义Runnable接口的实现类。
2】重写**run()**方法,这个 run() 方法和Thread中的run()方法一样是线程的执行体。
3】通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。(为什么?因为线程的任务都封装在Runnable接口子类的对象的run方法中,所以要在线程对象创建时就必须明确要运行的任务)
4】通过调用线程对象的**start()**方法来启动线程。
public class MyThread2 implements Runnable {//实现Runnable接口 |
使用Callable和Future创建线程
和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。
-
call()方法可以有返回值
-
call()方法可以声明抛出异常
Java5 提供了 Future 接口来代表 Callable 接口里 call() 方法的返回值,并且为 Future 接口提供了一个实现类FutureTask,这个实现类既实现了 Future 接口,还实现了 Runnable 接口,因此可以作为 Thread 类的 target。在 Future 接口里定义了几个公共方法来控制它关联的 Callable 任务。
- boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务
- get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
- get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException
- boolean isDone():若Callable任务完成,返回True
- boolean isCancelled():如果在Callable任务正常完成前被取消,返回True
使用Callable和Future创建线程的步骤如下:
1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。
3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口。
4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
public class Main { |
三种创建线程方法对比
实现Runnable和实现Callable接口的方式基本相同,不过是后者执行call()方法有返回值,后者线程执行体run()方法无返回值,因此可以把这两种方式归为一种,这种方式与继承Thread类的方法之间的差别如下:
1、线程只是实现Runnable或实现Callable接口,还可以继承其他类。
2、这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
3、但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。
4、继承Thread类的线程类不能再继承其他父类(Java单继承决定)。
注:一般推荐采用实现接口的方式来创建多线程。
线程的生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
(1)新建:即新生状态,创建Thread对象
(2)就绪:使用stard()方法
(3)运行:调用run方法
(4)阻塞:调用sleep()方法,阻塞执行后再次回到就绪状态(注意:后期加上 synchronized 的时候,使用 sleep 是不会释放权利的)
(5)死亡:即程序终止,终止的方式有两种:
-
正常执行完毕,循环 次数已经到达
-
外部干涉 (设置到达时间后,让线程停止)
线程的优先级
只代表概率,不代表绝对先后顺序。
- MIN_PRIORITY : 1
- NORM_PRIORITY : 5 默认优先级
- MAX_PRIORITY :10
线程的同步
线程安全问题产生的原因
1、多个线程在操作共享数据。
2、操作共享数据的线程的线程代码有多条。
解决思路
将多条操作共享数据的线程封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。必须要当前线程把这些代码执行完毕,其他线程参与运行。
1)同步块
synchronized +块:同步块 |
2)同步方法
修饰符 synchronized 返回类型|void 方法签名{ |
同步的好处
解决了线程安全性问题。
同步的弊端
相对降低了效率,因为同步外的线程都会判断同步锁。
同步的前提
同步中必须右多个线程并使用同一个锁。
线程池
什么线程池
线程池中,当需要使用线程时,会从线程池中获取一个空闲线程,线程完成工作时,不会直接关闭线程,而是将这个线程退回到池子,方便其它人使用。
简而言之,使用线程池后,原来创建线程变成了从线程池获得空闲线程,关闭线程变成了向池子归还线程。
java.util.concurrent.Executors 提供了一个 java.util.concurrent.Executor 接口的实现用于创建线程池。
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
线程池带来的好处
1、降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的性能消耗。
2、提高响应速度,当任务到达时,任务可以不需要等待线程创建,可以直接执行。
3、提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池四个基本组成
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
线程池技术正是关注如何缩短或调整 T1,T3 时间的技术,从而提高服务器程序性能的。它把 T1,T3 分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有 T1,T3 的开销了。
线程池不仅调整 T1,T3 产生的时间段,而且它还显著减少了创建线程的数目,看一个例子:
假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。
线程池的常见参数
1、corePoolSize:核心线程数
* 核心线程会一直存活,及时没有任务需要执行
* 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
* 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
2、queueCapacity:任务队列容量(阻塞队列)
* 当核心线程数达到最大时,新任务会放在队列中排队等待执行
3、maxPoolSize:最大线程数
* 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
* 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
4、 keepAliveTime:线程空闲时间
* 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
* 如果allowCoreThreadTimeout=true,则会直到线程数量=0
5、allowCoreThreadTimeout:允许核心线程超时
6、rejectedExecutionHandler:任务拒绝处理器
* 两种情况会拒绝处理任务:
- 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
- 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
* 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常
* ThreadPoolExecutor类有几个内部实现类来处理这类情况:
-
AbortPolicy 丢弃任务,抛运行时异常
RunsPolicy 执行任务DiscardPolicy 忽视,什么都不会发生
dOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
* 实现RejectedExecutionHandler接口,可自定义处理器
线程池接口、类关系一览
【说明】
Executor 是一个顶级接口,它里面只声明一个方法:execute(Runnable command),用来执行传进去的任务。
ExecutorService 接口继承了 Executor 接口,并声明了一些方法:submit、shutdown、invokeAll 等。
AbstractExecutorService 抽象类实现了 ExecutorService 接口,基本实现了 ExecutorService 接口的所有方法。
ThreadPoolExecutor 继承了类 AbstractExecutorService。
常见线程池
-
newSingleThreadExecutor()
创建一个单线程的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
public class ExecutorDemo1 {
public static void main(String[] args) {
MyTask myTask = new MyTask();
//只有一个线程的线程池
ExecutorService es = Executors.newSingleThreadExecutor();
for(int i=0;i<10;i++){
es.submit(myTask);
}
}
}
class MyTask implements Runnable{
public void run() {
System.out.println(System.currentTimeMillis()/1000 + ":Thread ID:" + Thread.currentThread().getId());
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
}
} -
newFixedThreadExecutor()
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
public class ExecutorDemo2 {
public static void main(String[] args) {
MyTask task = new MyTask();
ExecutorService es = Executors.newFixedThreadPool(5); //创建固定线程数大小为5的线程池
for(int i=0;i<10;i++){ //依次向线程池提交了10个任务
es.submit(task);
}
}
}
class MyTask implements Runnable{
public void run() {
System.out.println(System.currentTimeMillis()
+":Thread ID:"+Thread.currentThread().getId());
try{
Thread.sleep(1000); //1秒
}catch(Exception e){
e.printStackTrace();
}
}
} -
newCacheThreadExecutor()(推荐使用)
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程(一般是60秒无执行),若无可回收,则新建线程。
public class ExecutorDemo3 {
public static void main(String[] args) {
MyTask myTask = new MyTask();
//可根据实际情况调整线程数量的线程池
ExecutorService es = Executors.newCachedThreadPool();
for(int i=0;i<10;i++){
es.submit(myTask);
}
}
}
class MyTask implements Runnable{
public void run() {
System.out.println(System.currentTimeMillis()/1000
+ ":Thread ID:" + Thread.currentThread().getId());
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
}
} -
newScheduleThreadExecutor()
创建一个定长的线程池,而且支持定时的以及周期性的任务执行。
public class ExecutorDemo4 {
public static void main(String[] args) {
ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);
/**
* scheduleAtFixedRate方法 :如果前面的任务没有完成,则调度也不会执行!
* scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
*/
ses.scheduleAtFixedRate(new ScheduledTimeTask(), 0, 2, TimeUnit.SECONDS); //设置每定时2s执行一次
}
}
class ScheduledTimeTask implements Runnable {
public void run() {
try {
//修改这里的任务执行时间
Thread.sleep(1000);
System.out.println( System.currentTimeMillis()/1000 +
" : ThreadId = " + Thread.currentThread().getId());
} catch (Exception e) {
e.printStackTrace();
}
}
}【设置任务的执行时间为1s( < 定时的2s )的运行结果
【设置任务的执行时间为3s( > 定时的2s )的运行结果(即代码改成Thread.sleep(3000))】