在一个多线程的程序中,当多个线程同时访问一个或者多个资源时(变量、文件等),程序的执行结果是不可预测的。这个问题又被称为由竞态条件而造成的不确定性。为了避免这种不确定性,多线程程序需要使用同步机制来确保每一步的执行是确定的,执行结果是可预知的。在Java语言中,使用关键字synchronized是一种最常见的保护共享资源的方法。本章节将介绍关键字synchronized的用法和内部实现细节。
关键字synchronized可用于以下三种场景。
在定义一个类的成员方法时,可以声明该方法是synchronized的。其意义是:在某一时刻,针对某一个对象,只能有一个线程在这个对象上调用这个方法。这个描述很拗口,不易理解。我们将通过下面的例子来解释其意义。
在给出例子之前,我们先介绍一些synchronized的内部实现细节。在Java语言中,所有对象(除基本数据类型以外)的内部都包含了一个monitor对象。实际上,monitor是一把锁(lock);synchronized关键字是由对对象内部的monitor上锁(acquire)和解锁(release)实现的。因此,synchronized成员方法是由对对象this的上锁和解锁实现的。
例如:在SynchronizedInstanceMethodExample类中定义了一个synchronized的成员方法increment()。在increment()方法中自增成员变量i。在编译的过程中,Java编译器会在increment()方法的首尾自动加上对this对象的monitor的上锁和解锁操作。
public class SynchronizedInstanceMethodExample {
private int i = 0;
public synchronized void increment() {
i++;
}
}
上述代码可以翻译成如下的等效代码。注意,下面翻译的代码只是示意代码,它不是合法的Java程序。
public class SynchronizedInstanceMethodExample {
private int i = 0;
public void increment() {
acquire(this.monitor); // 对this.monitor上锁
i++;
release(this.monitor); // 对this.monitor解锁
}
}
由于synchronized成员方法是对this的monitor上锁和解锁,所以,在如下的程序中,在某一时刻,只有一个线程能进入increment()方法执行。
public class SynchronizedInstanceMethodExample {
private int i = 0;
public synchronized void increment() {
System.out.println(String.format("Enter increment(), i=%d.", i));
i++;
try {
Thread.sleep(1000);
} catch (Exception ex){}
System.out.println(String.format("Exit increment(), i=%d.", i));
}
public static void main(String[] args) {
// 运行increment()时,thread1和thread2尝试获得obj.monitor
SynchronizedInstanceMethodExample obj = new SynchronizedInstanceMethodExample();
Runnable runnableCode = () -> {obj.increment();};
Thread thread1 = new Thread(runnableCode);
Thread thread2 = new Thread(runnableCode);
thread1.start();
thread2.start();
}
}
程序运行结果:
> java SynchronizedInstanceMethodExample
Enter increment(), i=0.
Exit increment(), i=1.
Enter increment(), i=1.
Exit increment(), i=2.
但是,如果是调用不同对象的synchronized方法,相互是不影响的。例如,在下例中,两个线程分别调用了obj1和obj2的increment()方法,因此,这两个线程可以独立运行,相互不影响。
public class SynchronizedInstanceMethodExample {
private int i = 0;
public synchronized void increment() {
System.out.println(String.format("Enter increment(), i=%d.", i));
i++;
try {
Thread.sleep(1000);
} catch (Exception ex){}
System.out.println(String.format("Exit increment(), i=%d.", i));
}
public static void main(String[] args) {
// thread1调用increment()时,尝试获得obj1.monitor
SynchronizedInstanceMethodExample obj1 = new SynchronizedInstanceMethodExample();
Runnable runnableCode1 = () -> {obj1.increment();};
Thread thread1 = new Thread(runnableCode1);
// thread2调用increment()时,尝试获得obj2.monitor
SynchronizedInstanceMethodExample obj2 = new SynchronizedInstanceMethodExample();
Runnable runnableCode2 = () -> {obj2.increment();};
Thread thread2 = new Thread(runnableCode2);
thread1.start();
thread2.start();
}
}
程序运行结果:
> java SynchronizedInstanceMethodExample
Enter increment(), i=0.
Enter increment(), i=0.
Exit increment(), i=1.
Exit increment(), i=1.
对象内部的monitor是一个可重入的锁(Reentrant Lock)。即,当一个线程获得了某对象的monitor之后,该线程可以多次调用对象的synchronized成员方法。可重入原则适用于所有的synchronized语句,包括接下来将介绍的synchronized静态方法和synchronized语句。正是此可重入原则,Java程序可以在一个synchronized成员方法中调用其他synchronized成员方法或者静态方法,而不被阻塞。
public class SynchronizedInstanceMethodExample {
private int i = 0;
public synchronized void increment() {
i++;
}
public synchronized void incrementAndPrint() {
// 因为monitor是可重入的锁,所以,当调用increment()方法时,可再次获得该锁
this.increment()
System.out.println("The value of i is " + i);
}
}
关键字synchronized还可以用在静态方法上。其使用方法十分类似。synchronized静态方法是对类的对象上锁和解锁。如下例所示:类SynchronizedStaticMethodExample定义了一个synchronized的静态方法increment()。
public class SynchronizedStaticMethodExample {
private static int i = 0;
public static synchronized void increment() {
i++;
}
}
上述代码的等效代码如下。注意,下面翻译的代码只是示意代码,它不是合法的Java程序。
public class SynchronizedStaticMethodExample {
private static int i = 0;
public static synchronized void increment() {
acquire(Class.forName("SynchronizedStaticMethodExample").monitor);
i++;
release(Class.forName("SynchronizedStaticMethodExample").monitor);
}
}
所以,在某一时刻,下面的三个线程中,只能有一个线程进入increment()方法运行。
public class SynchronizedStaticMethodExample {
private static int i = 0;
public static synchronized void increment() {
System.out.println(String.format("Enter increment(), i=%d.", i));
i++;
try {
Thread.sleep(1000);
} catch (Exception ex){}
System.out.println(String.format("Exit increment(), i=%d.", i));
}
public static void main(String[] args) {
SynchronizedStaticMethodExample obj1 = new SynchronizedStaticMethodExample();
SynchronizedStaticMethodExample obj2 = new SynchronizedStaticMethodExample();
Runnable runnableCode1 = () -> {obj1.increment();};
Runnable runnableCode2 = () -> {obj2.increment();};
Runnable runnableCode3 = () -> {SynchronizedStaticMethodExample.increment();};
Thread thread1 = new Thread(runnableCode1);
Thread thread2 = new Thread(runnableCode2);
Thread thread3 = new Thread(runnableCode3);
thread1.start();
thread2.start();
thread3.start();
}
}
程序运行结果:
> java SynchronizedStaticMethodExample
Enter increment(), i=0.
Exit increment(), i=1.
Enter increment(), i=1.
Exit increment(), i=2.
Enter increment(), i=2.
Exit increment(), i=3.
synchronized语句又常常被称为synchronized代码块。与synchronized成员方法和静态方法不同的是,synchronized语句只保护一个代码块,即:在某一时刻,只能有一个线程进入该代码块运行。例如:
public class SynchronizedStatementExample {
private int i = 0;
private static int j = 0;
private static Integer k = new Integer(10);
// 此方法等效于synchronized increment成员方法。
public void incrementI() {
synchronized (this) {
i++;
}
}
// 此方法等效于synchronized increment静态方法。
public static void incrementJ() {
synchronized (SynchronizedStatementExample.class) {
j++;
}
}
public void decrementK() {
synchronized (k) {
synchronized(k) { //没关系,synchronized可以重入
k -= 1;
}
}
}
}
因为只有对象的内部才有monitor,所以,synchronized语句不能用于基本数据类型(Primitive Data Type)。
本章节介绍了一种Java语言保护共享资源的常用方法。在后续章节中,我们还会介绍其他的同步方法,例如:wait/notify机制。
注册用户登陆后可留言