zl程序教程

您现在的位置是:首页 >  Java

当前栏目

并发编程tips

2023-02-25 18:27:50 时间

早在两周前说要分享一下并发编程的小技巧,之前整理了一点,又重新翻了一下书,把自己认为比较有用的小技巧分享一下。所谓并发编程是指在一台处理器上 “同时” 处理多个任务。并发是在同一实体上的多个事件。多个事件在同一时间间隔发生。

并发编程,从程序设计的角度来说,是希望通过某些机制让计算机可以在一个时间段内,执行多个任务。从计算机 CPU 硬件层面来说,是一个或多个物理 CPU 在多个程序之间多路复用,提高对计算机资源的利用率。从调度算法角度来说,当任务数量多于 CPU 的核数时,并发编程能够通过操作系统的任务调度算法,实现多个任务一起执行。

无状态的对象一定是线程安全的,比如Servlet对象。

不可变对象一定是线程安全的,当满足以下条件时,对象才是不可变的:

  • 对象创建以后其状态就不能修改
  • 对象的所有域都是final类型
  • 对象是正确创建的《在对象的创建期间,this引用没有逸出》

正如“除非需要更高的可见性,否则应将所有的域都声明为私有域是一个良好的好的编程习惯,除非需要某个域是可变的,否则应将其声明为final域,也是个好的编程可惯。

在并发程序中使用和共享对象时,可以使用一些实用的策略,

  • 包括线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
  • 只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
  • 线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
  • 保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。

线程池大小设置(我们按IO密集型来,设置2*Ncpu个数,等待/计算时间比给1利用率给1)。

没说线程安全,就当成线程不安全考虑。

在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具,它们能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。

在使用限时任务时需要注,当这些任务超时后应该立即停止,从而避免为继续计算-个不再使用的结果而浪费计算资源。

调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。对中断操作的正确理解是: 它并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。通常,中断是实现取消的最合理方式。

在任务、线程、服务以及应用程序等模块中的生命周期结束问题,可能会增加它们在设计和实现时的复杂性。Java 并没有提供某种抢占式的机制来取消操作或者终结线程。相反,它提供了一种协作式的中断机制来实现取消操作,但这要依赖于如何构建取消操作的协议,以及能否始终遵循这些协议。通过使用 FutureTask 和 Executor 框架,可以帮助我们构建可取消的任务和服务。

活跃性故障是一个非常严重的问题,因为当出现活跃性故障时,除了中止应用程序之外没有其他任何机制可以帮助从这种故障时恢复过来。最常见的活跃性故障就是锁顺序死锁。在设计时应该避免产生锁顺序死锁:确保线程在获取多个锁时采用一致的顺序。最好的解决方法是在程序中始终使用开放调用。这将大大减少需要同时持有多个锁的地方,也更容易发现这些地方。

线程的最主要目的是提高程序的运行性能。线程可以使程序更加充分地发挥系统的可用处理能力,从而提高系统的资源利用率。此外,线程还可以使程序在运行现有任务的情况下立即开始处理新的任务,从而提高系统的响应性。

避免不成熟的优化。首先使程序正确,然后再提高运行速度一如果它还运行得不够快。以测试为基准,不要猜测。

在并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁。有3种方式可以降低锁的竞争程度。

  • 减少锁的持有时间
  • 降低锁的请求频率
  • 使用带有协调机制的独占锁,这些机制允许更高的并发性

如果所有 CPU 的利用率并不均匀(有些 CPU 在忙碌地运行,而其他 CPU 却并非如此)那么你的首要目标就是进一步找出程序中的并行性。不均匀的利用率表明大多数计算都是由一小组线程完成的,并且应用程序没有利用其他的处理器。监控到每一核心CPU。

由于使用线程常常是为了充分利用多个处理器的计算能力,因此在并发程序性能的讨论中,通常更多地将侧重点放在吞吐量和可伸缩性上,而不是服务时间。Amdahl 定律告诉我们程序的可伸缩性取决于在所有代码中必须被串行执行的代码比例。因为 Java 程序中串行操作的主要来源是独占方式的资源锁,因此通常可以通过以下方式来提升可伸缩性:减少锁的持有时间,降低锁的粒度,以及采用非独占的锁或非阻塞锁来代替独占锁。

性能是一个不断变化的指标。如果再昨天的测试基准中发现X比Y更快,那么在今天就可能已经过时了。

读一写锁允许多个读线程并发地访问被保护的对象,当访问以读取操作为主的数据结构时,它能提高程序的可伸缩性。

非阻塞算法通过底层的并发原语(例如比较并交换而不是锁)来维持线程的安全性。这些底层的原语通过原子变量类向外公开,这些类也用做一种“更好的 volatile 变量”,从而为整数和对象引用提供原子的更新操作。非阻塞算法在设计和实现时非常困难,但通常能够提供更高的可伸缩性,并能更好地防止活跃性故障的发生。在JVM 从一个版本升级到下一个版本的过程中,并发性能的主要提升都来自于(在JVM内部以及平台库中)对非塞算法的使用。

初始化安全性只能保证通过 final域可达的值从构造过程完成时开始的可见性。对于通过非final域可达的值,或者在构造过程完成后可能改变的值,必须采用同步来确保可见性。

Java 内存模型说明了某个线程的内存操作在哪些情况下对于其他线程是可见的。其中包括确保这些操作是按照一种 Happens-Before 的偏序关系进行排序,而这种关系是基于内存操作和同步操作等级别来定义的。如果缺少充足的同步,那么当线程访向共享数据时,会发生一些非常奇怪的问题。

因为并发编程是 Java 语言中最为晦涩的知识点,它涉及操作系统、内存、CPU、编程语言等多方面的基础能力,更为考验一个程序员的内功。涉及内功的东西,一定要不断的学习和深入,只有花拳绣腿是不行的。

精进自省:崇尚学习及成长,拒绝渴望知识却嗷嗷待哺,不懂就问是真懒惰 。抓住问题本质,不浪费别人时间和自己时间。