Java多线程开发系列之四:玩转多线程(线程的控制2)

时间:2023-12-11 08:04:49

   在上节的线程控制(详情点击这里)中,我们讲解了线程的等待join()、守护线程。本节我们将会把剩下的线程控制内容一并讲完,主要内容有线程的睡眠、让步、优先级、挂起和恢复、停止等。

  废话不多说,我们直接进入正题:


 3、线程睡眠  sleep()

  所有介绍多线程开发的学习案例中,基本都有用到这个方法,这个方法的意思就是睡眠(是真的,请相信我...)。好吧,如果你觉得不够具体,可以认为是让当前线程暂停一下,当前线程随之进入阻塞状态,当睡眠时间结束后,当前线程重新进入就绪状态,开始新一轮的抢占计划!

那么这个方法在实际开发中,有哪些用途呢?我举个例子,很多情况下,当前线程并不需要实时的监控或者是运行,只是会定期的检查一下某个状态是否达标,如果符合出发条件了,那么就做某一件事情,否则继续睡眠。比如心跳模式下,我们会派一个守护线程向服务端发送数据请求,当收到回应时,那么我们会睡眠一段时间,当再次苏醒后,我们继续发送这样的请求。现实生活中的例子,比如我们在等某个电视是否开播,可是又不想看之前的广告,所以我们可能会等一会将电视频道切换到要播放的位置查看一下,如果还在播放广告,那么我们就跳到其他频道观看,然后定期的切换到目标频道进行查看一下。

代码如下:

     public class ThreadStudy
{
public static main(String[] arg)throws Exception
{
for(int i=0;i<=1000;i++)
{
if(IsInternetAccess())
{
Thread.sleep(1000*6);//注意这里
}
else
{
System.out.println("Error! Can not Access Internet!")
break;
}
}
}
private Boolean IsInternetAccess()
{
//bala bala
return true;
}
}

  代码的意思是检查网络是否通畅,如果通畅的话那么进入睡眠,睡眠6秒钟后再次苏醒进行一次检查。通过让线程睡眠,我们可以有效的分配资源,在闲时让其他线程可以更快的拿到cpu资源。这里有一点需要注意的是,线程睡眠后,进入阻塞状态(无论此时cpu是否空闲,都仍然会暂停,是强制性的),当睡眠时间结束,进入的是就绪状态,需要再次竞争才可以抢占到cpu权限,而非睡眠结束后立即可以执行方法。所以实际间隔时间是大于等于睡眠时间的。

java Thread类提供了两个静态方法来暂停线程

  static void sleep(long millis)  

  static void sleep(long millis,int nanos)

  millis为毫秒,nanos为微秒,与线程join()类似,由于jvm和硬件的缘故,我们也基本只用方法1。


4、 线程让步 yield()

在生活中我们都遇到过这样的例子,在公交车、地铁上作一名安静的美男子(或者是女汉子),这时候进来了一位老人、孕妇等,你默默的站起来,将座位让给了老人。自己去旁边候着,等着新的空闲座位。或者是你默默的玩着电脑游戏,然后你妈妈大声的喊你的全名(是的,是全名),这时候你第一反应是,我又做错什么了,第二反应就是放下手上的鼠标,乖乖的跑到你老妈面前接受训斥。所有的这一切都是由于事情的紧急性当前正在处理的线程被搁置起来,我们(cpu)处理当前的紧急事务。在软件开发中,也有类似的场景,比如一条线程处理的任务过大,其他线程始终无法抢占到资源,这时候我们就要主动的进行让步,给其他线程一个公平抢占的机会。

这里附加一份来自网络的图片:在我们强大的时候,我们应该给弱者一个机会。咳咳  回归正题。

Java多线程开发系列之四:玩转多线程(线程的控制2)

下面是代码

 public class TestThread extends Thead
{
public testThread(String name)
{
super(name);
} public void run()
{
for(int i=0;i<=1000000;ii++)
{
send("MsgBody");
if(i%100==0)
{
Thread.yield();//注意看这里
}
}
} public static void main(String[] args) throws Exception
{
TestThread thread1=new TestThread("thread1");
thread1.setPriority(Thread.MAX_PRIORITY);//注意看这里 TestThread thread2=new TestThread("thread2");
thread1.setPriority(Thread.MIN_PRIORITY);//注意看这里
thread1.start();
thread2.start();
}
}

我们启动线程后,当线程每发送一百次消息后,我们暂停一次当前线程,使当前线程进入就绪状态。此时CPU会重新计算一次优先级,选择优先级较高者启动。
此处比较一下 sleep方法和yield()方法。

(1)sleep方法 暂停线程后,线程会进入阻塞状态(即使是一瞬间),那么在这一刻cpu只会选择已经做好就绪状态的线程,故不会选择当前正在睡眠的线程。(即使没有其他可用线程)。而yield()方法会使当前线程即刻起进入就绪状态,cpu选择的可选线程范围中,包含当前执行yield()方法的线程。如若没有其他线程的优先级高于(或者等于) yield()的线程,则cpu仍会选择原有yield()的线程重新启动。

(2)sleep方法会抛出 InterruptedException 异常,所以调用sleep方法需要声明或捕捉该异常(比C#处理异常而言是够麻烦的),而yield没有声明抛出异常。

(3)sleep方法的移植性较好,可以对应很多平台的底层方法,所以用sleep()的地方要多余yield()的地方;

(4)sleep 暂停线程后,线程会睡眠 一定时间,然后才会变为就绪状态,倘若定义为sleep(0)后,则阻塞状态的时间为0,即刻进入就绪状态,这种用法与yield()的用法基本上是相同的:即都是让cpu进行一次新的选择,避免由于当前线程过度的霸占cpu,造成程序假死。

这两个方法最大的不同点是 sleep会抛出异常需要处理,yield()不会; 而且两者的微小区别在各个版本的jdk中也不一样,大家看以参阅*上的这个问题:Are Thread.sleep(0) and Thread.yield() statements equivalent?(点此进入


5、线程的优先级设定

  线程的优先级相当于是一个机会的权重,优先级高时,获得执行机会的可能性就越大,反之获得执行机会的可能性就越小。(记住只是可能性越大或越小)。

  在本节的线程让步这一部分的代码里我们已经用代码展示了如何设置线程的优先级此处不做特别的代码展示。(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )

Thread为我们提供了两个方法来分别设置和获取线程的优先级。

  

 setPriority(int newPriority)
getPriority()

setPriority为设置优先级,参数的取值范围是 1~10之前。

同时还设定了三个静态常量:
Tread.MAX_PRIORITY=10;

Tread.NORM_PRIORITY=5;

Tread.MIN_PRIORITY=1;

  尽管java为线程提供了10个优先级,但是底层平台线程的优先级往往并不为10,所以就导致了两者不是意义对应的关系。(比如OS只有五个优先级,这样每两个优先级只对应一个OS的优先级)。 此时我们常常只用这三个静态常量来设置优先级,而不是详细的指明具体的优先级值(因为可能多个优先级对应OS的某一个优先级),造成不必要的麻烦。

  另外每个线程默认的优先级都与创建他的父进程的优先级相同,在默认情况下Main线程优先级为普通,所以上述代码创建的新线程默认也为普通优先级。

  下面是优先级概念的重点:

  其实你设置的优先级并不能真正代表该线程的或者启动的优先级,这只是OS启动线程时计算优先级的一个参考指标。OS还会查看当前线程是否长时间的霸占cpu,如果是这样的话,OS会适度的调高对其它“饥饿”线程的优先级。对于那些长期霸占cpu的线程进行强制的挂起。进行这种设置只是能在某种程度上增加该线程被执行的机会。其实那些长期霸占cpu的线程也并非单次霸占的时间长,而是被连续选中的情况非常多,造成一种长期霸占的假象。

  所以设置优先级后,线程真正执行的顺序并不可以预测甚至可以说是有点混乱的。在明白了这点以后,我们在开发控制多线程,并不能完全的寄希望于通过简单的设置优先级来安排线程的执行顺序。

此处参考了两篇文章,更多详情请参考原文:

(1)Java多线程 -- 线程的优先级(原文链接

(2)Thread.sleep(0)的意义(原文链接)


6、强制结束线程Stop()

有时我们会发现有些正在运行的线程,已经没有必要继续执行下去了,但是距离多线程结束还有一段时间,这时我们就需要强制结束多线程。java曾经提供过一个专门用于结束线程的方法Stop(),但是这个方法现在已经被废弃掉了,并不推荐开发者使用。

  这是由于这个方法具有固有的不安全性。用Thread.stop 来结束线程,jvm会强制释放它锁定的所有对象。当某一时刻对象的状态并不一致时(正在处理事务的过程中),如果强制释放掉对象,则可能会导致很多意想不到的后果。说的具体一点就是:系统会以被锁定资源的栈顶产生一个ThreadDeath异常。这个unchecked Exception 会默默的关闭掉相关的线程。此时对象内部的数据可能会不一致,而用户并不会收到任何对象不一致的报警。这个不一致的后果只会在未来使用过程中才会被发现,此时已经造成了无法预料的后果。

  有些人可能会考虑通过调用Stop方法,然后再捕捉ThreadDeath的形式,避免这种形式。这种想法看似可以实现,其实由于ThreadDeath这个异常可能在任何位置抛出,需要及细致的考虑。而且即使考虑到了,在捕捉处理该异常时,系统可能又会抛出新的ThreadDeath。所以我们应该在源头上就扼杀掉这种方式,而不是通过不断的打补丁来修复。

那么问题来了,如果我们真的要关闭掉某个线程,应该怎么处理呢?

通过Stop方法的讲解我们可以明白,在线程的外部来关闭线程往往很难处理好数据一致性、以及线程内部运行过程的问题。那么我们可以通过设定一直标志变量,然后线程定期的检查这个变量是否为结束标识来确定是否继续运行。

例如笔者曾经写过一个监控计算机指标的线程。这个线程会定期的检查缓存中的状态变量。这个状态缓存是外部可以设定的。当线程发现此变量已经被设定为“结束”时,则会在内部处理好剩余工作,直接运行完Run方法。


7、线程的挂起和恢复 suspend()和resume()

我们有时需要对线程进行挂起,而具体挂起的时间并不清楚,只可能在未来某个条件下,通知这个线程可以开始工作了。java为我们专门提供了这样的两个方法:

挂起 suspend()/恢复resume。

通过标题我们已经知道这两个方法也同样不被java所推荐,但是为什么会这样呢?

suspend是直接挂起当前线程,使其进入阻塞状态,而对他内部控制和锁定的资源并不进行修改(这与stop方法类似,线程外部往往很难查看内部运行的状态和控制的资源,所以也就很难处理)。这样这个被挂起的线程所锁定的资源就再也不能被其他资源所访问,造成了一种假死锁的状态。只有当线程被恢复(resume)后,并且释放掉手里的资源,其他线程才可以重新访问资源,但是倘若其他线程在恢复(resume)被挂起(suspend)的线程直线,需要先访问被锁定的资源,此时就会形成真正的锁定。

那么问题来了,如果我们真的要挂起某个线程,应该怎么处理呢?

  这个与stop()同理,我们可以在可能被挂起的线程内部设置一个标识,指出这个线程当前是否要被挂起,若变量指示要挂起,则使用wait()命令让其进入等待状态,若标识指出可以恢复线程时,则用notify()重新唤醒这个线程。(这两个方法我会在后文的线程通信中讲解)。

此处参考了两篇文章,更多详情请参考原文:

(1)为何不赞成使用Thread.stopsuspend和resume()(原文链接

  (2)JAVA STOP方法的不安全性(原文链接