多线程
目录前言
本文为B站Java教学视频BV1Kb411W75N的相关笔记,主要用于个人记录与分享,如有错误欢迎留言指出。
本章笔记涵盖视频内容P406~P446
1. 基本概念
1.1 什么是线程
- 程序(program):是为完成特定任务,用某种语言编写的指令的集合。即一段静态的代码,静态对象。
-
进程(process):是程序 的一次执行过程,或正在运行的一个程序。是一个动态 的过程。
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
-
线程(thread):进程可以进一步细化为线程,是一个程序内部的一条执行路径。
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器
-
并行与并发:
- 并行:多个CPU同时执行多个任务
- 并发:一个CPU同时执行多个任务
1.2 多线程程序的优点
- 提高应用程序的响应。对图形化界面更有意义,可以增强用户体验
- 提高计算机系统CPU的利用率
- 改善程序结构,将长而复杂的进程分为多个线程,独立运行,利于理解和修改
2.创建多线程
2.1 方式一:继承于Thread类
- 创建一个继承于Thread类的子类
- 重写Thread类的run() (将此线程执行的操作声明在run()中)
- 创建Thread类的子类的对象
- 通过此对象调用start() (start()是Thread类内的方法)
//1.创建一个继承于Thread类的子类
class MyThread extends Thread{
//2.重写Thread类的run()
public void run(){
for(int i = 0;i < 100;i++){
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
public class Test{
public static void main(String[] args) {
//3.创建Thread类的子类的对象
MyThread t1 = new MyThread();
//4.通过此对象调用start()
t1.start();
}
}
-
注意事项:
- 不能通过直接调用run()的方式启动线程
- 不可以让已经start()的线程再去执行其它线程,会报错IllegalThreadExecption;若要执行其它线程,需要重新创建一个线程的对象
-
Thread类的匿名子类
new Thread(){ public void run(){ //执行体 } }.start();
2.2 方式二:实现Runnable接口
- 创建一个实现Runnable接口的类
- 实现类实现Runnable中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
//1.创建一个实现Runnable接口的类
class MThread implements Runnable{
//2.实现类实现Runnable中的抽象方法:run()
public void run(){
//......
}
}
public class ThreadTest{
public static void main(String[] args){
//3.创建实现类的对象
MThread mThread = new MThread();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(mThread);
//5.通过Thread类的对象调用start()
t1.start();
}
}
/*
Runnable接口创建和Thread直接创建有诸多不同
Runnable本质只是一个接口,内部没有Thread的诸多方法,所以调用Runnable的对象不能使用Thread内的方法
比如上面的MThread,但是t1可以,此处用mThread作为构造器参数创建了一个Thread对象
同时由于Runnable不是Thread的子类,此处run()也不应该是被"重写"了,但一些特殊的设置使得其效果等同于被重写
*/
2.3 比较两种创建线程的方式
- 开发中优先选择:实现Runnable接口的方式
- 实现的方式没有类的单继承性的局限性
- 实现的方式更适合来处理多个线程有共享数据的情况
- 两种方式的相同点:
- 两种方式都需要重写run(),将线程要执行的逻辑声明在run()中
2.4 方式三:实现Callable接口
- 创建一个实现Callable的实现类
- 实现call方法,将此线程所需要执行的操作声明在call()中
- 创建Callable接口实现类的对象
- 将此Callable接口实现类的对象,作为创建FutureTask的参数,传递到FutureTask构造器中
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
- 获取Callable中call方法的返回值(可选)
//创建一个实现Callable的实现类
class NumThread implements Callable{
//实现call方法,将此线程所需要执行的操作声明在call()中(相当于run())
public Object call() throws Exception{
//......
}
}
public class ThreadNew{
public static void main(String[] ards){
//创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//将此Callable接口实现类的对象,作为创建FutureTask的参数,传递到FutureTask构造器中
FutureTask futuretask = new FutureTask(numThread);
//将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futuretask).start();
//获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数——Callable实现类重写的call()的返回值
try {
Object sum = futuretask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
-
Callable()相较于Runnable()优点
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
2.5 方式四:线程池
- 定义:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁线程,实现重复利用。
public class ThreadPool{
public static void main(String[] args){
//1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//2.执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(...);//适用于Runnable
service.submit(...);//适用于Callable
//3.关闭线程池
service.shutdown();
}
}
-
线程池的优点:
- 提高响应速度,减少了创建新线程的时间
- 降低资源消耗,重复利用线程池中的线程,不需要重复创建
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会中止
3. 多线程的常用方法
方法 | 功能 |
---|---|
start() | 启动当前线程,调用当前线程的run() |
run() | 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中 |
currentThread() | 静态方法,返回执行当前代码的线程 |
getName() | 获取当前线程的名字 |
setName() | 设置当前线程的名字 |
yield() | 释放当前cpu的执行权 |
join() | 在线程a中调用线程b的join(),此时线程a进入阻塞状态,直到线程b完全执行后,线程a才会结束阻塞状态 |
sleep(long milli) | 让当前线程"睡眠"指定的milli毫秒,在指定的millitime毫秒时间内,当前的线程是阻塞状态 |
isAlive() | 判断当前线程是否存活 |
4. 多线程的优先级
-
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5(默认优先级)
-
获取和设置当前线程的优先级
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级
-
注意:高优先级的线程会抢占低优先级线程cpu的执行权,但这只是从概率上讲,高优先级的线程被执行的概率较高。并不意味着只有当高优先级的线程执行完后,低优先级的线程才会被执行。(和阻塞不同)
HelloThread h1 = new HelloThread("Thread:1"); //用构造器初始化线程名
//设置线程的优先级
h1.setPriority(Thread.MAX_PRIORITY); //设置当前线程优先级为10
h1.setPriority(8); //设置当前线程优先级为8
h1.getPriority(); //获取当前线程的优先级
5. 线程的生命周期
- 线程的一个完整的生命周期中通常要经历如下的五种状态:
- 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已经具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作 时,让出CPU并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了全部工作或线程被提前强制性中止或出现异常导致结束
6. 线程安全问题
- 定义:当某个线程操作过程中,尚未操作完成时,其它线程也参与进来进行操作,此时数据就会出现重复,溢出等问题。为了解决这个问题,需要当一个线程a在操作的时候,其它线程不能参与进来,直到线程a操作完成时,其它线程才可以开始操作。这种情况即便线程a出现了阻塞也不能被改变。Java中,通过同步机制来解决线程安全的问题。
6.1 方式一:同步代码块
- 格式:synchronized (同步监视器) { //需要被监视的代码 }
6.1.1 处理实现Runnable的线程安全问题
class Windows implements Runnable{
//不需要static,接口实现类创建的多线程,内部属性天然就是共用的
private int ticket = 100;
public void run() {
while(true){
//使用自身充当锁,由于调用者是Windows且唯一所以可以用this指定;用obj做锁也可以
synchronized (this){
if (ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}
}
}
}
}
6.1.2 处理继承Thread类的线程安全问题
class Windows2 extends Thread{
//必须要用static,继承类的内部变量默认是不共用的,每个对象内都有自己的ticket
private static int ticket = 100;
public void run() {
while(true){
//由于继承类创建的对象不唯一,所以不能做锁,此处使用特殊的方法将Window2作为一个对象做锁;当然Object obj = new Object;然后用obj做锁也可以
synchronized (Windows2.class){
if (ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}
}
}
}
}
6.1.3 注意事项
- 操作共享数据的代码,即为需要被同步的代码
- 共享数据:多个线程共同操作的变量(如上方代码中的ticket)
- 理论上任何一个类的对象,都可以充当同步监视器(锁)。但要求多个线程必须共用同一把锁
- 同步的方式,解决了线程的安全问题。但是操作同步代码时,只能有一个线程参与,其它线程等待。相当于是一个单线程的过程,效率低。
- 使用synchronized时不能包含太多或太少的代码;少则线程不安全,多则全程一个线程执行任务
6.2 方式二:同步方法
- 格式:在方法中添加synchronized关键字
6.2.1 处理实现Runnable的线程安全问题
class Window1_ implements Runnable{
private int ticket = 100;
public void run() {
while(true){
show();
}
}
//直接添加synchronized关键字即可,默认监视器是this
public synchronized void show(){
if (ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}
}
}
6.2.2 处理继承Thread类的线程安全问题
class Window2_ extends Thread{
private static int ticket = 100;
public synchronized void run() {
while(true){
show();
}
}
//因为默认的是this,而继承类不能共用锁,所以必须要加static
public static synchronized void show(){
if (ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}
}
}
6.2.3 注意事项
-
同步方法仍然涉及到同步监视器,只是不需要显式声明
-
非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视是:当前类本身
6.3 方式三:Lock锁
- 格式:ReentrantLock 变量名 = new ReentrantLock();
class Window1 implements Runnable{
private int ticket = 100;
//实例化ReentrantLock
ReentrantLock lock = new ReentrantLock();
public void run() {
while(true){
//调用锁定方法:lock()
lock.lock();
if (ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}
//调用解锁方法:unlock()
lock.unlock();
}
}
}
-
synchronized 与 Lock的异同
-
相同:两者都可以解决线程安全问题
-
不同:synchronized机制在执行完相应的同步代码以后,会自动释放同步监视器
Lock需要手动的启动同步监视器,同时结束同步也需要手动的实现
-
6.4 死锁问题
- 定义:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方释放自己需要的同步资源。出现死锁后,不会出现异常或提示,只是所有的线程都处于阻塞状态,无法继续。
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
public void run(){
synchronized(s1){
s1.append("a");
s2.append("1");
synchronized(s2){
s1.append("b");
s2.append("2");
}
}
}
}.start();
new Thread(){
public void run(){
synchronized(s2){ //和上一个线程所需要的锁,顺序刚好相反
s1.append("c");
s2.append("3");
synchronized(s1){
s1.append("d");
s2.append("4");
}
}
}
}.start();
/*
线程1->s1->s2(等待线程2释放)
线程2->s2->s1(等待线程1释放)
这样就形成了死锁
*/
-
解决方法:
- 设计专门的算法/原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
7. 线程通信
-
关键字
- wait():一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器
- notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的线程
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程
-
注意事项
- wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中
- wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器;否则会出现IllegalMonitorStateException异常
- wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中
- wait()和notify()必须同时出现在代码中,否则阻塞的线程无法被唤醒
-
sleep()和wait()的异同
-
相同点
- 一旦执行方法,都可以使当前线程进入阻塞状态
-
不同点
- 两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()
- 调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或同步方法中
- 是否释放同步监视器:sleep()不会释放锁,wait()会释放锁
-
相同点