星空网 > 软件开发 > Java

编写高质量代码:改善Java程序的151个建议(第8章:多线程和并发___建议118~122)

  多线程技术可以更好地利用系统资源,减少用户的响应时间,提高系统的性能和效率,但同时也增加了系统的复杂性和运维难度,特别是在高并发、大压力、高可靠性的项目中。线程资源的同步、抢占、互斥都需要慎重考虑,以避免产生性能损耗和线程死锁。

建议118:不推荐覆写start方法

  多线程比较简单的实现方式是继承Thread类,然后覆写run方法,在客户端程序中通过调用对象的start方法即可启动一个线程,这是多线程程序的标准写法。不知道大家能够还能回想起自己写的第一个多线程的demo呢?估计一般是这样写的:

class MultiThread extends Thread{  @Override  public synchronized void start() {    //调用线程体
run();
} @Override public void run() { //MultiThread do someThing }}

覆写run方法,这好办,写上自己的业务逻辑即可,但为什么要覆写start方法呢?最常见的理由是:要在客户端调用start方法启动线程,不覆写start方法怎么启动run方法呢?于是乎就覆写了start方法,在方法内调用run方法。客户端代码是一个标准程序,代码如下 

public static void main(String[] args) {    //多线程对象    MultiThread m = new MultiThread();    //启动多线程    m.start();  }

  相信大家都能看出,这是一个错误的多线程应用,main方法根本就没有启动一个子线程,整个应用程序中只有一个主线程在运行,并不会创建任何其它的线程。对此,有很简单的解决办法。只要删除MultiThread类的start方法即可。

  然后呢?就结束了吗?是的,很多时候确实到此结束了。那为什么不必而且不能覆写start方法,仅仅就是因为" 多线程应用就是这样写的 " 这个原因吗?

  要说明这个问题,就需要看一下Thread类的源码了。Thread类的start方法的代码(这个是JDK7版本的)如下: 

public synchronized void start() {    // 判断线程状态,必须是为启动状态    /**     * This method is not invoked for the main method thread or "system"     * group threads created/set up by the VM. Any new functionality added     * to this method in the future may have to also be added to the VM.     *     * A zero status value corresponds to state "NEW".     */    if (threadStatus != 0)      throw new IllegalThreadStateException();    // 加入线程组中    /*     * Notify the group that this thread is about to be started so that it     * can be added to the group's list of threads and the group's unstarted     * count can be decremented.     */    group.add(this);    boolean started = false;    try {      // 分配栈内存,启动线程,运行run方法      start0();      started = true;    } finally {      try {        if (!started) {          group.threadStartFailed(this);        }      } catch (Throwable ignore) {        /*         * do nothing. If start0 threw a Throwable then it will be         * passed up the call stack         */      }    }  }
   // 本地方法 private native void start0();

  这里的关键是本地方法start0,它实现了启动线程、申请栈内存、运行run方法、修改线程状态等职责,线程管理和栈内存管理都是由JVM负责的,如果覆盖了start方法,也就是撤销了线程管理和栈内存管理的能力,这样如何启动一个线程呢?事实上,不需要关注线程和栈内存的管理,主需要编码者实现多线程的逻辑即可(即run方法体),这也是JVM比较聪明的地方,简化多线程应用。

  那可能有人要问了:如果确实有必要覆写start方法,那该如何处理呢?这确实是一个罕见的要求,不过覆写也容易,只要在start方法中加上super.start()即可,代码如下:

class MultiThread extends Thread {  @Override  public synchronized void start() {    /* 线程启动前的业务处理 */    super.start();    /* 线程启动后的业务处理 */  }  @Override  public void run() {    // MultiThread do someThing  }}

  注意看start方法,调用了父类的start方法,没有主动调用run方法,这是由JVM自行调用的,不用我们显示实现,而且是一定不能实现。此方式虽然解决了" 覆写start方法 "的问题,但是基本上无用武之地,到目前为止还没有发现一定要覆写start方法的多线程应用,所以要求覆写start的场景。都可以使用其他的方式实现,例如类变量、事件机制、监听等方式。

注意:继承自Thread类的多线程类不必覆写start方法。

建议119:启动线程前stop方法是不可靠的

  有这样一个案例,我们需要一个高效率的垃圾邮件制造机,也就是有尽可能多的线程来尽可能多的制造垃圾邮件,垃圾邮件重要的信息保存在数据库中,如收件地址、混淆后的标题、反应垃圾处理后的内容等,垃圾制造机的作用就是从数据库中读取这些信息,判断是否符合条件(如收件地址必须包含@符号、标题不能为空等),然后转换成一份真实的邮件发出去。

  整个应用逻辑很简单,这必然是一个多线程应用,垃圾邮件制造机需要继承Thread类,代码如下:

//垃圾邮件制造机class SpamMachine extends Thread{  @Override  public void run() {    //制造垃圾邮件    System.out.println("制造大量垃圾邮件......");  }}

  在客户端代码中需要发挥计算机的最大潜能来制造邮件,也就是说开尽可能多的线程,这里我们使用一个while循环来处理,代码如下:

public static void main(String[] args) {    //不分昼夜的制造垃圾邮件    while(true){      //多线程多个垃圾邮件制造机      SpamMachine sm = new SpamMachine();      //xx条件判断,不符合提交就设置该线程不可执行      if(!false){        sm.stop();      }      //如果线程是stop状态,则不会启动      sm.start();    }  }

  在此段代码中,设置了一个极端条件:所有的线程在启动前都执行stop方法,虽然它是一个过时的方法,但它的运行逻辑还是正常的,况且stop方法在此处的目的并不是停止一个线程,而是设置线程为不可启用状态。想来这应该是没有问题的,但是运行结果却出现了奇怪的现象:部分线程还是启动了,也就是在某些线程(没有规律)中的start方法正常执行了。在不符合判断规则的情况下,不可启用状态的线程还是启用了。这是为什么呢?

  这是线程启动start方法的一个缺陷。Thread类的stop方**根据线程状态来判断是终结线程还是设置线程为不可运行状态,对于未启动的线程(线程状态为NEW)来说,会设置其标志位为不可启动,而其他的状态则是直接停止。stop方法的JDK1.6源代码(JDk1.6以上源码于此可能有变化,需要重新观察源码)如下:  

  @Deprecated  public final void stop() {    // If the thread is already dead, return.  // A zero status value corresponds to "NEW".  if ((threadStatus != 0) && !isAlive()) {    return;  }  stop1(new ThreadDeath());  }

 private final synchronized void stop1(Throwable th) {  SecurityManager security = System.getSecurityManager();  if (security != null) {    checkAccess();    if ((this != Thread.currentThread()) ||    (!(th instanceof ThreadDeath))) {    security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);    }  }    // A zero status value corresponds to "NEW"  if (threadStatus != 0) {    resume(); // Wake up thread if it was suspended; no-op otherwise    stop0(th);  } else {      // Must do the null arg check that the VM would do with stop0    if (th == null) {     throw new NullPointerException();    }      // Remember this stop attempt for if/when start is used    stopBeforeStart = true;    throwableFromStop = th;    }  }

  这里设置了stopBeforeStart变量,标志着是在启动前设置了停止标志,在start方法中(JDK6源码)是这样校验的:  

public synchronized void start() {    /**   * This method is not invoked for the main method thread or "system"   * group threads created/set up by the VM. Any new functionality added   * to this method in the future may have to also be added to the VM.   *   * A zero status value corresponds to state "NEW".     */    if (threadStatus != 0)      throw new IllegalThreadStateException();    group.add(this);    start0();
// 在启动前设置了停止状态 if (stopBeforeStart) { stop0(throwableFromStop); } } private native void start0();

  注意看start0方法和stop0方法的顺序,start0方法在前,也就说既是stopBeforeStart为true(不可启动),也会启动一个线程,然后再stop0结束这个线程,而罪魁祸首就在这里!

  明白了原因,我们的情景代码就很容易修改了,代码如下:

public static void main(String[] args) {    // 不分昼夜的制造垃圾邮件    while (true) {      // 多线程多个垃圾邮件制造机      SpamMachine sm = new SpamMachine();      // xx条件判断,不符合提交就设置该线程不可执行      if (!false) {        new SpamMachine().start();      }    }  }

  不再使用stop方法进行状态的设置,直接通过判断条件来决定线程是否可启用。对于start方法的缺陷,一般不会引起太大的问题,只是增加了线程启动和停止的精度而已。

建议120:不使用stop方法停止线程

  线程启动完毕后,在运行时可能需要中止,Java提供的终止方法只有一个stop,但是我不建议使用这个方法,因为它有以下三个问题:

(1)、stop方法是过时的:从Java编码规则来说,已经过时的方法不建议采用。

(2)、stop方**导致代码逻辑不完整:stop方法是一种" 恶意 " 的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,这是非常危险的。看如下的代码:

public static void main(String[] args) {    Thread thread = new Thread() {      @Override      public void run() {        try {          // 子线程休眠1秒          Thread.sleep(1000);        } catch (InterruptedException e) {          // 异常处理        }        System.out.println("此处是业务逻辑,永远不会执行");      }    };    // 启动线程    thread.start();    // 主线程休眠0.1秒    try {      Thread.sleep(100);    } catch (InterruptedException e) {      e.printStackTrace();    }    // 子线程停止    thread.stop();  }

  这段代码的逻辑是这样的:子线程是一个匿名内部类,它的run方法在执行时会休眠一秒,然后执行后续的逻辑,而主线程则是休眠0.1秒后终止子线程的运行,也就说JVM在执行tread.stop()时,子线程还在执行sleep(1000),此时stop方**清除栈内信息,结束该线程,这也就导致了run方法的逻辑不完整,输出语句println代表的是一段逻辑,可能非常重要,比如子线程的主逻辑、资源回收、情景初始化等,但是因为stop线程了,这些都不再执行,于是就产生了业务逻辑不完整的情况。

  这是极度危险的,因为我们不知道子线程会在什么时候被终止,stop连基本的逻辑完整性都无法保证。而且此种操作也是非常隐蔽的,子线程执行到何处会被关闭很难定位,这位以后的维护带来了很多麻烦。

(3)、stop方**破坏原子逻辑

  多线程为了解决共享资源抢占的问题,使用了锁概念,避免资源不同步,但是正因为此,stop方法却会带来更大的麻烦,它会丢弃所有的锁,导致原子逻辑受损。例如有这样一段程序:

class MultiThread implements Runnable {  int a = 0;  @Override  public void run() {    // 同步代码块,保证原子操作    synchronized ("") {      // 自增      a++;      try {        //线程休眠0.1秒        Thread.sleep(100);      } catch (InterruptedException e) {        e.printStackTrace();      }      // 自减      a--;      String tn = Thread.currentThread().getName();      System.out.println(tn + ":a = " + a);    }  }}

  MultiThread实现了Runnable接口,具备多线程能力,其中run方法中加上了synchronized代码块,表示内部是原子逻辑,它会先自增然后自减,按照synchronized同步代码块的规则来处理,此时无论启动多少线程,打印出来的结果应该是a=0,但是如果有一个正在执行的线程被stop,就会破坏这种原子逻辑,代码如下:  

  public static void main(String[] args) {    MultiThread t = new MultiThread();    Thread t1 = new Thread(t);    // 启动t1线程    t1.start();    for (int i = 0; i < 5; i++) {      new Thread(t).start();    }    //停止t1线程    t1.stop();  }

  首先说明的是所有线程共享了一个MultiThread的实例变量t,其次由于在run方法中加入了同步代码块,所以只能有一个线程进入到synchronized块中。这段代码的执行顺序如下:

  1. 线程t1启动,并执行run方法,由于没有其它线程同步代码块的锁,所以t1线程执行后自加后执行到sleep方法即开始休眠,此时a=1
  2. JVM又启动了5个线程,也同时运行run方法,由于synchronized关键字的阻塞作用,这5个线程不能执行自增和自减操作,等待t1线程锁释放。
  3. 主线程执行了t1.stop方法,终止了t1线程,注意,由于a变量是所有线程共享的,所以其它5个线程获得的a变量也是1
  4. 其它5个线程依次获得CPU执行机会,打印出a值

  分析了这么多,相信大家也明白了输出结果,结果如下:

    Thread-5:a = 1
    Thread-4:a = 1
    Thread-3:a = 1
    Thread-2:a = 1
    Thread-1:a = 1

  原本期望synchronized同步代码块中的逻辑都是原子逻辑,不受外界线程的干扰,但是结果却出现原子逻辑被破坏的情况,这也是stop方法被废弃的一个重要原因:破坏了原子逻辑。

  既然终止一个线程不能使用stop方法,那怎样才能终止一个正在运行的线程呢?答案也简单,使用自定义的标志位决定线程的执行情况,代码如下:

class SafeStopThread extends Thread {  // 此变量必须加上volatile  /*   * volatile: 1.作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值.   * 2.被设计用来修饰被不同线程访问和修改的变量。如果不加入volatile   * ,基本上会导致这样的结果:要么无法编写多线程程序,要么编译器失去大量优化的机会。   */  private volatile boolean stop = false;  @Override  public void run() {    // 判断线程体是否运行    while (stop) {      // doSomething    }  }  public void terminate() {    stop = true;  }}

  这是很简单的办法,在线程体中判断是否需要停止运行,即可保证线程体的逻辑完整性,而且也不会破坏原子逻辑。可能大家对JavaAPI比较熟悉,于是提出疑问:Thread不是还提供了interrupt中断线程的方法吗?这个方法可不是过时方法,那可以使用吗?它可以终止一个线程吗?

  interrupt,名字看上去很像是终止一个线程的方法,但它不能终止一个正在执行着的线程,它只是修改中断标志而已,例如下面一段代码:

  public static void main(String[] args) {    Thread thread = new Thread() {      @Override      public void run() {        // 线程一直运行        while (true) {          System.out.println("Running......");        }      }    };    // 启动线程    thread.start();    // 中断线程    thread.interrupt();  }

  执行这段代码,你会发现一直有Running在输出,永远不会停止,似乎执行了interrupt没有任何变化,那是因为interrupt方法不能终止一个线程状态,它只会改变中断标志位(如果在thread.interrupt()前后输出thread.isInterrupted()则会发现分别输出了false和true),如果需要终止该线程,还需要自己进行判断,例如我们可以使用interrupt编写出更简洁、安全的终止线程代码:

class SafeStopThread extends Thread {  @Override  public void run() {    //判断线程体是否运行    while (!isInterrupted()) {      // do SomeThing    }  }}

   总之,如果期望终止一个正在运行的线程,则不能使用已过时的stop方法。需要自行编码实现,如此即可保证原子逻辑不被破坏,代码逻辑不会出现异常。当然,如果我们使用的是线程池(比如ThreadPoolExecutor类),那么可以通过shutdown方法逐步关闭池中的线程,它采用的是比较温和、安全的关闭线程方法,完全不会产生类似stop方法的弊端。

建议121:线程优先级只使用三个等级

  线程的优先级(Priority)决定了线程获得CPU运行的机会,优先级越高获得的运行机会越大,优先级越低获得的机会越小。Java的线程有10个级别(准确的说是11个级别,级别为0的线程是JVM的,应用程序不能设置该级别),那是不是说级别是10的线程肯定比级别是9的线程先运行呢?我们来看如下一个多线程类:

class TestThread implements Runnable {  public void start(int _priority) {    Thread t = new Thread(this);    // 设置优先级别    t.setPriority(_priority);    t.start();  }  @Override  public void run() {    // 消耗CPU的计算    for (int i = 0; i < 100000; i++) {      Math.hypot(924526789, Math.cos(i));    }    // 输出线程优先级    System.out.println("Priority:" + Thread.currentThread().getPriority());  }}

  该多线程实现了Runnable接口,实现了run方法,注意在run方法中有一个比较占用CPU的计算,该计算毫无意义,

public static void main(String[] args) {    //启动20个不同优先级的线程    for (int i = 0; i < 20; i++) {      new TestThread().start(i % 10 + 1);    }  }

 这里创建了20个线程,每个线程在运行时都耗尽了CPU的资源,因为优先级不同,线程调度应该是先处理优先级高的,然后处理优先级低的,也就是先执行2个优先级为10的线程,然后执行2个优先级为9的线程,2个优先级为8的线程......但是结果却并不是这样的。

  Priority:5
  Priority:7
  Priority:10
  Priority:6
  Priority:9
  Priority:6
  Priority:5
  Priority:7
  Priority:10
  Priority:3
  Priority:4
  Priority:8
  Priority:8
  Priority:9
  Priority:4
  Priority:1
  Priority:3
  Priority:1
  Priority:2
  Priority:2

  println方法虽然有输出损耗,可能会影响到输出结果,但是不管运行多少次,你都会发现两个不争的事实:

(1)、并不是严格按照线程优先级来执行的

  比如线程优先级为5的线程比优先级为7的线程先执行,优先级为1的线程比优先级为2的线程先执行,很少出现优先级为2的线程比优先级为10的线程先执行(注意,这里是" 很少 ",是说确实有可能出现,只是几率低,因为优先级只是表示线程获得CPU运行的机会,并不代表强制的排序号)。

(2)、优先级差别越大,运行机会差别越明显

  比如优先级为10的线程通常会比优先级为2的线程先执行,但是优先级为6的线程和优先级为5的线程差别就不太明显了,执行多次,你会发现有不同的顺序。

  这两个现象是线程优先级的一个重要表现,之所以会出现这种情况,是因为线程运行是需要获得CPU资源的,那谁能决定哪个线程先获得哪个线程后获得呢?这是依照操作系统设置的线程优先级来分配的,也就是说,每个线程要运行,需要操作系统分配优先级和CPU资源,对于JAVA来说,JVM调用操作系统的接口设置优先级,比如windows操作系统优先级都相同吗?

  事实上,不同的操作系统线程优先级是不同的,Windows有7个优先级,Linux有140个优先级,Freebsd则由255个(此处指的优先级个数,不同操作系统有不同的分类,如中断级线程,操作系统级等,各个操作系统具体用户可用的线程数量也不相同)。Java是跨平台的系统,需要把这10个优先级映射成不同的操作系统的优先级,于是界定了Java的优先级只是代表抢占CPU的机会大小,优先级越高,抢占CPU的机会越大,被优先执行的可能性越高,优先级相差不大,则抢占CPU的机会差别也不大,这就是导致了优先级为9的线程可能比优先级为10的线程先运行。

  Java的缔造者们也觉察到了线程优先问题,于是Thread类中设置了三个优先级,此意就是告诉开发者,建议使用优先级常量,而不是1到10的随机数字。常量代码如下: 

public class Thread implements Runnable {  /**   * The minimum priority that a thread can have.   */  public final static int MIN_PRIORITY = 1;  /**   * The default priority that is assigned to a thread.   */  public final static int NORM_PRIORITY = 5;  /**   * The maximum priority that a thread can have.   */  public final static int MAX_PRIORITY = 10;}

  在编码时直接使用这些优先级常量,可以说在大部分情况下MAX_PRIORITY的线程回比MIN_PRIORITY的线程优先运行,但是不能认为是必然会先运行,不能把这个优先级做为核心业务的必然条件,Java无法保证优先级高肯定会先执行,只能保证高优先级有更多的执行机会。因此,建议在开发时只使用此三类优先级,没有必要使用其他7个数字,这样也可以保证在不同的操作系统上优先级的表现基本相同。

  大家也许会问,如果优先级相同呢?这很好办,也是由操作系统决定的。基本上是按照FIFO原则(先入先出,First Input First Output),但也是不能完全保证。




原标题:编写高质量代码:改善Java程序的151个建议(第8章:多线程和并发___建议118~122)

关键词:JAVA

*特别声明:以上内容来自于网络收集,著作权属原作者所有,如有侵权,请联系我们: admin#shaoqun.com (#换成@)。
相关文章
我的浏览记录
最新相关资讯
海外公司注册 | 跨境电商服务平台 | 深圳旅行社 | 东南亚物流