一、三种用法

1.1、修饰在对象的成员方法上,锁住的监视器对象是对象的实例。
1.2、修饰在对象的静态方法上,锁住的监视器对象是类。
1.3、修饰在代码块上,锁住的监视器对象是关键字后花括号中的对象(也有可能是类)。

二、验证

2.1 用两个synchronized修饰的成员方法测试
假设某个类中定义了两个公开的方法(分别是method1和method2),这两个方法都用了synchronized来修饰。假设线程A调用了method1但并未执行完,这时另外一个线程B开始调用method2,会发现线程B被阻塞。因为线程A和B尝试获取的监视器锁是同一个,A未释放,所以B无法得到。代码如下:

  1. public static void main(String[] args) {
  2. final SynchronizedTest test = new SynchronizedTest();
  3. Thread t1 = new Thread(() -> {
  4. test.method1();
  5. }, "synchronized-method-1");
  6. t1.start();
  7. Thread t2 = new Thread(() -> {
  8. test.method2();
  9. }, "synchronized-method-2");
  10. t2.start();
  11. try {
  12. TimeUnit.SECONDS.sleep(1);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. System.out.println(t1.getState());
  17. System.out.println(t2.getState());
  18. }

注意:
虽然线程B调用synchronized修饰的同步方法被阻塞,但是线程C却可以调用对象的其他非同步的成员方法,而不被阻塞。

2.2 结合synchronized修饰的同步方法和同步代码块测试
同2.1的结果一致,线程B会被阻塞。这样也就证明synchronized修饰的同步方法在执行时锁住的是执行方法的实例对象本身。

  1. public class SynchronizedTest {
  2. private CountDownLatch cdl = new CountDownLatch(1);
  3. public synchronized void method1(){
  4. System.out.println("invoke synchronized method1.");
  5. try {
  6. cdl.await();
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. }
  11. public void method2(){
  12. System.out.println("invoke method2.");
  13. }
  14. public static void main(String[] args) {
  15. final SynchronizedTest test = new SynchronizedTest();
  16. Thread t1 = new Thread(() -> {
  17. test.method1();
  18. }, "synchronized-method-1");
  19. t1.start();
  20. Thread t2 = new Thread(() -> {
  21. synchronized (test) {
  22. test.method2();
  23. }
  24. }, "synchronized-method-2");
  25. t2.start();
  26. try {
  27. TimeUnit.SECONDS.sleep(1);
  28. } catch (InterruptedException e) {
  29. e.printStackTrace();
  30. }
  31. System.out.println(t1.getState());
  32. System.out.println(t2.getState());
  33. }
  34. }

2.3 用一个同时被synchronized和static修饰的方法和synchronized修饰的同步代码块测试
假设某一个类中定义了一个静态方法method1,且method1被synchronized修饰。另外还定义了一个普通成员方法method2。有一个线程A执行时调用类的静态对象method1,未执行完没有退出。同时,有一个线程B执行的任务是在同步代码块(待锁定的对象是这个类本身)执行普通方法method2,那么B线程肯定会被阻塞的。

  1. public class SynchronizedTest {
  2. public static synchronized void method1(){
  3. System.out.println("invoke synchronized method1.");
  4. CountDownLatch cdl = new CountDownLatch(1);
  5. try {
  6. cdl.await();
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. }
  11. public void method2(){
  12. System.out.println("invoke method2.");
  13. }
  14. public static void main(String[] args) {
  15. final SynchronizedTest test = new SynchronizedTest();
  16. Thread t1 = new Thread(() -> {
  17. method1();
  18. }, "synchronized-method-1");
  19. t1.start();
  20. Thread t2 = new Thread(() -> {
  21. synchronized (SynchronizedTest.class) {
  22. test.method2();
  23. }
  24. }, "synchronized-method-2");
  25. t2.start();
  26. try {
  27. TimeUnit.SECONDS.sleep(1);
  28. } catch (InterruptedException e) {
  29. e.printStackTrace();
  30. }
  31. System.out.println(t1.getState());
  32. System.out.println(t2.getState());
  33. }
  34. }

2.4 可重入锁的测试
假设某一类中定义了2个synchronized修饰的方法,分别是method1和method2,并且method1中需要调用method2。按照正常的逻辑来说线程A在执行method1的时候已经获取了test实例对象的监视器锁,method1方法没有执行完,这个监视器锁是不会被释放的。那么在method1中调用method2,method2也需要重新获取test实例对象的监视器锁,这样锁在线程A手中没有释放,也就无法再次获取啊!但程序的实际运行结果是成功了,没有发生死锁,这种情况就叫做可重入锁。代码如下:

  1. public class SynchronizedTest {
  2. public synchronized void method1(){
  3. System.out.println("invoke synchronized method1.");
  4. method2();
  5. }
  6. public synchronized void method2(){
  7. System.out.println("invoke synchronized method2.");
  8. }
  9. public static void main(String[] args) {
  10. SynchronizedTest test = new SynchronizedTest();
  11. test.method1();
  12. }
  13. }

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自己控制。

  1. public class WaitNotifyCase {
  2. private static final String TARGET = "target";
  3. public static void main(String[] args) {
  4. Runnable runnable1 = () -> {
  5. try{
  6. synchronized (TARGET) {
  7. for (int i = 0; i < 10; i++) {
  8. Thread t = Thread.currentThread();
  9. TARGET.notify();
  10. System.out.println(t.getName() + " " + i + ", then wait.");
  11. TARGET.wait();
  12. }
  13. TARGET.notify();
  14. }
  15. }catch (InterruptedException e){
  16. e.printStackTrace();
  17. }
  18. };
  19. Thread t1 = new Thread(runnable1, "t-1");
  20. t1.start();
  21. try {
  22. TimeUnit.MILLISECONDS.sleep(100);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. Runnable runnable2 = () -> {
  27. try{
  28. synchronized (TARGET) {
  29. for (int j = 10; j > 0; j--) {
  30. Thread t = Thread.currentThread();
  31. TARGET.notify();
  32. System.out.println(t.getName() + " " + j + ", then wait.");
  33. TARGET.wait();
  34. }
  35. TARGET.notify();
  36. }
  37. }catch (InterruptedException e){
  38. e.printStackTrace();
  39. }
  40. };
  41. Thread t2 = new Thread(runnable2, "t-2");
  42. t2.start();
  43. }
  44. }

以前会认为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.”“:()V
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、自旋锁一定比重量级锁效率高吗?轻量级锁一定比重量级锁效率高吗?【阿里】