背景
最近做一个报表,基础数据比较大,然后开了3个线程处理,然后分别向Map里面里面插入数据,这个的Map用的是HashMap,所以为了避免多线程安全性问题。需要在HashMap添加数据的地方要加一个锁。那么有人来说了,你可以用线程安全的CurrentHashMap啊,那你有没有思考过在多线程的环境下CurrentHashMap所有的操作都是安全的吗?这里对CurrentHashMap安不安全先不展开讨论。因为要用到锁,所以绕不过去的肯定就synchronized和ReentrantLock。那这两中锁从实现的方式上有什么不同呢。
首先来先看synchronized,毕竟使用起来简单嘛哈哈哈哈哈,因为synchronized是jvm里面的一个关键字,所以如果你想通过常规的查看源码的方式来弄明白synchronized估计就不行了。所以我们只能通过jdk自带的工具javap -c [class文件] 来通过字节码分析。
先放上测试代码和他对应的字节码。
1 public void method1() {
2 synchronized (this)
3 {
4
5 }
6 }
相对于普通方法,我们是不是一眼就可以看出有差别的地方了,那就是在code:5和code:10那里。根据JVM规范要求,当在执行在执行monitorenter指令的时候,首先要去尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,就把锁的计数器加1,相应地,在执行monitorexit的时候会把计数器减1,当计数器减小为0时,锁就释放了。
注意一点,synchronized是可以修改方法,代码块,静态方法的。所以不同场景锁的对象是不一样的。
通过字节码我们可以知道synchronized锁执行过程,那他底层是怎么来实现的呢?首先在jvm规范中任何一个对象都由三部分组成的,对象头,数据,填充位。在对象头中又分为两部分,第一部分我们叫MarkWord主要用于储存对象自身的运行时数据,比如hashCode,GC年龄,锁信息等。
对象头的另外一部分是类型执指针,即对象指向他的类元数据的指针。
当一个线程在准备获取共享资源的时候:
第一步:检查对象头中的MarkWord放的是不是自己的ThreadID,如果是,表示当前线程是处于 “偏向锁”.跳过轻量级锁直接执行同步体。
第二步:如果第一步中,不是自己的Thread,锁升级轻量级锁。这个时候,用CAS切换操作,这个操作是具体干了什么呢?就是我们当前的这个线程根据MarkWord中的ThreadID,通知之前的线程停止。之前线程将MarkWord中的内容置空。然后这个时候新的线程和之前的线程,分别把锁对象中的HashCode复制到自己LockRecord中,然后通过CAS把自己的LockRecord的地址放到MarkWord中,用这种方式来争抢锁对象的MarkWord。
第三步:如果在第二步中争抢成功,那么抢占到了资源了,把自己的ThreadID放到MarkWord中;如果请战失败了,则进入自旋。
第四步:自旋的在自旋的过程中,如果之前获取到资源的线程释放了共享资源,当前线程获取到锁,则整个状态依然处于轻量级锁的状态。
第五步,如果在第四步中自旋失败,进入重量级锁状态,自旋失败,就是超过了自旋的次数(XX:PreBlockSpin设定)。这个时候自旋的线程会阻塞,等待之前的线程执行完毕之后并唤醒自己。挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力,所以大家为什么总是说,如果线程并发量大,尽量不要去用synchronized的原因吧。
从下表我们可以了解一下不同锁的应用场景以及优缺点。
结尾
概括性来讲就是,对于同步块的实现使用了monitorenter和monitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方式,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。
从上面篇幅中,我们大致的了解到了java中两种常用的锁的实现,那么这样我们在选择使用锁的时候,也更清楚了在什么场景下怎么选择的去使用。比如在执行同步块中,你突然想中断锁,或者想使用公平锁等可以自由操作锁那么就用lock。如果业务本来就很简单,比如想在map.add()加锁就可以直接用synchronize的。