currenthashmap如何实现线程安全_深入理解Java虚拟机:线程安全,两种同步锁实现...

48d22a964af063ab0017055c6f64fd45.png

线程安全定义

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

线程安全的实现方法

保证线程安全最常见的一种并发手段是互斥同步,互斥是方法,同步是目的。

第一种:在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_STATIC    Code:      stack=2, locals=1, args_size=0         0: getstatic     #13                 // Field obj:Ljava/lang/Object;         3: dup                    4: astore_0               5: monitorenter           6: 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;)V        14: invokestatic  #34                 // Method method2:()V        17: aload_0               18: monitorexit           19: goto          25        22: aload_0               23: mmethod2onitorexit           24: athrow                25: return  

在代码进入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块中释放锁;

  9. 通过Lock可以知道有没有成功获取锁,而synchronized无法得知获得锁情况;

  10. Lock可以提高多个线程进行读操作的效率,既就是实现读写锁等。

通过以上对比,ReentrantLock可能更加灵活一些,但是从性能考虑,JDK1.6以后对synchronized做了很多优化锁,实际中还是提倡synchronized来进行同步。

下一节分享虚拟机对synchronized的锁优化。

内容来源于《深入理解Java虚拟机》第十三章13.2节内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值