1、synchronized修饰的对象
《Java中Synchronized的用法》中对synchronized修饰对象给出了很好的说明与案例,本文不再赘述,仅附上该文章得到的结论。
1.1、修饰一个代码块
- 被修饰的代码块称为同步语句块。
- 作用范围:大括号
{}
括起来的代码。 - 作用对象:调用这个代码块的对象。
在平常的开发中,我们更倾向于使用这种更细粒度的方式去synchronized。
public void test() {
// ...
synchronized(this) {
// ...
}
// ...
}
1.2、修饰一个方法
- 被修饰的方法称为同步方法。
- 作用范围:整个方法。
- 作用对象:调用这个方法的对象。
public synchronized void test() {
// ...
}
1.3、修饰一个静态的方法
- 作用范围:整个静态方法。
- 作用对象:是这个类的所有对象。
public synchronized static void method() {
// ...
}
1.4、修饰一个类
- 作用范围:synchronized后面括号括起来的部分
- 作用对象:这个类的所有对象。
class ClassName {
public void method() {
synchronized(ClassName.class) {
// ...
}
}
}
2、synchronized锁定对象案例分析
一定要清楚一个概念,那就是synchronized锁定的是对象,而不是引用。我们通过案例来“深刻理解”这句话的意思:
public class LockReferenceDemo {
public Object obj = new Object();
public void act() {
System.out.println(Thread.currentThread().getName() + " start!");
synchronized (obj) { // 锁定了obj指向的对象
while (true) {
// 输出当前线程 及 obj对象地址
System.out.println(Thread.currentThread().getName() + " - " + obj);
// 延迟1s执行
try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) {}
}
}
}
public static void main(String[] args) {
final LockReferenceDemo demo = new LockReferenceDemo();
// 定义并启动线程1
Thread thread1 = new Thread(demo::act, "Thread-1");
thread1.start();
// 延迟1s执行
try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) {}
// 启动Thread-2前,修改obj引用的对象
demo.obj = new Object();
// 定义并启动线程2
Thread thread2 = new Thread(demo::act, "Thread-2");
thread2.start();
}
}
输出结果:
Thread-1 start!
Thread-1 - java.lang.Object@4b7f7470
Thread-1 - java.lang.Object@4b7f7470
Thread-2 start!
Thread-2 - java.lang.Object@5e130a4c
Thread-1 - java.lang.Object@5e130a4c
Thread-2 - java.lang.Object@5e130a4c
我们可以看到,Thread-2正常运行,同时,demo.obj = new Object()
这一步没有终止Thread-1的执行。
这是因为,对象的引用是存在栈桢中的,而对象是存在堆中的。synchronized只是锁住了堆中的对象,并没有锁栈桢中的引用。demo.obj = new Object()
这一步只是将引用obj
指向了一个新的对象。
我们可以把“获取锁”这个修饰词替换成“挂锁”去理解整个过程。在整个过程中,Thread-1发现原对象(假设为A)上没有锁,就在A上挂了一把锁。
这个时候,如果没有demo.obj = new Object()
这一步,则Thread-2也需要尝试在A上挂锁,发现该对象上已经挂了一把锁,那么Thread-2就必须等待Thread-1将A对象上的锁释放后才能挂锁。
而有了demo.obj = new Object()
这一步之后就不一样了,Thread-1在A中挂了一把锁,经过demo.obj = new Object()
执行后,obj引用引向了新的对象(假设为B),B显然是没有挂锁的,所以Thread-2就可以在新的对象中挂锁并正常执行。
在main方法中,Thread-1与Thread-2之间我添加了TimeUnit.SECONDS.sleep(1);
去延迟执行,目的在于,若代码执行过快,在Thread-1还没来得及执行synchronized时,demo.obj = new Object()
这一步已然发生,那么Thread-1就会直接锁定B对象,这从侧面反映出synchronized锁定的是对象这个事实。
此时还有一个严重的问题,在输出的时候,我们发现,当demo.o = new Object()
执行之后,Thread-1输出的obj的地址变成了新的对象的地址。这是因为Thread-1调用obj是调用obj指向的对象,而不是Thread-1锁定的对象。我们可以通过新建引用去接收引用参数的方式去解决这个问题:
public class LockReferenceDemo {
Object obj = new Object();
void act() {
System.out.println(Thread.currentThread().getName() + " start!");
synchronized (obj) {
Object o1 = obj; // 用新的引用引向原对象,线程就可以访问o1指向的原对象继续操作。
while (true) {
System.out.println(Thread.currentThread().getName() + " - " + o1);
// ...
}
}
}
// main
}
输出结果:
Thread-1 start!
Thread-1 - java.lang.Object@1437c889
Thread-1 - java.lang.Object@1437c889
Thread-2 start!
Thread-2 - java.lang.Object@2303252c
Thread-1 - java.lang.Object@1437c889
Thread-2 - java.lang.Object@2303252c