Java Concurrency —— 《Java并发编程实战》读书笔记
1. 线程的优势: 发挥多处理器的强大能力; 建模的简单性; 异步事件的简化处理; 响应更灵敏的用户界面
线程带来的风险: 安全性问题(永远不发生糟糕的事情); 活跃性问题(某件正确的事情最终会发生, 死锁,饥饿,活锁); 性能问题
2. 框架通过在框架线程中调用应用程序代码将并发性引入到程序中. 在代码中将不可避免地访问应用程序状态, 因此所有访问这些状态的代码路径都必须是线程安全的. Timer, servlet和jsp, 远程方法调用RMI, Swing和AWT都将在应用程序之外的线程中调用应用程序的代码. 所以不仅框架的代码具有并发性, 程序代码也具有并发性.
3. 当多个线程访问同一个可变的状态变量时没有使用适合的同步, 那么程序就会出现错误. 有三种方式可以修复该问题:
- 不在线程间共享该状态变量
- 将状态变量修改为不可变的变量
- 在访问状态变量时使用同步
4. 当多个线程访问某个类时, 不管运行时环境采用何种调度方式或者运行这些线程将如何交替执行, 并且在主调代码中不需要任何额外的同步或协同, 这个类都能表现出正确的行为, 那么就称这个类时线程安全的.
5. 一般的Servlet是线程安全的, 因为它们既不包含任何域, 也不包含任何对其他类中域的引用. 无状态兑现一定是线程安全的.
6. 竞态条件(Race Condition) : 由于不恰当的执行时序而出现不正确的结果. 当某个计算的正确性取决于多个线程的交替执行时序性时, 就会发生竞态条件. 换句话说, 就是正确的结果取决于运气. 最常见的竞态条件类型就是"先检查后执行(Check-Then-Act)", 即通过一个可能失效的观测结果来决定下一步的动作. 还有++i操作(读取-修改-写入).
1 /* 非线程安全的懒加载示例 */ 2 public class LazyInitRace { 3 private Object obj = null; 4 5 /*线程A,B同时执行该函数, A看到obj为空, 因而创建一个实例. B判断obj是否为空时, 结果取决于不可预测的时序, 6 包括线程的调度方式, 以及A需要花多长时间来初始化Object并设置obj */ 7 public Object getInstance() { 8 if (obj == null) { //先检查后执行, 不是线程安全的. 9 obj = new Object(); 10 } 11 return obj; 12 } 13 }
7. 假设有两个操作A和B, 如果从执行A的线程来看, 当另一个线程执行B时, 要么B将全部执行完, 要么完全不执行B, 那么A和B对彼此来说是原子的. 原子操作是指, 对于访问同一个状态的所有操作(包括该操作本身)来说, 这个操作是一个以原子方式执行的操作.
8. "先检查后执行"和"读取-修改-写入"等操作统称为复合操作: 包含一组必须以原子方式执行的操作以确保线程安全性.
9. 当在无状态的类中添加一个状态时, 如果该状态完全由线程安全的对象来管理, 那么这个类仍然是线程安全的.
10. 以关键字 synchronized 来修饰的方法是一种横跨整个方法体的同步代码块, 其中该同步代码块的锁就是方法调用所在的对象, 静态的 synchronizd 方法以 Class 对象作为锁.
11. 每个Java对象都可以用做一个实现同步的锁, 这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock). 线程在进入同步代码块之前会自动获得锁, 并且在退出同步代码块时自动释放锁, 无论是通过正常的控制路劲退出, 还是通过才代码块中抛出异常退出. 获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法.
12. 内置锁相当于一种互斥体(或互斥锁), 这意味着最多只有一个线程能持有这种锁. 所以性能较差.
13. 当某个线程试图获取一个由其他线程持有的锁时, 该线程会阻塞. 由于内置锁是可重入的, 因此如果某个线程视图获取一个已经由它自己持有的锁, 那么这个请求就会成功.
1 public class ReentrantLockTest { 2 public static void main(String[] args) { 3 foo(); 4 } 5 6 public static synchronized void bar() { 7 System.out.println("bar"); 8 } 9 10 public static synchronized void foo() { 11 /*当线程进入该方法体时, 就获得了内置锁. 当该线程调用bar()时要求获得内置锁, 12 而该内置锁已经获得了, 所以bar方法不会阻塞. 13 如果内置锁不可重入, 在此处调用bar()时会导致死锁, 因为调用foo()时获得了锁, 14 而bar()也需要内置锁, 这样bar()就会阻塞, 进而导致foo()也阻塞, 且都将永远阻塞下去*/ 15 bar(); 16 System.out.println("foo"); 17 } 18 }
14. 对于包含多个变量的不变性条件, 其中涉及的所有变量都需要由同一个锁来保护.
15. 在没有同步的情况下, 编译器,处理器以及运行时(还有内存)等都有可能对指令进行重排序.
16. Java内存模型要求, 变量的读取和写入操作都必须是原子操作, 但对于非volatile类型的long和double变量, JVM允许将64位的读操作和写操作分解为两个32为操作.(但规范强烈建议不要这么做, 事实上绝大部分商业的虚拟机对64位的读写操作也是原子的)
17. 加锁的意义不仅仅局限于互斥行为, 还包括内存可见性. (加锁时, 线程本地内存失效, 从主内存读取数据; 释放锁时, 将本地内存同步到主内存)
18. Java语言提供了一种稍弱的同步机制, 即volatile变量. 访问volatile变量时不会加锁, 因此不会造成阻塞. 在当前大多数处理器架构上, 读取volatile变量的开销只比读取非volatile变量的开销稍高一些.
19. 仅当volatile变量能简化代码的实现以及对同步策略的验证时, 才应该使用它们. 如果在验证正确性时需要对可见性进行复杂的判断, 那么就不要使用volatile变量. volatile变量的正确使用方式包括: 确保它们自身状态的可见性, 确保它们所引用对象的状态的可见性, 以及标识一些重要的程序生命周期事件的发生(例如, 初始化和关闭). volatile变量通常用作某个操作完成,发生中断或者状态的标识.
20. 加锁机制既可以确保可见性又可以确保原子性, 而volatile变量只能确保可见性.
21. 仅当满足以下所有条件时, 才应该使用volatile变量:
- 对变量的写入操作不依赖变量的当前值, 或者你能确保只有单个线程更新变量的值
- 该变量不会与其他状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁
22. 发布(Publish)一个对象是指, 是对象能够在当前作用域之外的代码中使用. 例如将一个指向该对象的引用传递到其他类的方法中. 当某个不应该发布的对象被发布时, 就称为逸出(Escape). 例如在对象构造完成前就发布该对象.
23. 假定有一个类C, 对于C来说, 外部(Alien)方法是指行为并不完全由C来规定的方法, 包括其他类中定义的方法以及类C中可以被改写的方法(既不是private方法也不是final方法). 当把一个对象传递给某个外部方法时, 就相当于发布了这个对象. 当发布一个对象时, 在该对象的非私有域中引用的所有对象同样会被发布.
24. 不要在构造过程中使this引用逸出. 如在构造函数中启动一个线程, 或者调用一个可改写的实例方法.
25. 当访问共享的可变数据时, 通常需要使用同步.一种避免使用同步的方式就是不共享数据. 如果仅在单线程内访问数据, 就不需要同步. 这种技术被称为线程封闭.
26. Ad-hock线程封闭是指, 维护线程封闭性的职责完全由程序实现来承担. Ad-hock线程封闭是非常脆弱的, 因为没有任何一种语言特性能将对象封闭到目标线程上. 所以在程序中尽量少用它, 在可能的情况下, 应该使用更强的线程封闭技术. 如栈封闭或ThreadLocal类.
27. 栈封闭是线程封闭的一种特例, 在栈封闭中, 只能通过局部变量才能访问对象. 局部变量的固有属性之一就是封闭在执行线程中. 不要发布对象的引用, 这样会破坏封闭性. 如果在线程内部(Within-Thread)上下文中使用非线程安全的对象, 那么该对象仍是线程安全的. 当如果没有明确说明, 后期维护人员可能会将这些对象逸出. 栈封闭比Ad-hock线程封闭更易于维护, 也更加强壮.
28. 维护线程封闭性的一种更规范的方法是使用ThreadLocal. Struts2中使用了该方式. ThreadLocal对象通常用于防止对可变的单实例变量或全部变量进行共享.
1 private static ThreadLocal<Connection> connectionHolder 2 = new ThreadLocal<Connection>() { 3 protected Connection initialValue() { 4 try { 5 return DriverManager.getConnection("http://localhost:3306/test"); 6 } catch (SQLException e) { 7 e.printStackTrace(); 8 } 9 return null; 10 }; 11 }; 12 13 // 每个线程中保存一个数据库连接 14 public static Connection getConnection() { 15 /* 变量保存在当前线程的一个类型为ThreadLocal.ThreadLocalMap的threadLocals中 16 且以当前Threadlocal实例为key */ 17 return connectionHolder.get(); 18 }
29. 不可变对象一定是线程安全的. 当满足以下条件时, 对象才是不可变的:
- 对象创建以后其状态就不能修改
- 对象所有的域都是final类型
- 对象时正确创建的(在对象构造期间, this引用没有逸出)
30. 要安全的发布一个对象, 对象的引用以及对象的状态必须同时对其他线程可见. 一个正确构造的对象可以通过以下方式来安全的发布:
- 在静态初始化函数中初始化一个对象引用.
- 将对象的引用保存到volatile类型的域或者AtomicReference对象中
- 将对象的引用保存到某个正确构造对象的final类型域中
- 将对象的引用保存到一个由锁保护的域中
31. Java线程安全库中的容器类提供了一些的安全发布保证
- 通过将一个键或者值放入HashTable, synchronizedMap或者 ConcurrentMap中, 可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)
- 通过将某个元素放入Vector, CopyOnWriteArrayList, CopyOnWriteSet, synchronizedList或 synchronizedSet中, 可以将该元素安全地发布到任何从这些容器中访问该元素的线程.
- 通过将某个元素放入BlockingQueue或者 ConcurrentLinkedQueue中, 可以将该元素安全地发布到任何从这些队列中访问该元素的线程.
32. 对象的发布需求取决于它的可变性:
- 不可变对象可以通过任意机制来发布
- 事实不可变对象必须通过安全方式来发布
- 可变对象必须通过安全方式来发布, 并且必须是线程安全的或者由某个锁保护起来
33.