JAVA17
多线程
进程和线程的概念
进程
l 正在运行中的程序
l 程序存储在硬盘中 当进入内存运行时
l 内存中的每一个程序就叫进程
l 并且每一个进程都有一个独立的功能
线程
l 迅雷中的多线程下载
l main方法中有很多方法一次排列
- 如果其中一个方法执行了很多次循环
- 那么下一个方法就不会执行
- 从入口main到结束,一条路走到底 必须一个一个的执行
- 这种程序是单线程程序
- 效率慢
l 例如
多台电脑网吧上网
多条路通行
线程
概念
l CPU 中央处理器 inter AWD
l 四核心 八线程
l 360杀毒工具
- 进入内存
- 有一个进程 360safe.exe
- 所有功能进入内存
- 所有功能都是程序中的一个方法
- 所有功能可以单独同时运行
- 所有功能由CPU运行
- 每开启一个功能 cpu就会开启新的执行路径
- 也就是一个新的线程
- 早期cpu一次只能执行一个线程 时间片轮回切换运行
- 当前cpu有多核心 那么同一时刻就能执行多个线程
迅雷下载
l 多线程下载
l 并不是提高了网速 而是多线程提高了速度
线程的运行模式
分时调度
l 所有线程轮流使用cpu资源 平均分配对个线程占用CPU时间
抢占式调度
l 优先级高的多使用cpu资源 优先级相同的随机选择一个进行调度
l
main主线程
l 编译
l JVM运行main方法
l 找操作系统 开线程
l 对于cpu有了一个执行的路径 运行方法main路径有个名字main
l 这个就是主线程
l 多线程就算一条线程出现错误 其他线程也会执行
Thread类
概述
l Java.lang包中
l Jvm允许程序运行多个线程
l 每一个线程都有一个优先级
l 创建线程有两个方法
- 继承Thread类 重写run方法
- 实现Runable接口 实现run方法
实现线程继承Thread
l 定义一个类继承Thread 重写run方法
l 在测试类中实例化这个类的对象 调用start方法
l Start方法只能执行一次
l 执行顺序跟之前不一样
l 图解
- Jvm开启main主线程
- cpu运行主线程
- 运行中 方法中开了一个新线程
- Cpu执行新线程
- 继续执行start方法启动新线程 准备执行run方法
- cpu有了两个执行路径
- cpu自己控制运行main方法的循环还是run方法的循环
- cpu分配时间片给线程
为什么要继承Thread
l 调用run和start区别
- Start开启线程 并让jvm调用run方法在开启的线程中运行
- run就是一个方法而已 等待被执行 没有其他功能
l Thread是线程类 继承这个类就是 线程类
l 直接创建Thread类对象,run方法是这个类的方法,没有任何操作 不能运行自定义的代码 所以需要继承这个类 重写run方法然后执行我们需要在另一个线程中执行的代码
l Run方法的作用是为了执行我们要在新线程中执行的代码
线程运行的内存图
l Main方法先进入
l 然后调用start方法 run方法准备被jvm调用
l Run方法不进入这个栈 会新开一个栈空间单独执行run方法
l
获取线程名称
l main主线程名称就是 main
l 控制台获取默认线程名字
l getName获取线程名
调用父类方法可以不写super
l 获取main线程名字
Static Thread currentThread();
获取正在运行本方法的线程名字 返回Thread类型
简化
设置线程名称
l setName()
- 需要在主线程中改名
- main线程名改不了
l 构造方法改线程名
- 父类中有一个构造方法可以改名称
Sleep方法
l 线程停止特定时间
l 抛异常
因为休眠过程中被唤醒会抛此异常
Runnable接口
l 实现线程的的另一种方式
l 实现接口Runnable
l 实现方法run
示例
- 先创建接口实现类
- 然后创建Thread对象 传递实现类对象
- 然后调用start方法
原理
- Thread有一个构造器方法 可以传入Runnable类型对象
- 定义一个类实现Runable接口 新建对象传入Thread的构造方法中就可以开启一个新线程运行代码
- 避免了单继承局限性
- 线程分为了两个部分 一部分是线程对象 一部分是线程任务
- 从而降低了耦合度
好处
匿名内部类实现线程程序
l 前提 继承或接口实现
l new 父类或者接口实现(){ 重写抽象方法 }
l 示例
- 继承Thread
- 实现接口
线程的状态图
l NEW 新建状态
l RUNNABLE 运行状态 正在JVM虚拟机中执行此线程
l BLOCKED 受阻塞
死锁 CPU资源被抢走
l WAITTING 等待 无限休眠
Object类中的方法
l TIMED_ WAITTING 休眠
l TREMINATED 死亡状态
l 受阻塞具有cpu的执行资格 等待CPU的资源
l 休眠等待 线程放弃CPU的执行资格
线程池
概述
l 是一个容器可以存放多个线程
l 程序一开始的时候 创建多个线程 存放到集合中
l 使用时 用remove方法取出线程 使用完毕 再用add重新添加进去
l 之前都是自己开发线程池
l Jdk1.5之后添加了线程池技术
使用
使用线程池方式 Runable接口
l 由线程池工厂创建
l 再调用线程池中的方法创建线程
l Executors类
- Java.util.concurrent包
- 方法
a) 创建多个
b) 创建单个
c) 返回值是线程池类对象
- 示例
控制台没停止 线程用完后有回到了线程池
线程名字
停止线程
Shutdown 线程停止
实现线程Callable方法
l Runnable接口 线程运行完没有结果 不能抛异常
l Jdk1.5后有个Callable接口 call方法 等同于run
l call方法有返回值 可以抛异常
l 先用工厂类静态方法newFixedRhreadPool创建线程池对象
l 线程对象 调用方法submit提交线程任务 传入一个Callable接口实现类
l 示例
父类接口抛了异常 子类可抛异常也可不抛异常
练习
l 异步计算
线程操作共享数据的安全问题
售票示例
l 多个线程同时运行 同时运行某段代码
l 每次运行结果和单线程运行结果一样
l 售票
l 多种方式购票 多个线程操作同一个数据
l 此时应该数据同时更新 否则会出现安全问题
l 示例
数据正常 但是存在安全隐患
安全问题引发
l T0判断完毕 准备开始操作
l 此时cpu被t1线程抢占
l T1判断完毕 准备进行操作
l 此时被t2线程抢占
l T2判断完毕 准备进行操作
l 此时cpu资源获得 t0执行操作 – 数据为0
l T1也执行—数据为-1
l T2也执行--数据为-2
l 此时出现了线程安全问题
l 模拟示例
- 执行前停顿一下 sleep
解决
- 同步代码块
- 当一个线程进入数据操作是无论是否休眠 其他线程只能等待
- Sun公司提供了一个技术实现了这样的解决
- 公式
Synchronzied(任意对象){
线程操作的共享数据
}
- 同步代码块
- 示例
不能写匿名对象
变得安全了
但是速度变慢了
执行原理
l 同步对象 任意对象
l 对象 :同步锁 对象监视器
l 同步保证安全性 没有锁的线程不能执行 只能等
l 线程遇到同步代码块后 判断同步锁还有没有
l 如果没有就等待
l 如果有 获取锁 将同步锁设为0
l 进入同步代码块 此时如果休眠了
l 另一个线程过来 要执行代码 会判断同步锁是否有 此时显然是没有的 因此就进不去代码块 不能执行代码
l 此时如果第一个线程 唤醒了 就继续执行代码 然后释放同步锁
l 因此一个线程需要判断锁 获取锁 释放锁 所以就延长了很长时间
l 同步锁原理和上厕所
- 对象就是厕所的门
- 同步锁就是厕所门的锁
l 多个线程访问一个共享数据就要设置同步锁
同步方法
l 代码简洁
l 将线程共享数据和同步抽取到一个方法中
l 方法的声明加上同步关键字 然后把同步不代码块删除
l StringBuffer 就有这种同步方法 跑的慢
l StringBuilder 是线程不安全 跑的快
l 同步方法有锁吗?
- 肯定有
- 对象锁是本类的对象引用 this
l 如果方法是静态的
- 静态方法中的锁不是this本类对象引用
- 静态不属于对象引用
- 静态方法中对象锁是 本类类名.class
- 涉及到反射的原理
JDK1.5新特新 Lock接口
l 释放同步锁 看不到
l 如果代码出现异常 锁就不会释放
l 因此JDK1.5后出现了Lock接口
l 此接口提供了比使用synchornized更多的方法和语句可获得的跟广泛的锁定操作
l 示例
- 接口方法
lock 获取锁
unlock 释放锁
- 实现类
ReentrantLock
- 在成员变量 通过实现类创建lock接口的实现类对象
- 在要锁的代码前 调用lock方法获取锁
- 在要锁的代码后调用 unlock方法释放锁
- 如果有异常在异常后面的finally代码里面写unclock 可保证异常出现时正常释放锁
死锁
原理
l 同步锁里面又写了一个同步
l 程序中出现了无限等待
l 前提必须是多线程出现同步嵌套
l 线程进入同步获取锁 不出去同步不会释放锁
l 第一个人需要第二个人的锁 第二个人有需要第第一个人的锁
l
模拟实现
l 定义两个锁对象 A B
- 需要定义两个类
- 并且建立私有构造方法 是外类不能新建实例
- 然后提供一个静态final对象 让外类直接使用 但不可更改
l 循环用奇数偶数确定线程1 和线程2
l 然后一个测试类
- 创建两个线程来执行run方法
l 程序运行结果 程序永远都不会停止运行
- 前几次都是抢占成功的
- 最后两次 第一次 执行偶数循环,获取了a锁,但是还没进入b 锁,就被奇数循环抢占了,获取了b锁
- 此时第一次需要b锁才能继续执行 第二次需要a锁才能继续执行
- 那么此时谁都拿不到所需的资源 所以就死锁了
线程等待和唤醒
概述
l 又叫线程通信
l 多个线程处理同一个资源
l 每个线程任务不一样
l 如果要合理的运用资源
l 就需要通过一种手段使各个线程能有效的利用资源
l 这种手段就叫做 等待唤醒机制
l 比如售票 之前只是减票 现在有的线程需要加票
示例
l 资源类
- 定义公共两个成员变量
l 两个线程类 输入 输出
- 输入
- 输出
l 测试类
因为有两个对象
l 解决
- 死锁 资源类中写私有对象 让外类通过调用新建实例
- 在输入类和输入类中 写一个可接受资源类对象的构造器
- 然后在main方法中 新建一个资源类对象 传入输出类和输入类的构造方法中
- 结果
- 出现了性别乱套
l 解决
- 问题出现原理
a) 输入类抢到了cpu 进行赋值 张三 男
b) 赋值 完 输出没有抢到cpu
c) 输入类仍然抢到了cpu 进行赋值 lisi
d) 刚赋值完lisi 还没赋值nv
e) 输出类抢到了cpu资源 就直接输出 了 lisi nv
f) 此时就出现了 性别乱套
- 解决
a) 只有一种方法加同步锁
b) 找共享数据
c) 在输入类中加锁
输出类加入锁
还是没解决
l 解决
- 原因
a) 两个线程是不是同一个锁
- 将对象锁this改为对象锁为 资源对象 r
- 结果 解决了问题
案例分析
l 最终目标 一个输入 一个输出 交替出现
l 线程只会执行run方法 不分赋值和取值
l 理想状态应该是 一次赋值一次打印
l 只有上一次赋值输出后 下一次赋值才能开始
l 反过来 输出一次后 必须等待下一次输入完毕后才能进行输出
l 实现步骤
- 输入:赋值后执行方法wait永远等待
- 输出 变量值打印输出后 notify输入唤醒 然后输出执行wait永远等待
- 输入:被唤醒后,重新对变量赋值 赋值后必须唤醒输出的线程botify 输入wait
实现
l 为了保证程序执行 输入首先拿到cpu执行权
l 在资源类中加一个变量boolean flag
l flage为真 说明赋值完成
l flage为假 说明获取值完成
l 输入 需要判断标记 flage是否为真
如果为真 则等待
如果为假 就进行赋值 并把标记改为false
l 输出也是如此
l 示例
- 加变量值
输入
输出
结果 抛出了异常
异常
就是说唤醒和等待方法 调用者错了
应该是锁对象 调用
修改
l 结果