并发编程反模式

整理自java并发编程实践第12章 12.4测试方法补遗

 

不连贯的同步性:

为了同步某个对象或者对象本身的某个域的访问,使用同步锁(内部锁或者显式锁,例如,对象本身的内部锁)来保护同步对象.但是如果访问该同步对象没有一贯性地通过同步锁获得访问,就意味着同步策略没有一贯地执行.通俗地讲就是在访问同步对象的时候,采用了双重标准,既有同步的操作也有非同步的操作,破坏了同步的连贯性和完整性.

 

直接调用Thread.run

直接调用Thread的run方法几乎总是个错误,应该调用Thread.start

 

未释放的显式锁

不同于内部锁,显式锁在控制退出了它们被请求的访问的时候,不会自动地释放.标准的技巧是由final块中释放锁;否则如果产生异常,锁仍会保存未释放状态.

		try{
			
		}finally{
			//release lock
		}
 

空synchronized块

空的synchronized块在java存储模型下是有语义的,也是会被执行的.无论开发者试图去解决什么问题,通常会有更好的解决方案.

 

双重检查

在惰性初始化时候,双重检查作为降低同步开销的技巧是有缺陷的.涉及的问题是当读取一个共享可变域的时候,缺乏适当的同步.

双重检查产生的原因是早期jvm缓慢的无竞争同步和缓慢的jvm启动.

双重检查的运作方式是:首先检查在没有同步的情况下,能够被多个线程共享的resource是否被初始化,如果resource不为null就使用它,否则进入初始化的同步块,并再次检查resource是否为null.需要初始化.以便确保只有唯一的线程或者唯一的一次真正地初始化了多个线程共享的resource.通常的代码路径是:获取一个已经构建的resource的引用,并没有用到同步.这就是问题所在,当前线程可能看到一个部分创建的resource.

双重检查的真正问题在于:基于这样的一种假设:当没有使用同步时读取一个共享变量,可能发生的最坏的事情不过是错误地看到过期值(具体而言是null);在这种情况下,通过获取锁后再次检查一次,希望能避免之前看到过期值的风险.但是糟糕的情况是获取到对象的过期值是有效的(具体而言是非空),而当前对象的状态是无效或者错误的(例如是null).这样就会认为对象已经创建成功,而得到一个实际上无效,错误或者为null的resource引用.

public class UnsafeLazyInit {

	private static UnsafeLazyInit resource;
	
	private final static Object initLock = new Object();
	
	private UnsafeLazyInit(){
		
	}
	
	public static UnsafeLazyInit getInstance(){
		if(null == resource){
			synchronized(initLock){
				if(null == resource){
					resource = new UnsafeLazyInit();
				}
			}
		}
		return resource;
	} 
}
 

Java5.0以后,如果把resource声明为volatile类型,双重检查方式就能够很好地工作,因为读取未经缓存过的resource引用.另外jvm做到读取volatile变量相比读取非volatile变量的性能开销并没有增加很多,因此这种方法的性能开销也很低.另外早期jvm缓慢的无竞争同步和缓慢的jvm启动的问题现在已经解决,这种优化的效果也越发不明显.

另外为了确保初始化的安全性,可以通过创建不可变的安全对象在没有同步的状态下,可以被安全地跨线程共享,而不用关心它们是何时发布的.

public class SafeLazyInit {

	private volatile static SafeLazyInit resource;
	
	private final static Object initLock = new Object();
	
	private SafeLazyInit(){
		
	}
	
	public static SafeLazyInit getInstance(){
		if(null == resource){
			synchronized(initLock){
				if(null == resource){
					resource = new SafeLazyInit();
				}
			}
		}
		return resource;
	} 
}
 

一个正确创建的对象,任何可以通过final域访问到的对象或者对象中的域,对看到它的线程都是实时,可见的.另外对于含有final域的对象,final域的初始化安全性可以抑制重排序,否则这些重排序会发生在对象的构造期间以及内部加载对象引用的时刻.

final域的对象只有在对象构造函数完成时才是可见的,对于非final域的对象,或者创建完成后可能改变的值,必须使用同步策略来确保其可见性.

下面的代码是构造安全的,虽然states是使用非线程安全的hashmap,但是在构造器中仍然是安全的,因为states是final域的,并且构造器中的代码是没有重排序,串行执行的.states的引用直到构造器完成之后才对其他线程可见.

import java.util.HashMap;
import java.util.Map;

public class SafeStatus {
	
	private final Map<String,String> states;
	
	public SafeStatus(){
		states = new HashMap<String,String>();
		states.put("color", "blue");
		states.put("length", "123");
		states.put("width", "456");
		states.put("weight", "789");
	}
}
 

由构造函数中启动线程

由构造函数中启动线程,会引入子类化问题的风险,同时还会引起this引用由构造函数中发布溢出.

 

通知错误

notify和notifyAll方法预示着,一个对象的状态可能已经以某种方式发生改变,进而通知那些等待与相关对象关联的条件队列的线程解除堵塞,重新获取条件. 因此只有在与条件队列关联的状态发生改变后才应该去调用这些方法.否则在没有修改任何状态的情形下调用这些方法,通常是一种错误.

 

条件等待错误

在一个队列中等待时,Object.wait和Condition.wait不仅应该在持有正确的锁的情况下,在循环中被调用,而且要在测试过一系列谓词之后.在不持有锁,不在循环中或者没有测试某些状态的情形下调用Object.wait和Condition.wait,通常是一种错误.

 

 

休眠或者等待的时候持有锁

调用Thread.sleep的时持有锁,会导致其他的线程在很长的一段时间内无法执行,因此这是一个潜在的严重的活跃度危险.调用Object.wait和Condition.wait时持有锁也会导致同样的问题.

 

自旋循环

如果代码除了循环检查(忙等待)一个域是否有期望值之外,不做任何事情,就会浪费cpu时间,并且如果域不是volatile类型的,就无法保证循环检查可以终止,因为可能会持有一个过期的状态.如果等待一个状态装换的发生,采用闭锁或者条件等待通常是更好的技术

 

 

老实说java并发编程实践这本书的翻译质量的确不高,不过原著内容写的还是极其精彩.看来接下来要读一些java存储模型方面的内容了.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值