莫那·鲁道

惯于闲看花飞花落, 心念天际云卷云舒.

Coder, 欢迎留言 😆.


GitHub

并发编程之-Thread-类过期方法和常用方法

前言

在 Java 刚诞生时,Thread 类就已经有了很多方法,但这些方法由于一些原因(有一些明显的bug或者设计不合理)有些已经废弃了,但是他们的方法名却是非常的好,真的是浪费。我们在进行并发必编程的时候一定要注意这些。

  1. 过期方法1—– stop 方法
  2. 过期方法2——suspend 方法和 resume 方法
  3. 常用方法1——线程中断方法 interrupt,isInterrupted,static interrupted
  4. 常用方法2——等待线程结束 join 方法
  5. 常用方法3——线程让出时间片 yield 方法

1. 过期方法1—– stop 方法

JDK 源码:

该方法被定义了 @Deprecated 注解,并在注释中说明了为什么废弃:

该方法具有固有的不安全性。用 Thread.stop 来终止线程将释放它已经锁定的所有监视器(作为沿堆栈向上传播的未检查 ThreadDeath 异常的一个自然后果)。如果以前受这些监视器保护的任何对象都处于一种不一致的状态,则损坏的对象将对其他线程可见,这有可能导致任意的行为。stop 的许多使用都应由只修改某些变量以指示目标线程应该停止运行的代码来取代。目标线程应定期检查该变量,并且如果该变量指示它要停止运行,则从其运行方法依次返回。如果目标线程等待很长时间(例如基于一个条件变量),则应使用 interrupt 方法来中断该等待。

很官方对不对?还是用楼主的话来解释一下吧。最重要的原因就i是 stop 太粗鲁了,强行把执行到一半的线程终止,引起数据不一致。比如有些数据处理到一半,该方法就强行停止线程,导致数据不一致。

可使用一个条件判断来代替此功能,比如设置一个变量,如果这个变量是ture 则跳出循环,结束线程的执行。反正不要使用该方法就对了。

2. 过期方法2——suspend 方法和 resume 方法

JDK 源码: suspend 方法

resume 方法

这两个方法都被标注为过期,楼主解释一下为什么不能使用。

suspend 方法的作用是挂起方法,而 resume 方法的作用是继续执行,可以说这两个方法是对应的,先挂起,然后继续执行。这两个动作是相反的。但是为什么不建议使用呢?原因就似乎 suspend 方法在导致线程暂停的同时,并不会释放任何锁资源。此时,其他任何线程想要访问被他暂用的锁时,都会被牵连,导致无法正常运行。知道对应的 resume 方法被调用,被挂起的线程才能继续。但是,请注意,这里严格要求 resume 方法在 suspend 方法后面执行,如果 resume 方法意外的在suspend 方法之前执行了,就会导致死锁,该线程拥有不会恢复。

最坑的是,当产生死锁的时候,你肯定会使用 jps 命令和 jstack 命令去查看死锁。但是你会发现你根本找不到,因为这个线程的状态是 Rannable。你根本无法判断是哪个线程被挂起了,所以,该方法一定要废弃。

比如楼主写了一个例子:

package cn.think.in.java.two;

public class BadSuspend {

  static Object u = new Object();
  static ChangeObjectThread t1 = new ChangeObjectThread("t1");
  static ChangeObjectThread t2 = new ChangeObjectThread("t2");


  static class ChangeObjectThread extends Thread {

    public ChangeObjectThread(String name) {
      super.setName(name);
    }

    public void run() {
      synchronized (u) {
        System.out.println("in " + getName());
        // 暂停
        Thread.currentThread().suspend();
      }
    }
  }

  public static void main(String[] args) throws InterruptedException {
    t1.start();
    Thread.sleep(100);
    // 此时 t1 已经暂停
    t2.start();
    // t1 恢复
    t1.resume();
    // t2 这时恢复,但是 t2在恢复之后进入了暂停,导致死锁。
    // 除非使用 sleep 让 t2 先暂停就可以。
//    Thread.sleep(100);
    t2.resume();
    t1.join();
    t2.join();

  }

}

该方法会发生死锁。然而我们在命令行中使用 jstack 命令查看时,会发现该线程状态是 Rannable。

死锁 状态 却是 Rannable

因此在以后的并发编程一定不要使用该方法。

3. 常用方法1——线程中断方法 interrupt,isInterrupted,static interrupted

关于线程中断还有3个方法:

public void interrupt() 

public boolean isInterrupted()

public static boolean interrupted()

public void interrupt() 作用:中断线程,也就是设置中断标记,注意,是设置标记,不会中断。 public boolean isInterrupted() 作用:判断线程是否中断 static boolean Thread interrupted 作用:判断是否中断,并清除当前中断状态。

我们解释解释这三个方法: 在 java 中,线程中断是一种重要的线程协作机制。可以用来代替 stop方法,严格来讲, 线程中断并不会使线程立即退出, 而是给线程发一个通知,告知目标线程,有人希望你退出了。而什么时候退出,完全由线程自己自行决定,避免了stop 的问题。但是该方法只是设置标记,所以需要自己判断状态然后跳出循环之类的结束线程运行。

那么我们怎么使用这三个方法进行并发编程呢?下面楼主写了一个例子:

  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
      for (; ; ) {
      }
    });
    t1.start();
    Thread.sleep(2000);
    // 不会起任何作用,所以需要判断他的中断位状态
    t1.interrupt();
  }

该测试方法在死循环了一个线程,然后启动 interrupt 方法,根本不会起任何作用,所以各位不要这样使用该方法。那么如何使用呢?示例代码如下:

  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
      for (; ; ) {
        if (Thread.currentThread().isInterrupted()) {
          System.out.println("interrupt");
          break;
        }
        Thread.yield();
      }
    });
    t1.start();
    Thread.sleep(2000);
    t1.interrupt();
  }

使用 isInterrupted 方法进行判断,如果返回 ture ,表示有中断标记,那么则 break 循环。结束运行。

还有一个需要注意的地方就是,如果线程在 sleep 或者 wait 状态,如果你调用 interrput 方法就会导致InterruptedException 异常,但是,抛出异常时会清除中断标记,因此,线程也就中断不了了,如果你想在异常后仍然中断线程,那么你需要在 catch 中 继续设置状态,也就是调用 interrupt 方法。我们来个例子看看:

  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
      for (; ; ) {
        if (Thread.currentThread().isInterrupted()) {
          System.out.println("interrupt");
          break;
        }
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
          System.err.println("Interrupt When Sleep");
          // 由于在 sleep 之间中断线程导致抛出异常,此时,他会清楚中断位,所以需要在这里重新设置中断位,下次循环则会直接判断中断标记,从而break。
          Thread.currentThread().interrupt();
          // 该方法会清除中断状态,导致上面的一行代码失效
//          boolean isInterrupt = Thread.interrupted();
//          System.out.println(isInterrupt);
        }
        Thread.yield();
      }
    });

    t1.start();
    Thread.sleep(1000);
    t1.interrupt();
  }

运行结果:

interrupt Interrupt When Sleep

该测试方法中,在线程中调用了 sleep 方法,并在 main 线程中调用了 interrupt 方法,因此导致该线程异常,但是,如果我们不在 catch 中重新设置中断位,该线程永远不会停止。这个时需要注意的。

还有一个静态方法,Thread.interrupted(),其实我们上面的例子也测试了,该方法会返回线程是否中断,并且会清除状态,使用的时候需要注意。

4. 常用方法2——等待线程结束 join 方法

JDK 源码:

join 方法

该方法注释写到:等待该线程直到死。。。。还真是痴情啊。说正经的的。该方法实际上时等待线程结束。说明意思呢?

假如你有2个线程,A线程在算 1+1 ,而B线程需要 A线程算出的结果,那么B线程就需要等待A线程,那么这时候,B线程就需要调用 A线程的 join 方法,调用该方法后, B线程就会被挂起,直到A线程死亡,B线程才会被唤醒。实际上,如果看 join 的源码,会发现内部调用了A线程的 wait 方法。也就是说,B 线程 wait 在了 A 线程上。A 线程执行完毕会调用 notifyAll 方法,唤醒B线程。

我们写个demo:

package cn.think.in.java.two;

public class JoinTest {

  static int i;

  public static void main(String[] args) throws InterruptedException {
    AddThread addThread = new AddThread();
    addThread.start();
    // 主函数等待 addThread
    // join 的本质是调用了 wait方法,让调用线程 wait 在当前线程对象实例上。也就是main线程 wait 在 addThread 线程实例上。
    // 当 addThread 执行结束后,会调用 notifyAll 方法,注意,不要再程序中调用线程的 wait 或者 notify 方法,
    // 可能会影响系统API 的工作。
    addThread.join();// 重载方法 join(long) 如果达到给定的毫秒数,则不等了
    System.out.println(i);
  }


  static class AddThread extends Thread {

    public void run() {
      for (; i < 10000000; i++) {
      }
    }
  }

}

该测试方法运行了一个对变量 i 自增运算的线程,并且主线程在等待 addThread 线程执行完才打印 i 的结果。如果不使用 join , 那么 ,打印 i 的值永远会小于10000。

而 join 的内部实现,我们刚刚说了,使用 wait 方法,我们看看该方法:

    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

该方法时同步的,同时内部调用了自身的 wait 方法。注意:我们最好不要调用线程的 wait 方法和 notify 方法,可能会导致系统 api 出现问题。

5. 常用方法3——线程让出时间片 yield 方法

这个方法就比较简单了。这是一个静态方法,yield 谦让出CPU时间片;

yieid 会让出时间片,但是是随机的。如果你觉得一个线程不是很重要,那就可以适当的调用该方法,给予其他线程更多的机会。

拾遗

sleep 方法

虽然用的很多,但有必要说一下,该方法不会释放当前线程的锁。面试中常有该问题,wait 方法和 sleep 方法有什么不同,wait 方法会释放锁,sleep 方法不会释放锁。

##### holdsLock 方法: 仅当当前线程在指定的对象上保持监视器锁时,才返回  true。该方法旨在使程序能够断言当前线程已经保持一个指定的锁。 参数: obj - 用于测试锁所属权的对象 返回: 如果当前线程在指定的对象上保持监视器锁,则返回 true。

##### setContextClassLoader 方法: 设置该线程的上下文 ClassLoader。上下文 ClassLoader 可以在创建线程设置,并允许创建者在加载类和资源时向该线程中运行的代码提供适当的类加载器。 首先,如果有安全管理器,则通过 RuntimePermission(“setContextClassLoader”) 权限调用其 checkPermission`方法,查看是否可以设置上下文 ClassLoader。该方法在违反 JDK 默认的类加载模型时能起到很大作用。

参数: 该线程的上下文 ClassLoader

抛出: SecurityException - 如果当前线程无法设置上下文 ClassLoader。