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

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

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

  多线程比较简单的实现方式是继承Thread类,而后覆写run方法,在客户端程序中经过调用对象的start方法便可启动一个线程,这是多线程程序的标准写法。不知道你们可以还能回想起本身写的第一个多线程的demo呢?估计通常是这样写的:windows

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比较聪明的地方,简化多线程应用。ide

  那可能有人要问了:若是确实有必要覆写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),但也是不能彻底保证。

相关文章
相关标签/搜索