文章目录
jdk1.6对锁的实现引入了大量的优化。 锁主要存在四中状态, 依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。 注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
自旋锁
自旋锁认为:如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就没必要做用户态和内核态之间的切换进入阻塞,挂起状态,只需等一等(自旋),在等待持有锁的线程释放锁后即可立即获得该锁,这样就避免了用户线程在内核态的切换导致锁时间消耗.
线程在自旋时会占用CPU,在线程长时间自旋获取不到锁时,将会产生CPU的浪费,甚至有时线程永远无法获取锁而导致CPU资源被永久占用,所以需要设定一个自旋等待的最大时间,在线程执行的时间超过自旋等待的最大时间后,线程就会停止自旋并释放持有的锁.
自旋锁的优缺点:
优点:
自旋锁可以减少CPU上下文切换,对于占用锁时间非常短或锁竞争不激烈的代码块来说性能大幅提升,因为自旋锁CPU耗时明显少于线程阻塞,挂起,再唤醒时两次CPU上下文切换所需要的时间.
缺点:
在持有锁的线程占用锁时间过长或者锁竞争过于激烈时,线程在自旋过程中会长时间获取不到锁资源,导致CPU的浪费,所以在系统中有复杂锁依赖的情况下不适合使用自旋锁.
Synchronized
synchronized的作用范围
- synchronized作用于成员变量和非静态方法时,锁住的是对象的实例,即this对象
- synchronized作用于静态方法时,锁住的是Class实例,因为静态方法属于Class而不属于对象
- synchronized作用于一个代码块时,锁住的所有代码块括号中配置的对象
synchronized的实现原理
在synchronized内部包括ContentionList,EntryList,WaitSet,Ondeck,Owner,!Owner这6个区域,每个区域都代表锁的不同状态.
- ContentionList:锁竞争队列,所有请求锁的线程都被放在竞争队列中
- EntryList:竞争候选列表,在ContentionList中有资格成为候选者来竞争锁资源的线程被移动到了EntryList中.
- WaitSet:等待集合,调用wait方法后被阻塞的线程将被放在WaitSet中.
- Ondeck:竞争候选者,在同一时刻最多只有一个线程在竞争锁资源,该线程的状态被称为Ondeck
- Owner:竞争到锁资源的线程被称为Owner状态线程
- !Owner:在Owner线程释放锁后,会从Owner的状态变成!Owner.
synchronized在收到新的锁请求时首先自旋,如果通过自旋也没有获取到锁资源,则将被放入锁竞争队列ContentionList中.
为了防止锁竞争时ContentionList尾部的元素被大量的并发进程进行CAS访问而影响性能,Owner线程会在释放锁资源时将ContentionList中的部分线程移动到EntryList中,并指定EntryList中的某个线程(一般是最先进入的线程)为Ondeck线程.Owner线程并没有直接把锁传递给Ondeck线程,而是把锁竞争的权利交给Ondeck,让Ondeck线程重新竞争锁.在Java中把该行为称为"竞争切换",该行为牺牲了公平性但提高了性能.
获取到锁资源的Ondeck线程会变成Owner线程,而未获取到锁资源的线程仍然停留在EntryList中.
Owner线程在被wait方法阻塞后,会被转移到WaitSet队列中,直到某个时间被notify方法或者notifyAll方法唤醒,会再次进入EntryList中,ContentionList,EntryList,WaitSet中的线程均为阻塞状态,该阻塞是由操作系统来完成的.
Owner线程在执行完毕后会释放锁的资源并变为!Owner状态,
在synchronized中,在线程进入ContentionList之前,等待的线程会先尝试以自旋的方式获取锁,如果获取不到就进入ContentionList,该做法对于已经进入队列的线程是不公平的,因此synchronized是非公平锁.另外,自旋获取锁的线程也可以直接抢占Ondeck的锁资源.
JDK1.6对synchronized做了很多优化,引入了适应自旋,锁消除,锁粗化,轻量级锁及偏向锁等以提高锁的效率.锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,这种过程就叫做锁膨胀.在JDK1.6中默认开启了偏向锁和轻量级锁.
ReentrantLock
ReentrantLock继承了Lock接口并实现了在接口中定义的方法,是一个可重入的独占锁.ReentrantLock通过自定义队列同步器(AQS)来实现锁的获取与释放.
ReentrantLock支持公平锁和非公平锁的实现.ReentrantLock不但提供了synchronized对锁的操作功能,还提供了诸如可响应中断锁,可轮询锁请求,定时锁等避免多线程死锁的方法。
synchronized和ReentrantLock/Lock的比较
共同点:
- 都用于控制多线程对共享对象的访问
- 都是可重入锁
- 都保证了可见性和互斥性
不同点:
- ReentrantLock可以显式的获取和释放锁;synchronized隐式的获取和释放锁.为了避免程序出现异常而无法释放锁,在使用ReentrantLock时必须在finally控制块中进行解锁操作.
- ReentrantLock可响应中断,可轮回,为处理锁提供了更多灵活性
- ReentrantLock是API级别的,synchronized是JVM级别的
- ReentrantLock可以定义公平锁
- 二者的实现底层不一样:synchrobnized是同步阻塞,采用的是悲观并发策略;Lock是一个接口,而synchronized是Java中的关键字,synchronized是由内置的语言实现的.
- 我们可以通过Lock知道有没有成功获取锁,通过synchronized却无法做到
- Lock可以通过分别定义读写锁提高多个线程读操作的效率
synchronized: 修饰方法的时候
1.方法是非静态 this
2.方式是静态 类的Class对象
重量级锁
轻量级锁
偏向锁
除了在多线程之间存在竞争获取锁的情况,还会经常出现同一个锁被同一个线程多次获取的情况.偏向锁用于在某个线程获取某个锁后,消除这个线程锁重入的开销,看起来是这个线程获得了锁的偏袒.
偏向锁的主要目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径,
因为轻量级锁的获取及释放需要多次CAS原子操作,而偏向锁只需要在切换ThreadID时执行一次CAS原子操作,因此可以提高锁的运行效率.
在出现多线程竞争锁的情况时,JVM会自动撤销偏向锁,因此偏向锁的撤销操作的耗时必须少于节省下来的CAS原子操作的耗时.
综上所述,轻量级锁用于提高线程交替执行同步块时的性能,偏向锁则在某个线程交替执行同步块时进一步提高性能.
锁的状态总共有4种:无锁,偏向锁,轻量级锁和重量级锁.随着锁竞争越来越激烈,锁可能从偏向所升级到轻量级锁,在升级到重量级锁,但在Java中只单向升级.
分段锁
分段锁只是一种思想,用于将数据分段并在每个分段上都单独加锁,把锁进一步细粒度化,以提高并发效率.ConcurrentHashMap 在内部就是使用分段锁实现的.
同步锁与死锁
在有多个线程同时被阻塞时,他们之间若相互等待对方释放锁资源,就会出现死锁.为避免出现死锁,可以为锁操作添加超时时间,在线程持有锁超时后自动释放该锁.
公平锁与非公平锁
ReentrantLock支持公平锁和非公平锁两种方式.非公平锁虽然放弃了锁的公平性,但执行效率明显高于公平锁.
如何进行锁优化
1.减少锁持有的时间
2.减小锁的粒度
减小锁粒度指将单个耗时较多的锁操作拆分为多个耗时较少的锁操作来增加锁的并行度,减少同一个锁上的竞争,在减少锁的竞争后,偏向锁,轻量级锁的使用率才会提高.减少锁粒度最典型的案例就是ConcurrentHashMap中的分段锁.
3.锁分离
锁分离是指根据不同的应用场景将锁的功能进行分离,以应对不同的变化,最常见的锁分离思想就是读写锁(ReadWriteLock),它根据锁的功能将锁分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,既保证了线程的安全性,又提高了性能.
操作分离思想可以进一步延伸为只要操作互不影响,就可以进一步拆分,比如LinkedBLockingQueue从头部取出数据,并从尾部加入数据.
4.锁粗化
锁粗化是为了保障性能,会要求尽可能的将锁的操作细化以减少线程持有锁的时间,但是如果锁分的太细,将会导致系统频繁获取锁和释放锁,反而影响性能的提升.在这种情况下,建议将关联性强的锁操作集中起来处理,以提高系统的效率.
5.锁消除
经常会出现在不需要使用锁的情况下误用了锁操作而引起性能下降,所以需要检查并消除这些不必要的锁,来提高系统的性能.