当前位置: 首页 > 图灵资讯 > 技术篇> Java锁

Java锁

来源:图灵教育
时间:2023-06-28 14:19:59

乐观锁:乐观锁是一种乐观的想法,即阅读更少,遇到并发写作的可能性低,每次拿数据认为别人不会修改,所以不会锁定,但在更新中会判断以下数据是否更新,使用当前版本编号,然后锁定操作(与上一个版本编号相比,如果更新相同),如果失败重复阅读比较写作操作。

Java中的乐观锁基本上是通过CAS操作实现的。CAS是一种更新的原子操作。比较当前值是否与传入值相同,然后更新,否则将失败。

悲观锁:悲观锁是悲观的想法,也就是说,认为写更多,遇到并发写作的可能性很高,每次去拿数据认为别人会修改,所以每次读写数据都会锁定,所以别人想读写数据会直到锁定。Java中的悲观锁是Synchronized,AQS框架下的锁是尝试CAS乐观锁获取锁。如果得不到,就会变成悲观锁,比如Reentrenlock。

自旋锁:自旋锁的原理很简单。如果持有锁的线程能在很短的时间内释放锁资源,那么等待竞争锁的线程就不需要做内核状态和用户状态之间的切换,进入堵塞挂起状态。他们只需要等待(自旋),等待持有锁的线程释放锁,就可以立即获得锁,避免用户线程和内核之间切换的消耗。

线程自旋需要消耗cpu。说白了,就是让cup做无用的工作。如果没有锁,线程就不能一直占用cpu自旋做无用的工作,所以需要设定最大的自旋等待时间。

如果持有锁的线程执行时间超过自旋等待的最大时间仍未释放锁,则其他争用锁的线程在最大等待时间内仍无法获得锁。此时,争用线程将自动停止自旋并进入自旋状态。

自旋锁的优缺点:自旋锁尽量减少线程阻塞,大大提高了锁竞争不激烈、占用锁时间很短的代码块的性能,因为自旋锁的消耗量会小于线程阻塞挂起再唤醒操作的消耗量,导致线程两次上下文切换!

但是,如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,则不适合此时使用自旋锁,因为自旋锁在获得锁之前一直被占用 cpu 做无用的工作,占领 XX 不 XX,与此同时,大量的线程正在竞争一把锁,这将导致长时间获得锁,而且线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cup 无法获得线程 cpu,造成 cpu 的浪费。因此,在这种情况下,我们必须关闭自旋锁。

适应性自旋锁时间阀值(1.6引入):自旋锁的目的是占用自旋锁 CPU 当获得锁时,不释放资源并立即处理。但是如何选择自旋执行时间呢?如果自旋执行时间过长,大量线程将被自旋占用 CPU 资源会影响整个系统的性能。因此,自旋循环的选择尤为重要!

JVM jdk1选择自旋周期.5 这个限度是一定程度的写死,在 1.6 引入适应性自旋锁,适应性自旋锁性自旋锁意味着自旋时间不是固定的,而是由同一锁上的自旋时间和锁的所有者的状态决定,基本上认为线程上下文切换的时间是最佳时间,同时 JVM 还针对当前 CPU 如果平均负载小于,则对负载进行了更多的优化 CPUs 一直自旋,如果有超过的话。(CPUs/2)如果一个线程自旋,线程直接堵塞,如果发现自旋线程 Owner 如果发生变化,则延迟自旋时间(自旋计数)或进入阻塞,如果 CPU 在节电模式下停止自旋,自旋时间最坏的情况是 存储CPU延迟(CPU A 存储数据,到 CPU B 了解数据的直接时差),自旋时会适当放弃线程优先级之间的差异。

打开自旋锁

JDK1.6 中-XX:+UseSpinning 开启;

-XX:PreBlockSpin=10 为自旋次数;

JDK1.7 之后,去掉这个参数,由 jvm 控制;

Synchronized同步锁:Synchronized可以将任何非NULL对象作为锁,属于独家悲观锁,属于可重入锁。

Synchronized的作用范围

1.当作用于方法时,锁定对象的例子(this)

2.作用于静态方法时,由于Class的相关数据存储在永久代Permgen(jdk1)中,Class的实例被锁定.8 是metaspace),永久代是全局共享的,所以静态方法锁相当于锁的全局锁,会锁定这种方法的所有线程。

3.当synchronized作用于对象实例时,锁定所有锁定对象的代码块。它有多个队列。当多个线程一起访问对象监视器时,对象监视器将这些线程存储在不同的容器中。

Synchronized核心组件:

1.Wait Set:这里放置了调用wait方法堵塞的线程;

2.Contention List:竞争队列,所有要求锁的线程首先放在这个竞争队列中;

3.Entry List: Contention 在List中,有资格成为候选资源的线程被移动到Entry List中;

4.OnDeck:在任何时候,最多只有一个线程在竞争锁定资源,这个线程被称为Ondeck。;

5.Owner:现在已经获得资源的线程被称为Owner。;

6.!Owner:目前释放锁的过程。

Synchronized实现

Java锁_加锁

  1. JVM 每次从队列的尾部取出一个锁定竞争候选人的数据(OnDeck),但并发情况下,ContentionList 大量并发线程将进行 CAS 访问,以减少对尾部元素的竞争,JVM 将部分线程移动到 EntryList 作为候选人的竞争线程。
  2. Owner 线程会在 unlock 时,将 ContentionList 部分线程迁移到中间 EntryList 中,并指定EntryList 中间的一个线程是 OnDeck 线程(通常是最先进的线程)。
  3. Owner 线程不直接将锁传递给 OnDeck 线程,但锁定竞争的权利 OnDeck,OnDeck 需要重新竞争锁。这样,虽然牺牲了一些公平性,但可以大大提高JVM系统的吞吐量 这种选择行为也被称为“竞争转换”。
  4. OnDeck 获得锁定资源后,线程将变成锁定资源 Owner 没有锁定资源的线程仍然停留在 在EntryList中。 Owner 线程被 wait 如果方法堵塞,则转移到 WaitSet 在队列中,直到某个时刻通过 或者notify notifyAll 觉醒,会再进去的 EntryList 中。
  5. 处于 ContentionList、EntryList、WaitSet 中间的线程处于阻塞状态,由操作系统完成(Linux 内核下采用 pthread_mutex_lock 实现内核函数)。
  6. Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待线程将首先尝试自旋获取锁,如果不能进入 ContentionList,这显然对已经进入队列的线程不公平。另一件不公平的事情是,自旋获取锁的线程也可以直接占用 OnDeck 锁定线程资源。
  7. 每个物体都有一个 monitor 对象,锁定就是竞争 monitor 对象,代码块加锁前后分别添加 monitorenter 和 monitorexit 实现指令的方法添加方法锁是通过标记位来判断的
  8. synchronized 它是一个重量级操作,需要调用操作系统的相关接口,性能低效,有可能比有用的操作消耗更多的时间来锁定线程。
  9. Java1.6,synchronized 经过大量的优化,适应自旋、消除锁、粗化锁、轻量级锁、偏向锁等,本质上提高了效率。后来推出的 Java1.7 与 1.8 在中间,优化了关键字的实现机制。介绍偏向锁和轻量级锁。对象头上有标记位,不需要通过操作系统锁定。
  10. 锁可以从偏向锁升级到轻量级锁,然后升级到重量级锁。这种升级过程称为锁膨胀;
  11. JDK 1.6 默认情况下,偏向锁和轻量级锁可以通过打开-XX:-UseBiasedLocking 禁用偏向锁。