外墙保温板材质|苏洋建材新闻资讯

四川建材 2021-10-18 阅读:198

外墙保温板材质

  关注

  “脚本之家

  ”,与百万开发者在一起

  外墙保温板材质|苏洋建材新闻资讯  外墙保温板材质 第1张

  出品 | Throwable(ID:throwable-doge)

  外墙保温板材质|苏洋建材新闻资讯  外墙保温板材质 第2张

  前提

  最近有点懒散,没什么比较有深度的产出。刚好想重新研读一下JUC线程池的源码实现,在此之前先深入了解一下Java中的线程实现,包括线程的生命周期、状态切换以及线程的上下文切换等等。编写本文的时候,使用的JDK版本是11。

  Java线程的实现

  在「JDK1.2之后」,Java线程模型已经确定了基于操作系统原生线程模型实现。因此,目前或者今后的JDK版本中,操作系统支持怎么样的线程模型,在很大程度上决定了Java虚拟机的线程如何映射,这一点在不同的平台上没有办法达成一致,虚拟机规范中也未限定Java线程需要使用哪种线程模型来实现。线程模型只对线程的并发规模和操作成本产生影响,对于Java程序来说,这些差异是透明的。

  对应Oracle Sun JDK或者说Oracle Sun JVM而言,它的Windows版本和Linux版本都是使用「一对一的线程模型」实现的(如下图所示)。

  外墙保温板材质|苏洋建材新闻资讯  外墙保温板材质 第3张

  j-t-l-s-1.png

  也就是一条Java线程就映射到一条轻量级进程(「Light Weight Process」)中,而一条轻量级线程又映射到一条内核线程(「Kernel-Level Thread」)。我们平时所说的线程,往往就是指轻量级进程(或者通俗来说我们平时新建的java.lang.Thread就是轻量级进程实例的一个"句柄",因为一个java.lang.Thread实例会对应JVM里面的一个JavaThread实例,而JVM里面的JavaThread就应该理解为轻量级进程)。前面推算这个线程映射关系,可以知道,我们在应用程序中创建或者操作的java.lang.Thread实例最终会映射到系统的内核线程,如果我们恶意或者实验性无限创建java.lang.Thread实例,最终会影响系统的正常运行甚至导致系统崩溃(可以在Windows开发环境中做实验,确保内存足够的情况下使用死循环创建和运行java.lang.Thread实例)。

  线程调度方式包括两种,协同式线程调度和抢占式线程调度。

  线程调度方式描述劣势优势协同式线程调度线程的执行时间由线程本身控制,执行完毕后主动通知操作系统切换到另一个线程上某个线程如果不让出CPU执行时间可能会导致整个系统崩溃实现简单,没有线程同步的问题抢占式线程调度每个线程由操作系统来分配执行时间,线程的切换不由线程自身决定实现相对复杂,操作系统需要控制线程同步和切换不会出现一个线程阻塞导致系统崩溃的问题

  Java线程最终会映射为系统内核原生线程,所以Java线程调度最终取决于系操作系统,而目前主流的操作系统内核线程调度基本都是使用抢占式线程调度。也就是可以死记硬背一下:「Java线程是使用抢占式线程调度方式进行线程调度的」。

  很多操作系统都提供线程优先级的概念,但是由于平台特性的问题,Java中的线程优先级和不同平台中系统线程优先级并不匹配,所以Java线程优先级可以仅仅理解为“「建议优先级」”,通俗来说就是java.lang.Thread#setPriority(int newPriority)并不一定生效,「有可能Java线程的优先级会被系统自行改变」。

  Java线程的状态切换

  Java线程的状态可以从java.lang.Thread的内部枚举类java.lang.Thread$State得知:

  publicenumState {

  NEW,

  RUNNABLE,

  BLOCKED,

  WAITING,

  TIMED_WAITING,

  TERMINATED;

  }

  这些状态的描述总结成图如下:

  j-t-l-s-3

  「线程状态之间关系切换」图如下:

  外墙保温板材质|苏洋建材新闻资讯  外墙保温板材质 第4张

  j-t-l-s-2

  下面通过API注释和一些简单的代码例子分析一下Java线程的状态含义和状态切换。

  NEW状态

  「API注释」:

  NEW,

  线程实例尚未启动时候的线程状态。

  ?

  ?

  一个刚创建而尚未启动(尚未调用Thread#start方法)的Java线程实例的就是处于NEW状态。

  publicclassThreadState{

  publicstaticvoidmain(String args)throwsException {

  Thread thread=newThread;

  System.out.println(thread.getState);

  }

  }

  // 输出结果

  NEW

  RUNNABLE状态

  「API注释」:

  RUNNABLE,外墙保温板材质

  可运行状态下线程的线程状态。可运行状态下的线程在Java虚拟机中执行,但它可能执行等待操作系统的其他资源,例如处理器。

  ?

  ?

  当Java线程实例调用了Thread#start之后,就会进入RUNNABLE状态。RUNNABLE状态可以认为包含两个子状态:READY和RUNNING。

  READY:该状态的线程可以被线程调度器进行调度使之更变为 RUNNING状态。

  RUNNING:该状态表示线程正在运行,线程对象的 run方法中的代码所对应的的指令正在被CPU执行。

  当Java线程实例Thread#yield方法被调用时或者由于线程调度器的调度,线程实例的状态有可能由RUNNING转变为READY,但是从线程状态Thread#getState获取到的状态依然是RUNNABLE。例如:

  publicclassThreadState1{

  Thread thread=newThread(-> {

  while( true){

  Thread.yield;

  }

  });

  thread.start;

  Thread.sleep( 2000);

  // 输出结果

  RUNNABLE

  WAITING状态

  「API注释」:

  WAITING,

  等待中线程的状态。一个线程进入等待状态是由于调用了下面方法之一:不带超时的Object#wait 不带超时的Thread#join LockSupport.park 一个处于等待状态的线程总是在等待另一个线程进行一些特殊的处理。例如:一个线程调用了Object#wait,那么它在等待另一个线程调用对象上的Object#notify或者Object#notifyAll;一个线程调用了Thread#join,那么它在等待另一个线程终结。

  ?

  ?

  WAITING是「无限期的等待状态」,这种状态下的线程不会被分配CPU执行时间。当一个线程执行了某些方法之后就会进入无限期等待状态,直到被显式唤醒,被唤醒后,线程状态由WAITING更变为RUNNABLE然后继续执行。

  RUNNABLE转换为WAITING的方法(无限期等待)WAITING转换为RUNNABLE的方法(唤醒)Object#waitObject#notify | Object#notifyAllThread#join-LockSupport.partLockSupport.unpart(thread)

  其中Thread#join方法相对比较特殊,它会阻塞线程实例直到线程实例执行完毕,可以观察它的源码如下:

  publicfinalvoidjointhrowsInterruptedException {

  join( 0);

  }

  publicfinalsynchronizedvoidjoin(longmillis)throwsInterruptedException {

  longbase=System.currentTimeMillis;

  longnow=0;

  if(millis < 0) {

  thrownewIllegalArgumentException( "timeout value is negative");

  }

  if(millis==0) {

  while(isAlive) {

  wait( 0);

  }

  } else{

  while(isAlive) {

  longdelay=millis - now;

  if(delay <=0) {

  break;

  }

  wait(delay);

  now=System.currentTimeMillis - base;

  }

  }

  }

  可见Thread#join是在线程实例存活的时候总是调用Object#wait方法,也就是必须在线程执行完毕isAlive为false(意味着线程生命周期已经终结)的时候才会解除阻塞。

  基于WAITING状态举个例子:

  publicclassThreadState3{

  LockSupport.park;

  while( true){

  Thread.sleep( 50);

  System.out.println(thread.getState);

  LockSupport.unpark(thread);

  // 输出结果

  WAITING

  RUNNABLE

  TIMED WAITING状态

  「API注释」:

  TIMED_WAITING,

  定义了具体等待时间的等待中线程的状态。一个线程进入该状态是由于指定了具体的超时期限调用了下面方法之一:Thread.sleep 带超时的Object#wait 带超时的Thread#join LockSupport.parkNanos LockSupport.parkUntil

  ?

  ?

  TIMED WAITING就是「有限期等待状态」,它和WAITING有点相似,这种状态下的线程不会被分配CPU执行时间,不过这种状态下的线程不需要被显式唤醒,只需要等待超时限期到达就会被VM唤醒,有点类似于现实生活中的闹钟。

  RUNNABLE转换为TIMED WAITING的方法(有限期等待)TIMED WAITING转换为RUNNABLE的方法(超时解除等待)Object#wait(timeout)-Thread#sleep(timeout)-Thread#join(timeout)-LockSupport.parkNanos(timeout)-LockSupport.parkUntil(timeout)-

  举个例子:

  publicclassThreadState4{

  try{

  Thread.sleep( 1000);

  } catch(InterruptedException e) {

  //ignore

  }

  });

  thread.start;

  System.out.println(thread.getState);

  Thread.sleep( 1000);

  // 输出结果

  TIMED_WAITING

  TERMINATED

  BLOCKED状态

  「API注释」:

  BLOCKED,

  此状态表示一个线程正在阻塞等待获取一个监视器锁。如果线程处于阻塞状态,说明线程等待进入同步代码块或者同步方法的监视器锁或者在调用了Object#wait之后重入同步代码块或者同步方法。

  ?

  ?

  BLOCKED状态也就是阻塞状态,该状态下的线程不会被分配CPU执行时间。线程的状态为BLOCKED的时候有两种可能的情况:

  A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method

  ?

  ?

  线程正在等待一个监视器锁,只有获取监视器锁之后才能进入 synchronized代码块或者 synchronized方法,在此等待获取锁的过程线程都处于阻塞状态。

  reenter a synchronized block/method after calling Object#wait

  ?

  ?

  线程X步入 synchronized代码块或者 synchronized方法后(此时已经释放监视器锁)调用 Object#wait方法之后进行阻塞,当接收其他线程T调用该锁对象 Object#notify/notifyAll,但是线程T尚未退出它所在的 synchronized代码块或者 synchronized方法,那么线程X依然处于阻塞状态(注意API注释中的 「reenter」,理解它场景2就豁然开朗)。

  更加详细的描述可以参考笔者之前写过的一篇文章:深入理解Object提供的阻塞和唤醒API

  针对上面的场景1举个简单的例子:

  publicclassThreadState6{

  privatestaticfinalObject MONITOR=newObject;

  Thread thread1=newThread(-> {

  synchronized(MONITOR){

  try{

  Thread.sleep(Integer.MAX_VALUE);

  } catch(InterruptedException e) {

  //ignore

  }

  }

  });

  Thread thread2=newThread(-> {

  synchronized(MONITOR){

  System.out.println( "thread2 got monitor lock...");

  }

  });

  thread1.start;

  thread2.start;

  System.out.println(thread2.getState);

  }

  }

  // 输出结果

  BLOCKED

  针对上面的场景2举个简单的例子:

  publicclassThreadState7{

  privatestaticfinalObject MONITOR=newObject;

  privatestaticfinalDateTimeFormatter F=DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss");

  System.out.println(String.format( "[%s]-begin...", F.format(LocalDateTime.now)));

  Thread thread1=newThread( -> {

  synchronized(MONITOR) {

  System.out.println(String.format( "[%s]-thread1 got monitor lock...", F.format(LocalDateTime.now)));

  try{

  MONITOR.wait;

  //ignore

  }

  System.out.println(String.format( "[%s]-thread1 exit waiting...", F.format(LocalDateTime.now)));

  }

  });

  Thread thread2=newThread( -> {

  System.out.println(String.format( "[%s]-thread2 got monitor lock...", F.format(LocalDateTime.now)));

  try{

  MONITOR.notify;

  Thread.sleep( 2000);

  } catch(InterruptedException e) {

  //ignore

  }

  System.out.println(String.format( "[%s]-thread2 releases monitor lock...", F.format(LocalDateTime.now)));

  }

  });

  thread1.start;

  thread2.start;

  // 这里故意让主线程sleep 1500毫秒从而让thread2调用了Object#notify并且尚未退出同步代码块,确保thread1调用了Object#wait

  Thread.sleep( 1500);

  System.out.println(thread1.getState);

  System.out.println(String.format( "[%s]-end...", F.format(LocalDateTime.now)));

  }

  }

  // 某个时刻的输出如下:

  [ 2019- 06- 2000: 30: 22]-begin...

  [ 2019- 06- 2000: 30: 22]-thread1 got monitor lock...

  [ 2019- 06- 2000: 30: 23]-thread2 got monitor lock...

  BLOCKED

  [ 2019- 06- 2000: 30: 23]-end...

  [ 2019- 06- 2000: 30: 25]-thread2 releases monitor lock...

  [ 2019- 06- 2000: 30: 25]-thread1 exit waiting...

  场景2中:

  线程2调用 Object#notify后睡眠2000毫秒再退出同步代码块,释放监视器锁。

  线程1只睡眠了1000毫秒就调用了 Object#wait,此时它已经释放了监视器锁,所以线程2成功进入同步块,线程1处于API注释中所述的 reenter a synchronized block/method的状态。

  主线程睡眠1500毫秒刚好可以命中线程1处于 reenter状态并且打印其线程状态,刚好就是 BLOCKED状态。

  线程2调用 Object#notify后睡眠2000毫秒再退出同步代码块,释放监视器锁。

  线程1只睡眠了1000毫秒就调用了 Object#wait,此时它已经释放了监视器锁,所以线程2成功进入同步块,线程1处于API注释中所述的 reenter a synchronized block/method的状态。

  主线程睡眠1500毫秒刚好可以命中线程1处于 reenter状态并且打印其线程状态,刚好就是 BLOCKED状态。

  这三点看起来有点绕,多看几次多思考一下应该就能理解。

  TERMINATED状态

  TERMINATED;

  终结的线程对应的线程状态,此时线程已经执行完毕。

  ?

  ?

  TERMINATED状态表示线程已经终结。一个线程实例只能被启动一次,准确来说,只会调用一次Thread#run方法,Thread#run方法执行结束之后,线程状态就会更变为TERMINATED,意味着线程的生命周期已经结束。

  举个简单的例子:

  publicclassThreadState8{

  Thread thread=newThread( -> {

  });

  thread.start;

  // 输出结果

  TERMINATED

  上下文切换

  多线程环境中,当一个线程的状态由RUNNABLE转换为非RUNNABLE(BLOCKED、WAITING或者TIMED_WAITING)时,相应线程的上下文信息(也就是常说的Context,包括CPU的寄存器和程序计数器在某一时间点的内容等等)需要被保存,以便线程稍后恢复为RUNNABLE状态时能够在之前的执行进度的基础上继续执行。而一个线程的状态由非RUNNABLE状态进入RUNNABLE状态时可能涉及恢复之前保存的线程上下文信息并且在此基础上继续执行。这里的对「线程的上下文信息进行保存和恢复的过程」就称为上下文切换(Context Switch)。

  线程的上下文切换会带来额外的性能开销,这包括保存和恢复线程上下文信息的开销、对线程进行调度的CPU时间开销以及CPU缓存内容失效的开销(线程所执行的代码从CPU缓存中访问其所需要的变量值要比从主内存(RAM)中访问响应的变量值要快得多,但是「线程上下文切换会导致相关线程所访问的CPU缓存内容失效,一般是CPU的L1 Cache和L2 Cache」,使得相关线程稍后被重新调度到运行时其不得不再次访问主内存中的变量以重新创建CPU缓存内容)。

  在Linux系统中,可以通过vmstat命令来查看全局的上下文切换的次数,例如:

  $vmstat 1

  对于Java程序的运行,在Linux系统中也可以通过perf命令进行监视,例如:

  $perf stat-e cpu-clock,task-clock,cs,cache-reference,cache-misses java YourJavaClass

  参考资料中提到Windows系统下可以通过自带的工具perfmon(其实也就是任务管理器)来监视线程的上下文切换,实际上笔者并没有从任务管理器发现有任何办法查看上下文切换,通过搜索之后发现了一个工具:Process Explorer。运行Process Explorer同时运行一个Java程序并且查看其状态:

  外墙保温板材质|苏洋建材新闻资讯  外墙保温板材质 第5张

  j-t-l-s-4.png

  因为打了断点,可以看到运行中的程序的上下文切换一共7000多次,当前一秒的上下文切换增量为26(因为笔者设置了Process Explorer每秒刷新一次数据)。

  监控线程状态

  如果项目在生产环境中运行,不可能频繁调用Thread#getState方法去监测线程的状态变化。JDK本身提供了一些监控线程状态的工具,还有一些开源的轻量级工具如阿里的Arthas,这里简单介绍一下。

  jvisualvm是JDK自带的堆、线程等待JVM指标监控工具,适合使用于开发和测试环境。它位于JAVA_HOME/bin目录之下。

  外墙保温板材质|苏洋建材新闻资讯  外墙保温板材质 第6张

  j-t-l-s-5.png

  其中线程Dump的按钮类似于下面要提到的jstack命令,用于导出所有线程的栈信息。

  使用jstack

  jstack是JDK自带的命令行工具,功能是用于获取指定PID的Java进程的线程栈信息。例如本地运行的一个IDEA实例的PID是11376,那么只需要输入:

  jstack 11376

  然后控制台输出如下:

  外墙保温板材质|苏洋建材新闻资讯  外墙保温板材质 第7张

  j-t-l-s-6.png

  另外,如果想要定位具体Java进程的PID,可以使用jps命令。

  使用JMC

  JMC也就是Java Mission Control,它也是JDK自带的工具,提供的功能要比jvisualvm强大,包括MBean的处理、线程栈已经状态查看、飞行记录器等等。

  外墙保温板材质|苏洋建材新闻资讯  外墙保温板材质 第8张

  j-t-l-s-7.png 小结

  理解Java线程状态的切换和一些监控手段,更有利于日常开发多线程程序,对于生产环境出现问题,通过监控线程的栈信息能够快速定位到问题的根本原因(通常来说,目前比较主流的MVC应用(准确来说应该是Servlet容器如Tomcat)都是通过一个线程处理一个单独的请求,当请求出现阻塞的时候,导出对应处理请求的线程基本可以定位到阻塞的精准位置,如果使用消息队列例如RabbitMQ,消费者线程出现阻塞也可以利用相似的思路解决)。

  推荐阅读:

  JetBrains调查:Java最流行,Python超越Java

  C、Java和Python争夺第一,TIOBE CEO看好Python

  图解Java中那18 把锁

  漫话:如何给女朋友解释为什么Java线程没有Running状态?

  送命题,选 C++ 还是 Java?

  每日打卡赢积分兑换书籍入口


外墙保温板施工视频 保温板怎么贴外墙上的 外墙保温板厂家 卫辉外墙保温板 一体化外墙保温板


这是外墙保温板外墙保温板施工 16:25:05)

评论(0)