《深入理解Java虚拟机》:线程安全,两种同步锁实现

《深入理解Java虚拟机》:垃圾收集器与内存分配策略
线程安全定义当多个线程访问一个对象时 , 如果不用考虑这些线程在运行时环境下的调度和交替执行 , 也不需要进行额外的同步 , 或者在调用方法进行任何其他的协调操作 , 调用这个对象的行为都可以获得正确的结果 , 那这个对象就是线程安全的 。
线程安全的实现方法保证线程安全最常见的一种并发手段是互斥同步 , 互斥是方法 , 同步是目的 。
第一种:在java中 , 最基本的互斥同步手段就是synchronized关键字 , synchronized关键字经过编译后会在同步快前后形成monitorenter和monitorexit指令 。
后续锁问题都是基于以下示例代码分析 , 设计的method1()和method2()都需要互斥同步 , method1()内部调用method2() , 是为了验证锁可重入问题:
public class Mysync{ static Object obj = new Object(); public static void method1(){synchronized (obj){System.out.println("method1……");method2();} }public static void method2(){synchronized (obj){System.out.println("method2……");} }public static void main(String[] args) {Mysync.method1(); }}先用javap -verbose Mysync.class > Mysync.txt命令生成字节码 , 截取method1()的字节码文件内容如下:可以看到第5行生成monitorenter指令 , 第18和23行生成了monitorexit指令 , 这两个指令都需要一个reference类型的参数来指定要锁定和解锁的对象 。 我们这里明确指定了锁对象是obj , 如果没有明确指定 , 那就根据synchronized修饰的是实例方法还是类方法 , 去取对应的对象实例或Class类来作为锁对象 。
为什么18和23行monitorexit出现了两次呢?
这是因为加锁之后 , 正常处理结束需要解锁 , 异常处理也需要解锁 , 否则锁不释放会造成死锁 , 所以会有两条解锁命令 , 但实际只会执行一次 。
#javap -verbose Mysync.class > Mysync.txtpublic static void method1();flags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=00: getstatic#13// Field obj:Ljava/lang/Object;3: dup4: astore_05: monitorenter6: getstatic#20// Field java/lang/System.out:Ljava/io/PrintStream;9: ldc#26// String method1……11: invokevirtual #28// Method java/io/PrintStream.println:(Ljava/lang/String;)V14: invokestatic#34// Method method2:()V17: aload_018: monitorexit19: goto2522: aload_023: mmethod2onitorexit24: athrow25: return【《深入理解Java虚拟机》:线程安全,两种同步锁实现】在代码进入method1()时 , 如果能获得对象锁 , 锁计数器加1 , 代码执行结束 , 锁计数器减1.当计数器为0时 , 释放锁;如果获取锁失败 , 那就阻塞等待 , 直到对象锁被另一个线程释放为止 。
jvm规范对monitorenter和monitorexit描述中有两点需要注意:
1、分析以上代码 , method1()获得了对象锁 , 调用method2()时 , 会不会造成死锁呢?
答案是:不会!synchronized同步块对于同一条线程而言是可重入的 , 执行method2()需要获取锁时 , 发现该对象被自己的线程已经锁住 , 这时候锁计算器加1 , 执行代码 , 执行结束 , 锁计算器减1 , 不会出现自己把自己锁死的问题 。
2、对象锁被其他线程占用 , 当前想要获得锁的线程需阻塞等待 。 而java的synchronized互斥锁的实现 , 是基于操作系统互斥锁机制 , 所以阻塞等待要进行用户态和核心态切换 , 是个很耗性能的操作 , 要慎重使用synchronized互斥锁 。
第二种:除了synchronized外 , JUC(java.util.concurrent)包还提供了重入锁ReentrantLock 。
两者的不同点:

  1. ReentrantLock显式的获得、释放锁 , synchronized隐式获得释放锁;synchronized不需要我们手动解锁 , 而ReentrantLock需要自己在finally中调用unlock()方法手动解锁 。
  2. ReentrantLock可响应中断、可轮回 ,, 为处理锁的不可用性提供了更高的灵活性 , synchronized是不可以响应中断的;
  3. ReentrantLock是API级别的 , synchronized是JVM级别的;
  4. ReentrantLock可以实现公平锁 , 而synchronized释放锁后 , 线程竞争无序 , 可能导致线程饥饿(先到的线程一直得不到锁);
  5. ReentrantLock通过Condition可以绑定多个条件;
  6. 底层实现不一样 ,synchronized是同步阻塞 , 使用的是悲观并发策略 , lock是同步非阻塞 , 采用的是乐观并发策略;
  7. Lock是一个接口 , 而synchronized是Java中的关键字 , synchronized是内置的语言实现;
  8. synchronized在发生异常时 , 会自动释放线程占有的锁 , 因此不会导致死锁现象发生;而Lock在发生异常时 , 如果没有主动通过unLock()去释放锁 , 则很可能造成死锁现象 , 因此使用Lock时需要在finally块中释放锁;