编写高质量代码 - 多线程和并发(2)

news2024/12/23 10:28:18

文章目录

  • 1. 使用线程异常处理器提升系统可靠性
  • 2. volatile不能保证数据同步
  • 3. 异步运算考虑使用Callable接口

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

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

Java1.5版本以后在Thread类中增加了setUncaughtExceptionHandler方法,实现了线程异常的捕捉和处理。可能大家会有一个疑问:如果Socket应用出现了不可预测的异常是否可以自动重启呢?其实使用线程异常处理器很容易解决,我们来看一个异常处理器应用实例,代码如下:

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

    }
}

这段代码的逻辑比较简单,在TcpServer类创建时即启动一个线程,提供TCP服务,例如接收和发送文件,具体逻辑在run方法中实现。同时,设置了该线程出现运行期异常(也就是Uncaught Exception)时,由TcpServerExceptionHandler异常处理器来处理异常。那么TcpServerExceptionHandler做什么事呢?两件事:
(1)记录异常信息,以便查找问题。
(2)重新启动一个新线程,提供不间断的服务。

有了这两点,TcpServer就可以稳定的运行了,即使出现异常也能自动重启,客户端代码比较简单,只需要new TcpServer()即可,运行结果如下:
在这里插入图片描述
从运行结果上可以看出,当Thread-0出现异常时,系统自动重启了Thread-1线程,继续提供服务,大大提高了系统的性能。

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

2. volatile不能保证数据同步

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

我们知道,每个线程都运行在栈内存中,每个线程都有自己的工作内存(Working Memory,比如寄存器Register、高速缓冲存储器Cache等),线程的计算一般是通过工作内存进行交互的,线程读取变量的示意图如下图所示:
在这里插入图片描述
从示意图上我们可以看到,线程在初始化时从主内存中加载所需的变量值到工作内存中,然后在线程运行时,如果是读取,则直接从工作内存中读取,若是写入则先写到工作内存中,之后刷新到主内存中,这是JVM的一个简单的内存模型,但是这样的结构在多线程的情况下有可能会出现问题,比如:A线程修改变量的值,也刷新到了主内存,但B、C线程在此时间内读取的还是本线程的工作内存,也就是说它们读取的不是最"新鲜"的值,此时就出现了不同线程持有的公共资源不同步的情况。

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

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

class UnsafeThread implements Runnable {
    // 共享资源
    private volatile int count = 0;

    @Override
    public void run() {
        // 增加CPU的繁忙程度,不必关心其逻辑含义
        // 增加消耗CPU计算是必须的,其目的是加重线程的负荷,以便出现单个线程抢占整个CPU资源的情景,否则很难模拟出volatile线程不安全的情况。
        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) throws InterruptedException {
    // 理想值,并作为最大循环次数
     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 {
             Thread.sleep(15);
         } while (tg.activeCount() != 1);
         // 检查实际值与理论值是否一致
         if (ut.getCount() != value) {
             // 出现线程不安全的情况
             System.out.println("循环到:" + loops + " 遍,出现线程不安全的情况");
             System.out.println("此时,count= " + ut.getCount());
             System.exit(0);
         }
     }

 }

想让volatite变量"出点丑",还是需要花点功夫的。此段程序的运行逻辑如下:

  • 启动1000个线程,修改共享资源count的值
  • 暂停15毫秒,观察活动线程数是否为1(即只剩下主线程在运行),若不为1,则再等待15毫秒。
  • 判断共享资源是否是不安全的,即实际值与理想值是否相同,若不相同,则发现目标,此时count的值为脏数据。
  • 如果没有找到,继续循环,直到达到最大循环为止。

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

在解释原因之前,我们先说一下自加操作。count++表示的是先取出count的值然后再加1,也就是count=count+1,所以,在某个紧邻时间片段内会发生如下神奇的事情:

(1)第一个时间片段
  A线程获得执行机会,因为有关键字volatile修饰,所以它从主内存中获得count的最新值为998,接下来的事情又分为两种类型:

  • 如果是单CPU,此时调度器暂停A线程执行,让出执行机会给B线程,于是B线程也获得了count的最新值998.
  • 如果是多CPU,此时线程A继续执行,而线程B也同时获得了count的最新值998.

(2)第二个时间片段

  • 如果是单CPU,B线程执行完+1操作(这是一个原子处理),count的值为999,由于是volatile类型的变量,所以直接写入主内存,然后A线程继续执行,计算的结果也是999,重新写入主内存中。
  • 如果是多CPU,A线程执行完加1动作后修改主内存的变量count为999,线程B执行完毕后也修改主内存中的变量为999

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

3. 异步运算考虑使用Callable接口

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

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 {
        // 复杂计算,运行一次需要10秒
        TimeUnit.MILLISECONDS.sleep(10000);
        return seedMoney / 10;
    }
}

这里模拟了一个复杂运算:税款计算器,该运算可能要花费10秒钟的时间,此时不能让用户一直等着吧,需要给用户输出点什么,让用户知道系统还在运行,这也是系统友好性的体现:用户输入即有输出,若耗时较长,则显示运算进度。如果我们直接计算,就只有一个main线程,是不可能有友好提示的,如果税金不计算完毕,也不会执行后续动作,所以此时最好的办法就是重启一个线程来运算,让main线程做进度提示,代码如下:

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("\n计算完成,税金是:" + future.get() + "  元 ");
     es.shutdown();
 }

在这段代码中,Executors是一个静态工具类,提供了异步执行器的创建能力,如单线程异步执行器newSingleThreadExecutor、固定线程数量的执行器newFixedThreadPool等,一般它是异步计算的入口类。future关注的是线程执行后的结果,比如有没有运行完毕,执行结果是多少等。此段代码的运行结果如下所示:
**********************…
计算完成,税金是:10 元

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/77916.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分布式计算MapReduce究竟是怎么一回事?

前言 如果要对文件中的内容进行统计&#xff0c;大家觉得怎么做呢&#xff1f;一般的思路都是将不同地方的文件数据读取到内存中&#xff0c;最后集中进行统计。如果数据量少还好&#xff0c;但是面对海量数据、大数据的场景这样真的合适吗&#xff1f;不合适的话&#xff0c;…

1560_AURIX_TC275_NMI Trap以及PMC

全部学习汇总&#xff1a; GreyZhang/g_TC275: happy hacking for TC275! (github.com) 1. 连同上一次的笔记中最后一页&#xff0c;看得出来NMI的trap软件触发至少是有三种方法。 2. 后半页给出了trap发生的状态的清除操作寄存器。 前面的文字描述部分&#xff0c;几个trap是…

基于jsp+mysql+ssm公共交通失信人员管理系统-计算机毕业设计

项目介绍 本南昌公共交通失信人员管理系统主要包括系统用户管理模块、用户信息管理模块、处罚类型管理、失信人员管理、登录模块、和退出模块等多个模块, 本系统基于SSM(SpringSpringMVCMyBatis)框架,适用于毕业设计&#xff0c;采用javaweb,基于B/S模式,Myeclipse或者eclipse…

手把手教你---猿如意之八大高效利器使用

开篇之前我们可能要来了解一下&#xff0c;《猿如意》是CSDN为提高开发者工作效率&#xff0c;发布客户端和低代码平台产品——《猿如意》&#xff1b;记得第一次在接触猿如意实在今年八月份&#xff0c;之前使用过其他的工具&#xff0c;但是有利有弊&#xff0c;先说下为啥选…

趋势分析 | 零信任实践之关键技术解读

SmartX 趋势分享 SmartX 趋势分享由 SmartX 团队内部分享的权威机构市场报告、全球重要媒体文章精选整理而成。内容涉及现代数据中心相关产业趋势以及金融、医疗、制造等行业全球用户需求与实践前沿洞察。本期&#xff0c;我们分享一篇 Gartner 关于零信任实践策略的文章[1]&am…

[附源码]JAVA毕业设计校园快递联盟系统(系统+LW)

[附源码]JAVA毕业设计校园快递联盟系统&#xff08;系统LW&#xff09; 项目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术…

【大数据入门核心技术-Azkaban】(二)Azkaban核心架构

目录 一、核心架构 1、Relational Database(Mysql) 2、Azkaban Web Server 3、Azkaban Executor Server 二、三种运行模式 1、solo server mode 2、two server mode 3、multiple executor mode 一、核心架构 Azkaban架构由三部分构成&#xff1a; 1、Relational Databa…

【chatGPT免注册】openAI机器人直接访问,不需要注册的方法

最近&#xff0c;chat GPT可谓是火出圈了。但是这个服务在国内不可用&#xff0c;网上你能找到的教程无非是注册一个虚拟的手机号去接受短信&#xff0c;可就算你能注册成功&#xff0c;还是无法访问。 还有人说可以去某宝买一个账号&#xff0c;这是可以的&#xff0c;账号的确…

安卓玩机搞机技巧综合资源-----干掉手机广告 禁用 冻结 关闭内置软件【八】

接上篇 安卓玩机搞机技巧综合资源------如何提取手机分区 小米机型代码分享等等 【一】 安卓玩机搞机技巧综合资源------开机英文提示解决dm-verity corruption your device is corrupt. 设备内部报错 AB分区等等【二】 安卓玩机搞机技巧综合资源------EROFS分区格式 小米红米…

Java 并发编程(三)之synchronized

带着BAT大厂的面试问题去理解Synchronized 请带着这些问题继续后文&#xff0c;会很大程度上帮助你更好的理解synchronized。 Synchronized可以作用在哪里? 分别通过对象锁和类锁进行举例。Synchronized本质上是通过什么保证线程安全的? 分三个方面回答&#xff1a;加锁和释放…

微信群营销方式微信群建群营销案例

今天我们以小区微信群营销为例&#xff0c;聊一聊具体的步骤和流程&#xff1a; 1、社群的建立&#xff0c;就是如何找到合适的小区&#xff0c;建立小区专属社群?因此&#xff0c;终端在做小区社群营销之前&#xff0c;需要先对当地所有的潜在小区做一个综合性的分析和评估&a…

Github使用

第一步 配置邮箱&#xff1a; ssh-keygen -t rsa -C 283589579qq.com然后一路回车不用管。 如下&#xff1a; 查看生成文件&#xff1a; cd .ssh ls如下&#xff1a; id_rsa是私钥&#xff0c;不能泄露出去&#xff0c;id_rsa.pub是公钥&#xff0c;可以放心地告诉任何人。…

【Java开发】 Spring 10 :Spring Boot 自动配置原理及实现

用了这么久的 SpringBoot &#xff0c;我们再来回顾一下它&#xff0c;本文介绍 Spring Boot 的自动配置&#xff0c;这是它区别于 Spring 的最大的点&#xff0c;本文的自动配置项目包含三个项目&#xff0c;建议拉取仓库里的代码进行实践&#xff1a;尹煜 / AutoConfigDemo …

kafka之ranger插件的一个坑

之前文章写过kafka的鉴权&#xff0c;以及集成ranger插件的配置使用。但真正在用起来后&#xff0c;发现里面有个坑&#xff0c;本文就来聊聊这个坑的情况以及排查过程。【问题现象】kafka在集成了ranger插件实现鉴权功能后&#xff0c;发现过一段时间后&#xff0c;controller…

Gaussian 计算静电云图确定吸附位点

计算背景&#xff1a; 利用高分子有机物等活性材料对有毒分子、原子、离子在真空、水溶液、有机溶液等环境下吸附&#xff0c;已是当今环境科学、矿物学、土壤化学等学科领域研究的热点。但如何确定最佳吸附位点以计算其吸附能就显得尤为重要。 现阶段多数物质的吸附均依据粒…

Eureka自我保护模式和InstanceID的配置

本节我们主要介绍 Eureka 自我保护模式的开启和关闭和自定义 Eureka 的 InstanceID 的配置。 关闭自我保护 保护模式主要在一组客户端和 Eureka Server 之间存在网络分区场景时使用。一旦进入保护模式&#xff0c;Eureka Server 将会尝试保护其服务的注册表中的信息&#xff…

PMP内容1

PMP目录概述需求&#xff1a;设计思路实现思路分析1.2.确认范围3.控制范围4.进度管理5.规划进度管理成本管理估算成本参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a bette…

Yocto系列讲解[驱动篇]89 - 内核通知事件notifier chain驱动示例

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 目录 内核通知事件知识Yocto中添加recipe第一个驱动模块:notifier chain provider驱动第二个驱动模块:notifier chain customer驱动两个驱…

使用 `laravel-nestedset` 实现动态权限路由

laravel-nestedset 是一款基于嵌套集合模型&#xff08;Nested Set Model&#xff09;的用于实现有序树的 laravel 扩展包。 什么是嵌套集合模型&#xff1f; 嵌套集合或嵌套集合模型是一种在关系数据库表中高效存储分层数据的方法&#xff0c;理论基础为预排序遍历树算法&am…

开源SCRM营销平台-MarketGo产品介绍(一)

1、MarketGo概述 MarketGo中国式营销自动化开源项目标杆。 MarketGo更像是一个 SDK 、引擎&#xff0c;通过提供的标准化功能和基础能力&#xff0c;让开发者能快速搭建一个营销自动化系统&#xff0c;快速完成从0-1的过程&#xff0c;并且能基于开放的能力和源码&#xff0c…