Java并发编程学习笔记3

83 阅读4分钟

 加锁机制

只有一个状态变量时,我们可以使用线程安全的对象来管理状态。当有多个状态变量呢?此时,用多个线程安全的对象来管理状态可能仍然不够。使用原子引用,我们不能同时更新 lastNumber 和 lastFactors,即使对 set 的每次调用都是原子的;当一个被修改而另一个没有被修改时,仍然存在漏洞窗口。在线程 A 获取这两个值的时间之间,线程 B 可能已经更改了它们。

为了保持状态一致性,在单个原子操作中更新相关的状态变量。

内置锁

Java 提供了一种内置的锁定机制来强制执行原子性:synchronized block。 同步块有两个部分:用对某个对象的引用来做为一个锁,以及一个由该锁保护的代码块。

synchronized (lock) {
// Access or modify shared state guarded by lock
}

为了同步的目的,每个 Java 对象都可以隐式地充当锁;这些内置锁称为内部锁或监视器锁。执行线程在进入同步块之前自动获取锁,并在控制退出同步块时自动释放,无论是通过正常控制路径还是通过从块中抛出异常。获取内部锁的唯一方法是进入由该锁保护的同步块或方法。

Java 中的内部锁充当互斥锁(或互斥锁),这意味着最多一个线程可能拥有该锁。当线程 A 试图获取线程 B 持有的锁时,A 必须等待或阻塞,直到 B 释放它。如果 B 从未释放锁,A 将永远等待。

可重入锁

所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁(虽然获取的锁并未被释放),而其他的线程是不可以的。即拥有对象锁的线程可以重复获取该对象锁而不发生死锁。

用锁来保护状态

在复合操作的整个持续时间内保持锁定可以使该复合操作成为原子操作。

然而,仅仅用同步块包装复合动作是不够的;如果使用同步来协调对变量的访问,则在访问该变量的任何地方都需要同步。此外,当使用锁来协调对变量的访问时,必须在访问该变量的任何地方使用相同的锁。一个常见的错误是认为只有在写入共享变量时才需要使用同步;这是不正确的。

对于可能被多个线程访问的每个可变状态变量,对该变量的所有访问都必须在持有相同锁的情况下执行。在这种情况下,我们说该变量由该锁保护。

获取与对象关联的锁并不会阻止其他线程访问该对象——唯一可以阻止的是任何其他线程获取同一个锁。

并非所有数据都需要锁保护——只有需要从多个线程访问的可变数据才需要。

如果同步是解决竞争条件的良方,为什么不将每个方法都声明为同步的呢?事实证明,这种不分青红皂白的 synchronized 应用可能是同步过多或过少

if (!vector.contains(element))
vector.add(element);

虽然同步方法可以使单个操作成为原子操作,但当多个操作组合成一个复合操作时,就需要额外的锁定。同时,同步每个方法会导致活性或性能问题。

活性和性能

从同步块中排除不影响共享状态的长时间运行的操作是合理的,以便在长时间运行的操作正在进行时不会阻止其他线程访问共享状态。

使用两种不同的同步机制会造成混淆,并且不会提供性能或安全优势。

获取和释放锁有一些开销,所以不希望将同步块分解得太远(例如将 ++hits 分解到它自己的同步块中),即使这不会损害原子性。

简单性和性能之间经常存在矛盾。在实施同步策略时,要抵制为了性能而过早牺牲简单性(可能危及安全性)的诱惑。

避免在耗时操作(例如网络或控制台 I/O)期间持有锁。