mysql并发 查询变慢

    技术2024-03-24  9

    我们大多数人都不会想到将自己的XML解析器,文本索引和搜索引擎,正则表达式编译器,XSL处理器或PDF生成器编写为需要这些实用程序之一的项目的一部分。 当我们需要这些功能时,我们会使用商业或开源实现来为我们执行这些任务,这有​​充分的理由-现有的实现做得很好,很容易获得,并且编写自己的实现将需要很多工作相对较少(或没有)的收益。 作为软件工程师,我们喜欢相信我们拥有艾萨克·牛顿(Isaac Newton)站在巨人肩膀上的热情,这种情况有时但并非总是如此。 (在图灵奖的演讲中,理查德·哈明(Richard Hamming)建议计算机科学家们宁愿“站在彼此的脚上”。

    重塑车轮,在内部查询

    当涉及到几乎每个服务器应用程序都需要的低级应用程序框架服务(例如日志记录,数据库连接池,缓存和任务调度)时,我们会一遍又一遍地重写这些基本基础结构服务。 为什么是这样? 不一定是因为现有选项不足,也不是因为自定义版本更好或更适合于手头的应用程序。 实际上,自定义版本通常不比广泛可用的通用实现更适合于为其开发应用程序,并且可能会更差。 例如,尽管您可能不喜欢log4j,但它可以完成工作。 而且,尽管本地日志记录系统可能具有log4j缺少的特定功能,但对于大多数应用程序,您很难辩称,成熟的自定义日志记录程序包值得从头开始编写,而不是使用现有的通用日志记录程序包。目的实现。 但是,许多项目团队最终一次又一次地编写自己的日志记录,连接池或线程调度程序包。

    看似简单

    我们不考虑编写自己的XSL处理器的原因之一是,这将需要大量工作。 但是这些底层框架服务看似简单,因此编写自己的框架似乎并不难。 但是,比起第一次出现时,要正确地执行它们难得多。 这些特殊的车轮不断被重新发明的主要原因是,在给定应用程序中对这些功能的需求通常开始时很小,但是随着您遇到无数其他项目遇到的相同问题而增长。 争论通常是这样的:“我们不需要完整的日志记录/调度/缓存程序包,我们只需要简单的内容,因此我们只需编写一些内容即可,并且将针对我们的特定需求进行量身定制”。 但是通常,您很快就超出了编写的简单功能,并倾向于添加更多的功能,甚至更多的功能,直到编写完备的基础架构服务为止。 到那时,无论是否更好,您通常都会对已写的内容有所了解。 您已经支付了构建自己的全部成本,因此,除了迁移到通用实现上的实际迁移成本之外,您还必须克服“降低成本”的障碍。

    并发构建块的宝库

    调度和并发基础结构类肯定比看起来难写。 Java语言提供了一组有用的低级同步原语wait() , notify()和synchronized ,但是使用这些原语的细节非常棘手,并且存在许多性能,死锁,公平性,资源管理和避免线程安全危害。 并发代码很难编写且难以测试-甚至专家有时也会在第一次遇到错误。 Java并发编程的作者Doug Lea(请参阅参考资料 )编写了一个出色的并发实用程序免费包,其中包括锁,互斥锁,队列,线程池,轻量级任务,高效的并发集合,原子算术运算和其他基本构建并发应用程序块。 该软件包通常称为util.concurrent (因为实际的软件包名称太长),它将构成JDK 1.5中的java.util.concurrent软件包的基础,该软件包在Java Community Process JSR 166下进行了标准化。 util.concurrent经过了充分的测试,已在许多服务器应用程序中使用,包括JBoss J2EE应用程序服务器。

    填补空白

    核心Java类库中明显省略了一组有用的高级同步工具,例如互斥体,信号量和阻塞线程安全收集类。 Java语言的并发原语( synchronization , wait()和notify()对于大多数服务器应用程序的需求来说太底层了。 如果您需要尝试获取一把锁,但是如果在一定时间内没有得到锁,那该怎么办呢? 如果线程中断,中止尝试获取锁? 创建一个最多N个线程可以持有的锁? 是否支持多模式锁定,例如并发读取和独占写入? 还是以一种方法获得锁然后以另一种方法释放? 内置锁定不直接支持这些功能,但是所有功能都可以基于Java语言提供的基本并发原语进行构建。 但是这样做很棘手,很容易出错。

    服务器应用程序开发人员需要简单的工具来强制相互排斥,同步事件响应,跨活动交流数据以及异步调度任务。 Java语言为此提供的低级原语很难使用且容易出错。 util.concurrent包旨在通过提供一组用于锁定,阻塞队列和任务调度的类来填补这一空白,这些类提供了处理常见错误情况或限制任务队列和在制品消耗的资源的能力。 。

    安排异步任务

    util.concurrent中使用最广泛的类是那些处理异步事件调度的类。 在本专栏的七月中,我们看了线程池和工作队列 ,以及如何“的队列中的模式Runnable ”被许多Java应用程序来安排工作的小单位。

    通过简单地为任务创建一个新线程来派生一个后台线程来执行任务是非常诱人的:

    new Thread(new Runnable() { ... } ).start();

    尽管此表示法很好且紧凑,但它有两个明显的缺点。 首先,创建一个新线程有一定的资源成本,因此产生许多线程,每个线程将执行一个简短的任务然后退出,这意味着JVM可能会比实际做更多的工作,并消耗更多的资源来创建和销毁线程。有用的工作。 即使创建和拆卸的开销为零,该执行模式仍然存在第二个更微妙的缺点-如何绑定用于执行某种类型任务的资源? 如果突然出现大量请求,那么如何阻止您一次生成一千个线程? 实际的服务器应用程序需要比这更仔细地管理其资源。 您需要限制一次执行的异步任务的数量。

    线程池解决了这两个问题-它们具有提高调度效率和同时限制资源利用率的优势。 尽管可以轻松地编写一个工作队列和线程池,该线程和线程池在池线程中执行Runnable (七月份专栏中的示例代码将完成此工作),但编写有效的任务调度程序还有很多事情,而不仅仅是简单地同步对共享队列的访问。 现实世界中的任务调度程序应该处理即将死亡的线程,杀死多余的池线程,以免不必要地消耗资源,根据负载动态管理池大小,并限制排队的任务数。 最后一项是限制排队的任务数量,对于防止服务器应用程序因过载而导致内存不足错误而崩溃至关重要。

    限制任务队列需要一项策略决策-如果工作队列溢出,您如何处理溢出? 扔掉最新的物品? 扔掉最旧的物品? 阻塞提交线程,直到队列上有可用空间? 在提交线程中执行新项目? 有多种可行的溢出管理策略,每种策略在某些情况下适用,而在另一些情况下则不合适。

    执行者

    Util.concurrent定义了一个接口Executor ,以异步执行Runnable ,并定义了具有不同调度特性的Executor几种实现。 将任务排队给执行者非常简单:

    Executor executor = new QueuedExecutor(); ... Runnable runnable = ... ; executor.execute(runnable);

    最简单的实现ThreadedExecutor为每个Runnable创建一个新线程,并且不提供资源管理-就像new Thread(new Runnable() {}).start()惯用语一样。 但是ThreadedExecutor具有一个显着的优势:通过仅更改执行程序的结构,您可以移至其他执行模型,而无需在整个应用程序源中进行爬网来查找创建新线程的所有位置。 QueuedExecutor使用单个后台线程来处理所有任务,就像AWT和Swing中的事件线程一样。 QueuedExecutor具有很好的属性,即任务按照排队的顺序执行,并且由于它们都是在单个线程中执行的,因此任务不一定需要同步对共享数据的所有访问。

    PooledExecutor是一个复杂的线程池实现,它不仅可以提供工作线程池中的任务调度,还可以提供灵活的池大小调整和线程生命周期管理,可以限制工作队列中的项目数以防止排队通过消耗所有可用内存来完成任务,并提供各种可用的关闭和饱和策略(阻止,丢弃,抛出,丢弃最旧,调用时运行等)。 所有Executor实现都为您管理线程的创建和拆除,包括在执行程序关闭时关闭所有线程,它们还提供了线程创建过程的挂钩,以便您的应用程序可以根据需要管理线程实例化。 例如,这允许您将所有工作线程放置在特定的ThreadGroup或为它们指定一个描述性名称。

    未来结果

    有时您希望异步启动一个过程,希望该过程的结果在以后需要时可用。 FutureResult实用程序类使此操作变得容易。 FutureResult表示可能需要花费一些时间才能执行的任务,并且可以在另一个线程中执行,而FutureResult对象充当该执行过程的句柄。 通过它,您可以找出任务是否已完成,等待任务完成并检索其结果。 FutureResult可以与Executor结合使用; 您可以创建FutureResult并将其排队到执行程序,同时保留对FutureResult的引用。 清单1展示了一个FutureResult和Executor的简单示例,它们以异步方式开始渲染图像,并继续进行其他处理:

    清单1.运行中的FutureResult和Executor
    Executor executor = ... ImageRenderer renderer = ... FutureResult futureImage = new FutureResult(); Runnable command = futureImage.setter(new Callable() { public Object call() { return renderer.render(rawImage); } }); // start the rendering process executor.execute(command); // do other things while executing drawBorders(); drawCaption(); // retrieve the future result, blocking if necessary drawImage((Image)(futureImage.get())); // use future

    FutureResult和缓存

    您还可以使用FutureResult改善按需加载缓存的并发性。 通过将FutureResult而不是计算本身的结果放入缓存中,可以减少对缓存持有写锁定的时间。 虽然它不会加快第一个线程在缓存中放置项目的速度,但会减少第一个线程阻止其他线程访问缓存的时间。 由于其他线程可以从缓存中检索FutureTask ,因此它也可以使结果更早地提供给其他线程。 清单2是使用FutureResult进行缓存的示例:

    清单2.使用FutureResult改善缓存
    public class FileCache { private Map cache = new HashMap(); private Executor executor = new PooledExecutor(); public void get(final String name) { FutureResult result; synchronized(cache) { result = cache.get(name); if (result == null) { result = new FutureResult(); executor.execute(result.setter(new Callable() { public Object call() { return loadFile(name); } })); cache.put(result); } } return result.get(); } }

    这种方法允许第一个线程快速进入和退出同步块,并允许其他线程像第一个线程一样快地获得第一个线程的计算结果,而没有两个线程都试图计算相同的结果目的。

    摘要

    util.concurrent包包含许多有用的类,您可能会认为其中一些是您已经编写的类的更好版本,甚至可能不止一次。 它们是多线程应用程序许多基本构建块的经过实践检验的高性能实现。 util.concurrent是JSR 166的起点,它将产生一组并发实用程序,这些实用程序将成为JDK 1.5中的java.util.concurrent包,但是您不必等到那时。 在以后的文章中,我将介绍util.concurrent中的一些自定义同步类,并探讨util.concurrent和java.util.concurrent API不同的一些方式。


    翻译自: https://www.ibm.com/developerworks/java/library/j-jtp1126/index.html

    Processed: 0.018, SQL: 8