【The Java™ Tutorials】【Concurrency】4. Synchronization

为什么需要同步

Threads communicate primarily by sharing access to fields and the objects reference fields refer to. This form of communication is extremely efficient, but makes two kinds of errors possible: thread interference and memory consistency errors. The tool needed to prevent these errors is synchronization.

同步带来了新的问题

However, synchronization can introduce thread contention, which occurs when two or more threads try to access the same resource simultaneously and cause the Java runtime to execute one or more threads more slowly, or even suspend their execution. Starvation and livelock are forms of thread contention.

Synchronized Methods

Java中实现同步有两种方式:synchronized methodssynchronized statements。Synchronized statements 更复杂一些,这一小节先介绍简单的synchronized methods。

定义同步方法很简单,只要在函数定义中添加synchronized关键字:

public class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }
}

使用synchronized关键字,会产生两个影响:

  • 当一个线程调用了一个object的同步方法时,如果其他线程也调用该object的同步方法,那么这些线程会被阻塞直到第一个线程执行完同步方法。
  • When a synchronized method exits, it automatically establishes a happens-before relationship with any subsequent invocation of a synchronized method for the same object. This guarantees that changes to the state of the object are visible to all threads.

需要注意的是构造方法不能使用synchronized关键字,不然会产生语法错误。对构造方法使用synchronized关键字是没有意义的,因为只有一个线程会调用构造方法。

Warning: 当构造一个会被多个thread的引用的object时,要小心“过早泄漏”。比如你想把所有的对象保存到一个列表中,你在构造函数中调用了instances.add(this),但是其他线程可能在构造函数结束之前访问instances。

Intrinsic Locks and Synchronization

Synchronization is built around an internal entity known as the intrinsic lock or monitor lock.

Every object has an intrinsic lock associated with it. By convention, a thread that needs exclusive and consistent access to an object's fields has to acquire the object's intrinsic lock before accessing them, and then release the intrinsic lock when it's done with them.

Locks in Synchronized Methods

当一个线程调用一个synchronized method时,它会自动去申请该方法所属的对象的intrinsic lock,当该方法返回时会自动释放。由uncaught exception引起的返回也会自动释放。

你可能会想如果是调用一个static synchronized method会怎样呢,因为static方法是属于类的,而不是属于对象的,这时线程去向谁申请intrinsic lock呢?在这种情况下,线程向该类的Class object申请intrinsic lock。因此访问静态域使用的锁和访问实例域使用的锁是不一样的。

Synchronized Statements

与synchronized methods不同的是,synchronized statements必须指定使用哪个对象提供的intrinsic lock:

public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount++;
    }
    nameList.add(name);
}

在这个例子中,addName方法需要同步对lastName和nameCount的更改,但也需要避免同步其他对象方法的调用(在同步代码块中调用其他对象的方法会引起一些问题,这会在后面的章节中具体介绍)。如果没有synchronized statements,就需要一个单独的非同步的方法来实现nameList.add

除此之外,synchronized statements还能实现更细粒度的同步。比如,假设类MsLunch具有两个实例字段c1和c2,但是它们从不一起使用。更新这两个字段都需要同步,但是没有理由在更新c2的时候阻止更新c1,这样会导致不必要的阻塞。Instead of using synchronized methods or otherwise using the lock associated with this, we create two objects solely to provide locks:

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++;
        }
    }
}

不过,使用这个方法要特别小心,你要确保c1和c2是可以交替访问的。

Reentrant Synchronization(重入同步)

一个线程不能申请被其他线程占用的锁,但是一个线程可以申请它已经拥有的锁(Reentrant Synchronization)。初看这句话,可能会觉得很奇怪,既然已经拥有了干嘛还要再申请。我们先来看一个场景,如果你在一个同步方法中直接或间接调用了该对象的另一个同步方法(也就是说这两个方法使用了同一个锁),如果没有Reentrant Synchronization,该线程就会自己阻塞自己。

Atomic Access

An atomic action cannot stop in the middle: it either happens completely, or it doesn't happen at all. No side effects of an atomic action are visible until the action is complete.

前面我们已经看到,简单的自增语句并不是原子操作。但是以下操作是原子的:

  • Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double)
  • Reads and writes are atomic for all variables declared volatile (including long and double variables)

原子操作不会产生交错,所以我们在使用的时候不用担心线程干扰。但是,这并不代表完全不需要同步原子操作,因为内存一致性错误仍然有可能发生。使用volatile关键字可以减少内存一致性错误的风险,因为对volatile变量的写操作会与后续对该变量的读操作建立起happens-before关系。这意味着volatile变量的任何改变对其他线程都是可见的。而且,当一个线程读取一个volatile变量的时候,它不仅能看到该变量最新的变化,还能看到导致变化的代码的副作用。

使用简单的原子操作比使用同步代码访问这些变量效率更高,不过需要程序员更小心地避免内存一致性错误。

你可能感兴趣的:(【The Java™ Tutorials】【Concurrency】4. Synchronization)