CH05
- 对象发布:
- 发布: 使一个对象能够被当前范围之外的代码所使用
- 对象逸出: 当一个对象还没有构造完成就被其他线程所见
- 安全发布对象的四种方法:
- 在静态初始化函数中初始化一个对象引用
- 将对象的引用保存到volatile类型域或者AtomicReference对象中
- 将对象的引用保存到某个正确构造对象的final类型域中
- 将对象的引用保存到一个由锁保护的域中
- 懒汉式单例线程不安全的坏处: 如果初始化中有很多操作, 则重复的实例化会产生错误的状态. 要改成双重检测同步锁单例(这还不是线程安全的, 要考虑对象初始化和引用赋值因指令重排而顺序颠倒, 在第二次判断前可能被切换).为了解决该问题, 需要将引用变量变成volatile来禁止指令重排(volatile的使用场景)
- 饿汉式的坏处: 首先饿汉式是线程安全的, 但是当初始化中有很多操作时, 会拖慢类加载的速度, 且可能造成资源浪费
- 一定要注意静态块和静态方法的执行顺序(类加载时顺序执行)
- 使用枚举来做单例是最安全高效的, 同时拥有饿汉式和懒汉式的特性. 原理是在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的, 也就表明只能被实例化一次. 在调用构造方法时, 我们的单例被实例化. 也就是说,因为enum中的实例被保证只会被实例化一次, 所以我们的INSTANCE也被保证实例化一次.
CH06
- 不可变对象
- 需要满足的条件:
- 对象创建以后其状态就不能修改
- 对象所有域都是final类型
- 对象是正确创建的(在对象创建期间, this引用没有逸出)
- 实现方式(多参考String类型):
- 声明为final类
- 将成员变量声明为私有的
- 不设置set方法, get方法返回对象的深度拷贝
- 需要满足的条件:
- Integer等包装类和String 一旦声明成final后被修改, 编译时就会出现错误
- 引用类型用final修饰只是保证引用(或者说地址)不变, 但引用指向的对象还是存在被修改的风险
- Java有提供不可变的集合类型: Collections.unmodifiableXXX : Collection、List、Set等等, 但是编译可以通过, 只是会抛出异常
- Guava包也有提供 ImmutableXXX…
- 线程封闭: 把变量限制在当前线程里
- 方法:
- Ad-hoc: 程序员自己控制, 最糟糕的方式
- 堆栈封闭: 局部变量, 无并发问题
- ThreadLocal类: 用Map来维护不同线程. 经典应用: JDBC的连接成管理的Connection对象
- 方法:
- 常见类
- StringBuilder: 线程不安全, 快(同时有多个线程), 在线程封闭的场景下尽可能用StringBuilder
- StringBuffer: 线程安全, 每个方法都有synchronized, 慢
- DateformatExample: 不是线程安全的, 正确写法是在方法里每次生成新对象来使用
- DateTimeFormmatter: 线程安全的, 在joda-time的包中, 实际开发中更推荐
- ArrayList、HashSet、HashMap等Collections: 线程不安全的
- 线程不安全类的写法: 先检查后判断: if(condition(a)){handle(a);}, 原因是判断和操作两个操作不是原子操作, 需要用synchronized或CAS解决
- 同步容器:接近线程安全
- List -> Vector(使用它时可能还需要额外的同步措施, 因为它的size()方法没有synchronized), Stack, 都用synchronized 方法
- Map -> HashTable(key, value不能为null), 线程同步的
- Collections.synchronizedXXX(List、Set、Map)
- 尽量不要在迭代或迭代器里面做remove()操作, 在遍历时标记, 完了之后再remove()
- 并发容器(JUC): 读写分离和最终一致性原理
- CopyOnWriteArrayList: 新数组被修改时, 拷贝旧的, 分配新的再重新引用;整个操作都在锁中完成;适合读多改少的场景
- CopyOnWriteArraySet(对应HashSet)、ConcurrentSkipListSet(对应TreeSet): 特性同上,前者基于List, 后者基于Map
- CopyOnWriteHashMap(对应HashMap)、ConcurrentSkipListMap(对应TreeMap)
- 安全共享对象策略:
- 线程限制: 对象由线程独占
- 共享只读: 提供多个线程并发访问, 但是任何线程都不能修改它
- 线程安全对象: 对象内部就保证线程安全的
- 被守护对象: 被守护对象只能通过获取特定的锁来访问
多线程笔记
- 如何理解notify/notifyAll的作用? 首先理解有等待池(线程提出wait之后到这里来)和锁池(没争到锁的线程被阻塞到这里,可以竞争锁), notify/notifyAll可以看作是将等待迟里的线程搬到锁池