首页 > 代码库 > 多线程(二)构建线程安全的类
多线程(二)构建线程安全的类
构建线程安全的类
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
多线程(二)构建线程安全的类