【笔记】java并发编程实战
1. 线程带来的问题:a)安全性问题b)活跃性问题c)性能问题
2. 要编写线程安全的代码其核⼼在于要对状态访问操作进⾏管理,特别是对共享的和可变的状态的访问
3. Java中的主要同步机制是关键字synchronized,它提供了⼀种独占的加锁⽅式,”同步”这个术语还包括volatile类型的变量,显⽰锁以
及原⼦变量
4. 在编写并发应⽤程序时,⼀种正确的编程⽅法是:⾸先使代码正确运⾏,然后在提⾼代码的速度。
5. 完全有线程安全类构成的程序并不⼀定就是线程安全的,⽽在线程安全类中也可以包含⾮线程安全的类
6. 线程安全性:当多个线程访问某个类时,这个类始终都能表现出正确的⾏为,那么就称这个类是线程安全的
7. ⽆状态对象⼀定是线程安全的
8. ⽆状态对象、原⼦性、竟态条件、符合操作
9. 当某个计算的正确性取决于多个线程的交替执⾏时序时,那么就会发⽣竟态条件。最常见的竟态条件类型就是”先检查后执⾏”操作,即
通过⼀个可能失效的观测结果来决定下⼀步的动作
10. 计数器,可以通过现有的线程安全类实现如AtomicLong
11. 在实际情况中,应尽可能地使⽤现有的线程安全对象(如AtomicLong)来管理类的状态
12. 同步代码块包括两部分:⼀个作为锁的对象引⽤,⼀个作为由这个锁保护的代码块。
13. 重⼊意味着获取锁的操作粒度是”线程”,⽽不是”调⽤”
14. 每个共享的和可变的变量都应该只由⼀个锁来保护,从⽽是维护⼈员知道是那⼀个锁
15. ⼀种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进⾏同步,使得
在该对象上不会发⽣并发访问,如Vector
16. 并⾮所有数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要通过锁来保护
java笔记总结
17. 对于每个包含多个变量的不可变性条件,其中涉及的所有变量都需要由同⼀个锁来保护
18. ⽆论是执⾏计算密集的操作,还是在执⾏某个可能阻塞的操作,如果持有锁的时间过长,那么都会带来活跃性或性能问题,当执⾏时
间较长的计算或者可能⽆法快速完成的操作时(如I/O),⼀定不要持有锁
19. 只要有数据在多个线程间共享,就使⽤正确的同步
20. 可见性问题,产⽣失效值,⾮原⼦的64位操作问题,使⽤volatile声明或同步保护
21. 当把变量声明为volatile类型后,编译器与运⾏时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作⼀起重排
序。
22. Volatile变量的正确的使⽤⽅式包括:确保它们⾃⾝状态的可见性,确保它们所引⽤对象的状态的可见性,以及标识⼀些重要的程序⽣
命周期事件的发⽣(例如,初始化或关闭)
23. 调试提⽰,在启动JVM时指定 –servcr命令,将进⾏更多优化,⽐如将循环中未被修改的变量提升到循环外部,发现⽆线循环
24. 加锁机制既可以确保可见性⼜可以确保原⼦性,⽽volatile变量只能确保可见性
25. 当且仅当满⾜⼀下所有条件时,才应该使⽤volatile变量:a)对变量的写⼊操作不依赖变量的当前值,或者能确保只有单个线程更新变
量的值b)该变量不会与其他状态变量⼀起纳⼊不变性条件中c)在访问变量时不需要加锁
26. 当访问共享的可变数据时,通常需要使⽤同步。⼀种避免使⽤同步的⽅式就是不共享数据。如果仅在单线程内访问数据,就不需要同
步。这种技术被称为线程封闭
27. 线程封闭技术:Ad-hoc/栈封闭/ThreadLocal类
28. 满⾜同步需求的另⼀种⽅法是使⽤不可变对象:某个对象在被创建后其状态就不能被修改。不可变对象只有⼀种状态,且有构造函数
来控制
29. 不可变:a)状态不可修改b)所有域都是final类型c)正确的构造过程
30. 可变对象必须通过安全的⽅式来发布,这通常意味着在发布和使⽤该对象的线程时都必须使⽤同步
31. 要安全地发布⼀个对象,对象的引⽤以及对象的状态必须同时对其他线程可见。⼀个正确构造的对象可以通过以下⽅式来安全的发
布:a)在静态初始化函数中初始化⼀个对象引⽤b)将对象的引⽤保存到volatile类型的域或者AtomicReferance对象中c)将对象的引⽤保存到某个正确构造对象的final类型域中d)将对象的引⽤保存到⼀个由锁保护的域中
32. 通过容器安全发布对象:a)通过将⼀个键或者值放⼊Hashtable、synchronizedMap或者ConcurrentMap中b)通过将某个元素放⼊
Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中c)通过将某个元素放⼊
BlockingQuere或者ConcurrentLinkedQuere中
33. 当获得对象的⼀个引⽤时,需要知道在这个引⽤上可以执⾏哪些操作。在使⽤它之前是否需要获得⼀个锁,是否可以修改它的状态,
或者只能读取它
34. 在并发程序中使⽤和共享对象时,可以使⽤⼀些实⽤的策略:a)线程封闭b)只读共享c)线程安全共享d)保护对象
35. 在设计线程安全类的过程中,需要包含⼀下三个基本要素:a)出构成对象状态的所有变量b)出约束状态变量的不变性条件c)建⽴对
象状态的并发访问管理策略
36. 等待某个条件为真的各种内置机制(包括等待和通知等机制)都与内置加锁机制紧密关联
37. 将数据封装在对象内部,可以将数据的访问限制在对象的⽅法上,从⽽更容易确保线程在访问数据时总能持有正确的锁
38. 使⽤私有的锁对象⽽不是对象的内置锁(或任何其他可通过公有⽅式访问的锁),可以将锁封装起来,使客户代码⽆法得到锁,但客户代
码可以通过共有⽅法来访问锁
39. 如果⼀个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就
可以安全的发布这个变量
40. Synchronized、volatile或者任何⼀个线程安全类都对应于某种同步策略,⽤于在并发访问时确保数据的完整性
41. 在设计同步策略时需要考虑多个⽅⾯,例如,将哪些变量声明为volatile类型,哪些变量⽤锁来保护,哪些锁保护那些变量,哪些变量
必须是不可变的或者被封闭在线程中的,哪些操作必须是原⼦操作等。
42. servletContext、Httpsession或dataSource等的线程安全性
43. 同步容器将所有对容器状态的访问都穿⾏化,以实现它们的线程安全性。这种⽅法的代价是严重降低并发性,当多个线程竞争容器的
锁时,吞吐量将严重减低
44. 通过并发容器来代替同步容器,可以极⼤的提⾼伸缩性并降低风险;ConcurrentHashMap、CopyOnWriteArrayList、
CopyonWriteArraySet、BlockingQueue
45. 阻塞队列可以作为同步⼯具类,其他类型的同步⼯具类还包括信号量(Semaphore)、栅栏(Barrier)以及闭锁(Latch)
46. 闭锁可以延迟线程的进度直到其到达终⽌状态,可以⽤来确保某些活动直到其他活动都完成偶才继续执⾏
47. FutureTask表⽰的计算是通过Callable来实现的,相当于⼀种可⽣成结果的Runnable,FutureTask在Executor框架中表⽰异步任务,
此外还可以⽤来表⽰⼀些时间较长的计算
48. 计数信号量⽤来控制同时访问某个特定资源的操作数量,或者同时执⾏某个指定操作的数量,可以⽤于实现资源池,例如数据库连接
池
49. 栅栏类似于闭锁,它能阻塞⼀组线程直到某个事件发⽣。栅栏与闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执
⾏,闭锁⽤于等待事件,⽽栅栏⽤于等待其他线程
50. 并发技巧:a)可变状态是⾄关重要的b)尽量将域声明为final类型,除⾮需要它们是可变的c)不可变对象⼀定是线程安全的d)封装有助于
管理复杂性e)⽤锁来保护每个可变变量f)当保护同⼀个不变性条件中的所有变量时,要使⽤同⼀个锁g)在执⾏复合操作期间,要持有锁
h)如果从多个线程中访问同⼀个可变变量时没有同步机制,那么程序会出现问题i)不要故作聪明的推断出不需要使⽤同步j)在设计过程
中考虑线程安全,或者在⽂档中明确地指出它不是线程安全的k)将同步策略⽂档化
51. 在线程池中执⾏任务⽐为每个任务分配⼀个线程优势更多:a)重⽤线程,分摊在线程创建和销毁过程中产⽣的巨⼤开销b)请求到达时,
⼯作线程已存在,不会由于等待创建线程⽽延迟任务的执⾏,提⾼了响应性c)通过调整线程池⼤⼩,可
以创建⾜够多的线程以便使处理器保持忙碌状态,同时还可以防⽌过多线程相互竞争资源⽽使应⽤程序耗尽内存或失败
52. 通过使⽤Executor,可以实现各种调优、管理、监视、记录⽇志、错误报告和其他功能,如果不使⽤任务执⾏框架,那么要增加这些
功能是很困难的
53. Executor框架将任务提交与执⾏策略解耦开来,同时还⽀持多种不同类型的执⾏策略。当需要创建线程来执⾏任务时,可以考虑使⽤
Executor
54. 在java中没有⼀种安全的抢占式⽅法来停⽌线程,因此也就没有安全的抢占式⽅法来停⽌任务。只有⼀些协作式的机制,使请求取消
的任务和代码都遵循⼀种协商好的协议:a)”已请求取消”标志
55. 对中断操作的正确理解是:它并不会真正地中断⼀个正在运⾏的线程,⽽只是发出中断请求,然后由线程在下⼀个合适的时刻中断⾃
⼰(这些时刻也被称为取消点),通常,中断是实现取消的最合理⽅式
56. 最合理的中断策略是某种形式的线程级(Thread-Level)取消操作或服务级(Service-Level)取消操作:尽快退出,在必要时进⾏清理,通
知某个所有者线程已经退出
57. 任务不会在其⾃⼰拥有的线程中执⾏,⽽是在某个服务(如线程池)拥有的线程中执⾏,这就是⼤多数可阻塞的库函数都只是抛出
interruptedException作为中断响应,它们永远不会在某个由⾃⼰拥有的线程中运⾏,因此它们为任务或库代码实现了最合理的取消策略:尽快退出执⾏流程,并把中断信息传递给调⽤者,从⽽使调⽤栈中的上层代码可以采取进⼀步的操作
58. 当取消⼀个⽣产者-消费者操作时,需要同时取消⽣产者和消费者
59. 关闭钩⼦是指通过Runtime.addShutdownHook注册的但尚未开始的线程
60. 线程可分为两种:普通线程和守护线程。在JVM启动时创建的所有线程中,除了主线程以外,其他的线程都是守护线程(例如垃圾回收
器以及其他执⾏辅助⼯作的线程)。当创建⼀个新线程时,新线程将继承它的线程的守护状态,因此在默认情况下,主线程创建的所有线程都是普通线程。普通线程与守护线程之间的差异仅在于当线程退出时发⽣的操作
61. 死锁:过度加锁可能导致”锁顺序死锁”,使⽤线程池和信号量来限制对资源的使⽤,可能会导致”资源死锁”
62. 在并发程序中,对可伸缩性的最主要威胁就是独占⽅式的资源锁
63. 有两个因素将影响在锁上发⽣竞争的可能性:锁的请求频率,以及每次持有该锁的时间
64. Amdahl定律告诉我们,程序的可伸缩性取决于在所有代码中必须被串⾏执⾏的代码⽐例
65. 提升可伸缩性可通过:减少锁的持有时间,降低锁的粒度,以及采⽤⾮独占的锁或⾮阻塞锁来代替独占锁
66. 当某个类第⼀次被加载时,JVM会通过解释字节码的⽅式来执⾏它。在某个时刻,如果⼀个⽅法运⾏的次数⾜够多,那么动态编译器
会将它编译为机器代码,当编译完成后,代码的执⾏⽅式将从解释执⾏变为直接执⾏
67. 当持有锁的时间相对较长,或者请求锁的平均时间间隔较长,那么应该使⽤公平锁。在这些情况下,”插队”带来的吞吐量提升则可能不
会出现
68. 在⼀些内置锁⽆法满⾜需求的情况下,ReentrantLock可以作为⼀种⾼级⼯具。当需要⼀些⾼级功能时才应该使⽤ReentrantLock,这
些功能包括:可定时的、可轮询的与可中断的锁获取操作,公平队列,以及⾮块结构的锁。否则,还是应该优先使⽤synchronized
69. 读/写锁:⼀个资源可以被多个读操作访问,或者被⼀个写操作访问,但两者不能同时进⾏
70. 可以使⽤java语⾔和类库提供的底层机制来构造⾃⼰的同步机制,包括内置的条件队列、显⽰的Condition对象以及
AbstractQueuedSynchronized框架
71. 当使⽤条件等待时(Object.wait或Condition.await):a)通产都有⼀个条件谓词-包括⼀些对象状态的测试,线程在执⾏前必须⾸先通过这
些测试b)在调⽤wait之前测试条件谓词,并且从wait中返回时再次进⾏测试c)在⼀个循环中调⽤wait d)确保使⽤与条件队列相关的锁来保护构成条件谓词的各个状态变量e)当调⽤wait、notify或notifyAll等⽅法时,⼀定要持有与条件队列相关的锁f)在检查条件谓词之后以及开始执⾏相应的操作之前,不要释放锁
72. 活跃性故障:死锁、活锁、丢失的信号。丢失的信号:线程必须等待⼀个已经为真的条件,但在开始等待之前没有检查条件谓词
73. ⼤多数情况下应该有限选择notifyAll⽽不是单个的notify。只有同时满⾜两个条件时,才能⽤单⼀的notify⽽不是notifyAll:a)所有等待线
程的类型都相同,只有⼀个条件谓词与条件队列相关,并且每个线程在从wait返回后将执⾏相同的操作b)单进单出,在条件变量上的每次通知,最多只能唤醒⼀个线程来执⾏
74. 对于每个依赖状态的操作,以及每个修改其他操作依赖状态的操作,都应该定义⼀个⼊⼝协议和出⼝协议,⼊⼝协议就是该操作的条
件谓词,出⼝协议则包括:检查被该操作修改的所有状态变量,并确认它们是否使⽤某个其他的条件谓词变为真,如果是,则通知相关的条件队列
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论