JavaSE 线程 Vol.1 基础 入门
1. 前言
· 本文章是用于 个人学习过程中 整理知识点的帖子,主题为:JavaSE 线程 Vol.1 基础 入门
· 本文章出现的 遗漏、错误 欢迎点开这篇文章的各位指出。
· 本文章的知识大纲根据 韩顺平老师 JavaSE 教学视频、秦疆老师 JavaSE 多线程视频 进行编写。
【感谢韩顺平老师和秦疆老师带来的优质教学和对教育作出的贡献】
2. 相关概念
● 程序 Program ●
· 为完成特定任务,使用某种语言编写的一组指令的集合,也就是我们所写的 代码 。
● 进程 Process ●
· 进程就是指 运行中的程序 。当我们执行程序时,操作系统会分配给当前程序一个内存空间。
· 它是 程序中的一次执行过程 或者 正在运行的一个程序 。
· 进程是一个 动态过程,它拥有自己的 生命周期 。
· 根据进程的内容,可以分为:单线程进程、多线程进程 。
① 单线程进程:同一个时刻,只允许执行一个线程。
② 多线程进程:同一个时刻,可以执行多个线程。
· 根据进程执行的方式,可以分为:并发、并行 。
① 并发:同一个时刻,多个线程任务 交替执行 。单核CPU实现的多任务就是并发。
② 并行:同一个时刻,多个线程任务 同时执行 。多核CPU可以实现并行。
● 线程 Thread ●
· 线程 [ Thread ] 是由 进程创建的 ,是进程的一个 实体 。一个进程可以有 多个线程 。
· 线程的常用方法:
指令 | 解释 |
---|---|
setName ( String name ) | 设置线程名称 |
getName ( ) | 获取线程名称 |
start ( ) | 使线程开始执行 |
run ( ) | 调用线程对象的 run ( ) 方法 |
setPriority ( int new Priority ) | 更改线程优先级 |
getPriority ( ) | 获取线程优先级 |
sleep ( long millis ) | 在指定毫秒时间内休眠线程 |
interrupt ( ) | 中断线程 |
yield ( ) | 礼让线程 |
join ( ) | 线程插队 |
setDaemon ( boolean on ) | 将线程设置为守护线程 |
getState ( ) | 获取线程当前状态 |
3. 线程的基本使用
● 使用方式 ●
· 线程的使用方式有:继承 Thread 类 、实现 Runnable 接口
· 下面将详细介绍 两种使用方式 ,同时将会 拓展关于线程的其他知识点 。
● 继承 Thread 类
● 继承 Thread 类 ●
● 进程执行流程 ●
· 在讲解多线程执行流程之前,我们来补充一个方法:
· Thread.currentThread().getName()
可以查看当前线程名 。
· 以上面 对线程的基本使用 为例子,我们来讲解一下 整个进程的执行流程 是如何进行的:
① 当我们执行一个java文件时,就是开启了一个进程。
② 在这个进程中包含了两个线程:main线程【也就是 main 方法】、Thread-0 线程 【也就是Cat 类】。
③ 其中 main线程 调用了 Thread-0 线程,此时两个线程是 并发执行 的关系,互不影响 。
④ 当一个线程执行完后,该线程将会消失,直到进程中最后一个线程执行完毕后 进程才会停止 。
● 查看进程程序 ●
· 我们可以在进程运行时,在终端输入 jconsole
命令,调用出 Java 监视和管理控制台 ,即可查看当前进程中线程的运行情况 。
· 当 该进程的最后一个线程执行完毕后,JConsole 也停止更新对该进程的执行信息 。
下面就来演示一下 JConsole 查看多线程执行 的过程。
● ① 线程编写 ●
● ② 执行进程 调用 JConsole ●
● 查看进程执行信息 ●
● 进一步查看进程信息 ●
● 进程结束 ●
● 多线程实现 ●
· 我们可以看到,上述例子的进程执行过程包含了多线程执行,那么进程是如何实现多线程的。
· 上述例子中,我们通过创建 Cat 类 并执行由 Thread类 继承来的 start()
方法,实现了多线程 。
· 但其实,真正实现多线程的方法,是 start()
方法 中更核心的方法 start0()
方法 。
· 由于该方法涉及到 操作系统 内核范围 ,就不继续深入分析 。
● 实现 Runnable 接口
● 基本介绍 ●
· 当一个继承了父类的子类想要成为一个线程时,是不允许再次继承 Thread 类 的 。
· 因此 Java 又提供了 Runnable 接口,以实现 将子类定义为线程 的操作 。
· 相比单继承 Thread 类 实现线程,实现 Runnable 接口 可以让多个线程共享同一资源 。
● 实现 Runnable 接口 ●
· 静态代理模式
· 从上述例子中我们会发现,实现了 Runnable 接口 的 Dog 类 在创建实例对象后并不是调用 start ()
方法,而是借由 Thread 类对象 调用了 start()
方法。
· 这是因为 Runnable 接口 中并没有 start()
方法,其本身只有一个 run()
方法,该方法中编写的就是我们的线程业务,但我们不能直接调用这个方法,单纯调用此方法并不能实现线程,只是基本的调用方法而已 。
· 那么为什么能够通过这样的操作进而实现线程创建的,我们需要引出 静态代理模式 这个概念 。
· 静态代理模式,简单的讲就是:通过其他类来帮助本类实现业务的过程。
· 下面将通过 ThreadProxy 类 来模拟 Thread 类 中实现代理模式的过程。
● ThreadProxy 线程代理类 ●
· 通过上述演示,可以得出结论:Thread 类 是 实现了 Runnable 接口 的类 的 代理类 。
· Lambda表达式
● 基本介绍 ●
Lambda表达式作为JDK1.8的特性,有着简化代码的作用,是一种语法糖。
其简化的对象一定是 函数式接口。所谓 函数式接口 就是只包含一个抽象方法的接口。
在此介绍 Lambda表达式 是因为 Runnable接口就是函数式接口 在后续的多线程教学中会多次运用到 Lambda表达式 。
下面来演示一下从 外部类 到 Lambda表达式 的过程:
1. 常规方法调用实现类
2. 使用静态内部类
3. 使用局部内部类
4. 使用匿名内部类
5.使用 Lambda 表达式
6.带参抽象方法的简化方式
● 实现 Callable 接口 [了解]
1.实现Callable接口,需要返回值类型
2.重写call方法,需要抛出异常
3.创建目标对象
4.创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
5.提交执行:Future result1 = ser.submit(t1);
6.获取结果: boolean r1 = result1.get();
7.关闭服务: ser.shutdownNow();
4. 线程的相关操作
● 终止线程
· 终止线程有两种情况:完成任务、通知方式 。
① 完成任务:即线程完成任务后,线程就会自行结束 。
② 通知方式:通过变量来控制一个或多个线程的 run ( ) 方法 ,使其线程结束 。
下面演示 通知方式 终止线程:
● 守护线程
· 根据线程的种类,可以分为:用户线程、守护线程 。
① 用户线程:也叫工作线程,当 线程的任务执行完成 或 通知方式 时,结束线程 。
② 守护线程:为工作线程服务,当所有的用户线程结束,守护线程自动结束 。例:垃圾回收机制
· 下面就来演示,守护线程的使用:
● 生命周期
· 线程的生命周期可以简单的分为 五个阶段:新建、就绪、运行、阻塞、销毁 。
· Thread 类 中包含了一个 枚举类 State ,该类存放的就是线程生命周期的 六个状态:
① 新建 NEW
② 可运行 RUNNABLE
【 其中包含:就绪 READY
、运行 RUNNING
】
③ 阻塞 BLOCKED
④ 等待 WAITING
⑤ 超时等待 TIMED_WAITING
⑥ 销毁 TERMINATED
下面来简单演示一下 线程的生命周期 :
5. 线程同步
● 场景模拟 ●
· 现在要使用线程机制来模拟三个售票窗口来售卖动车票,已知火车票有100张 。
· 下面就分别通过 继承 Thread 类、实现Runnable 接口 来实现售票模拟:
● 继承 Thread 类 ●
● 实现 Runnable 接口 ●
· 我们可以看到:不管是 继承 Thread 类 还是 实现 Runnable 接口 ,都会出现车票超卖现象。
· 为了防止这种现象发生,我们引出了: 线程的同步 Synchronized
● 基本介绍 ●
· 线程同步是指:当一个线程在对一块内存进行操作时,其他线程都不可以对这个内存地址进行操作。
· 直到该线程完成操作,其他线程才能对该内存地址进行操作 。
· 在多线程进程中,一些指定数据不希望被多个线程同时访问,此时就会使用 线程同步 。
· 线程同步可以保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性 。
● 使用方式 ●
· 线程同步常用的使用方式有两种:同步块、同步方法
① 同步块:synchronized ([当前对象]) { [这里编写代码块] }
② 同步方法:public synchronized void [方法名] ([参数]) { }
· 下面通过 同步方法 来解决 车票售票问题:
6. 互斥锁
● 概念比喻 ●
· 某银行的自助区域,有一台银行存取款一体机,现在有3个人排队使用 。
· 第一个人 使用一体机 进行 取钱 时,剩下两人就需要在外面等待 。当第一个人使用完后 离开一体机 ,轮到第二个人 使用一体机 …
· 此时我们把 人 = 线程 、 一体机 = 锁 、 取钱 = 业务 。
● 线程同步原理 ●
· 在上面的 线程同步演示中:我们也可以把 售票窗口 = 线程 、 售票方法 = 锁 、 售票 = 业务
· 也就是说,线程同步的原理是:
① 多线程执行时,线程会争夺锁来优先执行业务 。
② 当抢到锁的线程执行完业务后会返回锁,然后线程们继续争夺…
● 基本介绍 ●
· 互斥锁是最基本的一种锁,是通过给对象添加 synchronized
关键字 实现的,也就是线程同步的实现方式 。
● 使用方式 ●
· 互斥锁实现的方式跟线程同步描述的一样,有两种:同步块、同步方法
① 同步块:synchronized ([当前对象]) { [这里编写代码块] }
② 同步方法:public synchronized void [方法名] ([参数]) { }
● 相关细节 ●
① 当同步方法是 非静态方法 时:互斥锁就加在 当前类 中的 this对象 或 其他对象 中 。
② 当同步方法是 静态方法 时:互斥锁就加在 当前类 中 。
③ 多线程的锁必须加在同一个对象中 。
· 下面通过 同步块 来解决 车站售票问题:
● 非静态方法 ●
这里直接根据 线程同步 的例子进行修改:
● 静态方法 ●
这里只演示代码结构,后续会详细讲解 静态方法中 同步块的使用:
7. 死锁
● 基本介绍 ●
我们来模拟一个场景:
· 小明来到妈妈身边,想要玩手机 。妈妈说:你先做完作业,再给你玩手机。
· 小明说:你先让我玩会手机,我再做作业。
此时我们可以看到:双方互相制约了对方的资源,而且不肯想让,这种情况在多线程中被称为 死锁 。
● 场景模拟 ●
· 我们可以看到:
· 线程A进入了 true 的判断条件,拿起了o1对象锁,下一个业务方法需要o2对象锁
· 线程B进入了 false 的判断条件,拿起了o2对象锁,下一个业务方法需要o1对象锁
· 双方都占用着各自所需的锁,造成线程全部堵塞,形成了死锁 。
· 我们需要改变线程调用方式来避免死锁的发生 。
● 释放锁 ●
· 当线程执行线程同步时,遇到以下任何一个情况时,线程就会释放锁:
① 正常执行结束 。
② 遇到了返回终止语句, 结束线程 。
③ 出现了未处理的错误和异常,导致异常结束 。
④ 执行了线程对象的 wait( )
方法,当前线程暂停并释放锁 。
· 当线程执行线程同步时,遇到以下任何一个情况时,线程不会释放锁:
① 程序调用Thread.sleep( )
、Thread.yield( )
方法 ,暂停当前线程的执行,不会释放锁 。
② 其他线程调用了该线程的 suspend( )
方法,将该线程挂起,该线程不会释放锁。【该方法已经被废弃不再使用】
8. Lock锁
● 基本介绍 ●
从JDK 5.0开始,Java提供了更强大的线程同步机制:通过显式定义同步锁对象来实现同步。
同步锁使用 Lock对象 充当。
java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。
锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁
线程开始始访问共享资源之前应先获得 Lock对象。
ReentrantLock类 实现了Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁。
● 场景模拟 ●
1.定义 TestLock 类 模拟线程
2.定义主方法 创建三个线程
3.执行结果
● Lock锁 与 Synchronized锁 的区别 ●
· Lock是显式锁(手动开启和关闭锁) ;synchronized是隐式锁,出了作用域自动释放。
· Lock只有代码块锁 ;synchronized有代码块锁和方法锁。
· 使用 Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
· 优先使用顺序: Lock > 同步代码块 > 同步方法
9. 线程通信
● 基本介绍 ●
在多线程程序中,会出现线程之间通信的情况,其中出现的问题最为经典的是:生产者消费者问题。
通常会使用两种方法来解决:
· 管程法
· 信号灯法
两者都是通过生产者与消费者之间添加一个缓冲区作为业务调换的核心。
10. 线程池
● 基本介绍 ●
在多线程并发的程序中,常常伴随着线程的创建和销毁、使用量特别大的资源,这对性能的影响很大。为了解决这个问题,可以提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
使用线程池的好处有:
· 提高响应速度(减少了创建新线程的时间)
· 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
· 便于线程管理,常用的线程池属性有:
· corePoolSize:核心池的大小
· maximumPoolSize:最大线程数
· keepAliveTime:线程没有任务时最多保持多长时间后会终止
● 基本使用 ●
JDK 5.0起提供了线程池相关APl: ExecutorService和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
· void execute(Runnable command)∶执行任务/命令,没有返回值,一般用来执行Runnable
· Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
· void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池