公平锁(Fair) 加锁前检查是否有排队等待的线程,优先排队等待的线程,先到先得。
非公平锁(Nonfair) 加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。
广义上可重入锁,也叫做递归锁,指的是同一线程,外层函数获得锁之后,内层函数仍有获得该锁的代码,但不受影响。Java的ReentrantLock和synchronized都是可重入锁。
java并发包提供的加锁模式分为共享锁和独占锁。 独占锁 独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
共享锁 共享锁允许多个线程同时获取锁,并访问共享资源如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制。如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。
读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由JVM控制的,你只需要上好相应的锁就可以了。
读锁:如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁。 写锁:如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。
Java读写锁有对应的接口java.concurrent.locks.ReadWriteLock
,也有具体的实现ReentrantReadWriteLock
。
Synchronized
是通过对象内部的一个叫做监视器(monitor)锁来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock
。而操作系统实现线程之间的切换就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对较长的时间,这就是为什么Synchronized
效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁称之为重量级锁。JDK中对Synchronized
做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6之后。为来减少获得锁和释放锁带来的性能消耗,引入了“轻量级锁”和“偏向锁”。
Synchoronized锁的状态一种有四种:无锁状态、偏向锁、轻量级锁和重量级锁。 随着锁的竞争,锁可以从偏向锁升到轻量级锁,再升级到重量级锁(锁的升级是单向的,只能从低到高,不会降级)。底层实现依靠对象头中的markword实现,如下图。
无锁状态:后三位为001 偏向锁状态:后三位为101 轻量级锁状态:后两位为00 重量级锁状态:后两位为10
“轻量级”是相对于操作系统互斥量实现传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。 轻量级锁所适用的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。
偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。
轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
补充相关知识: Java中的偏向锁,轻量级锁, 重量级锁解析
ConcurrentHashMap,JDK7及之前实现同步的一种方式,对整个数组分段加锁,不是一种实际的锁。
自旋锁:如果线程没有得到锁,它不会阻塞,而是会一直占据CPU看能否获得锁。
适应性自旋锁:适应性意味着自旋次数不再固定,而是由前一次在同一个锁上的自旋时间和锁的拥有者的状态来决定。即:如果同一个锁对象上,自旋锁等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机认为这次自旋很有可能再次成功,因而允许它自旋等待相对长的时间。如果对于某个锁,自旋锁很少获得成功,那么以后尝试获取这个锁时,将可能忽略掉自旋过程,直接阻塞线程,避免浪费处理器资源。