抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

Java线程

Java线程的一些基本概念。

内核级线程和用户级线程

内核: 应用程序硬件沟通的桥梁.

  • 用户空间. Java运行在用户空间.
  • 内核空间

内核级线程内核调度,用户级线程(Green Thread)由应用本身调度。

java程序运行起来, 其实就是 JVM 在内存中的执行副本, 是进程. 在创建之初, 会创建一个主线程. 主线程是内核调度的, 如果java是用户级线程, 操作系统的无法调度的, 而操作系统把cpu的执行权限给了主线程, 这样的话, 其它线程就不能并行执行了, 只能共用主线程的时间片段, 也就是只能用一个核心.

在jdk1.1之后, Java的线程都是内核级线程了. 内核线程切换成本高, 实际上更多逻辑集中在用户态. 协程本质就是用户级线程,然后减少了线程的切换,所以成本更低。

Java 采用用户-内核级线程映射的方式实现. n-m的关系, n个内核线程响应M个JVM的线程. 在linuxwindows下的比例是1:1.

Java进程和线程的关系

  • Java 对操作系统提供的功能进行封装, 包括进程和线程.
  • 每运行一个 Java 程序, 就会产生一个进程, 进程包含至少一个线程.
  • 每个进程对应一个 JVM 实例, 多个线程共享 JVM 里的堆(heap).
  • Java 采用单线程编程模型, 程序会自动创建主线程.
  • 主线程可以创建子线程, 原则上要后于子线程完成执行.

Thread 中的 start 和 run 方法

直接调用 run 还是会用主线程来执行方法, 而使用 start 会用非主线程, 就是子线程的方式执行.

大致流程: 调用 start 方法, 会调用start0 这个 native 方法, 而 native 方法最终会创建一个新的线程来调用 Threadrun 方法.

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 的区别

  • sleepThread 类的方法, 而 waitObject 的方法。
  • 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(), 通知线程应该中断了.
    1. 如果线程处于被阻塞状态(sleep, wait, join等), 那么线程将立即退出被阻塞状态, 并抛出InterruptedException异常.
    2. 如果线程处于正常活动状态, 那么会将线程的中断标志设置为 true. 被设置中断标志的线程将继续正常运行, 不受影响.
  • 需要被调用的线程配合中断.
    1. 在正常运行任务时, 经常检查本线程的中断标志位, 如果被设置了中断标志就自行停止线程。

线程的状态

基本的三个状态

  • 运行态(Running): 执行。
  • 就绪态(Ready): 排队。
  • 休眠态(Sleeping): 通常说的阻塞态(Blocking)是休眠的一种情况。

就绪态 -> 休眠态? 不能, 线程没有执行, 没有办法进入阻塞态。

阻塞态 -> 运行态? 不能, 需要重新排队。

Java特有状态: new/terminated

  1. runnable 对应排队执行, 也就是就绪态运行态.

  2. waitng/time_waiting/blocking对应休眠状态.

  3. time_waiting: 定时休眠. sleep, timeout.

waiting: 线程之间互相等待. 等待的是信号.

blocking: I/O请求, 锁

详解六个状态:

  1. 新建(New) : 新建后尚未启动的线程状态.
  2. 运行(Runnable) : 包含 RunningReady(有可能正在执行, 或者等待分配到CPU的时间片), 调用 start 方法. 处于 running 状态的线程, 位于可运行线程池当中, 等待被线程调度选中, 获取cpu使用权; 处于 ready 状态线程, 在获取cpu时间片后, 就变为 running 状态.
  3. 无限等待(Waiting) : 不会被分配CPU执行时间, 需要显示被唤醒.
    • 没有设置 Timeout 参数的 Object.wait() 方法
    • 没有设置 Timeout 参数的 Thread.join() 方法
    • LockSupport.park() 方法
  4. 限期等待(Timed Waiting) : 在一定时间后会由系统自动唤醒.
    • Thread.sleep() 方法
    • 设置了 Timeout 参数的 Object.wait() 方法
    • 设置了 Timeout 参数的 Thread.join() 方法
    • LockSupport.parkNanos() 方法
    • LockSupport.parkUntil() 方法
  5. 阻塞(Blocked) : 等待获取排它锁. 等待其他线程释放锁.(synchroized关键字修饰的方法)
  6. 结束(Terminated) : 已终止线程的状态, 线程已经结束执行. 在一个终止的线程再一次调用 start() 方法, 会抛出 java.lang.IllegalThreadStateException 异常.

Thread.join 是哪种状态? 等待其它线程通知自己, 没有设置 timeout 参数. 属于waiting.

Thread.sleep 是哪种状态? 有时间限制的,属于time_waiting.

网络请求linuxwindows下都属于文件操作.

线程状态以及状态之间切换

线程的切换

线程是执行单位. 线程拥有计算资源. 所以切换成本低. ThreadLocal 如何理解? 实际是线程可以看到了进程上的所有资源. 而用起来像一个 HashMap, 以线程对象本身, 去做 Hash, 拿这个对象, 在 HashMap 中的另外 key-value 的存储. 是在语言级别实现限制,看上去是线程独有的,实际上是所有线程都能看到的,只是java语言做了限制。本质上是绑定在 Thread 的 Map 上,而 key 就是 线程本身。

ThreadLocal 数据存储于 ThreadLocalMap, 其内部是弱引用。

Context Switch(上下文切换)

涉及到用户态和内核态之间的切换。

  • CPU中断,保存当前线程的状态(CPU寄存器的值)
  • OS执行调度程序, 重新选择其它线程执行
  • 恢复寄存器, 运行新线程

评论