1、无状态的类一定是线程安全的
2、最常见的竞态条件是:先检查后执行
3、不恰当的执行时序造成的程序错误叫静态条件
4、一个线程写入变量而一个线程接下来读取这个变量或者读取一个之前由另一个线程写入的变量时并且在两个线程之间没有使用同步,就有可能出现数据竞争。
5、某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。重入意味着获取锁的操作的粒度是线程,而不是调用。
6、通过缩小同步代码块的作用范围,我们很容易做到既确保Servlet的并发性,同时又维护线程安全性。
7、并发程序要在简单性、安全性和性能之间达到某种合理的平衡
8、当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络IO或控制台IO)一定不要持有锁。
9、要编写正确的并发程序,关键问题在于:在访问共享的可变状态时需要进行正确的管理。
10、在缺少同步的情况下,Java内存模型允许编译器对操作顺序进行冲排序,并将数值缓存在寄存器中,此外,它还允许CPU对操作顺序进行冲排序,并将数值缓存在处理器特定的缓存中。
11、Java内存模型要求,变量的读取和写入操作都必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。
12、不可变对象一定是线程安全的
13、任何线程都可以在不需要额外同步的情况下安全的访问不可变对象,即使在发布这些对象时没有使用同步
14、线程安全的库中的容器类提供了以下安全发布保证:
Hashtable、synchronizedMap、ConcurrentMap、Vector、CopyOnWriteArraylist、CopyOnWriteArraySet、synchronizedList、synchronizedSet、BlockingQueue、ConcurrentLinkedQueue中的元素可以安全的发布到任何从这些容器中访问该元素的线程
类库中其他数据传递机制(例如Future和Exchanger)同样能实现安全发布
15、ConcurrentHashMap并不是讲每个方法都在同一锁上同步并使每次只有一个线程访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种锁称为分段锁。
16、中断是一种协作机制。一个线程不能强制其他线程停止正在执行的操作而去执行他的操作。当线程A中断B时,A仅仅是要求B在执行到某个可以暂停的地方停止正在执行的操作,前提是如果线程B愿意停下来。
17、在A线程获取的信号量,在B线程可以被释放
18、在栅栏中,如果对await调用超时或者await阻塞的线程被中断,那么栅栏就被认为是打破了,所有阻塞的wait调用都将终止并抛出BrokenBarrierException。
19、Timer负责管理延迟任务或者周期任务,但是Timer存在一些缺陷,因此应该考虑使用ScheduledThreadPoolExecutor来代替它。
20、如果要构建自己的调度服务,可以使用DelayQueue,它实现了BlockingQueue,并未SheduledThreadPoolExecutor提供调度功能。
21、CompletionService将Executor和BlockingQueue的功能融合在一起。你可以将Callable任务提交给它来执行,然后使用类似于队列操作的take和poll等方法来获得已完成的结果
而这些结果会在完成时将结果封装为Future。ExecutorCompletionService实现了CompletionService,并将计算部分委托给一个Executor。
22、要使任务和线程能安全、快速、可靠的停止下来,并不是一件容易的事。Java没有提供任何机制来安全的终止线程。但它提供了中断,这是一种协作机制,能够使一个线程终止另一个线程的当前工作。
23、一个在行为零号的软件与勉强运行的软甲之间的最主要区别就是,行为良好的软件能很完善的处理失败、关闭和取消等过程。
24、通常中断是实现取消最合理的方式。
25、另一种关闭生产者-消费者服务的方式就是使用”毒丸“对象:”毒丸“对象时指一个放在队列上的对象,其含义是:”当得到这个对象时,立即停止“。
26、只有通过execute提交的任务,才能将它抛出的异常交给未捕获异常处理器,而通过submit提交的任务,无论是抛出的未检查异常还是已检查异常,都将被认为是任务返回状态的一部分。如果一个由
submit提交的任务由于抛出了异常而结束,那么这个异常将被Future.get()封装在ExecutionException中重新抛出。
27、避免使用终结器
28、Thread.join、BlockingQueue.put、CountDownLatch.await以及Selector.select等方法都同时提供了限时版本和无限时版本
29、只有当线程池是无界的或者可以拒绝任务时,SynchronousQueue才有实际价值
30、只有当任务相互独立时,为线程池或工作队列设置界限才是合理的。如果任务之间存在依赖性,那么有界的线程池或队列就有可能导致线程“饥饿”死锁问题。此时应该使用无界的线程池,例如newCachedThreadPool。
31、JDK提供了几种不同的RejectedExecutionHandler实现:中止、调用者运行、丢弃、丢弃最老的
32、ThreadPoolExecutor是可扩展的,它提供了几个可以在子类化中改写的方法:beforeExecute、afterExecute和terminated
33、如果所有的线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题。
34、避免不成熟的优化。首先使程序正确,然后再提高运行速度-如果它还运行的不够快。
35、以测试为基准,不要猜测。
36、所有由用的计算都会生成某种结果或产生某种效应,如果不会,那么可以将它们作为“死亡代码”删除掉
37、在所有并发程序中都包含一些串行部分。如果你认为在你的程序中不存在串行部分,那么可以再仔细检查一遍
38、降低锁粒度的技术:锁分解、锁分段
39、在synchronized和volatile提供的可见性保证中可能会使用一些特殊指令,即内存栅栏。内存栅栏可以刷新缓存使缓存无效,舒心硬件的写缓存,以及停止执行管道。内存栅栏可能会同样
对性能带来间接的影响,因为它们会抑制一些编译器的优化操作。在内存栅栏中,大多数操作都是不能重排序的。
40、有三种方式可以降低锁的竞争程度:减少锁的持有时间、降低锁的请求频率、使用带有协调机制的独占锁,这些机制允许更高的并发性。
41、减少锁的竞争:缩小锁的范围(快进快出)、减小锁的粒度(锁分解)、锁分段、避免热点域、一些替代古老锁的方法(使用并发容器、读写锁、不可变对象、院子变量)
42、向“对象池”说不:通常,对象分配操作的开销比同步的开销更低
43、由于使用线程常常是为了充分利用多个处理器的计算能力,因此在并发程序性能的讨论中,通常更多地将侧重点放在吞吐量和可伸缩性上,而不是服务时间。Amdahl定律告诉我们,
程序的可伸缩性取决于在所有代码中必须被串行执行的代码比例。因为Java程序中串行操作的主要来源是独占方式的资源,因此通常可以通过以下方式来提升可伸缩性:减少锁的持有时间,
降低锁的粒度,以及采用非独占的锁或非阻塞锁来代替独占锁。
44、在一些内置所无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的、可轮询的与可中断的锁获取操作,
公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。
45、在Java5.0中,内置锁与ReentranLock相比还有另外一个优点:在线程转储中能给出在哪些线程调用获得了哪些锁,并能够检测和识别发生死锁的线程。JVM并不知道哪些线程持有ReentrantLock,
因此在调试使用ReentrantLock的线程的问题时,将起不到帮助作用。
46、ReentrantLock的非块结构特性仍然意味着,获取锁的操作不能与特定的栈帧关联起来,而内置锁却可以。
47、未来更可能会提升synchronized而不是ReentrantLock的性能。因为synchronized是JVM的内置属性,它能执行一些优化,例如对线程封闭的锁对象的锁消除优化,通过增加锁的粒度来消除内置锁的同步,
而如果通过基于类库的锁来实现这些功能,则可能性不大。
48、在许多情况下,数据结构上的操作都是”读操作“,虽然它们也是可变的并且在某些情况下被修改,但其中大多数访问操作都是读操作。此时,如果能够放宽加锁需求,允许多个执行读操作的线程同时访问数据结构
,那么将提升程序的性能。只要每个线程都能确保读取到最新的数据,并且在读取数据时不会有其他的线程修改数据,那么久不会发生问题。在这种情况下就可以使用读写锁:一个资源可以被多个读操作访问
,或者被一个写操作访问,但两者不能同时进行。
49、读写锁允许多个线程并发的访问被保护的对象,当访问以读取操作为主的数据结构时,它能提高程序的可伸缩性。
50、内置锁和条件队列:synchronized、wait、notify、notifyAll
51、显式的锁和条件队列:Lock和Condition
52、Condition比内置条件队列提供了更丰富的功能:在每个锁上课存在多个等待、条件等待是可中断的或不可中断的、基于时限的等待以及公平的、非公平的操作
53、在Condition对象中,与wait、notify、notifyAll方法对应的分别是await、signal、signalAll。但是,Condition对Object进行了子类扩展,因而它也包含wait、notify方法。一定要确保使用正确的版本-await、signal
54、signal比signalAll更高效,它能极大的减少在每次缓存操作中发生的上下文切换和锁请求的次数
55、在使用显式的Condition和内置条件队列之间进行选择时,与在ReentrantLock和synchronized之间的选择一样:如果需要一些高级的功能,例如使用公平的队列操作或者在每个线程对应多个等待的线程集,那么应该
优先使用Condition而不是条件队列
56、在FutureTask中,AQS的同步状态被用来保存任务的状态,例如,正在运行、已完成或已取消。FutureTask还维护一些额外的状态变量,用来保存计算结果或者抛出异常。此外,还维护了一个引用,指向
正在执行计算任务的线程(如果它当前处于运行状态),因而如果任务取消,该线程会中断。
57、即使原子变量没有用于非阻塞算法的开发,它们也可以用作一种“更好的volatile变量”。原子变量提供了与volatile类型变量相同的内存语义,此外还支持原子的更新操作,从而使它们更加适用于计数器、序列发生器和统计数据收集等
,同时还能比基于锁的方法提供更高的可伸缩性。
58、现在,几乎所有的现代处理器中都包含了某种形式的原子读改写指令。
59、虽然Java语言的锁定语法比较简洁,但JVM和操作在管理锁时需要完成的工作却并不简单。在实现锁定时需要遍历JVM中一条非常复杂的代码路径,并可能导致操作系统级的锁定、线
程挂起以及上下文切换等操作。
CAS的主要缺点是,它将使调用者处理竞争问题(通过重试、回退、放弃),而在锁中能自动处理竞争问题(线程在获得锁之前将一直阻塞)。
60、一个很管用的经验法则是:在大多数处理器上,在无竞争的锁获取和释放的“快速代码路径”上的开销,大约是CAS开销的两倍。
61、性能比较:锁与原子变量
在高度竞争的情况下,锁的性能将超过原子变量的性能,但在更真实竞争的情况下,原子变量的性能将超过锁的性能。这是因为锁在发生竞争时将挂起线程,从而降低了CPU的使用率
和共享内存总线上的同步通信量。另一方面,如果使用原子变量,那么发出调用的类负责对竞争进行管理。与大多数基于CAS的算法一样,使用原子变量的并发代码在遇到竞争时会
立即重试,这通常是一种正确的做法,但在激烈竞争环境下导致更多竞争。
任何一个真实的环境都不会除了竞争锁或原子变量,其他什么工作都不作。在实际情况中,原子变量的可伸缩性将高于锁,因为在应对常见的竞争程度时,原子变量的效率会更高。
锁与原子变量在不同竞争程度上的性能差异很好的说明各自的优势和劣势。在中低程度的竞争下,原子变量能提供更高的可伸缩性,而在高强度的竞争下,锁能够更有效的避免竞争。
如果能够避免共享状态,那么开销将会更小。我们可以提高处理竞争的效率来提高可伸缩性,但只有完全消除竞争才能实现真正的可伸缩性。
62、非阻塞算法:如果在某种算法中,一个线程的失败或挂起不会导致其他线程失败或挂起,那么这种算法就被称为非阻塞算法。
如果在算法的每个步骤都存在某个线程能够执行下去,那么这种算法也称为无锁算法。
如果在算法中仅将CAS用作线程之间的协调,并且能正确实现,那么它既是一种无阻塞算法,也是一种无锁算法。
63、非阻塞算法的所有特性:某项工作的完成具有不确定性,必须重新执行。
CAS使用的基本模式:在更新某个值存在不确定性,以及在更新失败时重新尝试。构建非阻塞算法的技巧在于:将执行原子修改的范围缩小到单个变量上。
64、在几乎所有的情况下,普通原子变量的性能都很不错,只有在很少的情况下才需要使用原子的域更新器。如果在执行原子更新的同时还需要维持现有类的串行化形式,那么原子的
域更新器将非常有用。
65、解决ABA问题相对简单的解决方案:不是更新某个引用的值,而是更新两个值,包括一个引用和一个版本号。即使这个值由A变为B,又由B变成A,版本号也将使不同的。
AtomicStampedReference以及AtomicMarkableReference支持在两个变量上执行原子条件的更新。AtomicStampedReference将更新一个“对象-引用”二元组,通过在引用上加上版本
从而避免ABA问题。类似的,AtomicMarkableReference将更新一个“对象引用-布尔值”二元组,在某些算法中将通过这种二元组使节点保存在链表中同时又将其标记为“已删除的
节点“。
66、非阻塞算法通过底层的并发原语(例如比较并交换而不是锁)来维持线程的安全性。这些底层的原语通过原子变量类向外公开,这些类也用作一种“更好的volatile变量”,从而为证书和对象引用提供原子的更新操作。
非阻塞算法在设计和实现时非常困难,但通常能够提供更高的可伸缩性,并能更好的防止活跃性故障的发生。在JVM从一个版本升级到下一个版本的过程中,并发性能的主要提升都来自于(JVM内部以及平台类库中)
对非阻塞算法的使用。
67、Java语言规范要求JVM在线程中维护一种类似串行的语义:只要程序的最终结果与在严格串行环境中执行的结果相同,那么上述所有操作都是允许的。
68、随着处理器变得越来越强大,编译器也在不断的改进:通过对指令重新排序来实现优化执行,以及使用成熟的全局寄存器分配算法。由于时钟频率越来越难以提高,因此许多处理器制造厂商都开始转而生产多核处理器,
因为能够提高的只有硬件并行性。
69、程序执行一种简单的假设:想象在程序中只存在唯一的操作执行顺序,而不考虑这些操作在何种处理器上执行,并且在每次读取变量时,都能获得在执行序列中(任何处理器)最近一次写入该变量的值。这种乐观的模
型被称为串行一致性。软件开发人员经常会错误的假设存在串行一致性,但在任何一款现代多处理器架构中都不会提供这种串行一致性,JMM也是如此。冯诺依曼模型这种经典的串行计算模型,只能近似描述现代多处
理器的行为。
70、在类库中提供的其他Happens-Before排序包括:
1将一个元素放入一个线程安全的容器的操作将在另一个线程从该容器获得这个元素操作之前执行
2在CountDownLatch上的倒数操作将在线程从闭锁上的await方法返回之前执行
3释放semaphore许可的操作将在从该Semaphore上获得一个许可之前执行
4Future表示的任务的所有操作将在从Future.get()中返回之前执行
5向Executor提交一个Runnable或Callable的操作将在任务开始执行之前执行
6一个线程到达CyclicBarrier或Exchanger的操作将在其他到达该栅栏或交换点的线程被释放之前执行。如果CyclicBarrier使用一个栅栏操作,那么到达栅栏的操作之前执行,而栅栏操作
又会在线程从栅栏中释放之前执行
71、造成不安全发布的真正原因,就是在“发布一个共享对象”与“另一个线程访问该对象”之前缺少一个Happens-Before排序包括:
72、除了不可变对象之外,使用被另一个线程初始化的对象通常都是不安全的,除非对象的发布操作是在使用该对象的线程开始使用之前执行。
73、如果线程A将X放入BlockingQueue,线程B从队列中获取X,那么可以确保B看到X与A放入的X相同。这是因为在BlockingQueue的实现中有足够的内部同步确保了put方法在take方法之前执行。同样,通过使用一个由锁保护共享
或者使用共享的volatile类型变量,也可以确保对该变量的读取操作和写入操作按照Happens-Before关系来排序。
74、静态初始化器是由JVM在类的初始化阶段执行,即在类被加载后并且被线程使用之前。
由于JVM将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。
因此无论是在被构造期间还是被引用时,静态初始化的对象都不需要显式的同步。
75、对于含有final域的对象,初始化安全性可以防止对对象的初始化引用被冲排序到构造过程之前。当构造函数完成时,构造函数对final域的所有写入操作,以及通过这些域可以到达的人和网变量的写入操作,
都将被“冻结”,并且任何获得该对象引用的线程都至少能确保看到被冻结的值。对于通过final域可到达的初始变量的写入操作,将不会与构造过程后的操作一起被冲排序。
76、初始化安全性只能保证通过final域可达的值从构造过程完成时开始的可见性。对于通过非final域可达的值,或者在构成过程完成后可能改变的值,必须采用同步来确保可见性。
77、volatile具备两种特性:1保证此变量对所有线程的可见性2禁止指令重排序优化
大多数场景下,volatile总开销比锁要低,我们再volatile与锁中选择的唯一判断依据仅仅是volatile的语义能否满足使用场景的需求。
78、long和double的非原子性协定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来执行,即允许虚拟机实现选择可以不保证64位数据类型的
load、store、read、write这四个操作的原子性
79、volatile、synchronized、final都能保证变量可见性
80、时间上的先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准
81、轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。
网友评论