Java - 多线程
一、线程概述
一个软件中至少有一个应用程序,应用程序的一次运行就是一个进程,一个进程中至少有一个线程
- 软件:应用程序和相关资源文件等构成一个软件系统
- 程序:编程语言编写的一组指令的集合
- 进程:是操作系统调度和分配资源的最小单位
- 线程:进程中的一个执行单元,是CPU调度的最小单位
单核CPU只能并发,多核CPU可以并行+并发
- 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行
- 并发(concurrency):指两个或多个事件在同一个时间段内发生(同一个时刻只能有一条指令执行,但多个进程的指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果)
不同的进程之间不共享内存,不同的线程共享同一个进程的内存
进程之间切换的复杂度要远远高于线程调度
- 线程调度:当系统只有一个CPU时,以某种顺序执行多个线程
- 分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用 CPU 的时间
- 抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,Java使用的为抢占式调度
多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU使用率更高
二、实现多线程
Java使用
java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Thread
类实际上也是实现了Runnable
接口的类当运行Java程序时,已经有一个main线程,实现多线程有两种方式,最终都是通过Thread对象的API来控制线程的
- 方法一:继承
Thread
类
- 方法一:继承
- 定义Thread类的子类,并重写该类的
run()
方法,该方法的方法体就代表了线程需要完成的任务,因此把run()
方法称为线程执行体 - 创建Thread子类的实例,即创建线程对象
- 调用线程对象的
start()
方法来启动该线程
- 定义Thread类的子类,并重写该类的
- 方法二:实现
Runnable
接口
- 方法二:实现
- 实现
Runnable
接口,重写run()
方法 - 创建
Thread
类的实例,代理启动执行run()
方法
- 实现
- 使用匿名内部类对象来实现线程的创建和启动
1 | new Thread("新的线程!"){ |
三、Thread类
1、构造方法
Thread()
:分配一个新的线程对象Thread(String name)
:分配一个指定名字的新的线程对象Thread(Runnable target)
:分配一个带有指定目标的新的线程对象Thread(Runnable target,String name)
:分配一个带有指定目标的新的线程对象并指定名字
2、常用方法
静态方法
sleep(long millis)
:暂时停止执行(使当前正在执行的线程以指定的毫秒数暂停)yield()
:暂停一下当前线程(重新调度一次系统的线程调度器)
实例方法
run()
:此线程要执行的任务getName()
:获取当前线程名称currentThread()
:返回对当前正在执行的线程对象的引用isAlive()
:测试线程是否处于活动状态getPriority()
:返回线程优先级setPriority(int newPriority)
:改变线程的优先级
- 每个线程都有一定的优先级,优先级高的线程将获得较多的执行机会
- 每个线程的默认优先级:与创建它的父线程具有相同的优先级
- setPriority方法需要一个整数,并且范围在[1,10]之间,推荐三个优先级常量:
- MAX_PRIORITY(10):最高优先级
- MIN _PRIORITY (1):最低优先级
- NORM_PRIORITY (5):普通优先级(默认情况下main线程具有普通优先级)
start()
:开始执行此线程(Java虚拟机调用此线程的run方法)join()
:等待该线程终止join(long millis)
:等待该线程终止的时间最长为millis毫秒join(long millis, int nanos)
:等待该线程终止的时间最长为millis毫秒+nanos纳秒
stop()
:强迫线程停止执行,该方法具有固有的不安全性,已经标记为@Deprecated
,不建议再使用(如果需要通过其他方式来停止线程,可以使用布尔变量的值来控制)
3、守护线程(了解)
- 守护线程:在后台运行的,为其他线程提供服务(例:JVM的垃圾回收线程)
- 守护线程的特点:如果所有非守护线程都死亡,那么守护线程自动死亡
setDaemon(true)
:将指定线程设置为守护线程(必须在线程启动之前设置,否则会报IllegalThreadStateException异常)isDaemon()
:判断线程是否是守护线程
四、线程安全
1、线程安全问题
- 线程之前资源共享情况如下
不能共享 | 可以共享 |
---|---|
局部变量(方法栈) | 静态变量(方法区) |
不同对象的实例变量(堆) | 同一对象的实例变量(堆) |
- 如果多个线程中对同一资源都有读和写的操作,就容易出现线程安全问题
2、解决线程安全问题
- 为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制
- 同步机制(同步监视器)的原理:相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”
Java对象在堆中的数据分为对象头、实例变量、空白的填充。对象头中包含
Mark Word:记录了和当前对象有关的GC、锁标记等信息
指向类的指针:每一个对象需要记录它是由哪个类创建出来的
数组长度(只有数组对象才有)
同步方法:
synchronized
关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法- 静态方法锁对象:默认当前类的类类对象
- 非静态方法锁对象:默认当前类的实例对象
同步代码块:
synchronized(锁对象)
关键字修饰某个代码块,表示只对这个区块的资源实行互斥访问- 锁对象:可以是任意类型,但是必须保证竞争【同一个共享资源】的多个线程必须使用同一个【同步锁对象】,常用Object的非多态实例对象、String的实例对象
1 | package com.atguigu.safe; |
五、等待唤醒机制
等待唤醒机制:多个线程间的一种协作机制,在一个线程满足某个条件时,就进入等待状态,等待其他线程执行完或指定时间到期后自动唤醒,有多个线程进行等待时,可以唤醒所有的等待线程
被唤醒的线程如果成功获取锁,则从 WAITING(等待)状态变成RUNNABLE(准备运行)状态,在当初中断的地方恢复执行;反之,则从WAITING(等待)状态又变成 BLOCKED(等待监视器锁) 状态
等待唤醒机制通过Object类的实例方法实现(因为锁对象可以是任意对象)
wait()
/wait(时间)
:释放锁,线程不再参与调度,因此不会浪费 CPU 资源notify()
:释放一个所通知对象的线程notifyAll()
:释放全部所通知对象的线程
wait()
方法与notify()
方法必须要由同一个锁对象调用,且必须在同步代码块或同步方法中使用
(3)sleep()在Thread类中声明的静态方法,wait方法在Object类中声明
六、线程生命周期
1、5种状态(JDK1.5之前)
新建(New):一个
Thread
类或其子类的对象被声明并创建- 此时和其他Java对象一样,仅仅由JVM为其分配了内存,并初始化了实例变量的值
- 此时的线程对象并没有任何线程的动态特征,程序也不会执行它的线程体
run()
就绪(Runnable):线程对象调用了
start()
方法(只能对新建状态的线程调用,且只能调用一次)- JVM会为其创建方法调用栈和程序计数器,表示已具备了运行的条件,随时可以被调度
运行(Running):处于就绪状态的线程获得了CPU,调用了
run()
方法的线程体代码- 如果计算机只有一个CPU,在任何时刻只有一个线程处于运行状态,如果计算机有多个CPU,将会有多个线程并行(Parallel)执行
- 对于抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务,当该时间用完,系统会剥夺该线程所占用的资源,让其回到就绪状态等待下一次被调度
阻塞(Blocked):
- 在运行过程中的线程遇到如下情况时,线程会进入阻塞状态
- 线程调用了
sleep()
方法,主动放弃所占用的CPU资源 - 线程试图获取一个同步监视器,但该监视器正被其他线程持有
- 线程同步监视器调用了
wait()
或wait(time)
,让它等待通知 - 其他线程对象加塞,调用了
join()
方法; - 线程被调用
suspend
方法挂起(已过时,因为容易发生死锁)
- 线程调用了
- 发生如下情况时会解除阻塞,让该线程重新进入就绪状态,等待线程调度器再次调度
- 线程的
sleep()
时间到 - 线程成功获得了同步监视器
- 线程等到了通知
notify()
或线程已到达等待时间 - 加塞的线程结束了
- 被挂起的线程又被调用了
resume
恢复(已过时,因为容易发生死锁)
- 线程的
死亡(Dead):遇到如下情况时,线程就处于死亡状态
run()
方法执行完成,线程正常结束- 线程执行过程中抛出了一个未捕获的异常(
Exception
)或错误(Error
) - 直接调用该线程的
stop()
来结束该线程(已过时,因为容易发生死锁)
2、6种状态(JDK1.5之后)
新建(New)
准备运行(Runnable)
阻塞-等待同步监视器(Blocked)
阻塞-等待通知(Wating):
- 进入等待状态:Object类的
wait()
,唤醒:Object的notify()
或notifyAll()
- 进入等待状态:Condition类的
await()
,唤醒:Conditon类的signal()
- 进入等待状态:LockSupport类的
park()
,唤醒:LockSupport类的unpark()
- 进入等待状态:Thread类的
join()
,唤醒:调用join方法的线程对象结束
- 进入等待状态:Object类的
阻塞-定时等待(Time_Wating):
- 进入定时等待状态:Object类的
wait(时间)
- 进入定时等待状态:LockSupport类的
park(时间)
- 进入定时等待状态:Thread类的
sleep(时间)
或join(时间)
- 进入定时等待状态:Object类的
结束(Terminated)
当线程从WAITING或TIMED_WAITING恢复到Runnable状态时,如果发现当前线程没有得到同步监视器,那么会立刻转入BLOCKED状态
七、释放锁操作与死锁
可以释放锁的操作
- 当前线程正常执行结束
- 当前线程出现了未处理的
Error
或Exception
,导致当前线程异常结束 - 当前线程执行了锁对象的
wait()
方法,被挂起并释放锁
不会释放锁的操作
- 程序调用
sleep()
、yield()
方法暂停当前线程的执行 - 其他线程调用了该线程的
suspend()
方法将该该线程挂起(已过时)
- 程序调用
死锁:不同的线程分别锁住对方需要的同步监视器不释放,都在等待对方先放弃。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续
八、代码练习
- 简介:丈夫存钱,妻子取钱,余额不足时需要等待
1 | package com.sguigu.homework3; |