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

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

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

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

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

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

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

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

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

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

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

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

一、stop方法是过期的:从Java编码规则来讲,已通过时的方法不建议采用,弃了。

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

三、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);
        }
    }
}

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

一、并非严格按照线程优先级来执行的

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

二、优先级差异越大,运行机会差异越明显

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作什么呢?两件事:

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

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

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

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

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

一、共享资源锁定:若是线程产生异常的缘由是资源被锁定,自动重启应用会增长系统的负担,没法提供不间断服务。例如一个即时通讯服务出现信息不能写入的状况,即时再怎么重启服务,也没法解决问题。在此状况下最好的办法是中止全部的线程,释放资源。

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

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

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

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

咱们知道,每一个线程都运行在栈内存中,每一个线程都有本身的工做内存(Working Memory,好比寄存器Register、高速缓存存储器Cache等),线程的计算通常是经过工做内存进行交互的,其示意图以下图所示:

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

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

明白了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++<value){
            // 共享资源清零
            UnsafeThread ut = new UnsafeThread();
            for (int i = 0; i < value; i++) {
                new Thread(ut).start();
            }
            // 先等15毫秒,等待活动线程为1
            do {
                try {
                    Thread.sleep(15);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }while (tg.activeCount()!=1);
            //检查实际值与理论值是否一致
            if(ut.getCount()!=value){
                //出现线程不安全的状况
                System.out.println("循环到:"+loops+" 遍,出现线程不安全的状况");
                System.out.println("此时,count= "+ut.getCount());
                System.exit(0);
            }
        }
    }

此段代码的逻辑以下:

一、启动1000个线程,修改共享资源count的值

二、暂停15毫秒,观察活动线程数是否为1(即只剩下主线程再运行),若不为1,则再等待15毫秒。

三、共享资源是不是不安全的,即实际值与理想值是否相同,若不相同,则发现目标,此时count的值为脏数据。

四、若是没有找到,继续循环,直到达到最大循环为止。

运行结果:

执行完了,没出现不安全的状况,证实volatile性能仍是能够的。

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

书中的运行结果:

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

此时,count=999

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

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

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

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

建议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<Integer> 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个建议@目录

相关文章
相关标签/搜索