内部锁和同步
同步是使用被叫做内部锁或者监控锁(intrinsiclock or monitor lock.)的内部实体来实现的。(API文档里面经常简称做monitor)内部锁在同步的两个方面都扮演了重要的角色:强制对一个对象状态的独占性访问,建立“happens-before”关系,happens-before关系对可见性很重要。
每个对象都有一个与它相关的内部锁。通常,一个线程需要对一个对象的域进行独占性访问或者一致性访问的时候,这个线程就需要在访问前去获得这个对象的内部锁,然后在做完相关操作后释放锁。可以说一个线程在它请求和释放锁的这段时间拥有这个内部锁。只要一个线程拥有一个对象的内部锁,其他的对象就不能再拥有它,其他对象在尝试获得时会被阻塞。
当一个线程释放一个内部锁的时候,这个释放行为和后续对这个锁的获得行为就建立了“happens-before”关系。
同步方法中的锁
当一个线程调用一个同步方法的时候,它会自动取请求这个方法的对象所拥有的锁,方法return的时候自动释放。即便方法是因为一个未捕获异常而返回的锁依然会被释放。
你或许会疑惑一个静态的同步方法被调用会发生什么,因为static的方法是跟类相关的而不是跟实例相关。这种情况下,线程会去尝试获得与这个类相关的Class对象的内部锁。所以对类的静态域的访问和对实例的访问是受控于不同的锁。
同步块
另一种创建同步代码的方式是使用同步块,与使用同步方法不同,使用同步块时必须指定提供内部锁的对象。
public void addName(String name){
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}
在这个例子中,addName方法需要去同步对lastName和nameCount的改变,但是也需要防止对其他对象的方法的同步调用。(对其他对象的方法在同步块中调用可能产生生命性问题。)如果不用同步块的话,那就需要为只为了调用nameList.add(name)而创建一个单独的,非同步的方法。
同步块对于提高非常精细的同步的并发性也是很有用的。假设,类MsLunch有两个字段,c1和c2,他们不会被同时使用。对于两个字段的所有单独的更改操作都需要被同步,但是没有理由组织对c1的更改操作和对c2的更改操作交错执行,这样反而因为多余的阻塞而降低了并发。我们可以创建两个单独的对象来提供不同的锁:
public class MsLunch {
private long c1 = 0;
private long c2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1() {
synchronized(lock1) {
c1++;
}
}
public void inc2() {
synchronized(lock2) {
c2++;
}
}
}
使用这种方式需要特别小心,你必须绝对的肯定相关的方法可以被交错的调用。
折返同步
回想前面所说的一个线程不能获得另外一个线程已经拥有的锁。但是一个线程可以获得它自己已经拥有的锁。允许一个线程不只一次的获得它自己已经拥有的锁,这样能够保证折返同步。这描述了这样一个场景,一个同步的代码,直接或者间接的调用了另外一个同步方法,而这两个同步方法拥有同一个锁。如果没有折返同步的话,同步代码需要做更多额外的工作来保证它不被自己的锁阻塞。