synchronized 是 Java 中的一种关键字,用于实现同步,保证在多线程环境下,只有一个线程可以访问代码块或方法,确保线程安全。
作用
- 互斥访问:防止多个线程同时访问临界区代码,从而避免并发问题。
- 内存可见性:确保一个线程修改的共享变量对其他线程可见。
使用方式
synchronized
可以用来修饰方法或代码块,主要有以下两种使用方式:
-
修饰实例方法:
public synchronized void method() { // 需要同步的代码 }
当一个线程调用该方法时,会自动获得该对象实例的锁,其他线程在获得锁之前无法调用这个方法或其他被
synchronized
修饰的实例方法。 -
修饰静态方法:
public static synchronized void staticMethod() { // 需要同步的代码 }
当一个线程调用该方法时,会自动获得该类的类锁,其他线程在获得类锁之前无法调用这个静态方法或其他被
synchronized
修饰的静态方法。 -
修饰代码块:
public void method() { synchronized(this) { // 需要同步的代码 } }
synchronized(this)
锁定的是当前对象实例。如果锁定一个特定的对象,可以用synchronized(特定对象)
。
原理
synchronized
关键字可以用来修饰静态方法和实例方法,但它们的锁机制是不同的:
- 实例方法上的
synchronized
:锁住的是当前实例对象(this
),即对象的监视器锁(monitor lock)。 - 静态方法上的
synchronized
:锁住的是当前类的类对象(Class
),即类的监视器锁。
当 synchronized
修饰静态方法时,锁的信息不放在对象的头信息里,而是放在类对象的头信息里。具体来说,静态方法的锁是类的 Class
对象上的锁。
synchronized
的底层实现依赖于对象的监视器(Monitor),每个对象都有一个监视器。当一个线程试图访问被 synchronized
修饰的代码时,它必须首先获得对象的监视器。
- 对象头:每个 Java 对象的头部有两部分信息,第一部分是 Mark Word,用于存储对象的标志位,包括锁标志位、GC 分代年龄等。
- Monitor:每个对象都有一个与之关联的监视器,
synchronized
关键字会尝试获得对象的监视器锁。一个线程进入synchronized
方法或块时,如果对象的监视器可用,则该线程进入临界区,并将监视器锁定;否则,线程会被阻塞,直到监视器变为可用。 - 锁的状态:对象的锁有四种状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁的状态会随着竞争的激烈程度而升级,但不会降级。这种机制被称为锁的升级。
而对于静态方法,锁的信息放在类对象的头信息里。无论是实例方法还是静态方法,Java 都会确保同一个类的不同实例方法同步访问时不会发生并发问题,而静态方法锁住的是整个类,这意味着在静态同步方法执行期间,其他线程不能执行任何其他静态同步方法。
示例
public class SynchronizedExample {
private int count = 0;
// synchronized 修饰实例方法
public synchronized void increment() {
count++;
}
// synchronized 修饰代码块
public void decrement() {
synchronized(this) {
count--;
}
}
// synchronized 修饰静态方法
public static synchronized void staticMethod() {
// 静态同步方法
}
public int getCount() {
return count;
}
public static void main(String[] args) {
SynchronizedExample example = new SynchronizedExample();
// 创建线程1
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
// 创建线程2
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.decrement();
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(example.getCount());
}
}
在这个示例中,increment
方法被 synchronized
修饰,保证了 count
变量在多线程环境下的安全递增。decrement
方法使用同步代码块,实现了相同的效果。
在一个类中,如果有两个 synchronized
方法,它们不会同时去获得对象实例的锁。具体来说,当一个线程进入某个 synchronized
方法时,它会获得对象实例的锁,而其他线程在获得该对象的锁之前,无法进入任何其他被 synchronized
修饰的方法。也就是说,这两个 synchronized
方法在同一时间只能有一个被执行。
示例
public class SyncExample {
public synchronized void method1() {
System.out.println("Method 1 start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Method 1 end");
}
public synchronized void method2() {
System.out.println("Method 2 start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Method 2 end");
}
public synchronized static void method3() {
System.out.println("Method 3 start");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("method 3 end");
}
public static void main(String[] args) {
SyncExample example = new SyncExample();
Thread t1 = new Thread(example::method1);
Thread t2 = new Thread(example::method2);
Thread t3 = new Thread(SyncExample::method3);
t1.start();
t2.start();
t3.start();
}
}
结果分析
在上述代码中,两个线程 t1
和 t2
分别调用 method1
和 method2
。由于两个方法都被 synchronized
修饰,它们在同一时间只能有一个被执行。
输出结果可能是:
Method 1 start
Method 3 start
Method 1 end
method 3 end
Method 2 start
Method 2 end
无论顺序如何,都表明两个方法不会同时执行,因为它们共享同一个对象锁。
原因
当一个线程调用某个 synchronized
方法时,它会获得该对象的监视器锁。直到该线程退出 synchronized
方法并释放锁之后,其他线程才能获得锁并进入其他 synchronized
方法。这种机制保证了同一时间只能有一个线程能够访问任何 synchronized
方法,从而避免线程间的竞争条件。
结论
- 如果一个类有多个
synchronized
方法,同一时间只有一个synchronized
方法可以被执行。 - 当一个线程执行某个
synchronized
方法时,其他线程必须等待,直到该线程释放对象锁后才能执行其他synchronized
方法。