在 Java 中,synchronized
关键字是一个非常重要的同步机制,用于确保在多线程环境下,同一时刻只有一个线程可以执行某个方法或代码块。本文将详细介绍 synchronized
关键字的三种应用方式、其作用以及注意事项。
1 synchronized
的三种应用方式
synchronized
关键字主要有以下三种应用方式:
- 同步方法:为当前对象(
this
)加锁,进入同步代码前要获得当前对象的锁。 - 同步静态方法:为当前类加锁(锁的是
Class
对象),进入同步代码前要获得当前类的锁。 - 同步代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
2 同步方法
通过在方法声明中加入 synchronized
关键字,可以保证在任意时刻,只有一个线程能执行该方法。
public class AccountingSync implements Runnable {
//共享资源(临界资源)
static int i = 0;
// synchronized 同步方法
public synchronized void increase() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync instance = new AccountingSync();
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("static, i output:" + i);
}
}
输出结果:
static, i output:2000000
如果不加 synchronized
,由于 i++
不具备原子性,最终结果会小于 2000000。
注意:一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他 synchronized
方法,但是其他线程还是可以访问该对象的其他非 synchronized
方法。
3 同步静态方法
当 synchronized
同步静态方法时,锁的是当前类的 Class
对象,不属于某个对象。当前类的 Class
对象锁被获取,不影响实例对象锁的获取,两者互不影响。
public class AccountingSyncClass implements Runnable {
static int i = 0;
public static synchronized void increase() {
i++;
}
public synchronized void increase4Obj() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new AccountingSyncClass());
Thread t2 = new Thread(new AccountingSyncClass());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
输出结果:
2000000
4 同步代码块
某些情况下,我们编写的方法代码量比较多,存在一些比较耗时的操作,而需要同步的代码块只有一小部分,如果直接对整个方法进行同步,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹。
public class AccountingSync2 implements Runnable {
static AccountingSync2 instance = new AccountingSync2();
static int i = 0;
@Override
public void run() {
synchronized (instance) {
for (int j = 0; j < 1000000; j++) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
输出结果:
2000000
除了用 instance
作为对象外,我们还可以使用 this
对象(代表当前实例)或者当前类的 Class
对象作为锁。
// this, 当前实例对象锁
synchronized (this) {
for (int j = 0; j < 1000000; j++) {
i++;
}
}
// Class对象锁
synchronized (AccountingSync.class) {
for (int j = 0; j < 1000000; j++) {
i++;
}
}
5 synchronized
与 happens-before
synchronized
关键字可以防止临界区内的代码与外部代码发生重排序,确保执行顺序和内存可见性。
class MonitorExample {
int a = 0;
public synchronized void writer() {
a++;
}
public synchronized void reader() {
int i = a;
}
}
假设线程 A 执行 writer()
方法,随后线程 B 执行 reader()
方法。根据 happens-before 规则,这个过程包含的 happens-before 关系可以分为:
- 根据程序次序规则,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。
- 根据监视器锁规则,3 happens before 4。
- 根据 happens-before 的传递性,2 happens before 5。
在 Java 内存模型中,监视器锁规则是一种 happens-before 规则,它规定了对一个监视器锁(monitor lock)或者叫做互斥锁的解锁操作 happens-before 于随后对这个锁的加锁操作。简单来说,这意味着在一个线程释放某个锁之后,另一个线程获得同一把锁的时候,前一个线程在释放锁时所做的所有修改对后一个线程都是可见的。
6 synchronized
的可重入性
synchronized
是可重入锁,当一个线程再次请求自己持有对象锁的临界资源时,请求将会成功。
public class AccountingSync implements Runnable {
static AccountingSync instance = new AccountingSync();
static int i = 0;
static int j = 0;
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
synchronized (this) {
i++;
increase(); // synchronized 的可重入性
}
}
}
public synchronized void increase() {
j++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
代码解析:
AccountingSync
类中定义了一个静态的AccountingSync
实例instance
和两个静态的整数i
和j
,静态变量被所有的对象所共享;- 在
run
方法中,使用了synchronized(this)
来加锁。这里的锁对象是this
,即当前的AccountingSync
实例。在锁定的代码块中,对静态变量i
进行增加,并调用了increase
方法; increase
方法是一个同步方法,它会对 j 进行增加。由于increase
方法也是同步的,所以它能在已经获取到锁的情况下被run
方法调用,这就是synchronized
关键字的可重入性;- 在
main
方法中,创建了两个线程t1
和t2
,它们共享同一个Runnable
对象,也就是共享同一个AccountingSync
实例。然后启动这两个线程,并使用join
方法等待它们都执行完成后,打印i
的值; - 此程序中的
synchronized(this)
和synchronized
方法都使用了同一个锁对象(当前的AccountingSync
实例),并且对静态变量i
和j
进行了增加操作,因此,在多线程环境下,也能保证i
和j
的操作是线程安全的。
7 总结
synchronized
关键字在 Java 中用于实现线程同步,确保在多线程环境下,同一时刻只有一个线程可以执行某个方法或代码块。它有三种应用方式:同步方法、同步静态方法和同步代码块。synchronized
还具有可重入性,允许线程在持有锁的情况下再次请求锁。
合理使用 synchronized
可以保证线程安全,但也要注意同步范围的合理性,避免不必要的性能开销。在 JVM 的早期版本中,synchronized
是重量级的,但在后续版本中进行了大量优化,如偏向锁、轻量级锁和适应性自旋等,使得 synchronized
在许多情况下性能良好。