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