一、三种用法
1.1、修饰在对象的成员方法上,锁住的监视器对象是对象的实例。
1.2、修饰在对象的静态方法上,锁住的监视器对象是类。
1.3、修饰在代码块上,锁住的监视器对象是关键字后花括号中的对象(也有可能是类)。
二、验证
2.1 用两个synchronized修饰的成员方法测试
假设某个类中定义了两个公开的方法(分别是method1和method2),这两个方法都用了synchronized来修饰。假设线程A调用了method1但并未执行完,这时另外一个线程B开始调用method2,会发现线程B被阻塞。因为线程A和B尝试获取的监视器锁是同一个,A未释放,所以B无法得到。代码如下:
public static void main(String[] args) {
final SynchronizedTest test = new SynchronizedTest();
Thread t1 = new Thread(() -> {
test.method1();
}, "synchronized-method-1");
t1.start();
Thread t2 = new Thread(() -> {
test.method2();
}, "synchronized-method-2");
t2.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t1.getState());
System.out.println(t2.getState());
}
注意:
虽然线程B调用synchronized修饰的同步方法被阻塞,但是线程C却可以调用对象的其他非同步的成员方法,而不被阻塞。
2.2 结合synchronized修饰的同步方法和同步代码块测试
同2.1的结果一致,线程B会被阻塞。这样也就证明synchronized修饰的同步方法在执行时锁住的是执行方法的实例对象本身。
public class SynchronizedTest {
private CountDownLatch cdl = new CountDownLatch(1);
public synchronized void method1(){
System.out.println("invoke synchronized method1.");
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void method2(){
System.out.println("invoke method2.");
}
public static void main(String[] args) {
final SynchronizedTest test = new SynchronizedTest();
Thread t1 = new Thread(() -> {
test.method1();
}, "synchronized-method-1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (test) {
test.method2();
}
}, "synchronized-method-2");
t2.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
2.3 用一个同时被synchronized和static修饰的方法和synchronized修饰的同步代码块测试
假设某一个类中定义了一个静态方法method1,且method1被synchronized修饰。另外还定义了一个普通成员方法method2。有一个线程A执行时调用类的静态对象method1,未执行完没有退出。同时,有一个线程B执行的任务是在同步代码块(待锁定的对象是这个类本身)执行普通方法method2,那么B线程肯定会被阻塞的。
public class SynchronizedTest {
public static synchronized void method1(){
System.out.println("invoke synchronized method1.");
CountDownLatch cdl = new CountDownLatch(1);
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void method2(){
System.out.println("invoke method2.");
}
public static void main(String[] args) {
final SynchronizedTest test = new SynchronizedTest();
Thread t1 = new Thread(() -> {
method1();
}, "synchronized-method-1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (SynchronizedTest.class) {
test.method2();
}
}, "synchronized-method-2");
t2.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
2.4 可重入锁的测试
假设某一类中定义了2个synchronized修饰的方法,分别是method1和method2,并且method1中需要调用method2。按照正常的逻辑来说线程A在执行method1的时候已经获取了test实例对象的监视器锁,method1方法没有执行完,这个监视器锁是不会被释放的。那么在method1中调用method2,method2也需要重新获取test实例对象的监视器锁,这样锁在线程A手中没有释放,也就无法再次获取啊!但程序的实际运行结果是成功了,没有发生死锁,这种情况就叫做可重入锁。代码如下:
public class SynchronizedTest {
public synchronized void method1(){
System.out.println("invoke synchronized method1.");
method2();
}
public synchronized void method2(){
System.out.println("invoke synchronized method2.");
}
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
test.method1();
}
}
2.5 使用递归来测试可重入锁
有一个著名的数学对象——斐波那契数列,最简单获取斐波那契数列第N个元素的值就是使用递归,这样我们可以在递归方法的方法签名上加上synchronized,程序依然正常执行。不过synchronized锁的重入次数好像是有最大限制的,这一点需要验证。
三、synchronized VS ReentrantLock
1、锁的争抢和释放是否可以手动控制,synchronized是隐式锁,锁的争抢和释放由JVM控制,开发者无法干预;而ReentrantLock是显示锁,开发者需要自己手动去做锁的抢占和释放操作。
2、锁的公平性,依赖于synchronized底层实现方案(C++),它是非公平锁,而ReentrantLock默认是非公平锁,但也可以通过调整参数成为公平锁。非公平锁的吞吐效率比公平锁要高,但是会存在线程饥饿的问题。
3、synchronized场景下的线程抢占锁失败后会阻塞,无法响应线程的中断请求,而ReentrantLock是有响应线程中断的API的。
4、synchronized场景下的线程抢占锁失败后会阻塞(线程状态为Blocked),直到抢到锁,而ReentrantLock下线程抢占锁失败后会挂起(线程状态为waiting),另外ReentrantLock有带有超时参数的API,在一定的时间返回内若无法抢占到锁,便会返回,不再挂起。
5、最大的区别,是否存在条件队列,synchronized不存在条件队列,假设用它实现1生产者-N消费者模型,需要通知同步队列中的某一个具体等待线程,synchronized处理的方式很不优雅,只能全部通知(使用notifyAll),但是ReentrantLock它是有自己的条件队列的,并且可以有多个条件队列,所以它可以直接唤醒相应条件队列中的线程,控制更加灵活方便。
四、同步理解
synchronize关键字修饰的代码块都是同步执行的,如果是对象锁,在同步代码块中可以通过wait/notify的方式释放锁和等待锁资源。如果同步代码块中没有wait/notify指令,那么锁的释放是由JVM自己控制。
public class WaitNotifyCase {
private static final String TARGET = "target";
public static void main(String[] args) {
Runnable runnable1 = () -> {
try{
synchronized (TARGET) {
for (int i = 0; i < 10; i++) {
Thread t = Thread.currentThread();
TARGET.notify();
System.out.println(t.getName() + " " + i + ", then wait.");
TARGET.wait();
}
TARGET.notify();
}
}catch (InterruptedException e){
e.printStackTrace();
}
};
Thread t1 = new Thread(runnable1, "t-1");
t1.start();
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Runnable runnable2 = () -> {
try{
synchronized (TARGET) {
for (int j = 10; j > 0; j--) {
Thread t = Thread.currentThread();
TARGET.notify();
System.out.println(t.getName() + " " + j + ", then wait.");
TARGET.wait();
}
TARGET.notify();
}
}catch (InterruptedException e){
e.printStackTrace();
}
};
Thread t2 = new Thread(runnable2, "t-2");
t2.start();
}
}
以前会认为synchronize修饰的同步代码块,在执行过程是不会释放锁的,理解太狭隘了。上面的例子证明在执行过程中,是可以释放锁,然后重新获得锁的。
五、杂记
1、描述synchronized和ReentrantLock的底层实现及可重入原理。【百度、阿里】
2、请描述锁的4种状态和升级的过程。【百度、阿里】
3、CAS的ABA问题如何解决?【百度】
4、什么是AQS机制?为什么AQS的底层是CAS+volatile?【百度】
5、请谈一下你对volatile的理解。【美团、百度】
6、volatile的保证可见性和防止指令重排序是如何实现的?【美团】
7、请描述一下对象的创建过程。(半初始化问题)【美团、顺丰】
8、请描述一下对象在内存中的内存布局。【美团、顺丰】
9、DCL双重检锁方式的单例为什么需要用volatile修饰?
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object.”
7: astore_1
8: return
astore_1有可能与invokespecial指令重排,导致返回了半初始化的对象。
10、Object obj = new Object();在内存中占了多少字节?【顺丰】
11、请描述synchronized和ReentrantLock的异同。【顺丰】
12、聊聊你对as-if-serial和happens-before的理解。【京东】
13、了解ThreadLocal吗?你知道ThreadLocal是如何解决内存泄漏的问题吗?【阿里、京东】
14、描述一下锁的分类以及在JDK中的应用。【阿里】
15、自旋锁一定比重量级锁效率高吗?轻量级锁一定比重量级锁效率高吗?【阿里】