Java线程
Java线程的一些基本概念。
内核级线程和用户级线程
内核: 应用程序和硬件沟通的桥梁.
- 用户空间. Java运行在用户空间.
- 内核空间
内核级线程由内核调度,用户级线程(Green Thread)由应用本身调度。
java程序运行起来, 其实就是 JVM 在内存中的执行副本, 是进程. 在创建之初, 会创建一个主线程. 主线程是内核调度的, 如果java是用户级线程, 操作系统的无法调度的, 而操作系统把cpu的执行权限给了主线程, 这样的话, 其它线程就不能并行执行了, 只能共用主线程的时间片段, 也就是只能用一个核心.
在jdk1.1之后, Java的线程都是内核级线程了. 内核线程切换成本高, 实际上更多逻辑集中在用户态. 协程本质就是用户级线程,然后减少了线程的切换,所以成本更低。
Java 采用用户-内核级线程映射的方式实现. n-m的关系, n个内核线程响应M个JVM的线程. 在linux
和windows
下的比例是1:1.
Java进程和线程的关系
- Java 对操作系统提供的功能进行封装, 包括进程和线程.
- 每运行一个 Java 程序, 就会产生一个进程, 进程包含至少一个线程.
- 每个进程对应一个 JVM 实例, 多个线程共享 JVM 里的堆(heap).
- Java 采用单线程编程模型, 程序会自动创建主线程.
- 主线程可以创建子线程, 原则上要后于子线程完成执行.
Thread 中的 start 和 run 方法
直接调用 run
还是会用主线程来执行方法, 而使用 start
会用非主线程, 就是子线程的方式执行.
大致流程: 调用 start
方法, 会调用start0
这个 native
方法, 而 native
方法最终会创建一个新的线程来调用 Thread
的 run
方法.
Thread#start()
-> Thread.c
-> JVM_StartThread
(源文件, jvm.cpp
) -> thread_entry
(回调Java的方法) -> Thread#run()
JNI 命名规范: Java_packageName_ClassName_methodName. e.g. Java_java_lang_Thread_registerNatives
结论: 调用 start()
方法会创建一个新的子线程并启动, 而 run()
方法只是 Thread
的一个普通方法调用, 还是在主线程中执行的.
Thread 和 Runnable 的关系
Thread
是一个类, 而 Runnable
是一个接口. Thread
实现了 Runnable
接口.
Thread
是实现了Runnable
接口的类, 使得run
支持多线程.- 因类的单一继承原则, 推荐使用
Runnable
接口.
如何实现处理线程的返回值
- 主线程等待法(精准度不好控制).
- 使用 Thread 类的 join() 阻塞当前线程以等待子线程处理完毕, 比上面的精度更高, 但是粒度还是不够细.
- 通过 Callable 接口实现 : 通过 Future Task Or 线程池获取.
sleep 和 wait 的区别
sleep
是Thread
类的方法, 而wait
是Object
的方法。sleep()
可以在任何地方使用。wait()
方法一般在synchronized
方法或synchroized
块中使用。Thread.sleep
只会让出 CPU, 不会导致锁行为的释放。Object.wait()
不仅让出 CPU, 还会释放已经占用的同步资源锁。
notify 和 notifyall 的区别
两个重要概念
- 锁池
EntrySet
。 - 等待池
WaitSet
。
锁池: 假设线程A已经拥有了某个对象(不是类锁)的锁, 而其它线程B, C想要调用这个对象的某个 synchronized
方法(或者块), 由于B, C线程在进入对象的 synchronized
方法(或者块)之前必须获得对象锁的拥有权, 而恰巧该对象的锁目前正被 线程A 所占用着, 此时 B, C 线程就会被阻塞, 进入一个地方等待锁的释放, 而这个地方就是该对象的锁池(EntrySet)。
等待池: 假设线程 A 调用了某个对象的 wait()
方法, 线程 A 就会释放该对象的锁, 同时线程 A 就进入到该对象的等待池中, 进入到等待池中的线程不会去竞争该对象的锁。
在真实的开发中, 会有很多线程去竞争这个锁, 此时优先级高的线程, 竞争到锁的概率高. 假设某个线程没有竞争到该对象锁, 它只会留在 锁池 中, 并不会重新进入到等待池中, 而竞争到对象锁的线程, 则继续往下执行, 直到执行完 synchronized
方法(或者块) 或者 遇到异常, 才会释放掉该对象锁, 这时锁池中的线程会继续竞争该对象锁。
notifyall
会让所有处于 等待池 的线程全部进入 锁池 去竞争获取锁的机会. 没有获取到锁, 而已经在 锁池 中的线程, 只能等待其它机会获取锁, 而不会再回到 等待池 中。notify
只会随机选取一个处于 等待池 中的线程进入 锁池 去竞争获取锁的机会。
yiedl 函数
当调用 Thread.yiedl()
[jiːld] 方法时, 给线程调度器一个暗示, 当前线程愿意让出 CPU 使用权. 但是调度器有可能忽略这个暗示.
中断线程
已经过时的方法
- 通过调用
stop()
方法停止线程. - 通过调用
suspend()
和resume()
方法恢复.
目前使用的方法
- 调用
interrupt()
, 通知线程应该中断了.- 如果线程处于被阻塞状态(sleep, wait, join等), 那么线程将立即退出被阻塞状态, 并抛出
InterruptedException
异常. - 如果线程处于正常活动状态, 那么会将线程的中断标志设置为
true
. 被设置中断标志的线程将继续正常运行, 不受影响.
- 如果线程处于被阻塞状态(sleep, wait, join等), 那么线程将立即退出被阻塞状态, 并抛出
- 需要被调用的线程配合中断.
- 在正常运行任务时, 经常检查本线程的中断标志位, 如果被设置了中断标志就自行停止线程。
线程的状态
基本的三个状态
- 运行态(Running): 执行。
- 就绪态(Ready): 排队。
- 休眠态(Sleeping): 通常说的阻塞态(Blocking)是休眠的一种情况。
就绪态 -> 休眠态? 不能, 线程没有执行, 没有办法进入阻塞态。
阻塞态 -> 运行态? 不能, 需要重新排队。
Java特有状态: new/terminated
runnable
对应排队和执行, 也就是就绪态和运行态.waitng/time_waiting/blocking
对应休眠状态.time_waiting
: 定时休眠. sleep, timeout.
waiting
: 线程之间互相等待. 等待的是信号.
blocking
: I/O
请求, 锁
详解六个状态:
- 新建(New) : 新建后尚未启动的线程状态.
- 运行(Runnable) : 包含
Running
和Ready
(有可能正在执行, 或者等待分配到CPU的时间片), 调用start
方法. 处于running
状态的线程, 位于可运行线程池当中, 等待被线程调度选中, 获取cpu使用权; 处于ready
状态线程, 在获取cpu时间片后, 就变为running
状态. - 无限等待(Waiting) : 不会被分配CPU执行时间, 需要显示被唤醒.
- 没有设置
Timeout
参数的Object.wait()
方法 - 没有设置
Timeout
参数的Thread.join()
方法 LockSupport.park()
方法
- 没有设置
- 限期等待(Timed Waiting) : 在一定时间后会由系统自动唤醒.
Thread.sleep()
方法- 设置了
Timeout
参数的Object.wait()
方法 - 设置了
Timeout
参数的Thread.join()
方法 LockSupport.parkNanos()
方法LockSupport.parkUntil()
方法
- 阻塞(Blocked) : 等待获取排它锁. 等待其他线程释放锁.(
synchroized
关键字修饰的方法) - 结束(Terminated) : 已终止线程的状态, 线程已经结束执行. 在一个终止的线程再一次调用
start()
方法, 会抛出java.lang.IllegalThreadStateException
异常.
Thread.join
是哪种状态? 等待其它线程通知自己, 没有设置 timeout
参数. 属于waiting
.
Thread.sleep
是哪种状态? 有时间限制的,属于time_waiting
.
网络请求在linux
和windows
下都属于文件操作.
线程状态以及状态之间切换
线程的切换
线程是执行单位. 线程拥有计算资源. 所以切换成本低. ThreadLocal
如何理解? 实际是线程可以看到了进程上的所有资源. 而用起来像一个 HashMap, 以线程对象本身, 去做 Hash
, 拿这个对象, 在 HashMap
中的另外 key-value
的存储. 是在语言级别实现限制,看上去是线程独有的,实际上是所有线程都能看到的,只是java
语言做了限制。本质上是绑定在 Thread 的 Map 上,而 key
就是 线程本身。
ThreadLocal 数据存储于
ThreadLocalMap
, 其内部是弱引用。
Context Switch(上下文切换)
涉及到用户态和内核态之间的切换。
- CPU中断,保存当前线程的状态(CPU寄存器的值)
- OS执行调度程序, 重新选择其它线程执行
- 恢复寄存器, 运行新线程