原文链接:http://tutorials.jenkov.com/java-concurrency/synchronized.html
摘要:这是翻译自一个大概30个小节的关于Java并发编程的入门级教程,原作者Jakob Jenkov,译者Zhenning Lang,转载请注明出处,thanks and have a good time here~~~(希望自己不要留坑)
一个 Java 同步代码块是将一个方法或者一段代码标记为同步的(synchronized),同步代码块被用来防止竞争的发生。
1. Java 同步关键字: synchronized
Java 中用关键字“synchronized”来标记同步代码块,并且在 Java 中同步代码块是被同步到某个对象上。所有被同步到相同对象上的代码块在同一时间只能被一个线程来执行,其他试图执行这个同步块的线程都将被阻塞,直到正在执行同步块的线程执行完毕。
synchronized 关键字可以用来标记四种不同的代码段:
- 实例方法(Instance methods)
- 静态方法(Static methods)
- 实例方法中的代码段(Code blocks inside instance methods)
- 静态方法中的代码段(Code blocks inside static methods)
这些代码块被同步到不同的对象上,而实际中究竟要使用哪种同步代码段要根据具体问题来具体分析。
1.1 同步的实例方法(Synchronized Instance Methods)
下面是一个同步的实例方法:
public synchronized void add(int value){
this.count += value;
}
请注意在方法声明中所使用的 synchronized 关键字,用于告诉 Java 这个方法是同步的。
Java 中一个同步的实例方法被同步到拥有这个方法的实例(对象)上。因此,每个实例的同步方法被同步到不同的对象上:即实例本身。在实例同步方法内部只可能有一个线程被执行。如果创建了几个实例,则同一时间每个实例的同步方法都可以被单独的线程执行 —— 一个实例一个线程。
1.2 同步的静态方法(Synchronized Static Methods)
静态方法同实例方法一样,可以被 synchronized 关键字修饰,如下例所示:
public static synchronized void add(int value){
count += value;
}
同步的静态方法被同步到静态方法所属的类对象上。因为在 Java 虚拟机中每个类只有一个类对象存在,所以同一个类的静态同步方法只能被一个线程执行。
如果静态的同步方法属于不同的类,那么每个类的静态同步方法可以被一个线程运行。无论是谁调用了静态同步方法,一个类一个线程。
1.3 实例方法中的同步代码块(Synchronized Blocks in Instance Methods)
事实上并不需要将整个方法声明为同步。有时将方法中的一段代码声明为同步的可能会更好,如下例所示:
public void add(int value){
synchronized(this){
this.count += value;
}
}
上例中使用 Java 的 synchronized 块结构将一个代码块声明为同步的。特别注意 synchronized 将一个“对象”作为了参数,例如例子中的 this,即调用 add() 方法的实例。被 synchronized 用作参数的对象被称为“监视对象”,代码段被称作同步到了监视对象上。在 1.1 中介绍的同步实例方法实际是将其所属的实例对象作为了监视对象。
注意:具有同一个监视对象的代码段只能被不多于一个的线程执行。
下面的两个例子都同步到了调用他们的实例对象,因此就同步的意义来说二者是等价的:
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);
}
}
}
因此同一个线程同时只可能执行 log1 或者 log2 方法二者其一。
然而一旦 log2 中的代码被同步到了不同的对象上,那么同一时间,二者可以各被一个线程执行。
1.4 静态方法中的同步代码块(Synchronized Blocks in Static Methods)
与上例类似,下面的例子只不过是将方法生命成了静态。以下两个方法都被同步到了方法所属的类的对象上:
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);
}
}
}
同一时间,这两个方法只能被一个线程执行。
类似于 1.3 中的例子,一旦 log2 方法被同步到了另一个对象上(不是 MyClass.class),那么同一个时间 log1 和 log2 可以各被一个线程执行。
2. Java 同步示例
本例展示了如何利用两个线程同时调用一个 Counter 实例的 add 方法。在本例中,相同的实例同一时间只能有一个线程调用 add 方法,因为 add 方法被同步到了其所属的实例上:
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 被传到了 threadA 和 threadB 的构造函数中。由于 Counter.add() 方法被同步到实例上,因此同一时间只有一个线程可以执行 add() 方法,另一个线程必须得等到第一个线程执行完 add() 方法才能开始执行该方法。
然而,如果两个线程初始化时使用的是两个不同的 Counter 实例,则他们调用 add 方法的时候将不会相互影响。由于调用了不同实例的 add() 方法,那么两个线程的 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 被同步到了不同的实例上,所以当 counterA 的 add() 方法被执行时,counterB 的 add() 方法也不会被阻塞。
3 Java 并发工具类
同步机制(synchronized mechanism)曾是第一个 Java 多线程同步获取共享对象的机制,这也使得 synchronized 并不是一种很高级的手段。同时这也是为什么 Java 5 提出了一整套的并发工具类,用于帮助开发者实现比 synchronized 更加细颗粒的并发控制,这些将在后面的教程中被介绍到。