并发编程

第一部分

线程安全

1.通过从框架线程中调用应用程序的组件,框架把并发引入了应用程序。组件总是需要访问程序的状态。因此要求在所有代码路径访问状态时,必须时线程安全的
2.设计线程安全的类时,优秀的面向对象技术–封装,不可变性以及明确的不变约束–会给你提供诸多的帮助
3.当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全
4.对于线程安全类的实例进行顺序或并发的一系列操作,都不会导致实例处于无效状态
5.线程安全的类封装了任何必要的同步,因此客户不需要自己提供
6.无状态对象永远是线程安全的
7.为了保护状态的一致性,要在单一的原子操作中更新相互关联的状态变量
8.对于每个可被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,这种情况下,我们称这个变量是由这个锁保护的
9.每个共享的可变变量都需要由唯一一个确定的锁保护。而维护者应该清楚这个锁
10.对于每个涉及多个变量的不变约束,需要同一个锁保护其所有的变量
11.通常简单性与性能之间是相互牵制的。实现一个同步策略时,不要过早地维克性能而牺牲简单性(这是对安全性潜在的妥协)
12.有些耗时的计算或操作,比如网络或控制台I/O,难以快速完成,执行这些操作期间不要占有锁

共享对象

1.在没有同步的情况下,编译器,处理器,运行时安排操作的执行顺序可能完全出人意料。在没有进行适当同步的多线程程序中,尝试推断那些“必然”发生在内存中的动作时,你总是会判断错误
2.锁不仅仅是关于同步与互斥的,也是关于内存可见的,为了保证所有线程都能够看到共享的,可变变量的最新值,读取和写入线程必须使用公共的锁进行同步
3.只由当volatile变量能够简化实现和同步策略的验证时,才使用它们。当验证正确性必须推断可见性问题时,应该避免使用volatile变量。正确使用volatile变量的方式包括:用于确保它们所引用的对象状态的可见性,或者用于标识重要的生命周期事件(比如初始化或关闭)的发生
4.枷锁可以保证可见性与原子性,volatile变量只能保证可见性
5.使用volatile变量的基本要求
1.写入变量时并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
2.变量不需要与其他的状态变量共同参与不变约束
3.而且,访问变量时,没有其他的原因需要加锁
6.不要让this引用在构造期间逸出
7.不可变对象永远是线程安全的
8.只有满足如下状态,一个对象才是不可变的:
1.它的状态不能在创建后再被修改
2.所有域都是final类型
3.被正确创建(创建期间没有发生this引用的逸出)
9.正如“将所有的域声明为私有的,除非它们需要更高的可见性”一样,“将所有的域声明为final型,除非它们是可变的”,也是一条良好的实践
10.不可变对象可以在没有额外同步的情况下,安全地用于任意线程,甚至发布它们时亦不需要同步
11.为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见,一个正确创建的对象可以通过下列条件安全地发布:
1.通过静态初始化器初始化对象的引用
2.将它的引用存储到volatile域或AtomicReference
3.将它的引用存储到正确创建的对象的final域中
4.或者将它的引用存储到由锁正确保护的域中
12.任何线程都可以在没有额外的同步下安全地使用一个安全发布的高效不可变对象
13.发布对象的必要条件依赖于对象的可变性
1.不可变对象可以通过任意机制发布
2.高效不可变对象必须要安全发布
3.可变对象必须要安全发布,同时必须要线程安全或者时被锁保护

组合对象

1.设计线程安全类的过程应该包括下面3面基本要素:
1.确定对象状态是由那些变量构成的
2.确定限制状态变量的不变约束
3.制定一个管理并发访问对象状态的策略
2.不理解对象的不变约束和后验条件,你就不能保证线程安全性。要约束状态变量的有效值或者状态转换,就需要原子性与封装性0
3.将数据封装在对象内部,把对数据的访问限制在对象的方法上,更易确保线程在访问数据时总能获得正确的锁
4.限制性使构造线程安全的类变得更容易,因为类的状态被限制后,分析它的线程安全性时,就不必检查完整的程序
5.如果一个类由多个彼此独立的线程安全的状态变量组成,并且类的操作不包含任何无效状态转换时,可以将线程安全委托给这些状态变量
6.如果一个状态变量是线程安全的,没有任何不变约束限制它的值,并且没有任何状态转换限制它的操作,那么它可以被安全发布
7.为类的用户编写类线程安全性担保的文档,为类的维护者编写类的同步策略文档

构建块

1.正如封装一个对象的状态,能够使它更加容易地保持不变约束一样,封装它的同步则可以迫使它符合同步策略
2.用并发容器替换同步容器,这种作法以有很小风险带来了可扩展性显著的提高
3.相比于Hashtable和synchronizedMap,ConcurrentHashMap有众多的优势,而且几乎不存在什么劣势,因此在大多数情况下用ConcurrentHashMap取代同步Map实现只会带来更好的可伸缩性。只有当你的程序需要在独占访问中加锁时,ConurrentHashMap才无法胜任(ConcurrentHashMap是线程安全的)
4.有界队列是强大的资源管理工具,用来建立可靠的应用程序,它们遏制那些可以产生过多工作量,具有威胁的活动,从而让你的程序在面对超负荷工作时更加健壮

第一部分总结

1.所有并发问题都归结为如何协调访问并发状态,可变状态越少,保证线程安全就越发容易
2.尽量将域声明为final类型,除非它们的需要是可变的
3.不可变对象天生是线程安全的
4.不可变对象极大地减轻了并发编程的压力。它们简单而且安全,可以在没有锁或者防御性复制的情况下自由地共享
5.封装使管理复杂度变得更可行
6.用锁来守护每一个可变变量
7.对同一不变约束中的所有变量都使用相同的锁
8.在运行复合操作期间持有锁
9.在非同步的多线程情况下,访问可变变量的程序是存在隐患的
10.不要依赖于可以需要同步的小聪明
11.在设计过程中就考虑线程安全。或者在文档中明确地说明它不是线程安全的
12.文档化你的同步策略

第二部分

构建并发应用程序

任务执行

1.如果要在你的程序中实现一个生产者-消费者的设计,使用Executor通常是最简单的方式
2.无论何时当你看到这种形式的代码:
new Thread(runnable).start()
并且你可能最终希望获得一个更加灵活的执行策略时,请认真考虑使用Executor代替Thread
3.Executors中的静态工厂方法

image-20230921090327806

4.大量相互独立且同类的任务进行并发处理,会将程序的任务量分配到不同的任务中,这样才能真正获得性能的提升

取消和关闭

1.在API和语言规范中,并没有把中断与任何取消的语意绑定起来,但是,实际上,使用中断来处理取消之外的任何事情都是不明智的,并且很难支撑起更大的应用
2.调用interrupt并不意味着必然停止目标线程正在进行的工作,它仅仅传递了请求中断的消息
3.中断通常是实现取消最明智的选择
4.因为每一个线程都有其自己的中断策略,所以你不应该中断线程,除非你知道中断对这个线程意味着什么
5.只有实现了线程中断策略的代码才可以接收中断请求。通用目的的任务和库的代码绝不应该接收中断请求
6.对于线程持有的服务,只要服务的存在时间大于创建线程的方法存在的时间,那么就应该提供生命周期方法
7.在一个长时间运行的应用程序中,所有的线程都要给未捕获异常设置一个处理器,这个处理器至少要将异常信息记入日志中

精灵线程

8.线程被分为两种:普通线程和精灵线程(daemon thread)。JVM启动时创建所有的线程,除了主线程以外,其他的都是精灵线程(比如垃圾回收器和其他类似线程)。当一个新的线程创建时,新线程继承了创建它的线程的后台状态,所以默认情况下,任何主线程创建的线程都是普通线程。
9.普通线程和精灵线程之间的差别仅仅在于退出时会发生什么
10.应用程序中,精灵线程不能替代对服务的生命周期恰当,。良好的管理
11.避免使用finalizer

应用线程池

1.一些任务具有这样的特征:需要或者排斥某种特定的执行策略。对其他任务具有依赖性的任务,就会要求线程池足够大,来保证它所依赖任务不必排队或者不被拒绝,采用线程限制的任务需要顺序地执行。把这些需求都写入文档,这样将来的维护者就不会使用一个与原先相悖的执行策略,而破坏安全性或活跃度
2.无论何时,你提交了一个非独立的Executor任务,要明确出现线程饥饿死锁的可能性,并且,在代码或者配置文件以及其他可以配置Executor的地方,任何有关池的大小和配置约束都要写入文档
3.newCachedThreadPool工厂提供了比定长的线程池更好的队列等候性能,它是Executor的一个很好的默认选择。出于资源管理的目的,当你需要限制当前任务的数量,一个定长的线程池就是很好的选择。就像一个接受网络客户端请求的服务器应用程序,如果不进行限制,就会很容易因为过载而遭受攻击。
4.当每个迭代彼此独立,并且完成循环体中每个迭代的工作,意义都足够重大,足以弥补管理一个新任务的开销时,这个顺序循环是适合并行化的

总结

对于并发执行的任务,Executor框架是强大且灵活的。它提供了大量可调节的选项,比如创建和关闭线程的策略,处理队列任务的策略,处理过剩任务的策略,并且提供了几个钩子函数用于扩展它的行为,然而,和大多数强大的框架一样,草率地将一些设定组合在一起,并不能很好地工作;一些类型的任务需要特定的执行策略,而一些调节参数组合在一起后可能产生意外的结果

GUI应用程序

1.Swing的单线程规则:Swing的组件和模型只能在事件分派线程中被创建,修改和请求
2.如果一个数据模型必须被多个线程共享,而且实现一个线程安全模型的尝试却由于阻塞、一致性或者复杂度等原因而失败,这时可以考虑运用分拆模型设计
3.GUI框架几乎都是作为单线程化子系统实现的,所有与表现相关的代码都作为任务在一个事件线程中运行。因为只要唯一一个线程,耗时任务会损害响应性,所以它们应该在后台线程中运行。像SwingWorker以及构建BackgroundTask这些助手类,提供了对取消、进度指示、完成指示的支持/无论是GUI组件还是非GUI组件,都能借助它们简化耗时任务的开发

活跃度,性能和测试

避免活跃度危险

1.安全性和活跃度通常相互牵制。我们使用锁开保证线程安全,但是滥用锁可能引起锁顺序死锁(lock-ordering deadlock)。类似的,我们使用线程池和信号量来约束资源的使用,但是却不能知晓那些管辖范围内的活动可能形成的资源死锁(resource deadlock)。Java应用程序不能从死锁中恢复,所以确保你的设计能够避免死锁出现的先决条件是非常有价值的
2.如果所有线程以通用的固定秩序获得锁,程序就不会出现锁顺序死锁问题了
3.在持有锁的时候调用外部方法是在挑战活跃度问题。外部方法可能会获得其他锁(产生死锁的风险),或者遭遇严重超时的阻塞。当你持有锁的时候会延迟其他试图获得该锁的线程
4.当调用的方法不需要持有锁时,这被称为开放调用
5.在程序中尽量使用开发调用。依赖于开发调用程序,相比于那些在持有锁的时候还调用外部方法的程序,更容易进行死锁自由度(deadlock-freedom)的分析
6.抵制使用线程优先级的诱惑,因为这会增加平台依赖性,并且可能引起活跃度问题。大多数并发应用程序可以对所有线程使用的优先级
7.可伸缩性指的是:当增加计算资源的时候(比如增加额外CPU数量、内存、存储器、I/O带宽),吞吐量和生产量能够相应地得以改进
8.避免不成熟的优化,首先使程序正确,然后再加快–如果它运行得还不够快
9.测评。不要臆测
10.所有得并发程序都要一些串行源;如果你认为你没有,那么去仔细检查吧
11.不要过分担心非竞争得同步带来的开销。基础的机制已经足够快了,在这个基础上,JVM能够进行额外的优化,大大减少或消除了开销。关注那些真正发生了锁竞争的区域中性能的优化
12.串行化会损害可伸缩性,上下文切换回损害性能。竞争性的锁会同时导致这两种损失,所以减少锁的竞争能够改进性能和可伸缩性
13.并发程序中,对可伸缩性首要的威胁是独占的资源锁
14.三种方式来减少锁的竞争
1.减少持有锁的时间
2.减少请求锁的频率
3.或者用协调机制取代独占锁,从而允许更强的并发性
15.Amdahl定律告诉我们,程序的可伸缩性是由必须连续执行的代码比例决定的

总结

16.可伸缩性通常可以通过以下这些方式提升:减少用于获取锁的时间,减小锁的粒度,减少锁的占用时间,或者用非独占或非阻塞锁来取代独占锁

测试并发程序

1.为并发类创建有效的安全测试,其挑战在于:如何在程序出现问题并导致某些属性极度可能失败时,简单地识别出这些检查的属性来,同时不要人为的让查找错误的代码限制住程序的并发性。最好能做到在检查测试的属性时,不需要任何的同步。
2.测试应该在多处理器系统上运行,以提高潜在交替运行的多样性。但是,多个CPU未必会使测试更加高效,为了能够最大程度地检测到时序敏感的数据竞争的发生机会,应该让测试中的线程数多于CPU数,这样在任何给定的时间里,都要一些线程在运行,一些被交换出执行队列,这样可以增加线程间交替行为的随机性
3.编写有效的性能测试,就需要哄骗优化器不要把你的基准测试当作死代码而优化掉。这需要每一个计算的结果都要应用在你的程序中–以一种不需要的同步或真实计算的方式

高级主题

显示锁

1.性能是一个不断变化的目标,昨天的基准显示X比Y更快,这可能已经过时了
2.正如默认的ReentrantLock一样,内部锁没有提供确定的公平性保证,但是大多数锁实现统计上的公平性保证,在大多数条件下已经足够好了,Java语言规范并没有要求JVM公平地实现内部锁,JVM也的确没有这样做。ReentrantLock并没有减少锁的公平性–它只不过使一些存在的部分更显性化了
3.在内部锁不能够满足使用时,ReentrantLock才被作为更高级的工具。当你需要以下高级特性时,才应该使用;可定时的、可轮询的与可中断的锁获取操作,公平队列,或者非块结构的锁。否则,请使用synchronized
4.读-写锁(ReadWriteLock)允许多个读者并发访问被守护的对象,当访问多为读取数据结构的时候,它具有改进可伸缩性的潜力

构建自定义的同步工具

1.条件谓词是先验条件的第一站,它在一个操作与状态之间建立起依赖关系
2.将条件谓词和与之关联的条件队列,以及在条件队列中等待的操作,都写入文档
3.每次调用wait都会隐式地与特定的条件谓词相关联。当调用特定条件谓词的wait时,调用者必须已经持有了与条件队列相关的锁,这个锁必须同时还保护着组成条件谓词的状态变量
4.一个单独的内部条件队列可以与多个条件谓词共同使用
5.当使用条件等待(Object.wait或者Condition。await)
1.永远设置一个条件谓词–一些对象状态的测试,线程执行前必须满足它;
2.永远在调用wait前测试条件谓词,并且从wait中返回后再次测试
3.永远在循环中调用wait
4.确保构成条件谓词的状态变量被锁保护,而这个锁正是与条件队列相关联的;
5.当调用wait,notify或者notifyAll时,要持有与条件队列相关联的锁,并且
6.在检查条件谓词之后,开始执行被保护的逻辑之前,不要释放锁
7.无论何时,当你在等待一个条件,一定要确保有人会在条件谓词变为真时通知你
8.只有同时满足下述条件后,才能用单一的notify取代notifyAll(一般使用notifyAll):相同的等待者。只有一个条件谓词与条件队列相关,每个线程从wait返回执行行相同的逻辑;并且,一进一出。一个对条件变量的通知,至多只激活一个线程执行
9.尽管使用notifyAll而非notify可能有些低效,但是这样做更容易确保你的类的行为时正确的
10.一个依赖于状态的类,要么完全将它的等待和通知协议暴露(并文档化)给子类,要么完全阻止子类参与其中
11.危险警告:wait,notify和notifyAll在Condition对象中的对象对等体是await,signal和signalAll。但是,Condition继承Object,这意味着它也有wait和notify方法。一定要确保使用了正确的版本–await和signal!

原子变量与非阻塞同步机制

1.如果能够避免的话,不共享状态的开销会更小。能够通过更有效地竞争改进可伸缩性,但是真正的可伸缩完全是通过减少竞争实现的
2.非阻塞算法通过使用底层级并发原语,比如比较并交换,取代了锁。原子变量类向用户提供了这些底层级原语,也能够当作“更佳的volatile变量”使用,同时提供了整数类和对象引用的原子化更新操作
3.非阻塞算法在设计和实现中很困难,但是在典型条件下能够提供更好的可伸缩性,并能更好地预防活跃度失败。从JVM的一个版本到下一个版本间并发性能的提升很大程度上来源于非阻塞算法的使用,包括在JVM内部以及平台类库。

Java存储模型

1.happens-before的法则包括:
程序次序法制:线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都出现在动作A之后
监视器锁法制:对一个监视器锁的解锁happens-before于每一个后续对同一监视器锁的加锁。
volatile变量法制:对volatile域的写入操作happens-before于每一个后续对同一域的读操作。
线程启动法制:在一个线程里,对Thread.start的调用会happens-before于每一个启动程序中的动作
线程启动法制:在一个线程里,对Thread.start的调用会happens-before于每一个启动线程中的动作
线程终结法制:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或者Thread.isAlive返回false。
中断法制:一个线程调用另一个线程的interrupt happens-before 于被中断的线程发现中断(通过抛出InterruptedException,获知调用isInterrupted和interrupted)
终结法制:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。
传递性:如果A happens-before于B,且B happens-before 于C,则A happens-before于C
2.除了不可变对象以外,使用被另一个线程初始化的对象,是不安全的,除非对象的发布时happens-before于对象的消费线程使用它
3.初始化安全可以保证,对于正确创建的对象,无论它如何发布的,所有线程都将看到构造函数设置的final域的值,更进一步,一个正确创建的对象中,任何可以通过其final域触及到的变量(比如一个final数组中的元素,或者一个final域引用的HashMap里面的内容),也可以保证对其他线程都是可见的。
4.初始化安全性保证只有以通过final域触及的值,在构造函数完成时才是可见的。对于通过非final域触及的值,或者创建完成后可能改变的值,必须使用同步来确保可见性。

同步Annotation

3个类级Annotation来描述类的可预期的线程安全性保证:@Immutable ,@ThreadSafe 和 @NotThreadSafe

@Immutable自然是意味着类是不可变的,并包含了@ThreadSafe的意义。@NotThreadSafe是可选的–如果类没有被标明是线程安全的,就无法肯定它是不是线程安全的,但是如果你想明确地表示出它不是线程安全的,就标注为@NotThreadSafe

这些Annotation相对是非侵入的,这对用户和维护者都是有益的。用户可以立即看出一个类是否线程安全的,维护者也可以直接检查是否遵循了线程安全性保证。Annotation对于第三个利益既得者也是有用的:工具。静态的代码分析工具可以有能力对代码进行验证,看它是否遵循了由Annotation指定的契约,比如标明为@Immutable的类是否真是不可变的

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2015-2025 Immanuel
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

微信