zoukankan      html  css  js  c++  java
  • 编写高质量代码:改善Java程序的151个建议 --[117~128]

    编写高质量代码:改善Java程序的151个建议 --[117~128]

    Thread 不推荐覆写start方法

    先看下Thread源码:

    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 besss
                     * passed up the call stack
                     */
                }
            }
        }
       // 本地方法
         private native void start0();
    

    启动线程前stop方法是不可靠的

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

    看下线程stop源码:

    @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;
            }
        }
    

    start源码:

    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结束这个线程。

    所以在start钱stop不一定能保证线程被stop掉。

    不使用stop方法停止线程
    • stop方法是过时的:从Java编码规则来说,已经过时的方法不建议采用。
    • stop方法会导致代码逻辑不完整:stop方法是一种" 恶意 " 的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,这是非常危险的。
    • 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;
        }
    }
    

    或者:

    class SafeStopThread extends Thread {
        @Override
        public void run() {
            //判断线程体是否运行
            while (!isInterrupted()) {
                // do SomeThing
            }
        }
    }
    
    线程优先级只使用三个等级
    • 并不是严格按照线程优先级来执行的
    • 优先级差别越大,运行机会差别越明显

    Java的优先级只是代表抢占CPU的机会大小,优先级越高,抢占CPU的机会越大,被优先执行的可能性越高,优先级相差不大,则抢占CPU的机会差别也不大,这就是导致了优先级为9的线程可能比优先级为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;
    
    
    }
    

    不能把这个优先级做为核心业务的必然条件,Java无法保证优先级高肯定会先执行,只能保证高优先级有更多的执行机会。

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

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

    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();
            }
    
        }
    }
    
    • 共享资源锁定:如果线程产生异常的原因是资源被锁定,自动重启应用知会增加系统的负担,无法提供不间断服务。例如一个即时通信服务(XMPP Server)出现信息不能写入的情况,即使再怎么启动服务,也是无法解决问题的。在此情况下最好的办法是停止所有的线程,释放资源。
    • 脏数据引起系统逻辑混乱:异常的产生中断了正在执行的业务逻辑,特别是如果正在处理一个原子操作(像即时通讯服务器的用户验证和签到这两个事件应该在一个操作中处理,不允许出现验证成功,但签到不成功的情况),但如果此时抛出了运行期异常就有可能会破坏正常的业务逻辑,例如出现用户认证通过了,但签到不成功的情况,在这种情境下重启应用服务器,虽然可以提供服务,但对部分用户却产生了逻辑异常。
    • 内存溢出:线程异常了,但由该线程创建的对象并不会马上回收,如果再重亲启动新线程,再创建一批对象,特别是加入了场景接管,就非常危险了,例如即时通信服务,重新启动一个新线程必须保证原在线用户的透明性,即用户不会察觉服务重启,在此种情况下,就需要在线程初始化时加载大量对象以保证用户的状态信息,但是如果线程反复重启,很可能会引起OutOfMemory内存泄露问题。
    volatile不能保证数据同步

    volatile关键字并不能保证线程安全,它只能保证当前线程需要该变量的值时能够获得最新的值,而不能保证线程修改的安全性。

    异步运算考虑使用Callable接口

    Java1.5开始引入了一个新的接口Callable,它类似于Runnable接口,实现它就可以实现多线程任务

    public interface Callable<V> {
        /**
         * Computes a result, or throws an exception if unable to do so.
         *
         * @return computed result
         * @throws Exception if unable to compute a result
         */
        V call() throws Exception;
    }
    

    看下基于Callable的实现:

    //税款计算器
    class TaxCalculator implements Callable<Integer> {
        // 本金
        private int seedMoney;
    
        // 接收主线程提供的参数
        public TaxCalculator(int _seedMoney) {
            seedMoney = _seedMoney;
        }
    
        @Override
        public Integer call() throws Exception {
            // 复杂计算,运行一次需要2秒
            TimeUnit.MILLISECONDS.sleep(2000);
            return seedMoney / 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()) {
                // 还没有运算完成,等待200毫秒
                TimeUnit.MICROSECONDS.sleep(200);
                // 输出进度符号
                System.out.print("*");
            }
            System.out.println("
    计算完成,税金是:" + future.get() + "  元 ");
            es.shutdown();
        }
    

    Callable的优点:
    尽可能多的占用系统资源,提供快速运算
    可以监控线程的执行情况,比如是否执行完毕、是否有返回值、是否有异常等。
    可以为用户提供更好的支持,比如例子中的运算进度等。

    优先选择线程池

    在容器(或系统)启动时,创建足够多的线程,当容器(或系统)需要时直接从线程池中获得线程,运算出结果,再把线程返回到线程池中。
    创建一个阻塞队列以容纳任务,在第一次执行任务时创建做够多的线程(不超过许可线程数),并处理任务,之后每个工作线程自行从任务对列中获得任务,直到任务队列中的任务数量为0为止,此时,线程将处于等待状态,一旦有任务再加入到队列中,即召唤醒工作线程进行处理,实现线程的可复用性。

    使用线程池减少的是线程的创建和销毁时间,这对于多线程应用来说非常有帮助。

    适时选择不同的线程池来实现

    ThreadPoolExecutor 构造函数参数:

    • corePoolSize:最小线程数。线程启动后,在池中保持线程的最小数量。需要说明的是线程数量是逐步到达corePoolSize值的,例如corePoolSize被设置为10,而任务数量为5,则线程池中最多会启动5个线程,而不是一次性的启动10个线程。
    • maximumPoolSize:最大线程数量。这是池中最大能容纳的最大线程数量,如果超出,则使用RejectedExecutionHandler 拒绝策略处理。
    • keepAliveTime:线程最大生命周期。这里的生命周期有两个约束条件,一是该参数针对的是超过corePoolSize数量的线程。二是处于非运行状态的线程。这么说吧,如果corePoolSize为10, maximumPoolSize为20,此时线程池中有15个线程正在运行,一段时间后,其中有3个线程处于等待状态的时间超过了keepAliveTime指定的时间,则结束这3个线程,此时线程池中还有12个线程正在运行。
    • unit:时间单位。这是keepAliveTime的时间单位,可以是纳秒、毫秒、秒、分等选项。
    • workQuene:任务队列。当线程池中的线程都处于运行状态,而此时任务数量继续增加,则需要一个容器来容纳这些任务,这就是任务队列。
    • threadFactory:线程工厂。定义如何启动一个线程,可以设置线程名称,并且可以确认是否是后台线程等。
    • handler:拒绝任务处理器。由于超出线程数量和队列容量而对继续增加的任务进行处理的程序。
      线程池管理过程:首先创建线程池,然后根据任务的数量逐步将线程增大到corePoolSize数量,如果此时仍有任务增加,则放置到workQuene中,直到workQuene爆满为止,然后继续增加池中的数量(增强处理能力),最终达到maximumPoolSize,那如果此时还有任务增加进来呢?这就需要handler处理了,或者丢弃任务,或者拒绝新任务,或者挤占已有任务等。

    Executors提供的几个线程创建线程池的便捷方法:

    • newSingleThreadExecutor:单线程池。顾名思义就是一个池中只有一个线程在运行,该线程永不超时,而且由于是一个线程,当有多个任务需要处理时,会将它们放置到一个无界阻塞队列中逐个处理;
    • newCachedThreadPool:缓冲功能的线程。建立了一个线程池,而且线程数量是没有限制的(当然,不能超过Integer的最大值),新增一个任务即有一个线程处理,或者复用之前空闲的线程,或者重亲启动一个线程,但是一旦一个线程在60秒内一直处于等待状态时(也就是一分钟无事可做),则会被终止;
    • newFixedThreadPool:固定线程数量的线程池。 在初始化时已经决定了线程的最大数量,若任务添加的能力超出了线程的处理能力,则建立阻塞队列容纳多余的任务;
    Lock与synchronized是不一样的

    对于同步资源来说(示例中的代码块)显示锁是对象级别的锁,而内部锁是类级别的锁,也就说说Lock锁是跟随对象的,synchronized锁是跟随类的,更简单的说把Lock定义为多线程类的私有属性是起不到资源互斥作用的,除非是把Lock定义为所有线程的共享变量。

    • Lock支持更细精度的锁控制:假设读写锁分离,写操作时不允许有读写操作存在,而读操作时读写可以并发执行,这一点内部锁就很难实现。
    • Lock锁是无阻塞锁,synchronized是阻塞锁
    • Lock可实现公平锁,synchronized只能是非公平锁
      什么叫非公平锁呢?当一个线程A持有锁,而线程B、C处于阻塞(或等待)状态时,若线程A释放锁,JVM将从线程B、C中随机选择一个持有锁并使其获得执行权,这叫非公平锁(因为它抛弃了先来后到的顺序);若JVM选择了等待时间最长的一个线程持有锁,则为公平锁(保证每个线程的等待时间均衡)。
    • Lock是代码级的,synchronized是JVM级的
    预防线程死锁

    两个线程A、B中使用了该资源,由于两个资源之间交互操作,并且都是同步方法,因此在线程A休眠一秒钟后,它会试图访问资源B的b2方法。但是B线程持有该类的锁,并同时在等待A线程释放其锁资源,所以此时就出现了两个线程在互相等待释放资源的情况,也就是死锁。

    互斥条件:一个资源每次只能被一个线程使用
    资源独占条件:一个线程因请求资源在未使用完之前,不能强行剥夺
    不剥夺条件:线程已经获得的资源在未使用完之前,不能强行剥夺
    循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

    class A {
        public synchronized void a1(B b) {
            String name = Thread.currentThread().getName();
            System.out.println(name + "  进入A.a1()");
            try {
                // 休眠一秒 仍持有锁
                Thread.sleep(1000);
            } catch (Exception e) {
                // 异常处理
            }
            System.out.println(name + "  试图访问B.b2()");
            b.b2();
        }
    
        public synchronized void a2() {
            System.out.println("进入a.a2()");
        }
    }
    
    class B {
        public synchronized void b1(A a) {
            String name = Thread.currentThread().getName();
            System.out.println(name + "  进入B.b1()");
            try {
                // 休眠一秒 仍持有锁
                Thread.sleep(1000);
            } catch (Exception e) {
                // 异常处理
            }
            System.out.println(name + "  试图访问A.a2()");
            a.a2();
        }
    
        public synchronized void b2() {
            System.out.println("进入B.b2()");
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
            final A a = new A();
            final B b = new B();
            // 线程A
            new Thread(new Runnable() {
                @Override
                public void run() {
                    a.a1(b);
                }
            }, "线程A").start();
            // 线程B
            new Thread(new Runnable() {
                @Override
                public void run() {
                    b.b1(a);
                }
            }, "线程B").start();
        }
    

    解决死锁方案:
    (1)、避免或减少资源共享

    (2)、使用自旋锁

    public void b2() {
            try {
                // 立刻获得锁,或者2秒等待锁资源
                if (lock.tryLock(2, TimeUnit.SECONDS)) {
                    System.out.println("进入B.b2()");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
  • 相关阅读:
    计算机网络知识 第一部分
    LAMP环境安装
    Axure RP 交互设计
    Axure RP 界面功能
    Axure RP 界面功能介绍
    Axure RP 第一部分
    Grub管理修改root口令
    MYSQL 部分练习题
    工作日志示例
    计算机网络的分类
  • 原文地址:https://www.cnblogs.com/androidsuperman/p/9475547.html
Copyright © 2011-2022 走看看