返回介绍

2.2 初始线程:线程的基本操作

发布于 2024-08-21 22:20:21 字数 18737 浏览 0 评论 0 收藏 0

进行Java并发设计的第一步,就是必须要了解Java中为线程操作所提供的一些API。比如,如何新建并且启动线程,如何终止线程、中断线程等。当然了,因为并行操作要比串行操作复杂得多,于是,围绕着这些常用接口,可能有些比较隐晦的“坑”等着你去踩。而本节也将尽可能地将一些潜在问题描述清楚。

2.2.1 新建线程

新建线程很简单。只要使用new关键字创建一个线程对象,并且将它start()起来即可。

Thread t1=new Thread(); t1.start();

那线程start()后,会干什么呢?这才是问题的关键。线程Thread,有一个run()方法,start()方法就会新建一个线程并让这个线程执行run()方法。

这里要注意,下面的代码也能通过编译,也能正常执行。但是,却不能新建一个线程,而是在当前线程中调用run()方法,只是作为一个普通的方法调用。

Thread t1=new Thread(); t1.run();

因此,在这里希望大家特别注意,调用start()方法和直接调用run()方法的区别。

注意:不要用run()来开启新线程。它只会在当前线程中,串行执行run()中的代码。

默认情况下,Thread的run()方法什么都没有做,因此,这个线程一启动就马上结束了。如果你想让线程做点什么,就必须重载run()方法,把你的“任务”填进去。

Thread t1=new Thread(){
  @Override
  public void run(){
    System.out.println("Hello, I am t1");
  }
};
t1.start();

上述代码使用匿名内部类,重载了run()方法,并要求线程在执行时打印“Hello, I am t1”的字样。如果没有特别的需要,都可以通过继承Thread,重载run()方法来自定义线程。但考虑到Java是单继承的,也就是说继承本身也是一种很宝贵的资源,因此,我们也可以使用Runnable接口来实现同样的操作。Runnable接口是一个单方法接口,它只有一个run()方法:

public interface Runnable {
  public abstract void run();
}

此外,Thread类有一个非常重要的构造方法:

public Thread(Runnable target)

它传入一个Runnable接口的实例,在start()方法调用时,新的线程就会执行Runnable.run()方法。实际上,默认的Thread.run()就是这么做的:

public void run() {
  if (target != null) {
    target.run();
  }
}

注意:默认的Thread.run()就是直接调用内部的Runnable接口。因此,使用Runnable接口告诉线程该做什么,更为合理。

public class CreateThread3 implements Runnable { public static void main(String[] args) { Thread t1=new Thread(new CreateThread3()); t1.start(); } @Override public void run() { System.out.println("Oh, I am Runnable"); } }

上述代码实现了Runnable接口,并将该实例传入Thread。这样避免重载Thread.run(),单纯使用接口来定义Thread,也是最常用的做法。

2.2.2 终止线程

一般来说,线程在执行完毕后就会结束,无须手工关闭。但是,凡事也都有例外。一些服务端的后台线程可能会常驻系统,它们通常不会正常终结。比如,它们的执行体本身就是一个大大的无穷循环,用于提供某些服务。

那如何正常的关闭一个线程呢?查阅JDK,你不难发现Thread提供了一个stop()方法。如果你使用stop()方法,就可以立即将一个线程终止,非常方便。但如果你使用的是eclipse之类的IDE写代码的话,就会立即发现stop()方法是一个被标注为废弃的方法。也就是说,在将来,JDK可能就会移除该方法。

为什么stop()被废弃而不推荐使用呢?原因是stop()方法太过于暴力,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。

为了让大家更好地理解本节内容,我先简单介绍一些有关数据不一致的概念。假设我们在数据库里维护着一张用户表,里面记录了用户ID和用户名。假设,这里有两条记录:

记录1:ID=1,NAME=小明
记录2:ID=2,NAME=小王

如果我们用一个User对象去保存这些记录,我们总是希望这个对象要么保存记录1,要么保存记录2。如果这个User对象一半存着记录1,另外一半存在记录2,我想大部分人都会抓狂吧!如果现在真的由于程序问题,出现了这么一个怪异的对象u,u的ID是1,但是u的Name是小王。那么,我们说,在这种情况下,数据就已经不一致了。说白了就是系统有错误了。这种情况是相当危险的,如果我们把一个不一致的数据直接写入了数据库,那么就会造成数据永久地被破坏和丢失,后果不堪设想。

也许有人会问,怎么可能呢?跑得好好的系统,怎么会出这种问题呢?在单线程环境中,确实不会,但在并行程序中,如果考虑不周,就有可能出现类似的情况。不经思考地使用stop()就有可能导致这种问题。

Thread.stop()方法在结束线程时,会直接终止线程,并且会立即释放这个线程所持有的锁。而这些锁恰恰是用来维持对象一致性的。如果此时,写线程写入数据正写到一半,并强行终止,那么对象就会被写坏,同时,由于锁已经被释放,另外一个等待该锁的读线程就顺理成章的读到了这个不一致的对象,悲剧也就此发生。整个过程如图2.4所示。

图2-4 stop()方法强行终止线程导致数据不一致

首先,对象u持有ID和NAME两个字段,简单起见,这里假设当ID等于NAME时表示对象是一致的,否则表示对象出错。写线程总是会将ID和NAME写成相同的值,并且在这里初始值都为0。当写线程在写对象时,读线程由于无法获得锁,因此必须等待,所以读线程是看不见一个写了一半的对象的。当写线程写完ID后,很不幸地被stop(),此时对象u的ID为1而NAME仍然为0,处于不一致状态。而被终止的写线程简单地将锁释放,读线程争夺到锁后,读取数据,于是,读到了ID=1而NAME=0的错误值。

这个过程可以用以下代码模拟,这里读线程ReadObjectThread在读到对象的ID和NAME不一致时,会输出这些对象。而写线程ChangeObjectThread总是会写入两个相同的值。注意,代码在第56行会通过stop()方法强行终止写线程。

01 public class StopThreadUnsafe {
02   public static User u=new User();
03   public static class User{
04     private int id;
05     private String name;
06     public User(){
07       id=0;
08       name="0";
09     }
10     //省略setter和getter方法
11     @Override
12     public String toString() {
13       return "User [id=" + id + ", name=" + name + "]";
14     }
15   }
16   public static class ChangeObjectThread extends Thread{
17     @Override
18     public void run(){
19       while(true){
20         synchronized(u){
21           int v=(int)(System.currentTimeMillis()/1000);
22           u.setId(v);
23           //Oh, do sth. else
24           try {
25             Thread.sleep(100);
26           } catch (InterruptedException e) {
27             e.printStackTrace();
28           }
29           u.setName(String.valueOf(v));
30         }
31         Thread.yield();
32       }
33     }
34   }
35
36   public static class ReadObjectThread extends Thread{
37     @Override
38     public void run(){
39       while(true){
40         synchronized(u){
41           if(u.getId() != Integer.parseInt(u.getName())){
42             System.out.println(u.toString());
43           }
44         }
45         Thread.yield();
46       }
47     }
48   }
49
50   public static void main(String[] args) throws InterruptedException {
51     new ReadObjectThread().start();
52     while(true){
53       Thread t=new ChangeObjectThread();
54       t.start();
55       Thread.sleep(150);
56       t.stop();
57     }
58   }
59 }

执行以上代码,可以很容易得到类似如下输出,ID和NAME产生了不一致。

User [id=1425135593, name=1425135592]
User [id=1425135594, name=1425135593]

如果在线上环境跑出以上结果,那么加班加点估计是免不了了,因为这类问题一旦出现,就很难排查,因为它们甚至没有任何错误信息,也没有线程堆栈。这种情况一旦混杂在动则十几万行的程序代码中时,发现它们就全凭经验、时间还有一点点运气了。因此,除非你很清楚你在做什么,否则不要随便使用stop()方法来停止一个线程。

那如果需要停止一个线程时,应该这么做呢?其实方法很简单,只是需要由我们自行决定线程何时退出就可以了。仍然用本例说明,只需要将ChangeObjectThread线程增加一个stopMe()方法即可。如下所示:

01 public static class ChangeObjectThread extends Thread {
02   volatile boolean stopme = false;
03
04   public void stopMe(){
05     stopme = true;
06   }
07   @Override
08   public void run() {
09     while (true) {
10       if (stopme){
11         System.out.println("exit by stop me");
12         break;
13       }
14       synchronized (u) {
15         int v = (int) (System.currentTimeMillis() / 1000);
16         u.setId(v);
17         //Oh, do sth. else
18         try {
19           Thread.sleep(100);
20         } catch (InterruptedException e) {
21           e.printStackTrace();
22         }
23         u.setName(String.valueOf(v));
24       }
25       Thread.yield();
26     }
27   }
28 }

代码第2行,定义了一个标记变量stopme,用于指示线程是否需要退出。当stopMe()方法被调用,stopme就被设置为true,此时,在代码第10行检测到这个改动时,线程就自然退出了。使用这种方式退出线程,不会使对象u的状态出现错误。因为,ChangeObjectThread已经没有机会“写坏”对象了,它总是会选择在一个合适的时间终止线程。

2.2.3 线程中断

在Java中,线程中断是一种重要的线程协作机制。从表面上理解,中断就是让目标线程停止执行的意思,实际上并非完全如此。在上一节中,我们已经详细讨论了stop()方法停止线程的害处,并且使用了一套自有的机制完善线程退出的功能。那在JDK中是否有提供更强大的支持呢?答案是肯定的,那就是线程中断。

严格地讲,线程中断并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出啦!至于目标线程接到通知后如何处理,则完全由目标线程自行决定。这点很重要,如果中断后,线程立即无条件退出,我们就又会遇到stop()方法的老问题。

与线程中断有关的,有三个方法,这三个方法看起来很像,所以可能会引起混淆和误用,希望大家注意。

public void Thread.interrupt()         // 中断线程
public boolean Thread.isInterrupted()    // 判断是否被中断
public static boolean Thread.interrupted()   // 判断是否被中断,并清除当前中断状态

Thread.interrupt()方法是一个实例方法。它通知目标线程中断,也就是设置中断标志位。中断标志位表示当前线程已经被中断了。Thread.isInterrupted()方法也是实例方法,它判断当前线程是否有被中断(通过检查中断标志位)。最后的静态方法Thread.interrupted()也是用来判断当前线程的中断状态,但同时会清除当前线程的中断标志位状态。

下面这段代码对t1线程进行了中断,那么中断后,t1会停止执行吗?

public static void main(String[] args) throws InterruptedException {
  Thread t1=new Thread(){
    @Override
    public void run(){
      while(true){
        Thread.yield();
      }
    }
  };
  t1.start();
  Thread.sleep(2000);
  t1.interrupt();
}

在这里,虽然对t1进行了中断,但是在t1中并没有中断处理的逻辑,因此,即使t1线程被置上了中断状态,但是这个中断不会发生任何作用。

如果希望t1在中断后退出,就必须为它增加相应的中断处理代码:

Thread t1=new Thread(){
  @Override
  public void run(){
    while(true){
      if(Thread.currentThread().isInterrupted()){
        System.out.println("Interruted!");
        break;
      }
      Thread.yield();
    }
  }
};

上述代码的加粗部分使用Thread.isInterrupted()函数判断当前线程是否被中断了,如果是,则退出循环体,结束线程。这看起来与前面增加stopme标记的手法非常相似,但是中断的功能更为强劲。比如,如果在循环体中,出现了类似于wait()或者sleep()这样的操作,则只能通过中断来识别了。

下面,先来了解一下Thread.sleep()函数,它的签名如下:

public static native void sleep(long millis) throws InterruptedException

Thread.sleep()方法会让当前线程休眠若干时间,它会抛出一个InterruptedException中断异常。InterruptedException不是运行时异常,也就是说程序必须捕获并且处理它,当线程在sleep()休眠时,如果被中断,这个异常就会产生。

01 public static void main(String[] args) throws InterruptedException {
02   Thread t1=new Thread(){
03     @Override
04     public void run(){
05       while(true){
06         if(Thread.currentThread().isInterrupted()){
07           System.out.println("Interruted!");
08           break;
09         }
10         try {
11           Thread.sleep(2000);
12         } catch (InterruptedException e) {
13           System.out.println("Interruted When Sleep");
14           //设置中断状态
15           Thread.currentThread().interrupt();
16         }
17         Thread.yield();
18       }
19     }
20   };
21   t1.start();
22   Thread.sleep(2000);
23   t1.interrupt();
24 }

注意上述代码中第10~15行加粗部分,如果在第11行代码处,线程被中断,则程序会抛出异常,并进入第13行处理。在catch子句部分,由于已经捕获了中断,我们可以立即退出线程。但在这里,我们并没有这么做,因为也许在这段代码中,我们还必须进行后续的处理,保证数据的一致性和完整性,因此,执行了Thread.interrupt()方法再次中断自己,置上中断标记位。只有这么做,在第6行的中断检查中,才能发现当前线程已经被中断了。

注意:Thread.sleep()方法由于中断而抛出异常,此时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中,再次设置中断标记位。

2.2.4 等待(wait)和通知(notify)

为了支持多线程之间的协作,JDK提供了两个非常重要的接口线程等待wait()方法和通知notify()方法。这两个方法并不是在Thread类中的,而是输出Object类。这也意味着任何对象都可以调用这两个方法。

这两个方法的签名如下:

public final void wait() throws InterruptedException
public final native void notify()

当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。这是什么意思呢?比如,线程A中,调用了obj.wait()方法,那么线程A就会停止继续执行,而转为等待状态。等待到何时结束呢?线程A会一直等到其他线程调用了obj.notify()方法为止。这时,obj对象就俨然成为多个线程之间的有效通信手段。

那wait()和notify()究竟是如何工作的呢?图2.5展示了两者的工作过程。如果一个线程调用了object.wait(),那么它就会进入object对象的等待队列。这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某一个对象。当object.notify()被调用时,它就会从这个等待队列中,随机选择一个线程,并将其唤醒。这里希望大家注意的是,这个选择是不公平的,并不是先等待的线程会优先被选择,这个选择完全是随机的。

图2.5 notify()唤醒等待的线程

除了notify()方法外,Object对象还有一个类似的notifyAll()方法,它和notify()的功能基本一致,但不同的是,它会唤醒在这个等待队列中所有等待的线程,而不是随机选择一个。

这里还需要强调一点,Object.wait()方法并不是可以随便调用的。它必须包含在对应的synchronzied语句中,无论是wait()或者notify()都需要首先获得目标对象的一个监视器。如图2.6所示,显示了wait()和notify()的工作流程细节。其中T1和T2表示两个线程。T1在正确执行wait()方法前,首先必须获得object对象的监视器。而wait()方法在执行后,会释放这个监视器。这样做的目的是使得其他等待在object对象上的线程不至于因为T1的休眠而全部无法正常执行。

图2.6 wait()和notify()的工作流程细节

线程T2在notify()调用前,也必须获得object的监视器。所幸,此时T1已经释放了这个监视器。因此,T2可以顺利获得object的监视器。接着,T2执行了notify()方法尝试唤醒一个等待线程,这里假设唤醒了T1。T1在被唤醒后,要做的第一件事并不是执行后续的代码,而是要尝试重新获得object的监视器,而这个监视器也正是T1在wait()方法执行前所持有的那个。如果暂时无法获得,T1还必须要等待这个监视器。当监视器顺利获得后,T1才可以真正意义上的继续执行。

为了方便大家理解,这里给出一个简单地使用wait()和notify()的案例:

01 public class SimpleWN {
02   final static Object object = new Object();
03   public static class T1 extends Thread{
04     public void run()
05     {
06       synchronized (object) {
07         System.out.println(System.currentTimeMillis()+":T1 start! ");
08         try {
09           System.out.println(System.currentTimeMillis()+":T1 wait for object ");
10           object.wait();
11         } catch (InterruptedException e) {
12           e.printStackTrace();
13         }
14         System.out.println(System.currentTimeMillis()+":T1 end!");
15       }
16     }
17   }
18   public static class T2 extends Thread{
19     public void run()
20     {
21       synchronized (object) {
22         System.out.println(System.currentTimeMillis()+":T2 start! notify one
thread");
23         object.notify();
24         System.out.println(System.currentTimeMillis()+":T2 end!");
25         try {
26           Thread.sleep(2000);
27         } catch (InterruptedException e) {
28         }
29       }
30     }
31   }
32   public static void main(String[] args) {
33     Thread t1 = new T1() ;
34     Thread t2 = new T2() ;
35     t1.start();
36     t2.start();
37   }
38 }

上述代码中,开启了两个线程T1和T2。T1执行了object.wait()方法。注意,在程序第6行,执行wait()方法前,T1先申请object的对象锁。因此,在执行object.wait()时,它是持有object的锁的。wait()方法执行后,T1会进行等待,并释放object的锁。T2在执行notify()之前也会先获得object的对象锁。这里为了让实验效果明显,特意安排在notify()执行之后,让T2休眠2秒钟,这样做可以更明显地说明,T1在得到notify()通知后,还是会先尝试重新获得object的对象锁。上述代码的执行结果类似如下:

1425224592258:T1 start!
1425224592258:T1 wait for object
1425224592258:T2 start! notify one thread
1425224592258:T2 end!
1425224594258:T1 end!

注意程序打印的时间戳信息,可以看到,在T2通知T1继续执行后,T1并不能立即继续执行,而是要等待T2释放object的锁,并重新成功获得锁后,才能继续执行。因此,加粗部分时间戳的间隔为2秒(因为T2休眠了2秒)。

注意:Object.wait()和Thread.sleep()方法都可以让线程等待若干时间。除了wait()可以被唤醒外,另外一个主要区别就是wait()方法会释放目标对象的锁,而Thread.sleep()方法不会释放任何资源。

2.2.5 挂起(suspend)和继续执行(resume)线程

如果你阅读JDK有关Thread类的API文档,可能还会发现两个看起来非常有用的接口,即线程挂起(suspend)和继续执行(resume)。这两个操作是一对相反的操作,被挂起的线程,必须要等到resume()操作后,才能继续指定。乍看之下,这对操作就像Thread.stop()方法一样好用。但如果你仔细阅读文档说明,会发现它们也早已被标注为废弃方法,并不推荐使用。

不推荐使用suspend()去挂起线程的原因,是因为suspend()在导致线程暂停的同时,并不会去释放任何锁资源。此时,其他任何线程想要访问被它暂用的锁时,都会被牵连,导致无法正常继续运行(如图2.7所示)。直到对应的线程上进行了resume()操作,被挂起的线程才能继续,从而其他所有阻塞在相关锁上的线程也可以继续执行。但是,如果resume()操作意外地在suspend()前就执行了,那么被挂起的线程可能很难有机会被继续执行。并且,更严重的是:它所占用的锁不会被释放,因此可能会导致整个系统工作不正常。而且,对于被挂起的线程,从它的线程状态上看,居然还是Runnable,这也会严重影响我们对系统当前状态的判断。

图2.7 suspend()方法导致线程进入类似死锁的状态

为了方便大家理解suspend()的问题,这里准备一个简单的程序。演示了这种情况:

01 public class BadSuspend {
02   public static Object u = new Object();
03   static ChangeObjectThread t1 = new ChangeObjectThread("t1");
04   static ChangeObjectThread t2 = new ChangeObjectThread("t2");
05
06   public static class ChangeObjectThread extends Thread {
07     public ChangeObjectThread(String name){
08       super.setName(name);
09     }
10     @Override
11     public void run() {
12       synchronized (u) {
13         System.out.println("in "+getName());
14         Thread.currentThread().suspend();
15       }
16     }
17   }
18
19   public static void main(String[] args) throws InterruptedException {
20     t1.start();
21     Thread.sleep(100);
22     t2.start();
23     t1.resume();
24     t2.resume();
25     t1.join();
26     t2.join();
27   }
28 }

执行上述代码,开启t1和t2两个线程。他们会在第12行通过对象锁u实现对临界区的访问。线程t1和t2启动后,在主函数中,第23~24行,对其进行resume()。目的是让他们得以继续执行。接着,主函数等待着两个线程的结束。

执行上述代码后,我们可能会得到以下输出:

in t1
in t2

这表明两个线程先后进入了临界区。但是程序不会退出。而是会挂起。使用jstack命令打印系统的线程信息可以看到:

"t2" #9 prio=5 os_prio=0 tid=0x15c85c00 nid=0x1ddc runnable [0x15f2f000]
   java.lang.Thread.State: RUNNABLE
    at java.lang.Thread.suspend0(Native Method)
    at java.lang.Thread.suspend(Thread.java:1029)
    at geym.conc.ch2.suspend.BadSuspend$ChangeObjectThread.run(BadSuspend. java:16)
    - locked <0x048b2e58> (a java.lang.Object)

这时我们需要注意,当前系统中,线程t2其实是被挂起的。但是它的线程状态确实是RUNNABLE,这很有可能使我们误判当前系统的状态。同时,虽然主函数中已经调用了resume(),但是由于时间先后顺序的缘故,那个resume并没有生效!这就导致了线程t2被永远挂起,并且永远占用了对象u的锁。这对于系统来说极有可能是致命的。

如果需要一个比较可靠的suspend()函数,那应该怎么办呢?回想一下上一节中提到的wait()和notify()方法,这也不是一件难事。下面的代码就给出了一个利用wait()和notify()方法,在应用层面实现suspend()和resume()功能的例子。

01 public class GoodSuspend {
02   public static Object u = new Object();
03
04   public static class ChangeObjectThread extends Thread {
05     volatile boolean suspendme = false;
06
07     public void suspendMe() {
08       suspendme = true;
09     }
10
11     public void resumeMe(){
12       suspendme=false;
13       synchronized (this){
14         notify();
15       }
16     }
17     @Override
18     public void run() {
19       while (true) {
20
21         synchronized (this) {
22           while (suspendme)
23             try {
24               wait();
25             } catch (InterruptedException e) {
26               e.printStackTrace();
27             }
28         }
29
30         synchronized (u) {
31           System.out.println("in ChangeObjectThread");
32         }
33         Thread.yield();
34       }
35     }
36   }
37
38   public static class ReadObjectThread extends Thread {
39     @Override
40     public void run() {
41       while (true) {
42         synchronized (u) {
43           System.out.println("in ReadObjectThread");
44         }
45         Thread.yield();
46       }
47     }
48   }
49
50   public static void main(String[] args) throws InterruptedException {
51     ChangeObjectThread t1 = new ChangeObjectThread();
52     ReadObjectThread t2 = new ReadObjectThread();
53     t1.start();
54     t2.start();
55     Thread.sleep(1000);
56     t1.suspendMe();
57     System.out.println("suspend t1 2 sec");
58     Thread.sleep(2000);
59     System.out.println("resume t1");
60     t1.resumeMe();
61   }
62 }

在代码第5行,给出一个标记变量suspendme,表示当前线程是否被挂起。同时,增加了suspendMe()和resumeMe()两个方法,分别用于挂起线程和继续执行线程。

在代码第21~28行,线程会先检查自己是否被挂起,如果是,则执行wait()方法进行等待。否则,则进行正常的处理。当线程继续执行时,resumeMe()方法被调用(代码第11~16行),线程t1得到一个继续执行的notify()通知,并且清除了挂起标记,从而得以正常执行。

2.2.6 等待线程结束(join)和谦让(yield)

在很多情况下,线程之间的协作和人与人之间的协作非常类似。一种非常常见的合作方式就是分工合作。以我们非常熟悉的软件开发为例,在一个项目进行时,总是应该有几位号称是“需求分析师”的同事,先对系统的需求和功能点进行整理和总结,然后,以书面形式给出一份需求说明或者类似的参考文档,然后,软件设计师、研发工程师才会一拥而上,进行软件开发。如果缺少需求分析师的工作输出,那么软件研发的难度可能会比较大。因此,作为一名软件研发人员,总是喜欢等待需求分析师完成他应该完成的任务后,才愿意投身工作。简单地说,就是研发人员需要等待需求分析师完成他的工作,然后,才能进行研发。

将这个关系对应到多线程应用中,很多时候,一个线程的输入可能非常依赖于另外一个或者多个线程的输出,此时,这个线程就需要等待依赖线程执行完毕,才能继续执行。JDK提供了join()操作来实现这个功能,如下所示,显示了2个join()方法:

public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException

第一个join()方法表示无限等待,它会一直阻塞当前线程,直到目标线程执行完毕。第二个方法给出了一个最大等待时间,如果超过给定时间目标线程还在执行,当前线程也会因为“等不及了”,而继续往下执行。

英文join的翻译,通常是加入的意思。在这里感觉也非常贴切。因为一个线程要加入另外一个线程,那么最好的方法就是等着它一起走。

这里提供一个简单点的join()实例,供大家参考:

public class JoinMain {
  public volatile static int i=0;
  public static class AddThread extends Thread{
    @Override
    public void run() {
      for(i=0;i<10000000;i++);
    }
  }
  public static void main(String[] args) throws InterruptedException {
    AddThread at=new AddThread();
    at.start();
    at.join();
    System.out.println(i);
  }
}

主函数中,如果不使用join()等待AddThread,那么得到的i很可能是0或者一个非常小的数字。因为AddThread还没开始执行,i的值就已经被输出了。但在使用join()方法后,表示主线程愿意等待AddThread执行完毕,跟着AddThread一起往前走,故在join()返回时,AddThread已经执行完成,故i总是10000000。

有关join(),我还想再补充一点,join()的本质是让调用线程wait()在当前线程对象实例上。下面是JDK中join()实现的核心代码片段:

while (isAlive()) {
  wait(0);
}

可以看到,它让调用线程在当前线程对象上进行等待。当线程执行完成后,被等待的线程会在退出前调用notifyAll()通知所有的等待线程继续执行。因此,值得注意的一点是:不要在应用程序中,在Thread对象实例上使用类似wait()或者notify()等方法,因为这很有可能会影响系统API的工作,或者被系统API所影响。

另外一个比较有趣的方法,是Thread.yield(),它的定义如下:

public static native void yield();

这是一个静态方法,一旦执行,它会使当前线程让出CPU。但要注意,让出CPU并不表示当前线程不执行了。当前线程在让出CPU后,还会进行CPU资源的争夺,但是是否能够再次被分配到,就不一定了。因此,对Thread.yield()的调用就好像是在说:我已经完成一些最重要的工作了,我应该是可以休息一下了,可以给其他线程一些工作机会啦!

如果你觉得一个线程不那么重要,或者优先级非常低,而且又害怕它会占用太多的CPU资源,那么可以在适当的时候调用Thread.yield(),给予其他重要线程更多的工作机会。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文