多线程
一、线程概述
1.程序、进程、线程
- 程序(program):静态的指令集合
- 进程(process):运行中的程序
- 独立性:独立地进行资源分配和调度
- 动态性:活动的指令集合,拥有自己的生命周期和不同状态
- 并发性:多个进程可在单个处理器上并发执行,多个进程之间不会相互影响
- 线程(thread):独立并发的执行流
- 线程是进程的组成部分,一个进程可以拥有一到多个线程,一个线程必须有一个父进程
- 线程拥有自己的堆栈、程序计数器、局部变量,但不拥有系统资源。多个线程间共享父进程的全部资源
- 线程是独立运行的,它并不知道进程中是否有其他线程存在。线程是抢占式的,当前运行的线程在任何时候都可能被挂起,以便另外一个线程运行
- 多个线程之间可以并发执行,一个线程可以创建和撤销另一个线程
- 一个程序运行后至少有一个进程,一个进程里至少有一个线程
- 并发与并行
- 并行:同一时刻,有多条指令在多个处理器上执行
- 并发:同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行
2.多线程的优势
- 进程之间不能共享内存,但线程之间共享内存十分容易
- 系统创建进程需要重新为其分配系统资源,但创建线程代价小很多,因此使用多线程比多进程效率高
二、线程的创建和使用
1.继承Thread类创建线程类
- 定义Thread类的子类,并重写该类的
run()
方法,该run()
方法的方法体就代表了线程需要完成的任务,因此把run()
方法称为线程执行体 - 创建Thread子类的实例,即创建了线程实例
- 调用线程对象的
start()
方法来启动该线程 - Java程序运行时自带默认的主线程,
main()
方法的方法体就是主线程的线程执行体 - 使用继承Thread类来创建线程类时,多个线程之间无法共享线程对象的实例变量
2.实现Runnable接口创建线程类
- 定义
Runnable
接口的实现类,并重写该接口的run()
方法,该run()
方法的方法体同样是该线程的线程执行体 - 创建
Runnable
实现类的实例,并以此作为Thread
的target
来创建Thread
对象,该Thread
对象才是真正的线程对象 - 调用线程对象的
start()
方法来启动该线程 Runnable
实现类的对象仅仅作为Thread
对象的target
,Runnable
实现类里包含的run()
方法仅作为线程执行体。实际的线程对象依然是Thread
实例,只是该Thread
线程负责执行其target
的run()
方法- 使用实现
Runnable
接口来创建线程类时,多个线程之间可以共享一个target
,所以多个线程可以共享同一个target
对象的实例变量
3.使用Callable和Future创建线程
- 创建
Callable
接口的实现类,并实现call()
方法,该call()
方法将作为线程执行体,且该call()
方法有返回值,再创建Callable
实现类的实例 - 使用
FutureTask
类来包装Callable
对象,该FutureTask
对象封装了该Callable
对象的call()
方法的返回值 - 使用
FutureTask
对象作为Thread
对象的target
创建并启动新线程 - 调用
FutureTask
对象的get()
方法来获得子线程执行结束后的返回值 Callable
接口有泛型限制,Callable
接口里的泛型形参类型与call()
方法返回值类型相同。而且Callable
接口是函数式接口,因此可以使用Lambda表达式创建Callable
对象
4.继承方式和实现方式的异同
- 推荐使用实现
Runnable
接口、Callable
接口的方式来创建多线程 - 实现方式的特点:
- 只是实现了
Runnable
接口或Callable
接口,还可以继承其他类 - 多个线程可以共享一个
target
,适用于多个相同线程处理同一份资源的情况 - 如果要访问当前线程,只能使用
Thread.currentThread()
方法
- 只是实现了
- 继承方式特点:
- 已经继承了
Thread
类,不能再继承其他类 - 如果要访问当前线程,可以使用
this
引用
- 已经继承了
5.线程的调度
- 同优先级线程,使用时间片策略
- 高优先级线程,使用抢占式策略
- 线程优先级,低优先级只是获得调度的概率低,并非一定是在高优先级线程后才被调用,子线程会继承父线程优先级
MAX_PRIORITY
:10MIN_PRIORITY
:1NORM_PRIORITY
:5
6. 线程生命周期
-
在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。当线程启动以后,CPU需要在多条线程之间切换,于是线程状态也会在运行、就绪之间切换
-
新建:程序使用
new
关键字创建一个线程后,该线程就处于新建状态,仅仅由JVM为其分配了内存,初始化了成员变量 -
就绪:线程对象调用了
start()
方法后,该线程就处于就绪状态,JVM为其创建方法调用栈和程序计数器,表示该线程可以运行了 -
运行:线程对象获得了CPU资源后,该进程就处于运行状态
-
阻塞:在某种特殊情况下,被认为挂起或执行输入输出操作时,让出CPU并终止自己的执行,进入阻塞状态。被阻塞线程的阻塞解除后,线程进入就绪状态
-
死亡:线程会以如下三种方式结束,结束后就处于死亡状态
run()
或call()
方法执行完成,线程正常结束- 线程抛出一个未捕获的
Exception
或Error
- 程序调用该线程的
stop()
方法来结束该线程——容易导致死锁,不推荐使用
-
主线程死亡时,其他线程不受任何影响,并不会随之结束,一旦子线程启动起来后,它就拥有和主线程相同地地位,它不受主线程的影响
-
不能对死亡状态的线程调用
start()
方法,程序只能对新建状态的线程调用start()
方法,对新建状态的线程两次效用start()
方法也是错误的,都会引发IllegalThreadStateException
异常
7.线程同步
- synchronized锁
- 当两个进程并发修改同一个文件时就有可能造成异常,Java引入了同步监视器来解决这个问题,同步监视器就是
synchronized
锁。- Java程序运行时所有的对象都存储在JVM中,而在JVM中所有对象都可以作为内置锁对象
synchronized
修饰的不论是方法还是代码块,想要执行其中的内容,必须先获取对应的内置锁才行,因此synchronized
的锁定对象不能是null
synchronized
方法不能阻止其他线程通过普通方法去访问其内置锁对象,所以应将所有涉及修改同一对象的操作都用synchronized
修饰
- 同步代码块和同步方法
- 同步代码块就是用
synchronized
修饰的代码块,同步方法就是使用synchronized
修饰的方法。synchronized
关键字只能修饰代码块和方法 synchronized
修饰代码块的时候可以获取内置锁的对象包括this
、XXX.class
以及其他对象。this
指向的对象本身,XXX.class
指向的是对象的类型类synchronized
修饰的方法时候可以获取的内置锁的对象包括this
、XXX.class
,修饰普通方法时获取的内置锁是调用该方法的对象本身,修饰静态方法时获取的内置锁是调用该方法的对象的类型类- 不要对线程安全类的所有方法都进行同步,只对哪些会改变竞争资源的方法进行同步
- 如果可变类有两种运行环境:单线程和多线程环境,则应该为该可变类提供两种版本,在单线程环境中使用线程不安全版本保证性能,在多线程环境中使用线程安全版本
- 释放同步监视器的锁定
- 线程无法显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定
- 当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器
- 当前线程在同步方法、同步代码块中遇到break、return终止了该代码块、该方法的继续执行
- 当前线程在同步方法、同步代码块中遇到未处理的Error或Exception,导致代码块、方法异常结束
- 当前线程在同步方法、同步代码块中执行了
wait()
方法,当前线程暂停,并释放同步监视器
- 在以下情况中,线程不会释放同步监视器
- 线程在执行同步方法、同步代码块时,程序调用
Thread.sleep()
、Thread.yiled()
来暂停当前线程的执行,当前线程不会释放同步监视器 - 线程在执行同步方法、同步代码块时,程序调用了该线程的
suspend
方法将该线程挂起,该线程不会释放同步监视器
- 线程在执行同步方法、同步代码块时,程序调用
- Lock锁
- Java5开始,Java提供了功能更加强大的线程同步机制——通过显式定义同步锁对象来实现同步,同步锁对象为Lock对象
- 在实现线程同步的方法中,常用的是
ReentrantLock
锁(可重入锁),可以显式地加锁、释放锁。ReentrantLock
锁具有可重入性,一个线程可以对已被加锁的ReentrantLock
锁再次加锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法 - 使用
ReentrantLock
锁时,若加锁和释放锁出现在不同的作用范围内时,建议使用finally
块来确保在必要时释放锁 Lock
是显式锁,只有代码块锁,synchronized
是隐式锁,有代码块锁和方法锁
- 死锁
- 当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机么有监测,也没有采取措施来处理死锁
三、线程通信
1.传统的线程通信
- 使用
synchronized
锁时,为了实现线程通信,Object
类提供了wait()
、notify()
、notifyAll()
三个方法,这三个方法必须由同步监视器对象来调用。分为以下两种情况:synchronized
修饰的方法:可以在方法中直接使用这三个方法synchronized
修饰的代码块:必须使用同步监视器对象来调用这三个方法
2.使用Condition控制线程通信
- 使用
Lock
锁时,为了实现线程通信,Lock
替代了同步方法或同步代码块,Condition
替代了同步监视器的功能。Conditon
实例必须被绑在一个Lock
对象上,要获得特定Lock
实例的Condition
实例,调用Lock
对象的newCondition()
方法即可 Condition
类提供了如下三个方法:await()
、signal()
、signalAll()