开发中常用的加锁方式就是使用synchronized关键字,可以在以下三种情景使用:
- 修饰static方法
- 修饰成员方法
- 修饰代码块
synchronized本质是一种独占锁,即某一时刻仅能有一个线程进入临界区,其他线程必须等待,处于block状态。下面以几个例子分别看下不同场景下的synchronized
修饰static方法
public class SyncArea {
public synchronized static void methodOne() {
System.out.println("method one executed!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized static void methodTwo() {
System.out.println("method two executed!");
}
}
//调用处:
new Thread(() -> SyncArea.methodOne()).start();
new Thread(() -> SyncArea.methodTwo()).start();
如上所示,synchronized修饰两个static方法,假如有线程A先执行methodOne,然后线程B执行methodTwo,则线程B需等待线程A执行完毕后才能执行。这是因为修饰static方法时,其实是为SyncArea.class这个对象加锁,线程A获取锁后会sleep两秒,这期间是不会释放锁的,在这段时间内,线程B试图获取SyncArea.class这个锁,但是获取不到,自己处于block状态。两秒后线程A执行完毕释放锁,线程B才能获取到锁。
由于锁的是SyncArea.class这个对象,所以以下代码与上边是相同的:
public class SyncArea {
public static void methodOne() {
//修饰代码块,锁的是.class对象
synchronized (SyncArea.class) {
System.out.println("method one executed!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized static void methodTwo() {
System.out.println("method two executed!");
}
}
注意:如果methodTwo没有用synchronized修饰,则不涉及同步问题,在上述情景下,线程B直接执行,不会试图获取锁。
修饰成员方法
public class SyncArea {
public synchronized void methodOne() {
System.out.println("method one executed!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void methodTwo() {
System.out.println("method two executed!");
}
}
//调用处:
final SyncArea sa = new SyncArea();
new Thread(() -> sa.methodOne()).start();
new Thread(() -> sa.methodTwo()).start();
假如线程A先执行methodOne,线程B再执行methodTwo,依然是线程A休眠两秒内线程B被阻塞(block)。这是因为synchronized修饰成员方法时,其实是对实例对象加锁,本例中即sa对象,线程A和B竞争这个对象锁。同理,上述代码与下边是一样的:synchronized修饰代码块,将this加锁。
public class SyncArea {
public void methodOne() {
synchronized (this) {
System.out.println("method one executed!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void methodTwo() {
System.out.println("method two executed!");
}
}
修饰代码块
上边已经穿插介绍了synchronized修饰代码块的情景,它的本质是对某个对象加锁,更确切的说是这个对象的监视器锁。synchronized后跟哪个对象就是多个线程需获取哪个对象的锁后才能进入临界区。为了进一步理解,改写上述修饰成员变量的例子:
public class SyncArea {
private final Object obj = new Object();
public void methodOne() {
synchronized (obj) {
System.out.println("method one executed!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void methodTwo() {
synchronized (obj) {
System.out.println("method two executed!");
}
}
}
//调用处:
final SyncArea sa = new SyncArea();
new Thread(() -> sa.methodOne()).start();
new Thread(() -> sa.methodTwo()).start();
新建一个对象,对此对象加锁,执行结果相同,依然是线程A休眠期间线程B被阻塞。与修饰成员变量方法不同的是,这次多个线程获取的锁是obj对象,不再是this,其他道理是一样的。
总结:
哪个线程能进入临界区,需要看synchronized究竟是对哪个对象加锁,修饰static方法是对.class对象加锁,修饰成员方法是对当前类的某个对象加锁,修饰代码块是对特定对象加锁。