Thread 源码分析

在分析线程Thread源码时候需要了解几个知识点:

1.线程状态(NEW ,RUNABLE,BLOCKED,WATING,TIMED_WATING,TERMINATED)

2.线程的基础特性

3.如何创建线程 (构造函数)

4.jvm本地方法如何通过native方法实现run,start,yeild,sleep,interrupt,interrupted,join,suspend ,resume等方法使用和原理。

线程状态

    • NEW
      至今尚未启动的线程处于这种状态。
    • RUNNABLE
      正在 Java 虚拟机中执行的线程处于这种状态。
    • BLOCKED
      受阻塞并等待某个监视器锁的线程处于这种状态。
    • WAITING
      无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。 WAITING 这个状态下是指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll 一遍该线程可以继续下一步操作, 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在理解点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束
    • TIMED_WAITING
      等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
    • TERMINATED
      已退出的线程处于这种状态。

在给定时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所有操作系统线程状态。

线程的一些基本特性

  • 每个线程均有优先级  
  • private volatile String name;
        private int            priority;
        private Thread         threadQ;
        private long           eetop;

    线程优先级通过priority属性

  • 线程能被标记为守护线程

    守护线程在所有前台线程执行完毕后会自动kill掉。

  • 每个线程均分配一个name

          默认为线程分配一个名字,也可以自定义,默认为thread-i(++)线程名字为默认自增,线程安全方法。

创建线程的几种方式

    1.继承thread并重写run方法

    class PrimeThread extends Thread {

    long minPrime; PrimeThread(long minPrime) {

        this.minPrime = minPrime;

     }

      public void run() { //需要实现的方法  . . . }

    }

  PrimeThread p = new PrimeThread(143);
  p.start();
2.实现runable接口并实现run方法
class PrimeRun implements Runnable {
long minPrime; PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() { // 需要实现的方法 }
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
线程常用方法和主要原理
当线程在初始化的时候会在调用本地方法,registerNatives()这个方法由jvm 虚拟机实现;

(1)线程复用

像线程池类高效的原因在于,线程池中的线程在完成任务后,不会销毁,而且缓存起来,每当用户请求一个线程处理任务时,线程池可以利用缓存的空闲线程来处理用户任务,这样避免了线程创建销毁带来的开销。

在Thread类中有一个Runnable target的域,只需将target替换成新的Runnable即可。

在实现run方法的时候

(2)wait()和notify/notifyAll()方法

wait()方法

  • 线程进入WAITING状态,并且释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁,等待其他线程调用“锁标志“对象的notify或notifyAll方法恢复
  • wait方法是一个本地方法,其底层是通过一个叫做监视器锁的对象来完成的,所以调用wait方式时必须获取到monitor对象的所有权即通过Synchronized关键字,否则抛出IllegalMonitorStateException异常

notify/notifyAll()方法

  • 在同一对象上去调用notify/notifyAll方法,就可以唤醒对应对象monitor上等待的线程了。notify和notifyAll的区别在于前者只能唤醒monitor上的一个线程,对其他线程没有影响,而notifyAll则唤醒所有的线程

(3)sleep/yield/join方法解析

sleep

  • sleep方法的作用是让当前线程暂停指定的时间(毫秒)
  • wait方法依赖于同步,而sleep方法可以直接调用
  • sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁

yield

  • yield方法的作用是暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止
  • yield只能使同优先级或更高优先级的线程有执行的机会

join

  • 等待调用join方法的线程结束,再继续执行。如:t.join(),主要用于等待t线程运行结束
  • 作用是父线程等待子线程执行完成后再执行,换句话说就是将异步执行的线程合并为同步的线程

suspend()和resume()

  • 这两个方法是配套使用的,suspend()是暂停线程,但并不释放资源,容易造成死锁情况

stop()

  • 因为调用stop会使线程释放所有的锁,导致不安全情况,在调用stop时候,由锁保护的临界区可能处于状态不一致的情况,这不一致状态将暴露给其他线程
  • 推荐的做法是,维护一个状态变量,当线程需要停止时更改这一状态变量,该线程应检查这一状态变量,看该线程是否应该终止了

(5)关于interrupt()中断函数

  • 其实调用这个函数并不是真的中断线程,这个函数只是将Thread中的interrupt标志设置为true,用户需自行检测这一变量,停止线程,这种做法避免了stop带来的问题
而jvm有是如何来让内核和cup帮助我们工作的呢?
当执行Thread.run 会内核函数调用fock产生一个线程
Java线程在Windows及Linux平台上的实现方式,现在看来,是内核线程的实现方式。这种方式实现的线程,是直接由操作系统内核支持的——由内核完成线程切换,内核通过操纵调度器(Thread Scheduler)实现线程调度,并将线程任务反映到各个处理器上。
内核线程是内核的一个分身。程序一般不直接使用该内核线程,而是使用其高级接口,即轻量级进程(LWP),也即线程。这看起来可能很拗口。看图:

KLT即内核线程Kernel Thread,是“内核分身”。每一个KLT对应到进程P中的某一个 轻量级进程LWP(也即线程),期间要经过用户态、内核态的切换,并在Thread Scheduler 下反应到处理器CPU上。如果是多N:1 会造成性能瓶颈,所有线程都在一个cup上。
从线程模型上看是1:1(内核线程), Java SE最常用的JVM是Oracle/Sun研发的HotSpot VM。在这个JVM的较新版本所支持的所有平台上,它都是使用1:1线程模型的——除了Solaris之外,它是个特例。
这种线程模型也有一定的缺陷,就是程序在使用内核线程时候会造成操作系统上多次来回切换内核态和用户态度,而且lwp支持数是有限的。应用程序开辟的线程至少等于cpu的内核数量。
java 最少内核数量Runtime.getRuntime().availableProcessors();能获得最小线程数。
这样就会带来一个问题,如何开辟合适的线程数?这里需要知道具体应用计算密集型还是io密集型。显然大部分java都是io密集型应用。
如果所有的任务都是计算密集型的,这个最小线程数量就是我们需要的线程数。在某个线程在执行io操作时候,那么线程被阻塞,处理器会立刻切换到另一个合适的线程去执行,如果我们只拥有与内核数量一样多的线程,即使我们有任务要执行,他们也不能执行,因为处理器没有可以用来调度的线程。
如果线程有50%的时间被阻塞,线程的数量就应该是内核数量的2倍。如果更少的比例被阻塞,那么它们就是计算密集型的,则需要开辟较少的线程。如果有更多的时间被阻塞,那么就是IO密集型的程序,则可以开辟更多的线程。于是我们可以得到下面的线程数量计算公式:
线程数量=内核数量 / (1 - 阻塞率)
我们可以通过相应的分析工具或者java的management包来得到阻塞率的数值。
 
 
 
 
 
 
 


猜你喜欢

转载自www.cnblogs.com/Cjbolgs/p/9228089.html