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

2023-10-27

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

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

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

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

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

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

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

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

建议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);
        }
    }
}

a8b2d30bbbb524c62f2012fc4d4f3f98696.jpg

创建了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()即可,运行结果如下:

e6eb3db43ed7e7fd8f83e6e32d31af86462.jpg

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

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

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

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

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

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

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

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

707063ccfd97dcf2cc8561eac28f27227c8.jpg

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

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

a1261af5e3bbbd34e26467c72e41dcd6faa.jpg

明白了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);
            }
        }
    }

此段代码的逻辑如下:

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

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

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

4、如果没有找到,继续循环,直到达到最大循环为止。

运行结果:

c9d4e78b1be3c61cf0b71c99fdb8bb65f54.jpg

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

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

书中的运行结果:

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

此时,count=999

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

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

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

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

f5ca226680d65b4d115cec818329b0df1c8.jpg

建议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关注的是线程执行后的结果,比如运行十分完毕,结果是多少等。

2184bdb2e0c9b7b109b1dd14b31d701990c.jpg

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

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

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

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

 

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

转载于:https://my.oschina.net/u/4006148/blog/3082264

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • 如何在服务器上跑python程序

    购买服务器 首先你需要一个服务器 阿里云云翼计划有一个9 9云服务器ECS服务 你怎么买我不管 反正你最后给我搞到一个云服务器 购买的配置界面 由于阿里云现在限量购买 所以这里只是截个图说明而已 主要说明一点公共镜像选择ubuntu14 0
  • 【软件测试】理论知识基础第一章

    前言 骗取自己的救赎 直到和染尘斑驳的玫瑰一起坠入深渊 软件测试 理论知识基础第一章 一 认识软件测试 1 什么是软件测试 二 常见的测试分类 1 阶段划分 2 代码可见度划分 3 扩展 总结 三 模型 1 质量模型 2 W模型 四 软件测
  • Webservice接口的生成及调用

    最近项目上要对接一个Webservice形式的接口 因为以前一直没有对接过这种类型的 所以这次专门查了一些资料学习下 一 Webservice的简单介绍 WebService是一种跨编程语言和跨操作系统平台的远程调用技术 它通过标准通信协议
  • AAA协议tacacs认证简单实验

    实验名称 AAA的tacacs验证 实验目的 在AAA认证服务器上认证客户端telnet登陆路由器 实验拓扑图 主要实验步骤 Router上的配置 Router gt en Router conf t Router config inter
  • 内存超频时序怎么调_超频技术之内存“时序”重要参数设置解说

    超频技术之内存 时序 重要参数设置解说 来源 华强电子网 作者 华仔 浏览 432 时间 2017 05 10 21 48 标签 摘要 相信大多数超频帖子里都会提到内存时序调整 也就是我们经常看到的5 5 5 15 1T 4 5 4 12
  • python爬虫requests源码链家_Python 爬虫 链家二手房(自行输入城市爬取)

    因同事想在沈阳买房 对比分析沈阳各区的房价 让我帮忙爬取一下链家网相关数据 然后打 算记下笔记 用于总结学到的东西 用到的东西 一 爬虫需要会什么 学习东西 首先你要知道它是干嘛的 爬虫 顾名思义就是爬取你所看到的网页内容 小说 新闻 信息
  • 当可变形注意力机制引入Vision Transformer

    GiantPandaCV导语 通过在Transformer基础上引入Deformable CNN中的可变性能力 在降低模型参数量的同时提升获取大感受野的能力 文内附代码解读 引言 Transformer由于其更大的感受野能够让其拥有更强的模
  • 你的键盘多久没测试过了?看看有没有失灵

    键盘是电脑外置设备中易损坏的一种 也是必不可少的电脑配件 特别是程序员每天都需要频繁地使用键盘 键盘常会出现故障 例如失灵或某些字母 数字无法正常输入 这样既浪费时间又令人烦恼 为此 我们在这里提供一个在线测试工具 方便您测试键盘按键是否正
  • 疫情期间科研记录(1)——异步电机矢量控制

    2020年二月至三月 受疫情影响 居家隔离 受毕业以及找工作压力 居家而未敢忘科研 两个月成果如下 在此期间 参考大量文献和往上的代码 基本完善了开题时硕士毕业论文的电机控制系统概念 实现了基于概念的仿真模型 记此文档以备忘 异步电机矢量控
  • 【ARIMA-WOA-LSTM】合差分自回归移动平均方法-鲸鱼优化-长短期记忆神经网络研究(Python代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 1 1 ARIMA模型 1 2 鲸鱼优化算法 1 3 LSTM 模型 2 运行结果 3 参考文献 4 Py
  • js数组对象取出指定元素相同的对象组成新数组

    要处理的数组 name 小明 age 18 address 天上 name 小红 age 19 address 地里 name 小明 age 20 address 叙利亚 name 小明 age 5 address 娘胎 想处理成 name
  • C++编译知识笔记(二)——Linux ELF文件解析

    目录 一 ELF格式概述 二 常见段及对应用途 三 目标文件内容解析 3 1 代码段 text 3 2 只读数据段 rodata 3 3 数据段 data 3 4 bss段 3 5 重定位表 Reloacation Table 相关段 re
  • Flink简单教学4-编程模型

    编程模型 此章编程模型是重点 理解Flink是如何工作的 虽然不涉及代码但非常有必要花时间阅读 2 4 节为重点 1 层次抽象 Levels of Abstraction 从底向上 抽象程都由低到高 以下说明了解以下即可 最低层次的抽象仅仅
  • 献给初学iOS的小盆友们------微博app项目开发之一项目初始化

    献给初学iOS的小盆友们 微博app项目开发之一 项目初始化 本人自学iOS也有七八个月了 不敢说学到很深入了 但也算入了门 此次微博app项目参考了传智播客培训教材 主要学习内容有架构思想 封装思想 代码重构 业务逻辑等内容 项目涵盖面广
  • 关于解决java环境配置好jdk但是在cmd中输入java等指令无反应的问题

    这是一个初学者经常犯的错误 在我们使用cmd窗口编译运行java文件时 有时候可以运行指令 但是环境变量就是一个很玄学的东西 可能你上午还在用cmd但是下午就不能用了 我这里有一种最简单的最容易理解和排除的方法 适用于你非常确定电脑上已经安
  • 十分钟弄懂最快的APP自动化工具uiautomator2(入门到精通)

    目录 导读 前言 一 介绍 二 环境部署 三 编写百度贴吧首页脚本 四 uiautomator2和appium运行速度比较 前言 相信很多使用appium做过APP自动化的人都深有感触 appium运行慢 时间长 uiautomatorvi
  • 批量将csv转换成shp

    转载 https blog csdn net u012131430 article details 90105857 根据自己的需求 对代码进行适当修改 并可以实现 输入数据 一个文件夹下所有csv数据 输出数据 一个文件夹下shp文件 具
  • Python 进阶(三):邮件的发送与收取

    1 发送邮件 SMTP 全称 Simple Mail Transfer Protocol 中文译为简单邮件传输协议 它能跨越网络传输邮件 可实现相同网络处理进程之间的邮件传输 也可通过中继器或网关实现进程与其他网络之间的邮件传输 Pytho
  • 查看svn账号密码

    参考他人链接 https blog csdn net Amnesiac666 article details 121355958 1 找到svn存放目录 窝的本地 C Users lenovo AppData Roaming Subvers
  • 编写高质量代码:改善Java程序的151个建议(第9章:多线程和并发___建议118~124)

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