【编写高质量代码:改善Java程序的151个建议】第9章:多线程和并发___建议118~124

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

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

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

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

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

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

3、stop方法破坏原子逻辑

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

如何关闭线程呢?

if (!thread.isInterrupted()) {
    thread.interrupt();
}

如果使用的是线程池,可以通过shutdown方法逐步关闭池中的线程。

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

线程的优先级(Priority)决定了线程获取CPU运行的机会,优先级越高获取的运行机会越大,优先级月底获取的机会越小。

package OSChina.Multithread;

public 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());
    }

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

【编写高质量代码:改善Java程序的151个建议】第9章:多线程和并发___建议118~124_第1张图片

创建了20个线程,优先级设置的不同,执行起来是这样的,5和6反了。

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

因为优先级只是表示线程获取CPU运行的机会,并不是代码强制的排序号。

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

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

public class Thread implements Runnable {
    public final static int MIN_PRIORITY = 1;
    public final static int NORM_PRIORITY = 5;
    public final static int MAX_PRIORITY = 10;
}

开发时只使用此三类优先级就可以了。

建议122:使用线程异常处理器提升系统可靠性

编写一个socket应用,监听指定端口,实现数据包的接收和发送逻辑,这在早起系统间进行数据交互是经常使用的,这类接口通常考虑两个问题:一个是避免线程阻塞,保证接收的数据尽快处理;二是接口的稳定性和可靠性,数据包很复杂,接口服务的系统也很多,一旦守候线程出现异常就会导致socket停止,这是非常危险的,那我们有什么办法避免呢?

Java1.5版本以后在thread类中增加了setUncaughtExceptionHandler方法,实现了线程异常的捕捉和处理。

代码实例:

package OSChina.Multithread;

public class TcpServer implements Runnable {
    public TcpServer() {
        Thread t = new Thread(this);
        t.setUncaughtExceptionHandler(new TcpServerExceptionHandler());
        t.start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            try{
                Thread.sleep(1000);
                System.out.println("系统正常运行:"+i);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        throw new RuntimeException();
    }

    private static class TcpServerExceptionHandler implements Thread.UncaughtExceptionHandler{
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("线程"+t.getName()+" 出现异常,自行重启,请分析原因。");
            e.printStackTrace();
            new TcpServer();
        }
    }

    public static void main(String[] args) {
        TcpServer tcpServer = new TcpServer();
    }
}

这段代码的逻辑比较简单,在TcpServer类创建时启动一个线程,提供TCP服务,例如接收和发送文件,具体逻辑在run方法中实现。同时,设置了该线程出现运行期异常时,由TcpServerExceptionHandler异常处理器来处理异常。那么TcpServerExceptionHandler做什么呢?两件事:

1、记录异常信息,以便查找问题

2、重新启动一个新线程,提供不间断的服务

有了这两点,TcpServer就可以稳定的运行了,即使出现异常也能自动重启,客户代码比较简单,只需要new TcpServer()即可,运行结果如下:

【编写高质量代码:改善Java程序的151个建议】第9章:多线程和并发___建议118~124_第2张图片

从运行结果可以看出,当Thread-0出现异常时,系统自动重启了Thread-1线程,继续提供服务,大大提高了系统的性能。

这段代码只是一个示例程序,若要在实际环境中应用,则需要注意以下三个方面:

1、共享资源锁定:如果线程产生异常的原因是资源被锁定,自动重启应用会增加系统的负担,无法提供不间断服务。例如一个即时通信服务出现信息不能写入的情况,即时再怎么重启服务,也无法解决问题。在此情况下最好的办法是停止所有的线程,释放资源。

2、脏数据引起系统逻辑混乱:异常的产生中断了正在执行的业务逻辑,特别是如果正在处理一个原子操作,但如果此时抛出了运行期异常就有可能会破坏正常的业务逻辑,例如出现用户认证通过了,但签到不成功的情况,在这种情况下重启应用程序,虽然可以提供服务,但对部分用户产生了逻辑异常。

3、内存溢出:线程异常了,但由该线程创建的对象并不会马上回收,如果再重新启动新线程,再创建一批对象,特别是加入了场景接管,就非常危险了,例如即时通信服务,重新启动一个新线程必须保证原在线用户的透明性,即用户不会察觉服务重启,在这种情况下,就需要在线程初始化时加载大量对象以保证用户的状态信息,但是如果线程反复重启,很可能会引起OutOfMemory内存泄漏问题。

建议123:volatile不能保证数据同步

volatile关键字比较少用,原因无外乎两点,一是在Java1.5之前该关键字在不同的操作系统上有不同的表现,所带来的问题就是移植性较差;而且比较难设计,误用较多,这也导致它的“名誉”受损。

我们知道,每个线程都运行在栈内存中,每个线程都有自己的工作内存(Working Memory,比如寄存器Register、高速缓存存储器Cache等),线程的计算一般是通过工作内存进行交互的,其示意图如下图所示:

【编写高质量代码:改善Java程序的151个建议】第9章:多线程和并发___建议118~124_第3张图片

从示意图中我们可以看到,线程在初始化时从主内存中加载需要的变量值到工作内存中,然后在线程运行时,如果是读取,直接从工作内存中读取,如果是写入,则先写入工作内存中,之后刷新到主内存中,这是JVM的一个简单的内存模型,但是这样的结构在多线程的情况下有可能会出现问题,比如:A线程修改变量的值,也刷新到了主内存,但B、C线程在此时间内读取的还是本线程的工作内存,也就是说它们读取的不是最新的值,此时就会出现不同线程持有的公共资源不同步的情况。

对于此问题有很多解决的办法,比如使用synchronized同步代码块,或者使用Lock锁来解决该问题,不过,Java可以使用volatile更简单的解决此类问题,比如在一个变量前加上volatile关键字,可以确保每个线程对本地变量的访问和修改都是直接与内存交互的,而不是与本线程的工作内存交互的,保证每个线程都能获取到最新的变量值,其示意图如下:

【编写高质量代码:改善Java程序的151个建议】第9章:多线程和并发___建议118~124_第4张图片

明白了volatile变量的原理,那我们来思考一下:volatile变量是否能够保证数据的同步性呢?两个线程同时修改volatile变量是否会产生脏数据呢?代码如下:

package OSChina.Multithread;

public class UnsafeThread implements Runnable {
    //共享资源
    private volatile int count = 0;
    @Override
    public void run() {
        // 增加CPU的繁忙程度,不必关心其逻辑含义
        for (int i = 0; i < 1000; i++) {
            Math.hypot(Math.pow(92456789,i),Math.cos(i));
        }
        count++;
    }
    public int getCount(){
        return count;
    }
}

上面的代码定义了一个多线程,run方法的主要逻辑是共享资源count的自加运算,而且我们还为count变量加上了volatile关键字,确保是从内存中读取和写入的,如果有多个线程运行,也就是多个线程执行count变量的自加操作,count变量会产生脏数据吗?模拟多线程代码如下:

    public static void main(String[] args) {
        // 理想值,并作为最大循环次数
        int value = 1000;
        // 循环次数,防止造成无限循环或者死循环
        int loops = 0;
        // 主线程组,用于估计活动线程数
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        while (loops++【编写高质量代码:改善Java程序的151个建议】第9章:多线程和并发___建议118~124_第5张图片

执行完了,没出现不安全的情况,证明volatile性能还是可以的。

书中自有黄金屋,书中自有颜如玉!

书中的运行结果:

循环到:40遍,出现不安全的情况

此时,count=999

这只是一种可能的结果,每次执行都有可能产生不同的结果。这也说明我们的count变量没有实现数据同步,在多个线程修改的情况下,count的实际值与理论值产生了偏差,直接说明了volatile关键字并不能保证线程的安全。

代码执行完毕,原本期望的结果为1000,但运行后的结果为999,这表示出现了线程不安全的情况。这也就说明了:volatile关键字只能保证当前线程需要该变量的值时能够获得最新的值,并不能保证线程修改的安全性。

顺便说一下,上面的代码中,UnsafeThread类消耗CPU计算时必须的,其目的是加重线程的负荷,以便出现单个线程抢占整个CPU资源的情景,否者很难模拟出volatile线程不安全的情况,大家可以实际测试一下。

UnsafeThread消耗CPU很严重,慎用啊。

【编写高质量代码:改善Java程序的151个建议】第9章:多线程和并发___建议118~124_第6张图片

建议124:异步运算考虑使用Callable接口

多线程应用有两种实现方式,一种是实现runnable接口,另一种是继承Thread类,这两种方法都有缺点:run方法没有返回值,不能抛出异常(这两个缺点归根到底就是runnable接口的缺陷,Thread类也是实现了runnable接口),如果需要知道一个线程的运行结果就需要用户自行设计,线程类本身并不能提供返回值和异常。但是Java1.5引入了一个新的接口callable,它类似于runnable接口,实现它也可以实现多线程任务。

好不好测一下:

package OSChina.Multithread;

import java.util.concurrent.*;

public class TaxCalculator implements Callable {
    //本金
    private int seedMoney;

    //接收主线程提供的参数
    public TaxCalculator(int _seedMoney){
        seedMoney = _seedMoney;
    }

    @Override
    public Integer call() throws Exception {
        // 复杂计算,运行一次需要2秒
        TimeUnit.MILLISECONDS.sleep(2000);
        return seedMoney/10;
    }
}

模拟一个复杂运算:税款计算器,该运算可能要花费10秒的时间,用户此时一直等啊等,很烦躁,需要给点提示,让用户知道程序在运行,没卡死。

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //生成一个单线程的异步执行器
        ExecutorService es = Executors.newSingleThreadExecutor();
        //线程执行后的期望值
        Future future = es.submit(new TaxCalculator(100));
        while (!future.isDone()){
            // 还没有运算完成,等待50毫秒
            TimeUnit.MILLISECONDS.sleep(50);
            System.out.print("*");
        }
        System.out.println("\n计算完成,税金是:"+future.get()+" 元");
        es.shutdown();
    }

Executors是一个静态工具类,提供了异步执行器的创建能力,如单线程异步执行器newSingleThreadExecutor、固定线程数量的执行器newFixedThreadPool等,一般它是异步计算的入口类。future关注的是线程执行后的结果,比如运行十分完毕,结果是多少等。

【编写高质量代码:改善Java程序的151个建议】第9章:多线程和并发___建议118~124_第7张图片

执行时,"*"会依次递增,表示系统正在运算,为用户提供了运算进度,此类异步计算的好处是:

1、尽可能多的占用系统资源,提高运算速度

2、可以监控线程的执行情况。比如执行是否完毕、是否有返回值、是否有异常等。

3、可以为用户提供更好的支持,比如例子中的运算进度等。

你可能感兴趣的:(#)