JAVA并发-探究sychronized底层实现原理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_40285302/article/details/82916082

前言:
之前一直对sychronized的原理不是特别清楚,只知道在jdk1.6之前使用sychronized锁性能较低,后续进行了优化,加入了偏向锁、自旋等机制来提高sychronized性能,但是抱着模棱两可的心态总是很受的,于是特意学习了一波,充实自己,也分享于大家,本文主要是对sychronized特性以及实现原理进行剖析,本文会以代码结合反编译字节码的形式来进行分析


简介:
1.sychronized是我们经常用到的一种同步机制,用于保证线程安全
2.sychronized可以保证原子性、可见性、有序性


sychronized的使用方式有哪些?
1.修饰方法:普通方法;静态方法
3.修饰代码块


sychronized修饰方法和修饰代码块有何区别?
1.获取锁不一样:修饰普通方法获取的是当前对象锁;修饰静态方法获取的是类对象锁;修饰代码块获取的是sychronized(Object)括号中的对象锁
2.作用范围不一样:修饰方法作用范围较大;修饰代码块作用范围更小,可以更灵活地进行操作
3.底层实现原理不一样:修饰代码块是通过Monitor相关指令实现的;修饰方法是通过jvm方法表结构中ACC_SYNCHRONIZED访问标志位实现的


sychronized修饰代码块底层具体是如何实现的?

首先编写验证代码:

 
  1. public class BlockSync

  2. {

  3. public void sync()

  4. {

  5. synchronized (this)

  6. {

  7. System.out.println("执行同步代码块的逻辑.");

  8. }

  9. }

  10. }

然后将编译后的字节码文件放到linux服务上,通过javap反编译命令查看具体信息:

执行命令:

javap -p -v /home/BlockSync.class

打印信息如下:

 
  1. Classfile /home/BlockSync.class

  2. Last modified Jul 2, 2018; size 580 bytes

  3. MD5 checksum 5ca0058a89445b19761e563d48903fec

  4. Compiled from "BlockSync.java"

  5. public class com.synchronize.service.BlockSync

  6. SourceFile: "BlockSync.java"

  7. minor version: 0

  8. major version: 49

  9. flags: ACC_PUBLIC, ACC_SUPER

  10. Constant pool:

  11. .... //常量池内容太多,这里手动忽略了

  12. {

  13. public com.synchronize.service.BlockSync();

  14. descriptor: ()V

  15. flags: ACC_PUBLIC

  16. Code:

  17. stack=1, locals=1, args_size=1

  18. 0: aload_0

  19. 1: invokespecial #8 // Method java/lang/Object."<init>":()V

  20. 4: return

  21. LineNumberTable:

  22. line 11: 0

  23. LocalVariableTable:

  24. Start Length Slot Name Signature

  25. 0 5 0 this Lcom/synchronize/service/BlockSync;

  26.  
  27. public void sync();

  28. descriptor: ()V

  29. flags: ACC_PUBLIC

  30. Code:

  31. stack=2, locals=2, args_size=1

  32. 0: aload_0

  33. 1: dup

  34. 2: astore_1

  35. 3: <span style="color:#ff0000;">monitorenter</span>

  36. 4: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;

  37. 7: ldc #21 // String 执行同步代码块的逻辑.

  38. 9: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

  39. 12: aload_1

  40. 13: <span style="color:#ff0000;">monitorexit</span>

  41. 14: goto 20

  42. 17: aload_1

  43. 18: <span style="color:#ff0000;">monitorexit </span>

  44. 19: athrow

  45. 20: return

  46. Exception table:

  47. from to target type

  48. 4 14 17 any

  49. 17 19 17 any

  50. LineNumberTable:

  51. line 15: 0

  52. line 17: 4

  53. line 15: 12

  54. line 19: 20

  55. LocalVariableTable:

  56. Start Length Slot Name Signature

  57. 0 21 0 this Lcom/synchronize/service/BlockSync;

  58. }

由上面标红的打印信息可以看出:
1.sychronized修饰代码块,是通过调用monitorenter指令获取到对象锁
2.通过调用monitorexit指令释放对象锁
3.这里一个monitorenter对应两个monitorexit,后面一个monitorexit是为了防止异常中断情况,这时也要释放锁


sychronized修饰普通方法底层具体是如何实现的?

首先编写验证代码:

 
  1. public class MethodSync

  2. {

  3. public synchronized void sync()

  4. {

  5. System.out.println("执行普通方法的同步逻辑.");

  6. }

  7. }

然后将编译后的字节码文件放到linux服务上,通过javap反编译命令查看具体信息:

执行命令:

javap -p -v /home/MethodSync.class

打印信息如下:

 
  1. Classfile /home/MethodSync.class

  2. Last modified Jul 2, 2018; size 550 bytes

  3. MD5 checksum ca3aa080a3bfa2b16f99d093acf5811d

  4. Compiled from "MethodSync.java"

  5. public class com.synchronize.service.MethodSync

  6. SourceFile: "MethodSync.java"

  7. minor version: 0

  8. major version: 49

  9. flags: ACC_PUBLIC, ACC_SUPER

  10. Constant pool:

  11. .... //常量池内容太多,这里手动忽略了

  12. {

  13. public com.synchronize.service.MethodSync();

  14. descriptor: ()V

  15. flags: ACC_PUBLIC

  16. Code:

  17. stack=1, locals=1, args_size=1

  18. 0: aload_0

  19. 1: invokespecial #8 // Method java/lang/Object."<init>":()V

  20. 4: return

  21. LineNumberTable:

  22. line 11: 0

  23. LocalVariableTable:

  24. Start Length Slot Name Signature

  25. 0 5 0 this Lcom/synchronize/service/MethodSync;

  26.  
  27. public synchronized void sync();

  28. descriptor: ()V

  29. flags: ACC_PUBLIC, <span style="color:#ff0000;">ACC_SYNCHRONIZED</span>

  30. Code:

  31. stack=2, locals=1, args_size=1

  32. 0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;

  33. 3: ldc #21 // String 执行普通方法的同步逻辑.

  34. 5: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

  35. 8: return

  36. LineNumberTable:

  37. line 15: 0

  38. line 16: 8

  39. LocalVariableTable:

  40. Start Length Slot Name Signature

  41. 0 9 0 this Lcom/synchronize/service/MethodSync;

  42. }

由上面标红的打印信息可以看出:
1.sychronized修饰普通方法时,没有使用monitor指令实现锁的获取与释放,而是使用了ACC_SYNCHRONIZED标识,表示该方法是同步方法

2.当方法有ACC_SYNCHRONIZED标识,则进入方法前尝试获取与对象关联的monitor,当方法执行完成则释放monitor,如果发生异常则释放monitor

sychronized修饰静态方法底层具体是如何实现的?

首先编写验证代码:

 
  1. public class StaticMethodSync

  2. {

  3. public synchronized static void sync()

  4. {

  5. System.out.println("执行静态方法的同步逻辑.");

  6. }

  7. }

然后将编译后的字节码文件放到linux服务上,通过javap反编译命令查看具体信息:

执行命令:

java -p -v /home/StaticMethodSync.class

打印信息如下:

 
  1. Classfile /home/StaticMethodSync.class

  2. Last modified Jul 2, 2018; size 558 bytes

  3. MD5 checksum cdf8b4f17971ac67cee30decc2167515

  4. Compiled from "StaticMethodSync.java"

  5. public class com.synchronize.service.StaticMethodSync

  6. SourceFile: "StaticMethodSync.java"

  7. minor version: 0

  8. major version: 49

  9. flags: ACC_PUBLIC, ACC_SUPER

  10. Constant pool:

  11. .... //常量池内容太多,这里手动忽略了

  12. {

  13. public com.synchronize.service.StaticMethodSync();

  14. descriptor: ()V

  15. flags: ACC_PUBLIC

  16. Code:

  17. stack=1, locals=1, args_size=1

  18. 0: aload_0

  19. 1: invokespecial #8 // Method java/lang/Object."<init>":()V

  20. 4: return

  21. LineNumberTable:

  22. line 11: 0

  23. LocalVariableTable:

  24. Start Length Slot Name Signature

  25. 0 5 0 this Lcom/synchronize/service/StaticMethodSync;

  26.  
  27. public static synchronized void sync();

  28. descriptor: ()V

  29. flags: ACC_PUBLIC, ACC_STATIC, <span style="color:#ff0000;">ACC_SYNCHRONIZED</span>

  30. Code:

  31. stack=2, locals=0, args_size=0

  32. 0: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream;

  33. 3: ldc #21 // String 执行静态方法的同步逻辑.

  34. 5: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

  35. 8: return

  36. LineNumberTable:

  37. line 15: 0

  38. line 16: 8

  39. LocalVariableTable:

  40. Start Length Slot Name Signature

  41. }

由上面标红的打印信息可以看出:

1.sychronized修饰静态法时,和修饰普通方法使用的同一种机制,即使用ACC_SYNCHRONIZED标识该方法是同步方法

sychronized底层访问Monitor具体是如何操作的?

1.多个线程访问对象的Monitor时,Monitor会将这些线程存储到不同容器中:

Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中

Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中

Wait Set:哪些调用wait方法被阻塞的线程被放置在这里

OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck

Owner:当前已经获取到所资源的线程被称为Owner

!Owner:当前释放锁的线程

2.JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),在并发情况下,Contention List会被大量线程进行CAS访问,为了降低对尾部元素的竞争,将一部分线程移动到Entry List中作为候选竞争线程

3.Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程),Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁,这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”

4.OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中

5.处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)

sychronized的非公平性如何实现的?

1.Synchronized在线程进入ContentionList时,当前线程会先尝试自旋获取锁,如果获取不到就进入ContentionList

2.自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源

sychronized的可重入性是如何实现的?

1.线程已经获取到对象的Monitor后,后续再次获取相同的对象锁时,就不需要再次发起锁请求,因为单线程下是同步执行的,所以可以减少不必要的资源消耗

2.如果是在偏向锁或轻量级锁的情况下,可以参照:JAVA并发-sychronized优化,其中偏向锁、轻量级锁加锁过程中,都会判断是否是同一线程,如果是同一线程则直接执行。

猜你喜欢

转载自blog.csdn.net/qq_40285302/article/details/82916082