java多线程
[toc]
java多线程
1.并发编程
1.1 优缺点
优点:
- 充分发挥多核CPU计算能力, 性能得以提升
- 方便业务拆分, 提高系统并发能力和性能 (后端服务)
缺点: - 并不能提高程序运行速度
- 容易引发: 内存泄漏, 上下文切换, 线程安全, 死锁等问题
1.2 并发编程三要素
- 原子性: 一个或者多个参数要么全部执行成功, 要么全部失败
- 可见性: 一个线程对共享元素修改. 另外一个线程能立即看到 (synchronized,volatile)
- 有序性: CPU按照代码先后顺序执行
2. 多线程
多线程: 程序中包含多个执行流, 一个程序同时运行多个线程来执行不同任务
优点: 提高CPU利用率(一个线程等待, CPU可以切换执行其他线程, 提高执行效率)
缺点:
- 线程占用内存, 线程越多占用内存也越多
- 多线程需要协调管理, 需要CPU时间来跟踪线程
- 多线程对共享资源的访问会相互影响, 需要解决资源竞争问题
3. 线程与进程
3.1 进程
- 一个在内存中运行的程序, 每个进程都有独立的内存空间, 一个进程有多个线程
- 进程为操作系统分派的基本单位
- 独立的代码和数据空间,程序上下文; 程序之前切换会有较大的开销
- 进程之间的地址空间和资源相互独立
3.2 线程
- 进程中的一个执行任务, 负责在当前进程任务的执行, 一个进程至少有一个线程, 多个线程共享数据
- 处理器任务调度与执行的基本单位
- 轻量级进程, 同一类线程共享堆和方法区, 每个线程有独立的运行栈和程序计数器, 虚拟机栈, 本地方法栈.线程切换开销小
- 同一进程线程共享本地进程的地址空间和资源
程序计数器为何私有:
字节码通过改变程序计数器来依次读取指令,从而实现流程控制(顺序执行,选择,循环,异常处理);在多线程情况下,程序计数器用于记录当前线程执行位置, 当线程切换时就知道当前线程执行到哪了(如果执行native方法, 则计数器记录undefined地址, 只有执行java代码时才记录下一条指令地址). 程序计数器私有主要是为了线程切换后恢复到正确位置
本地方法栈,虚拟机栈为何私有:
虚拟机栈: 每个方法执行同时会创建栈帧用来存储局部变量标. 操作数栈,常量引用等信息, 从方法开始到方法执行完成, 每个栈帧对应虚拟机入栈,出栈过程
本地方法栈: 虚拟机执行本地方法.
本地方法栈,虚拟机栈是为了局部变量不被其他线程访问
3.3 线程生命周期
名称 | 说明 |
---|---|
NEW | 初始状态, 线程被构建, 还没有调用start()方法 |
RUNABLE | 运行状态, java将操作系统中的就绪和运行笼统成为运行中 |
BLOCKED | 阻塞状态, 线程被锁阻塞了 |
WAITING | 等待状态, 当前线程需要其他线程特定动作(通知或中断) |
TIME_WAITING | 超时等待状态, 可以在指定时间后返回RUNNABLE状态 |
TERMINATED | 终止状态, 线程执行完毕 |
digraph threadGraph {
node [shape = circle, style=rounded];
"start";
"end" [style=filled, fillcolor=black, fontcolor=white];
node [shape=box];
compound=true;
"等待\n(WAITING)" -> "运行中\n(RUNNING)"[label = "Object.notify()\nObject.notifyAll()\nLockSupport.unpark(Thread)",lhead="cluster_thread_running"];
"运行中\n(RUNNING)" -> "等待\n(WAITING)"[label = "Object.wait()\nThread.join()\nLockSupport.park(Thread)", ltail="cluster_thread_running"];
"start" -> "初始\n(NEW)" [label = "实例化"]
"初始\n(NEW)" -> "运行中\n(RUNNING)"[lhead="cluster_thread_running"];
"就绪\n(READY)" -> "终止\n(TERMINATED)"[label = "执行完成", ltail="cluster_thread_running"];
"终止\n(TERMINATED)" -> "end";
"超时等待\n(TIME_WAITING)" -> "就绪\n(READY)"[label = "Object.notify()\nObject.notifyAll()\nLockSupport.unpark(Thread)",lhead="cluster_thread_running"];
"就绪\n(READY)" -> "超时等待\n(TIME_WAITING)"[label = "Thread.sleep(long)\nObject.wait(long)\nThread.join(long)\nLockSupport.parkNanos()\nLockSupport.partUtil()", ltail="cluster_thread_running"];
"阻塞\n(BLOCKED)" -> "运行中\n(RUNNING)"[label = "等待进入synchronized方法\n等待进入synchronized块",lhead="cluster_thread_running"];
"运行中\n(RUNNING)" -> "阻塞\n(BLOCKED)"[label = "获取到锁", ltail="cluster_thread_running"];
"就绪\n(READY)" -> "运行中\n(RUNNING)" [label="系统调度"]
"运行中\n(RUNNING)" -> "就绪\n(READY)" [label="Thread.yield()\n系统调度"]
subgraph "cluster_thread_running" {
"运行中\n(RUNNING)";
"就绪\n(READY)";
label="运行\n(RUNABLE)";
}
}
为何没区分RUNNING和READY状态:
CPU通过时间片的方式抢占轮转任务, 每个时间片最多执行10-20ms(此时处于RUNNING状态), 然后被CPU切换放到调度队列末尾等待再次调度(转为READY状态), 这个时间太短了.就不区分了.
3.4 上下文切换
线程执行会记录自己的运行状态(程序计数器, 执行栈等, 也称为上下文), 当出现一下情况,会从CPU占用中退出
- 主动让出CPU, 如sleep(), wait(), yield()
- 线程时间片用完了, 操作系统防止某个线程或者进程长时间占用CPU,导致其他线程或者进程饿死
- 调用了阻塞类型的系统中断, 如请求IO, 线程被阻塞
- 更高优先级线程出现
- 终止或结束运行
发生前3个情况就会进行线程切换,需要保存当前线程的上下文, 等下次占用CPU时恢复现场, 并加载下一个需要执行线程的上下文, 称为上下文切换
上下文切换都需要保存上下文, 切换下一个上下文,会占用内存,CPU等资源进行处理, 所以频繁上下文切换会造成整体效率低下
3.5 死锁
死锁发生条件:
- 互斥条件: 资源任意时刻只有一个线程占用
- 请求与保存条件: 请求资源保存阻塞或者占用资源不释放
- 不剥夺条件: 线程已获得资源未使用完成不能被其他资源强行剥夺,只有使用完成才能释放
- 循环等待条件: 若干线程形成头尾相连的循环等待资源释放
预防死锁:
- 破坏请求与保存条件: 一次性申请所有资源
- 破坏不剥夺条件: 占用部分资源线程进一步申请资源, 如果申请不到, 主动释放所有占用资源
- 破坏循环等待条件: 按照某一顺序申请资源, 释放则反序释放
3.6 创建线程
- 继承Thread类
- 实现Runable接口
- 实现Callable接口
FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable()); Thread thread = new Thread(futureTask);
- 使用Executors创建线程池
Runnable与Callable区别:
Runnable: 无返回值, 只能抛出运行时异常且无法捕获异常
Callable: 有返回值, 需要使用Future, FutureTask来获取结果; 运行抛出异常, 捕获异常
3.6 sleep()
与wait()
区别
- 都会暂停线程执行, sleep()不释放锁, wait()释放锁
- wait()用于线程通信/交换, sleep()用于暂停执行
- wait()需要其他线程notify()/notifyAll()唤醒, sleep()再等待之后自主唤醒, wait(long timeout)超时后, 也会自动唤醒
为什么wait(), notify(), notifyAll()会在Object类中定义
- java任何对象都可以加锁, wait()用于等待对象锁,或者唤醒线程, 而线程中没有提供任何对象使用的锁, 所以只能再Object中
- 线程如果提供任何对象锁, 但是一个线程可以持有多把锁, 放弃时不知道放弃哪一把. 让管理更加复杂
3.7 sleep()
和yield()
Thread.yield()作用:
让出当前线程时间片, 使当前线程从RUNNING变READY
Thread.sleep()和Thread.yield()为何静态:
都是再当前线程执行, 避免使用错误
区别:
- sleep给其他线程计划, 不考虑其他线程优先级, 给低优先级线程机会, yield()只会让出当前时间片给相同优先级或者更高优先级的线程执行机会
- sleep转为BLOCKED状态, yield转为READY状态
- sleep抛出异常, yield不抛出异常
- sleep更好移植性, yield()不好控制并发线程执行
3.8 start()
和run()
start()调用的native方法, 启动线程, 而调用run()相当于当前线程执行run()函数
3.9 退出线程方法
- 使用退出标志, 使线程正常退出, 相当于执行完成
- 使用
stop()(不推荐) - 使用interrupt方法中断线程
3.10 Thread.interrupt()
,Thread.interrupted()
,Thread.isInterrupted()
区别
interrupt(): 给线程实例设置中断标志
1) 如果线程处于阻塞状态(sleep, wait, join时), 立即退出被阻塞状态, 并抛出InterruptedException
2) 如果正常执行,设置中断标志为true, 线程还会正常执行
所以在线程中判断
Thread t = new Thread(() -> {
// 若未发生中断,就正常执行任务
while(!Thread.currentThread.isInterrupted()){
// do something
}
// 中断的处理代码……
doSomething();
})
interrupted(): 静态方法; 测试当前线程是否被中断(判断中断标志), 会清除中断状态(第二次调用就不算被中断了).
isInterrupted(): 测试线程实例是否被中断, 不会清除中断状态
3.11 线程协作通信
协作:
- synchronized加锁线程Object类的wait()/notify()/notifyAll()
- ReetrantLock类加锁线程的Condition类的await()/signal()/signalAll()
通信:
通过管道的字节流, 字符流
3.12 同步块与同步方法
同步块是更好的选择, 不会锁住整个对象, 只需要锁住代码块锁的对象, 避免死锁
3.13 线程优先级
高优先级具有优先权, 但依赖操作系统线程调度实现. 优先级 1 - 10. 一般不会去设置
3.14 线程构造方法, 静态块被谁调用
线程构造方法, 静态块被new这个线程的所在线程调用, 只有run才被线程自身调用
3.15 线程发生异常
如果线程没有被捕获. 则线程停止执行, Thread.UncaughtExceptionHandler
为线程捕获异常的接口, 然后通过Thread.getUncaughtExceptionHandler()
来查询异常handler, 调用handler来处理结果.