编写高质量代码:改善Java程序的151个建议(第8章:多线程和并发___建议118~121)
多线程技术可以更好地利用系统资源,减少用户的响应时间,提高系统的性能和效率,但同时也增加了系统的复杂性和运维难度,特别是在高并发、大压力、高可靠性的项目中。线程资源的同步、抢占、互斥都需要慎重考虑,以避免产生性能损耗和线程死锁。
建议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块中。这段代码的执行顺序如下:
- 线程t1启动,并执行run方法,由于没有其它线程同步代码块的锁,所以t1线程执行后自加后执行到sleep方法即开始休眠,此时a=1
- JVM又启动了5个线程,也同时运行run方法,由于synchronized关键字的阻塞作用,这5个线程不能执行自增和自减操作,等待t1线程锁释放。
- 主线程执行了t1.stop方法,终止了t1线程,注意,由于a变量是所有线程共享的,所以其它5个线程获得的a变量也是1
- 其它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),但也是不能完全保证。