JAVA多线程-锁
1. 乐观锁
乐观锁是一种乐观思想,认为读多写少。遇到并发写的可能性低,每次去拿数据的时候都认为别人不会去修改,所以不会上锁。但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,才去在写时先读取当前版本号,然后枷锁操作(比较更上一次版本号,如果一样则更新),如果失败,则要重复读-比较-写的操作。
JAVA中乐观锁基本都是通过CAS操作实现,CAS是一种原子更新操作,比较当前值跟传入的值是否一样,一样则更新,否则失败。
CAS详解,https://blog.csdn.net/qq_41282026/article/details/98089231
2. 悲观锁
悲观锁是一种悲观思想,认为写多读少。遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在写的时候都会去上锁,这样别人想读写这个数据就会 block 直到拿到锁。java中悲观锁就是 Synchronized, AQS框架下的锁则先尝试 cas 乐观锁去获取锁,获取不到才会转为悲观锁,如 Retreenlock。
3.自旋锁
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态的切换进入阻塞挂起状态,他们只需要等一等(自旋),等待持有锁的线程释放后即可立即获取锁,这样寄避免了用户线程和内核的切换消耗。
线程自旋是需要消耗CPU的,如果一直获取不到锁,那么线程会一直占用CPU,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行时间超过最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,这是争用线程会停止自旋进入阻塞状态。
自旋锁的优缺点:
-
对于竞争不激烈,且占用锁的时间非常短的代码块来说性能能大幅度的提升,自旋锁尽可能的减少线程的阻塞,自旋锁的消耗小于线程阻塞挂起再唤醒的操作消耗,这些操作会导致线程发生两次上下文切换。
- 如果锁竞争较为激烈,或者持有锁的线程需要长时间占用锁执行同步代码块,这时候及不适合使用自旋锁。因为自旋锁在获取锁之前需要一直占用CPU做无用功,同时又大量的线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于阻塞挂起操作消耗 ,其他需要CPU的线程又得不到CPU,造成CPU的浪费。
自旋锁的开启
JDK1.6中:-XX:+UseSpining 开启
-XX:PreBlockSpin=10设置自旋次数为10次
JDK1.7后去掉此参数,有JVM控制
4. Synchronized同步锁
synchronized可以把任意一个非NULL的对象当做锁。属于独占式的悲观锁,同时属于可重入锁。
synchronized作用范围:
-
作用于方法时,锁住的是对象的实例(this)
-
作用于静态方法时,锁住的是 Class 实例,又因为 Class 的相关数据存储在永久带 PermGen(JDK1.8之后是元空间 metaspace),永久带是全局共享的,因此静态方法锁相当于一个全局锁,会锁住所有调用该方法的线程。
-
synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
Synchronized核心组件:
-
Wait Set:那些调用wait方法被阻塞的线程被放置在这里;
-
Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
-
Entry List:Contention List中那些有资格成为候选资源的线程被移到Entry List 中;
-
OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为 Ondeck;
-
Owner:当前已经获取到资源的线程被称为 Owner;
-
!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 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的。
-
Synchronized是非公平锁。Synchronized在线程进入 ContentionList时,等待的线程会有限尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的。