深入理解JVM第13章——线程安全与锁优化

JVM线程安全与锁优化

java语言中的线程安全等级

  1. 不可变: final修饰的基本数据类型。即为不可变。
  2. 绝对线程安全:不管运行时环境如何,调用者都不需要任何额外同步措施。(java中一般达不到)。
  3. 相对线程安全:即为通常意义上的线程安全。对象单次的操作是线程安全的。比如:hashtable,vector等。
  4. 线程兼容:通常意义上的线程不安全。本身不安全,可以通过在调用段使用同步手段保证。比如:hashmap,arraylist等。
  5. 线程对立:无论如何都不能并发使用。(危险,java中一般不存在。)

线程安全的实现方法

1. 互斥同步

最常见的并发保障手段。java中最常用的是使用synchronized关键字进行同步。

synchronized
synchronized具体实现:

​ synchronized关键字经过javac编译后,会在同步块的前后形成monitorenter和monitorexit两个字节码指令。线程所持有的锁根据synchronized修饰的类型决定。在执行monitorenter时,先要尝试获取对象的锁,如果没锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器增加一,执行monitorexit后,计数器减一。一旦计数器值为0,锁随即被释放。如果获取对象锁失败,那么线程就被阻塞(Block),直到请求锁定的对象被持有它的线程释放。

synchronized特点:
  • 可重入
  • 无法强制剥夺,或终端等待
  • 重量级: 需要操作系统从用户态(user mode)转为核心态(kernel mode),消耗很大。
java.util.concurrent.locks.Lock

重入锁(ReentrantLock)是J.U.C中的Lock接口的一种实现。特点为:

  • 等待可中断
  • 公平锁:默认也不公平,可以设置为公平,但那样会使得性能急剧下降。
  • 锁绑定多个条件。synchronized锁只可以绑定单个条件。
对比synchronized锁与ReentrantLock锁:
synchronized锁ReentrantLock锁
重入可重入可重入
公平不公平公平/不公平 (可设定)
等待中断不可停止等待可以停止等待
条件单条件多条件
使用简单易用,虚拟机保证自动释放需要手动保证在finally快中释放锁,否则可能永远不会释放。

2.非阻塞同步

互斥同步属于悲观的,而非阻塞同步是基于冲突检测的乐观并发策略。重点介绍比较并交换(CAS)

CAS

有三个操作数,分别是:内存地址V,旧的预期值A,新的预期值B。当V满足A,才会使用B更新V。否则不更新。 最终返回V的旧值。

java中的应用:使用AtomicInteger代替int, 使用 AtomicInteger :: incrementAndGet() 方法,来代替int a++;可以保证原子性!
IncrementAndGet方法的实现其实就是,在死循环中,不断调用compareAndSet(java实现的CAS)方法!

3. 无同步方案

  1. 可重入代码:又称纯代码。可以在任何时刻终端他,去执行另外一段代码,而后继续执行后,不会出现任何错误。特征:不依赖全局变量。判断依据:如果一个方法的返回结果是可以预测的,只要输入了相同输入,就能返回相同结果,即满足可重入性。(就是数学中的一对一函数嘛。。。)

  2. 线程本地存储:对于某个变量,线程独享。 使用ThreadLocal类来实现。

    每一个线程的Thread对象中都有一个Thre adLocalMap对象,以ThreadLocal.threadLocalHashCode为键,以本地线程变量为值。

    扩展:

    SimpleDataformate为一个线程不安全的工具类,当多个线程同时使用,可能使得a线程刚要使用,b线程却把值clear的问题。

    重点理解:

    1. 为什么 ThreadLocalMap 放在 Thread ,而不放在 ThreadLocal 中呢?
      试想一下,如果 Thread 结束了生命周期,而此时 ThreadLocal 还没有结束生命周期,由于 ThreadLocal 引用了 ThreadLocalMap,ThreadLocalMap 引用了 Thread,使得 Thread 无法被垃圾收集器回收,导致内存泄漏。
    2. 为什么 Entry 对 ThreadLocal 是弱引用(WeakReference)?
      试想一下,如果 ThreadLocal 结束了生命周期,而此时 Thread 还没有结束生命周期(从线程池中获取线程),由于 Thread -> ThreadLocalMap -> entry -> key(ThreadLocal) 的引用存在,使得 ThreadLocal 无法被回收,有了弱引用之后,ThreadLocal 只能存活到下次GC。
    3. value 为什么不能被回收?
      Entry 中的 value 是被 Entry 强引用的,所以即便 value 的生命周期结束了,value 也是无法被回收的,从而导致内存泄露。
    4. 那要如何回收 value 呢?
      手动remove。

锁优化

1. 自旋锁与自适应自旋:稍等下,不阻塞。

​ 由于互斥同步中对性能影响最大的是阻塞的实现,线程的挂起和恢复都需要用户态转内核态。但是另一个线程往往很快就会释放锁。因此,我们让得不到锁的线程“稍等一会儿”,但不放弃处理器的执行时间。自旋锁默认关闭,需要手动打开。自旋次数由用户手动指定。

即排队的时候,让用户先在旁边等一下,而不走远,这样就不会浪费用户走来走去的时间了。

自适应自旋:在自旋锁中自旋次数由用户手动指定,很不明智。我们让虚拟机变聪明,根据以往的经验来决定是否自旋,以及自旋的时间。

就像排队的时候,以往我每次来买煎饼都很快,这次我买煎饼时就在旁边等会儿;如果要去买现炸的糕点,以往来的时候都要等很久,那么我干脆直接去远处坐着,不会在这旁边站着干等着。

2. 锁消除:不逃逸的数据不锁

​ 有很多数据,虽然被锁上了,但是其实根本不会被共享,那么这个锁会被虚拟机直接消除。(虚拟机可真聪明)

​ 以上就要依靠虚拟机的逃逸分析技术,(听起来很牛的样子,实现虽然我们不懂,但是实际上作用很好理解)。逃逸分析的结果(由低到高的逃逸程度)分为:

  • 不逃逸:不被其他方法或线程访问。
  • 方法逃逸:作为调用参数传递到其他方法中。
  • 线程逃逸:可以被外部线程访问到。

3. 锁粗化:由锁定一个序列,代替连续几个语句频繁锁定

如果一系列操作都是连续对同一个对象加锁解锁,会导致不必要的性能消耗。因此,JVM会把锁同步的范围粗化到整个操作序列的外部。。

4. 轻量级锁:在无竞争的情况下,利用CAS操作避免了互斥量的开销

​ 利用对象头中Mark Word(MW )部分的2bit作标志位。

  • 当标志位显示没有被锁定(01)。 把MW复制到线程的栈帧中的锁记录(Lock Record)的位置。
  • 尝试使用CAS操作把对象的MW指针,更新指向Lock Record,并且把标志位转为00(轻量级锁定的标识)。
  • 如果成功,即可进入同步块。
  • 如果不成功:检查MW是否只想当前线程的栈帧(被自己抢了),如果是自己,可以直接进入同步快。如果不是自己,说明被其他线程抢占了。如果两条以上的线程争用一个锁,那么轻量级锁膨胀为重量级锁。等待的线程进入阻塞状态!
  • 解锁的过程也是通过CAS操作进行。 如果替换失败说明有其他线程尝试过获取该锁,需要在释放锁的时候,唤醒被挂起的线程。

特点:“绝大部分的锁,在同步周期内不存在竞争”,才使得轻量级锁能提升性能(CAS避免了互斥量的开销)。而如果有竞争,使用CAS反而增加了开销。

5. 偏向锁:在无竞争的情况下,把整个同步都消除掉,进入高速模式

​ “偏”即为“偏心”,锁会骗你选哪个与第一个获得它的线程。如果接下来锁一直没有别其他线程获得,则持有偏向锁的线程将不需要进行同步。

​ 当锁对象第一次被线程获取时,把偏向模式字段设为1,利用CAS把线程ID记录进MW。之后该线程在进入时,不需要进行任何同步操作。

​ 一旦有其他线程尝试去获取这个锁,则偏向模式结束。如果对象未锁定,则撤销偏向,变成未被锁定的不可偏向对象,如果对象已经被锁定,则变成被轻量级锁定的对象

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值