编写高质量代码: 改善Java程序的151个建议 | 第九章 多线程和并发

编写高质量代码: 改善Java程序的151个建议

前言

前言 圈主 [Rocky编程日记] 学习 编写高质量代码: 改善Java程序的151个建议 笔记记录。希望我写得笔记你能够喜欢, 希望我写的笔记能够给你提供帮助。同时若笔记中存在不对的地方,那一定是圈主当时的理解还不够, 希望你能够及时指出嗷~

第九章 多线程和并发

在这里插入图片描述

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

  • 继承自Thread类的多线程类不必覆写start方法。
  • 原本的start方法中,调用了本地方法start0,它实现了启动线程、申请栈内存、运行run方法、修改线程状态等职责,线程管理和栈内存管理都是由JVM实现的,如果覆盖了start方法,也就是撤销了线程管理和栈内存管理的能力。
  • 所以除非必要,不然不要覆写start方法,即使需要覆写start方法,也需要在方法体内加上super.start调用父类中的start方法来启动默认的线程操作

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

  • 现象:使用stop方法停止一个线程,而stop方法在此处的目的不是停止一个线程,而是设置线程为不可启用状态。

  • 但是运行结果出现奇怪现象:部分线程还是启动了,也就是在某些线程(没有规律)中的start方法正常执行了。在不符合判断规则的情况下,不可启用状态的线程还是启用了,这是线程启动(start方法)一个缺陷。

  • Thread类的stop方法会根据线程状态来判断是终结线程还是设置线程为不可运行状态,对于未启动的线程(线程状态为NEW)来说,会设置其标志位为不可启动,而其他的状态则是直接停止

  • start方法源码中,start0方法在stop0方法之前,也就是说即使stopBeforeStart为true(不可启动),也会先启动一个线程,然后再stop0结束这个线程,而罪魁祸首就在这里!所以不要使用stop方法进行状态的设置。

    public class Suggest119 {
        public static void main(String[] args) {
    
            while (true) {
                // 多线程多个垃圾邮件制造机
                SpanMachine sm = new SpanMachine();
                // 条件判断, 不符合条件就设置该线程不可执行
                if (!false) {
                    sm.stop();
                }
                // 如果线程是 stop 状态 ,则不会启动
                sm.start();
            }
        }
    }
    
    // 垃圾邮件制造机
    class SpanMachine extends Thread {
    
        @Override
        public void run() {
            // 制造垃圾邮件
            System.out.println("制造大量垃圾邮件.......");
        }
    }
    

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

  • 线程启动完毕后,需要停止,Java只提供了一个stop方法,但是不建议使用,有以下三个问题:

    • stop方法是过时的;

    • stop方法会导致代码逻辑不完整,stop方法是一种“恶意”的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,这是非常危险的,以为stop方法会清除栈内信息,结束该线程,但是可能该线程的一段逻辑非常重,比如子线程的主逻辑、资源回收、情景初始化等,因为stop线程了,这些都不会再执行。子线程执行到何处会被关闭很难定位,这为以后的维护带来了很多麻烦;

      public class Suggest120 {
          public static void main(String[] args) throws InterruptedException {
      
              // 子线程
              Thread thread = new Thread() {
                  @Override
                  public void run() {
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          // 异常处理
                      }
                      System.out.println("此处代码不会执行");
                  }
              };
              thread.start();
              Thread.sleep(100);
              thread.stop();
          }
      }
      
    • stop方法会破坏原子逻辑,多线程为了解决共享资源抢占的问题,使用了锁概念,避免资源不同步,但是stop方法会丢弃所有的锁,导致原子逻辑受损。

      class SafeStopThread extends Thread {
          
          private volatile boolean stop = false;
      
          @Override
          public void run() {
              while (stop) {
                  
              }
          }
          public void terminate() {
              stop = true;
          }
      }
      

      Thread提供的interrupt中断线程方法,它不能终止一个正在执行着的线程,它只是修改中断标志唯一。总之,期望终止一个正在运行的线程,不能使用stop方法,需要自行编码实现。如果使用线程池(比如ThreadPoolExecutor类),那么可以通过shutdown方法逐步关闭池中的线程。

      public class Suggest120 {
          public static void main(String[] args) throws InterruptedException {
      
              // demo01();
              // demo02();
              Thread t1 = new Thread() {
                  public void run() {
                      while (true) {
                          System.out.println("Running......");
                      }
                  }
              };
              t1.start();
              t1.interrupt();
          }
      }
      

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

  • 线程优先级推荐使用MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY三个级别,不建议使用其他7个数字)(线程的优先级(Priority)决定了线程获得CPU运行的机会,优先级越高,运行机会越大。

    事实:

    • 并不是严格尊重线程优先级别来执行的,分为10个级别;
    • 优先级差别越大,运行机会差别越大;对于Java来说,JVM调用操作系统的接口设置优先级,比如Windows是通过调用SetThreadPriority函数来设置的。不同操作系统线程优先级设置是不相同的,Windows有7个优先级,Linux有140个优先级,Freebsd有255个优先级。Java缔造者也发现了该问题,于是在Thread类中设置了三个优先级,建议使用优先级常量,而不是1到10随机的数字

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

  • 可以使用线程异常处理器来处理相关异常情况的发生,比如当机自动重启,大大提高系统的可靠性。在实际环境中应用注意以下三点:
    • 共享资源锁定;
    • 脏数据引起系统逻辑混乱;
    • 内存溢出,线程异常了,但由该线程创建的对象并不会马上回收,如果再重新启动新线程,再创建一批新对象,特别是加入了场景接管,就危险了,有可能发生OutOfMemory内存泄露问题

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

  • volatile不能保证数据是同步的,只能保证线程能够获得最新值)(volatile关键字比较少用的原因:
    • Java1.5之前该关键字在不同的操作系统上有不同的表现,移植性差;
    • 比较难设计,而且误用较多。在变量钱加上一个volatile关键字,可以确保每个线程对本地变量的访问和修改都是直接与主内存交互的,而不是与本地线程的工作内存交互的,保证每个线程都能获得最“新鲜”的变量值。但是volatile关键字并不能保证线程安全,它只能保证当前线程需要该变量的值时能够获得最新的值,而不能保证多个线程修改的安全性

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

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

建议125:优先选择线程池

  • Java1.5以前,实现多线程比较麻烦,需要自己启动线程,并关注同步资源,防止出现线程死锁等问题,Java1.5以后引入了并行计算框架,大大简化了多线程开发。
  • 线程有五个状态:新建状态(New)、可运行状态(Runnable,也叫作运行状态)、阻塞状态(Blocked)、等待状态(Waiting)、结束状态(Terminated),线程的状态只能由新建转变为运行态后才可能被阻塞或等待,最后终结,不可能产生本末倒置的情况,比如想把结束状态变为新建状态,则会出现异常。
  • 线程运行时间分为三个部分:T1为线程启动时间;T2为线程体运行时间;T3为线程销毁时间。每次创建线程都会经过这三个时间会大大增加系统的响应时间。T2是无法避免的,只能通过优化代码来降低运行时间。T1和T3都可以通过线程池(Thread Pool)来缩短时间。
  • 线程池的实现涉及一下三个名词
    1、工作线程(Worker),线程池中的线程只有两个状态:可运行状态和等待状态;
    2、任务接口(Task),每个任务必须实现的接口,以供工作线程调度器调度,它主要规定了任务的入口、任务执行完的场景处理、任务的执行状态等。这里的两种类型的任务:具有返回值(或异常)的Callable接口任务和无返回值并兼容旧版本的Runnable接口任务;
    3、任务队列(Work Queue),也叫作工作队列,用于存放等待处理的任务,一般是BlockingQueue的实现类,用来实现任务的排队处理。线程池的创建过程:创建一个阻塞队列以容纳任务,在第一次执行任务时闯将足够多的线程(不超过许可线程数),并处理任务,之后每个工作线程自行从任务队列中获得任务,直到任务队列中任务数量为0为止,此时,线程将处于等待状态,一旦有任务加入到队列中,即唤醒工作线程进行处理,实现线程的可复用性

建议126:适时选择不同的线程池来实现

  • Java的线程池实现从根本上来说只有两个:ThreadPoolExecutor类和ScheduledThreadPoolExecutor类,还是父子关系。为了简化并行计算,Java还提供了一个Executors的静态类,它可以直接生成多种不同的线程池执行器,比如单线程执行器、带缓冲功能的执行器等,归根结底还是以上两个类的封装类

建议127:Lock与synchronized是不一样的

  • Lock类(显式锁)和 synchronized关键字(内部锁) 用在代码块的并发性和内存上时的语义是一样的,都是保持代码块同时只有一个线程具有执行权。显式锁的锁定和释放必须在一个try…finally块中,这是为了确保即使出现运行期异常也能正常释放锁,保证其他线程能够顺利执行。

  • Lock锁为什么不出现互斥情况,所有线程都是同时执行的?

  • 原因:这是因为对于同步资源来说,显式锁是对象级别的锁,而内部锁是类级别的锁,也就是说Lock锁是跟随对象的synchronized锁是跟随类的,更简单地说把Lock定义为多线程类的私有属性是起不到资源互斥作用的,除非是把Lock定义为所有线程共享变量。除了以上不同点之外,

  • 还有以下**4点不同:

    1、Lock支持更细粒度的锁控制,假设读写锁分离,写操作时不允许有读写操作存在,而读操作时读

    2、Lock是无阻塞锁,synchronized是阻塞锁,线程A持有锁,线程B也期望获得锁时,如果为Lock,则B线程为等待状态,如果为synchronized,则为阻塞状态;

    3、Lock可实现公平锁,synchronized只能是非公平锁什么叫做非公平锁?当一个线程A持有锁,而线程B、C处于阻塞(或等待)状态时,若线程A释放锁。JVM将从线程B、C中随机选择一个线程持有锁并使其获得执行权,这叫做非公平锁(因为它抛弃了先来后到的顺序);若JVM选择了等待时间最长的一个线程持有锁,则为公平锁。需要注意的是,即使是公平锁,JVM也无法准确做到“公平”,在程序中不能以此作为精确计算。显式锁默认是非公平锁,但可以在构造函数中加入参数true来声明出公平锁;
    4、Lock是代码级的,synchronized是JVM级的,Lock是通过编码实现的,synchronized是在运行期由JVM解释的,相对来说synchronized的优化可能性更高,毕竟是在最核心不为支持的,Lock的优化需要用户自行考虑。相对来说,显式锁使用起来更加便利和强大,在实际开发中选择哪种类型的锁就需要根据实际情况考虑了:灵活、强大则选择Lock,快捷、安全则选择synchronized

建议128:预防线程死锁

  • 线程死锁(DeadLock)是多线程编码中最头疼问题,也是最难重现的问题,因为Java是单进程多线程语言。要达到线程死锁需要四个条件:1、互斥条件;2、资源独占条件;3、不剥夺条件;4、循环等待条件;按照以下两种方式来解决:1、避免或减少资源贡献;2、使用自旋锁,如果在获取自旋锁时锁已经有保持者,那么获取锁操作将“自旋”在那里,直到该自旋锁的保持者释放了锁为止

建议129:适当设置阻塞队列长度

  • 阻塞队列BlockingQueue扩展了Queue、Collection接口,对元素的插入和提取使用了“阻塞”处理。但是BlockingQueue不能够自行扩容,如果队列已满则会报IllegalStateException:Queue full队列已满异常;这是阻塞队列非阻塞队列一个重要区别:阻塞队列的容量是固定的,非阻塞队列则是变长的。阻塞队列可以在声明时指定队列的容量,若指定的容量,则元素的数量不可超过该容量,若不指定,队列的容量为Integer的最大值。有此区别的原因是:阻塞队列是为了容纳(或排序)多线程任务而存在的,其服务的对象是多线程应用,而非阻塞队列容纳的则是普通的数据元素。阻塞队列的这种机制对异步计算是非常有帮助的,如果阻塞队列已满,再加入任务则会拒绝加入,而且返回异常,由系统自行处理,避免了异步计算的不可知性。可以使用put方法,它会等队列空出元素,再让自己加入进去,无论等待多长时间都要把该元素插入到队列中,但是此种等待是一个循环,会不停地消耗系统资源,当等待加入的元素数量较多时势必会对系统性能产生影响。offer方法可以优化一下put方法

建议130:使用CountDownLatch协调子线程

  • CountDownLatch协调子线程步骤
    • 一个开始计数器,多个结束计数器:
    • 每一个子线程开始运行,执行代码到begin.await后线程阻塞,等待begin的计数变为0;
    • 主线程调用begin的countDown方法,使begin的计数器为0;
    • 每个线程继续运行;
    • 主线程继续运行下一条语句,end的计数器不为0,主线程等待;
    • 每个线程运行结束时把end的计数器减1,标志着本线程运行完毕;
    • 多个线程全部结束,end计数器为0;
    • 主线程继续执行,打印出结果。类似:领导安排了一个大任务给我,我一个人不可能完成,于是我把该任务分解给10个人做,在10个人全部完成后,我把这10个结果组合起来返回给领导–这就是CountDownLatch的作用

建议131:CyclicBarrier让多线程齐步走

  • CyclicBarrier关卡可以让所有线程全部处于等待状态(阻塞),然后在满足条件的情况下继续执行,这就好比是一条起跑线,不管是如何到达起跑线的,只要到达这条起跑线就必须等待其他人员,待人员到齐后再各奔东西,CyclicBarrier关注的是汇合点的信息,而不在乎之前或者之后做何处理。CyclicBarrier可以用在系统的性能测试中,测试并发性
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值