本教程适用于JDK 8。本页面中描述的示例和实践不利用后续版本中引入的改进,并且可能使用不再可用的技术。
请参阅Java语言变化了解Java SE 9及后续版本中更新的语言特性的概述。
请参阅JDK发布说明了解所有JDK版本的新功能、增强功能以及已删除或弃用选项的信息。
同步是围绕着一个内部实体——内在锁(intrinsic lock)或监视器锁(monitor lock)来构建的。在同步的两个方面中,内在锁起着重要的作用:强制访问对象状态的独占性以及建立对可见性至关重要的happens-before关系。
每个对象都有一个与之关联的内在锁。根据约定,需要独占和一致访问对象字段的线程在访问之前必须先获取对象的内在锁,然后在完成访问后释放内在锁。线程在获取锁和释放锁之间被认为“拥有”内在锁。只要线程拥有内在锁,其他线程就无法获取相同的锁。其他线程在尝试获取锁时将被阻塞。
当一个线程释放内在锁时,该动作与后续对相同锁的获取之间建立了happens-before关系。
当线程调用同步方法时,它会自动获取该方法所属对象的内在锁,并在方法返回时释放该锁。即使方法的返回是由未捕获的异常引起的,锁也会被释放。
当调用静态同步方法时,你可能会想知道会发生什么,因为静态方法与类关联,而不是对象。在这种情况下,线程会获取与该类关联的Class
对象的内在锁。因此,对类的静态字段的访问受到一个与该类的任何实例的锁不同的锁的控制。
另一种创建同步代码的方法是使用同步语句。与同步方法不同,同步语句必须指定提供内在锁的对象:
public void addName(String name) { synchronized(this) { lastName = name; nameCount++; } nameList.add(name); }
在这个例子中,addName
方法需要同步对lastName
和nameCount
的修改,但同时也需要避免同步调用其他对象的方法。(在同步代码中调用其他对象的方法可能会导致在“活性”部分描述的问题。)如果没有同步语句,就必须有一个单独的非同步方法,只用于调用nameList.add
。
同步语句还可用于通过细粒度的同步来提高并发性能。例如,假设类MsLunch
有两个实例字段c1
和c2
,它们永远不会同时使用。对这些字段的所有更新都必须进行同步,但没有理由防止对c1的更新与对c2的更新交错进行——这样做会通过创建不必要的阻塞来减少并发性能。我们可以使用同步方法或使用与this
关联的锁来解决这个问题,而是创建两个对象来提供锁。
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++; } } }
请谨慎使用这个习语。您必须确信交叉访问受影响的字段是安全的。
回想一下,一个线程不能获得另一个线程拥有的锁。但是,一个线程可以多次获得同一个锁。这使得可重入同步成为可能。这种情况描述了在同步代码中直接或间接调用包含同步代码的方法,并且两组代码都使用相同的锁。如果没有可重入同步,同步代码将需要采取许多额外的预防措施,以避免线程导致自身阻塞。