Java 的 synchronized 块标记一个方法或一个代码块为同步的。synchronized 块能用于防止出现竞态条件。
Java 的 synchronized 关键字
java 中的 synchronized 块使用 synchronized 关键字进行标记。一个 synchronized 块在某个对象上被同步。所有在某个对象上同步的 synchronized 块同时只能有一个线程执行,所有其他试图进入 synchronized 块的线程被阻塞,知道进入 synchronized 块的线程退出。
synchronized 关键字能够用于编辑 4 种不同类型的块:
- 实例方法
- 静态方法
- 实例方法中的代码块
- 静态方法中的代码块
这些块被同步在不同的对象上。哪种 synchronized 块是你需要的取决于实际情况。
同步的实例方法
这里有一个同步的实例方法:
public synchronized void add(int value) {this.count += value;}
注意这里在方法定义中使用 synchronized 关键字。这告诉 Java 方法是同步的。
Java 中一个同步的实例方法同步在这个方法所在的实例(对象)上。因此,每个实例的同步方法同步在不同的对象上:拥有这个方法的实例。同时只有一个线程能在一个同步的实例方法上执行。如果多于一个实例存在,则一个线程一次能执行在每个实例的一个同步实例方法中,每个实例一个线程。
同步的静态方法
静态方法被标记为 synchronized 就像实例方法一样使用 synchronized 关键字。这里有一个同步的静态方法:
public static synchronized void add(int value) {count += value;}
这里 synchronized 关键字告诉 Java 这个方法是同步的。
同步的静态方法同步在方法所属的类对象(如 MyClass.class)上。由于每个类只有一个类对象存在于 JVM 中,因此全局同时只有一个线程能够进入到同一个类的静态同步方法中。
如果静态同步方法位于不同的类中,则一个线程能够在每个类的静态同步方法中执行。每个类一个线程。
实例方法的同步代码块
有时不必同步整个方法。有时更倾向于只同步一个方法中的一部分。Java 同步代码块使这种期望成为可能。
这里有一个在非同步 Java 方法中的同步代码块:
public void add(int value) {synchronized(this) {this.count += value;}
}
这个例子使用 Java 同步代码块使得一块代码可以被同步访问。这块代码现在就好像是一个同步方法被执行。
注意 Java 同步代码块如何在括号中接受一个对象。在上面的例子中“this”被使用,也就是这个方法调用所在的实例。括号中的对象叫做「monitor object」。这段代码被叫做同步在一个 monitor object 上。一个同步的实例方法使用它所属的对象作为 monitor object。
同时只有一个线程能执行在同步于相同 monitor object 上的 Java 代码块。
下面两个例子都是同步在他们被调用的实例上,两者等价:
public class MyClass {public synchronized void log1(String msg1, String msg2) {log.writeln(msg1);
log.writeln(msg2);
}
public void log2(String msg1, String msg2) {synchronized(this) {log.writeln(msg1);
log.writeln(msg2);
}
}
}
同时只有一个线程能够执行在上面两个同步代码块之一上面。
可能会有代码块同步在非 this 对象上。
静态方法中的同步代码块
这些方法同步在方法所属类的类对象上:
public class MyClass {public static synchronized void log1(String msg1, String msg2) {log.writeln(msg1);
log.writeln(msg2);
}
public static void log2(String msg1, String msg2) {synchronized(MyClass.class) {log.writeln(msg1);
log.writeln(msg2);
}
}
}
可能会有代码块同步在非 MyClass.class 上。
synchronized 例子
这里有一个例子,启动 2 个线程,每个线程都在同一个 Counter 对象上调用 add 方法。同时只有一个线程会调用同一个对象的 add 方法,因为这个方法被 synchronized 关键字修饰在它所属的同一个实例上。
public class Counter {
long count = 0;
public synchronized void add(long value) {this.count += value}
}
public class CounterThread extends Thread {
protected Counter counter = null;
public CounterThread(Counter counter) {this.counter = counter;}
public void run() {for(int i = 0; i < 10; i++) {counter.add(i);
}
}
}
public class Example {public static void main(String[] args) {Counter counter = new Counter();
Thread threadA = new CounterThread(counter);
Thread threadB = new CounterThread(counter);
threadA.start();
threadB.start();}
}
两个线程被创建。相同的 Counter 实例被传入两个不同的线程实例中。Counter.add() 方法在实例上同步,因为 add 方法是一个实例方法,并被标记为 synchronized。因此同时只有一个线程能调用 add() 方法。另一个线程会等待第一个线程离开 add() 方法,然后执行。
如果两个线程引用两个不同的 Counter 实例,则同时调用 add() 方法不会有问题。调用是对于不同的对象,因此方法调用也同步在不同的对象上。因此调用不会被阻塞。
public class Example {public static void main(String[] args) {Counter counterA = new Counter();
Counter counterB = new Counter();
Thread threadA = new CounterThread(counterA);
Thread threadB = new CounterThread(counterB);
threadA.start();
threadB.start();}
}
助理这里的 threadA 和 threadB 不再引用同一个 counter 实例。counterA 和 counterB 的 add 方法同步在各自所属对象上。调用 counterA 的 add() 方法不会紫塞调用在 counterB 的 add() 方法。
Java 的同步实用工具
synchronized 机制是 Java 的第一个用于同步访问共享在多线程间的同步机制。synchronized 机制现在看来不是很先进,这也就是为什么 Java5 推出了一组同步实用类帮助开发者实现更细粒度的同步控制。