yihau

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

  说到实现线程安全第一个想到的就应该是锁,同步,synchronized这样的字眼。但是synchronized是怎么实现同步呢,在JVM编译的时候会在synchronized块的前后分别添加一条指令,monitorenter/monitorexit,会在字节码的异常路径上也添加monitorexit。任何对象都有一个monitor与之关联,线程执行到monitorenter的时候,会尝试获取对象的monitor,如果获取到,那么就等于持有了这个对象的锁,这个monitor有且仅有一个线程可以持有,其他线程尝试获取该monitor的时候只能等着或者阻塞。当执行完同步块的代码后,执行monitorexit会释放掉改对象的monitor,并且唤醒在这个对象上阻塞的线程。那么这个对象到底是谁呢?我们一般会在三种情况下使用synchronized,

  • 使用synchronized修饰的普通方法,此时锁的对象调用此方法的类实例对象;
  • 使用synchronized修饰的静态方法,此时锁的对象是类的Class对象;
  • 使用synchronized修饰的同步块,此时锁的对象就是同步块前的括号中的对象。

synchronized是JVM层面上支持的同步手段,在JUC的包中也有类可以实现加锁的功能,ReentrantLock类是我们常用的另一种加锁手段,这种锁的时候需要我们手动创建ReentrantLock对象,显示调用的ReentrantLock的lock方法和unlock方法来加锁解锁。注意unlock方法一般需要写在try/finally块的finally中,保证无论何时都要解锁。除了和synchronized一样的锁功能,ReentrantLock还有一些高级特性,

  • 等待可中断,假如线程获取锁的时候需要等待很久,可以在等待的期间中断出来,就是等不到就不等了,当然不等了肯定是没获取到锁,但是线程可以去做别的事了。对应的方法为lockInterruptibly()。
  • 公平锁,公平锁就是锁多个线程尝试获取锁的时候,等待时间最长的那个线程会先获取到锁。在创建ReentrantLock对象的时候传入true参数得到的就是公平锁。
  • 锁绑定多个条件,使用synchronized的时候只能对锁的对象进行wait/notify,但是使用ReentrantLock,可以创建多个Condition,对多个Condition进行await/signal(多用于生产者消费者模式)。

我们可能又知道synchronized是一种重量级的锁,重量级的锁在同步问题中会使线程阻塞,而线程挂起和唤醒都需要操作系统转入内核态来完成,毫无疑问这很耗费资源,。而ReentrantLock基本是使用volatile+CAS算法来实现的,这种实现也可以称为乐观锁,对应着JVM中原来synchronized的实现就是悲观锁。这样是不是就是锁ReentrantLock比synchronized高效了呢,其实也不尽然。因为我们的JVM实现的锁也一直在优化呀,这就要谈到JVM一些锁优化技术了,

  • 自旋锁和自适应自旋,想象一下这种情况,线程B需要进入同步块的时候发现线程A持有锁,按照之前的处理,B会挂起,等待A来唤醒,很可能B刚刚挂起A就释放了锁,这样就很不爽,如果B多等一会儿就可以不用挂起了,这就是自旋锁出现的原因。自旋锁即碰到获取锁的情况会先让CPU空转一会儿,而不是放弃CPU的时间,可能很快就会得到锁了。但是可能有的情况下线程A会持有锁很长时间不放,那么B一直在这里空转也是很耗费资源的,所以就加入了自适应的自旋,这样JVM会根据同一个锁上一次的自旋时间和锁的拥有者的状态来决定自旋的时间。
  • 锁消除和锁粗化,这两种优化技术主要是在编译阶段来解决的,锁消除是编译器对于代码上同步,但是检测到不存在共享数据竞争时,会自动去掉锁,而锁粗化是解决在一段代码中频繁加锁解锁的情况,虽然说同步块的区域越小越好,但还是要避免在一段代码中频繁加锁解锁,这时编译器会直接将锁的范围扩大。
  • 偏向锁,线程进入锁的时候会使用CAS算法将锁对象的状态置为偏向状态,这样下次同一线程再次获取锁将不需要同步,消除同步,直接进入。但是如果有其他线程来获取这个锁,那么偏向模式就会结束,锁会升级到轻量级锁。
  • 轻量级锁,轻量级锁是使用CAS算法来代替阻塞,线程进入锁的时候使用CAS算法设置锁对象的状态,当其他线程也想进入锁的时候会也会尝试使用CAS算法来设置锁对象的状态,如果失败说明其他线程已经获取了锁。自旋等待一段时间仍然没有获取锁宣告失败,此时锁会升级到重量级锁,线程进入阻塞状态,然后持有锁的线程在解锁时会使用CAS算法更新锁对象的状态,如果成功,说明没有线程来竞争锁,失败说明有线程竞争,然后需要唤醒阻塞的线程。

所以,我们在使用锁的时候如果出于性能考虑完全不需要放弃synchronized,如果出于ReentrantLock的一些高级特性,那可以选择ReentrantLock

posted on 2018-03-10 21:35  yihau  阅读(161)  评论(0编辑  收藏  举报