Skip to content

Latest commit

 

History

History
100 lines (49 loc) · 10.1 KB

java并发编程实战.md

File metadata and controls

100 lines (49 loc) · 10.1 KB

"共享"意味着变量可以由多个线程同时访问,而"可变"则意味着变量的值在其生命周期内可以发生变化。我们将像讨论代码那样来讨论线程安全性,但更侧重于如何防止在数据上发生不受控的并发访问。

正确性的含义是,某个类的行为与其规范完全一致。在良好的规范中通常会定义各种不变性条件(Invariant)来约束对象的状态,以及定义各种后验条件(Postcondition)来描述对象操作的结果。由于我们通常不会为类编写详细的规范,那么如何知道这些类是否正确呢?我们无

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。"重入"意味着获取锁的操作的粒度是"线程",而不是"调用"9。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。

访问共享状态的复合操作,例如命中计数器的递增操作(读取-修改-写入)或者延迟初始化(先检查后执行),都必须是原子操作以避免产生竞态条件。如果在复合操作的执行过程中持有一个锁,那么会使复合操作成为原子操作。然而,仅仅将复合操作封装到一个同步代码块中是不够的。如果用同步来协调对某个变量的访问,那么在访问这个变量的所有位置上都需要使用同步。而且,当使用锁来协调对某个变量的访问时,在访问变量的所有位置上都要使用同一个锁。

一种常见的错误是认为,只有在写入共享变量时才需要使用同步,然而事实并非如此

一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问。在许多线程安全类中都使用了这种模式,例如Vector和其他的同步集合类。在这种情况下,对象状态中的所有变量都由对象的内置锁保护起来。

对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

不仅受到可用处理资源的限制,还受到应用程序本身结构的限制。幸运的是,通过缩小同步代码块的作用范围,我们很容易做到既确保 Servlet的并发性,同时又维护线程安全性。要确保同步代码块不要过小,并且不要将本应是原子的操作拆分到多个同步代码块中。应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,从而在这些操作的执行过程中,其他线程可以访问共享状态。

第三章 对象的共享

第2章的开头曾指出,要编写正确的并发程序,关键问题在于∶在访问共享的可变状态时需要进行正确的管理。第2章介绍了如何通过同步来避免多个线程在同一时刻访问相同的数据,而本章将介绍如何共享和发布对象,从而使它们能够安全地由多个线程同时访问。这两章合在一起,就形成了构建线程安全类以及通过javautilconcuret类库来构建并发应用程序的重要基础。

我们已经知道了同步代码块和同步方法可以确保以原子的方式执行操作,但一种常见的误解是,认为关键字synchronized只能用于实现原子性或者确定"临界区(Critical Section)"。同步还有另一个重要的方面∶内存可见性(Memory Visility)。我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。如果没有同步,那么这种情况就无法实现。你可以通过显式的同步或者类库中内置的同步来保证对象被安全地发布。

加锁机制既可以确保可见性又可以确保原子性号而volatile变量买能确保可见性。

当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volaile 类型的变量时总会返回最新写人的值。

"发布(Publish)"一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。 当某个不应该发布的对象被发布时,这种情况就被称为逸出(Escape)。

线程封闭技术的另一种常见应用是JDBC(Java Database Connectivity)的Conection对象。JDBC规范并不要求Connection对象必须是线程安全的9。在典型的服务器应用程序中,线程从连接池中获得一个Conection对象,并且用该对象来处理请求,使用完后再将对象返还给连接池。由于大多数请求(例如 Servlet 请求或EJB调用等)都是由单个线程采用同步的方式来处理,并且在Conection对象返回之前,连接池不会再将它分配给其他线程,因此,这种连接管理模式在处理请求时隐含地将Connection对象封闭在线程中。

应用程序服务器提供的连接池是线程安全的。连接池通常会由多个线程同时访问,因此非线程安全的连接池是毫无意义的。

与cache相关的操作不会相互干扰,因为OneValueCache是不可变的,并且在每条相应的代码路径中只会访问它一次。通过使用包含多个状态变量的容器对象来维持不变性条件,并使用一个volatile类型的引用来确保可见性,使得 Volatile Cached Factorizer在没有显式地使用锁的情况下仍然是线程安全的。

final 对象 + Volatile

5.2 并发容器

Java 5.0提供了多种并发容器类来改进同步容器的性能。同步容器将所有对容器状态的访问都串行化,以实现它们的线程安全性。这种方法的代价是严重降低并发性,当多个线程竞争容器的锁时,吞吐量将严重减低。 另一方面,并发容器是针对多个线程并发访问设计的。在Java 5.0中增加了Concurent-HashMap,用来替代同步且基于散列的Map,以及CopyOnWriteArayList,用于在遍历操作为主要操作的情况下代替同步的List。在新的ConcurrentMap接口中增加了对一些常见复合操作的支持,例如"若没有则添加"、替换以及有条件删除等。 通过并发容器来代替同步容器,可以极大地提高伸缩性并降低凤险。

分段锁 lock striping

弱一致性

闭锁

为什么要在TestHaness中使用闭锁,而不是在线程创建后就立即启动?或许,我们希望测试n个线程并发执行某个任务时需要的时间。如果在创建线程后立即启动它们,那么先启动的线程将"领先"后启动的线程,并且活跃线程数量会随着时间的推移而增加或减少,竞争程度也在不断发生变化。启动门将使得主线程能够问时释放所有工作线程,而结束门则使主线程能够等待最后一个线程执行完成,而不是顺序地等待每个线程执行完成。

6.3.2 携带结果的任务Callable与Future

kafka的python客户端源码结合future理解!

第七章 取消与关闭

生命周期结束(End-of-Lifecycle)的问题会使任务、服务以及程序的设计和实现等过程变得复杂,而这个在程序设计中非常重要的要素却经常被忽略。一个在行为良好的软件与勉强运行的软件之间的最主要区别就是,行为良好的软件能很完善地处理失败、关闭和取消等过程。本章将给出各种实现取消和中断的机制,以及如何编写任务和服务,使它们能对取消请求做出响应。

其中一种协作机制能设置某个"已请求取消(Cancellation Requested)"标志,而任务将定期地查看该标志。如果设置了这个标志,那么任务将提前结束。

小结

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

第八章 线程池的使用

第十三章 显式锁

小结

第一部分 基础知识 从理论的基础上介绍了各种基础知识,但实际使用不知道是怎么样的场景,但也值得了解。 第四章 对象的组合,介绍了并发锁组合的场景。第5章有简单介绍使用的一些场景。5.5的同步工具类重点。

第二部分 结构话并发应用程序

第六章, executor的介绍与引入。并用了具体的例子。 重点:6.3.2 携带结果的任务Callable与Future这个章节介绍了这些基本概念。

第7章,取消与关闭。介绍一个健康的程序的关闭的一些处理场景。但没有看完。

第三部分 性能。 没有阅读

第四部分 高级主题。

第十三章,显示锁,理论介绍公平性、读写锁等。

其余章节没有阅读下去了。觉得这本书可能对于我这种没有使用过java并且开发过并发程序的,可能感触不是很深。因为希望的是看到各种实际场景和各种锁的使用(直接文章总结吧)。但是这书应该是在这之上的,是比较总结性与归纳。而且好像也比较旧了。所以只看了些能看懂的理论,没有继续细读。待熟悉使用场景后再回来阅读吧。