Java 多线程
多线程的创建(JDK5之前)
继承Thread类
Thread类构造器
方法 | 作用 |
---|---|
Thread() | 创建新的Thread对象 |
Thread(String threadname) | 创建线程并指定线程实例名 |
Thread(Runnable target) | 指定创建线程的目标对象,它实现了Runnable接口中的run()方法 |
Thread(Runnable target, String name) | 创建新的Thread对象 |
创建步骤
- 创建一个继承于Thread类的子类
- 重写Thread类的run() --> 将此线程执行的操作声明在run()中
- 创建Thread类的子类的对象
- 通过此对象调用start()-->启动当前线程,调用当前线程的run()
注意点
- 我们不能通过直接调用run()的方式启动线程,直接调用run()方法不会新建线程,只相当于调用普通方法
- run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU 调度决定
- 想要启动多线程,必须调用start方法
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上 的异常“IllegalThreadStateException”
实现Runnable接口
创建步骤
- 定义子类,实现Runnable接口。
- 子类中重写Runnable接口中的run()方法。
- 通过Thread类含参构造器创建线程对象->将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
- 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
两者联系与对比
开发中:优先选择:实现Runnable接口的方式
原因:
- 实现的方式没有类的单继承性的局限性
- 实现的方式更适合来处理多个线程有共享数据的情况
注意:JDK1.5之前创建新执行线程只有以上两种方法。
线程的常用方法
方法 | 作用 |
---|---|
void start() | 启动线程,并执行对象的run()方法 |
void run() | 线程在被调度时执行的操作 |
String getName() | 返回当前线程的名称 |
void setName(String name) | 设置当前线程名称 |
static Thread currentThread() | 返回当前线程。 |
static void yield() | 线程让步 |
join() | 线程加入 |
static void sleep(long millis) | 线程休眠(指定时间:单位毫秒) |
stop() | 强制线程生命期结束(弃用) |
boolean isAlive() | 判断线程是否还活着 |
-
static Thread currentThread()
- 在Thread子类中就是this,通常用于主线程和Runnable实现类
-
static void yield()
- 暂停当前正在执行的线程,释放当前线程的CPU执行权,把执行机会让给优先级相同或更高的线程
- 释放后各线程继续争CPU执行权,可能刚刚释放就又抢到执行权
- 若队列中没有同优先级的线程,忽略此方法
-
join()
- 当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
- 低优先级的线程也可以获得执行
-
static void sleep(long millis)
- 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到了之后重新排队
线程的优先级
线程的优先级等级
- MAX_PRIORITY:10
- MIN _PRIORITY:1
- NORM_PRIORITY:5 默认优先级
优先级有关方法
- getPriority() :返回线程优先值
- setPriority(int newPriority) :改变线程的优先级
说明
- 线程创建时继承父线程的优先级
- 高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
调度策略
时间片:
抢占式:高优先级的线程抢占CPU
Java的调度方法
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
线程的生命周期
JDK中用Thread.State类定义了线程的5种状态
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类 及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态
- 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中 止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
线程的同步
线程的同步安全问题
- 多个线程执行的不确定性引起执行结果的不稳定
- 当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
线程如何同步
同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
- 需要被同步的代码:操作共享数据的代码,最小原则,非必要不包含
- 共享数据:多个线程共同操作的变量
- 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁;通常使用需要被同步线程类充当锁
- 要求:多个线程必须要共用同一把锁。
局限性:操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低
同步方法
如果操作共享数据的代码能完整的声明在一个方法内,我们不妨将此方法声明为同步的。
public synchronized void method(){
}
同步方法的监视器
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明
- 非静态的同步方法,同步监视器是:this,还是有线程安全问题
- 静态的同步方法,同步监视器是:当前类本身,线程安全锁
Lock(锁) - JDK5.0新增
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同 步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象 加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
class claszz{
private final ReentrantLock lock = new ReenTrantLock();
public void method(){
lock.lock();
try{
//保证线程安全的代码;
}
finally{
lock.unlock();
}
}
}
注意:如果同步代码有异常,要将unlock()写入finally语句块
synchronized 与 Lock 的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是 隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序
Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法 (在方法体之外)
同步范围
- 如何找问题,即代码是否存在线程安全?
- 明确哪些代码是多线程运行的代码
- 明确多个线程是否有共享数据
- 明确多线程运行代码中是否有多条语句操作共享数据
- 如何解决呢?
- 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其 他线程不可以参与执行
- 即所有操作共享数据的这些语句都要放在同步范围中
- 切记(重要)
- 范围太小:没锁住所有有安全问题的代码
- 范围太大:没发挥多线程的功能
死锁问题
原因
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源
说明
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于 阻塞状态,无法继续
解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
会释放锁的操作
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
不会释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
- 应尽量避免使用suspend()和resume()来控制线程
线程的通信
- wait() 与 notify() 和 notifyAll()
- wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有 权后才能继续执行。
- notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
- notifyAll ():唤醒正在排队等待资源的所有线程结束等待
- 这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报 java.lang.IllegalMonitorStateException异常
- 因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁, 因此这三个方法只能在Object类中声明。
wait() 方法
- 在当前线程中调用方法: 对象名.wait()
- 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify (或notifyAll) 为止。
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
- 调用此方法后,当前线程将释放对象监控权 ,然后进入等待
- 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行
notify()/notifyAll()
- 在当前线程中调用方法: 对象名.notify()
- 功能:唤醒等待该对象监控权的一个/所有线程。
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
多线程的创建(JDK5之后新增)
实现Callable接口
与Runnable相比
- Callable功能更强大
- 可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 支持泛型的返回值
Future接口
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是 否完成、获取结果等
- FutrueTask是Futrue接口的唯一的实现类
- FutureTask 同时实现了Runnable, Future接口。它既可以作为 Runnable被线程执行,又可以作为Future得到Callable的返回值
创建步骤
- 定义子类,实现Callable接口
- 子类中重写Callable接口中的call()方法,将此线程需要执行的操作声明在call()中
- 创建Callable接口实现类的对象
- 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
- 获取Callable中call()方法的返回值(get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。)
线程池
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。但是我们一般会提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
好处
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
通过Executors创建线程池的弊端
-
JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
-
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行 Runnable
Future submit(Callable task):执行任务,有返回值,一般用来执行 Callable - void shutdown() :关闭连接池
-
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
但是根据阿里编码规约:
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
- newFixedThreadPool和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
- newCachedThreadPool和newScheduledThreadPool:主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
通过ThreadPoolExecutor创建线程池
ThreadPoolExecutor 是线程池的核心实现。线程的创建和终止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使用线程池会节省系统资源,并且每个线程池都维护了一些基础的数据统计,方便线程的管理和监控。
// ThreadPoolExecutor源码
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
参数含义
参数 | 含义 |
---|---|
corePoolSize | 线程池核心池的大小 |
maximumPoolSize | 线程池的最大线程数 |
keepAliveTime | 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间 |
unit | 线程活动保持时间的单位 |
workQueue | 任务队列 |
threadFactory | 线程工厂 |
defaultHandler | 拒绝策略 |
参数详解
-
corePoolSize & maximumPoolSize:当一个新任务被提交到池中,如果当前运行线程小于核心线程数(corePoolSize),即使当前有空闲线程,也会新建一个线程来处理新提交的任务;如果当前运行线程数大于核心线程数(corePoolSize)并小于最大线程数(maximumPoolSize),只有当等待队列已满的情况下才会新建线程,值得注意的是如果使用了无界的任务队列则maximumPoolSize参数就没什么效果。
-
keepAliveTime:线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
-
unit:可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
-
workQueue:用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。(默认)
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
- DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
-
ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字,Debug和定位问题时非常有帮助。
-
defaultHandler:当线程池已经关闭或达到饱和(最大线程和队列都已满)状态时,那么必须采取一种策略处理新提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。ThreadPoolExecutor 定义了四种拒绝策略:
- ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。 (默认)
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
我们也可以自定义拒绝策略,只需要实现 RejectedExecutionHandler; 需要注意的是,拒绝策略的运行需要指定线程池和队列的容量。
最后编辑时间:2022年3月15日00:13:06
- 资料来源:https://www.bilibili.com/video/BV1Kb411W75N
- 感谢尚硅谷,感谢尚硅谷宋红康老师~