首页 > 代码库 > 多线程(二)构建线程安全的类

多线程(二)构建线程安全的类

  • 构建线程安全的类

    • volatile

      • 只保证数据可见性,不保证数据同步。也就是说,JVM只保证使用了volatile的数据变更后对所有线程暴露最新值,并不会对线程内部缓存数据的操作多限制。多线程同时变更某个共享的volatile数据并不会产生正确结果。

    • 在满足以下条件时,可使用volatile:

      • 1.写入变量时并不依赖变量当前值;或者能确保只有单一的线程修改变量的值。

      • 2.变量不需要与其他变量共同参与不变约束。

      • 3.访问变量时,没有其他原因需要加锁。

    • long和double

      • 64位数据,分为上下32位两部分,其操作不是原子操作。

    • 发布

      • 一个对象能够被当前范围之外的代码所使用。

      • 不要在构造函数中发布对象和启动线程。

    • 逸出

      • 一个对象在尚未准备好时就被发布了。此时类对象可能已经分配了空间(不为null),但是内部数据还未完成初始化。

    • 安全发布模式

      • 通过静态初始化器初始化对象引用;

      • 将它的引用存储到volatile或者atomicReference;

      • 将它的引用存储到正确创建的final域中,或者存储到由锁保护的域中。

    • 安全共享对象

      • 线程限制:一个线程限制的对象,被线程独占,且只能被占有它的线程修改(比如ThreadLocal类型数据)。

      • 共享只读:一个共享的只读对象。

      • 共享线程安全:一个线程安全的对象在内部进行同步,所以其他线程无需额外同步就可以访问。

      • 被守护的(guarded):被锁保护的已发布对象。

    • 线程安全的组合对象

      • 制订同步策略(如何在不违反不可变约束的情况下,协调其他线程对对象的访问。)并文档化。

      • 实例限制

        • 对象被封装在另一个对象中(如hashset中的对象),把对被封装对象的访问限制转换为对封装对象(hashset实例)的封装数据访问路径的限制。

      • java监视器模式(java内部锁)

        • 通过锁来实现。

      • 委托线程安全

        • 如果一个类由多个彼此独立的线程安全的状态变量组成,而且类的操作不包含任何无效状态的转换(比如check-then-act),可以讲线程安全委托给这些状态变量。

      • 扩展线程安全的类

        • 以组合对象的内部锁代替对待扩展对象的锁模拟。

  • 避免活跃度风险

    • 死锁

      • 锁顺序死锁(lock-ordering deadlock)

        • 如果所有线程以通用的固定顺序获得锁,就不会出现锁顺序死锁。

        • method(A,B)参数所代表的实例可能是同一实例但是顺序不同。比如转账,从我-他和从他-我时,锁顺序是不同的。

      • 资源死锁(resource deadlock)

        • 线程互相等待和持有对方所需的资源

      • 协作对象间死锁

        • 在持有锁的时候调用外部方法是在挑战活跃度问题。外部方法可能会获得其他锁(产生死锁风险),或者遭遇严重超时阻塞。当你持有锁的时候会延迟其他视图获得该锁的线程。

    • 开放调用

      • 不会产生锁问题。

    • 避免和诊断死锁

      • 尝试定时的锁

        • 如果锁是在嵌套方法中获取的,你无法仅仅释放外层的锁。

      • 通过线程转储(thread dump)分析死锁

        • unix:kill-3或者ctrl-\,windows:ctrl-break。jdk版本5下显示的lock不会转储,jdk版本6下会有粗略的信息

    • 其他活跃度危险(、丢失信号、活锁)

      • 饥饿(starvation)

        • 无尽等待资源。

      • 弱响应性

        • 与其他线程竞争资源造成响应性不良。

      • 活锁(livelock)

        • 未发生死锁但是由于不断重复错误场景而造成无法响应,通过随机等待和撤回来避免。

  • 性能和可伸缩性

    • 避免不成熟的优化:首先使程序正确,然后再加快。

    • 阿姆达尔定律(amdahl定律)

      • S=1/(1-A+A/N)

      • A为并行计算部分所占比例,N为并行处理节点数。

      • 1-a=0(只有并行)最大加速比为N;

      • 当a=0时(只有串行)最小加速比S=1;

      • 当N趋近于无穷大时,计算加速比s->1/(1-A),也就是加速比上限。

    • 线程引入的开销

      • 切换上下文:竞争CPU和数据缓存

      • 内存同步:存储关卡(memory barrier)指令-刷新缓存,使缓存无效,刷新硬件的写缓冲,并延迟执行的传递(存储关卡中,大多数操作不能被重排序)。

      • 阻塞:被阻塞的线程可能自旋等待或者挂起,自旋更适合短期等待,挂起适合长期等待。

      • 大多数通用处理器中,上下文切换开销相当于5k-10k个时钟周期,或者几微秒。

    • 减少锁竞争(锁被请求频率*锁的持有时间)

      • 减少持有锁的时间。

      • 减少请求锁的频率或者协调机制取代独占锁,从而提高并发性。

      • 缩小锁的范围:把与锁无关代码移出synchronized块,尤其是阻塞操作,如I/O操作。

      • 减小锁的粒度:通过分拆锁(lock splitting)、分离锁(lock striping)实现。但是需要对容器进行加锁的独占的访问时,会更困难,比如ConcurrentHashMap的值需要扩展、重排时。

      • 避免热点域

      • 独占锁的替代:并发容器,读写锁, 不可变对象,原子变量。

    • 检测CPU利用率

      • 不充足的负载

      • IO限制:可以通过iostat或者perfmon判断一个程序是否受限于磁盘,或者通过网络监测是否受限于带宽。

      • 外部限制:比如数据库、webservice等。

      • 锁竞争。

      • 对象池:需要考虑频繁创建对象带来的gc压力与池技术带来的锁切换之间的性能对比。

本文出自 “JAVA技术栈笔记” 博客,请务必保留此出处http://stroll.blog.51cto.com/11038467/1854528

多线程(二)构建线程安全的类