线程锁
AtomicInteger
AtomicInteger 是一个提供原子操作的 Integer 的类,常见的还有 AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等, 他们的实现原理相同,区别在于运算类型的不同。另外还可以通过 AtomicReference<V>
将一个对象的所有操作转化成原子操作。
在多线程中,诸如 ++i 或者 i++ 等运算不具有原子性,是不安全的线程操作之一
。 通常会使用 synchronized 将该操作变成一个原子操作,但 JVM 为此类操作特意提供了 一些同步类,使得使用更方便,且使程序原型效率变得更高。通过相关资料显示,通常 AtomicInteger 的性能是 ReentrantLock 的好几倍。
可重入锁(递归锁)
这里讲的是广义上的可重入锁,而不是单指 Java 下的 ReentrantLock。可重入锁,也叫递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码
。 在 Java 环境下 ReentrantLock 与 synchronized 都是可重入锁。
公平锁与非公平锁
公平锁
加锁前检查是否有排队等待的线程,优先排队等待的线程,即先来先得。
非公平锁
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到时才自动到队尾排队等待;
非公平锁性能比公平锁高 5 ~ 10 倍,因为公平锁需要在多核的情况下维护一个队列;
Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock() 方法采用的也是非公平锁。
ReadWriteLock 读写锁
为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制。
如果没有写锁的情况下,读是无阻塞的,在一定程度上提供了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥, 读锁与写锁互斥,这是由 JVM 自己控制的,使用时只需要在上好对应的锁即可。
读锁
如果临界区是只读数据,可以有很线程同时读,但不能同时写,那就上读锁。
写锁
如果临界区是修改数据,只能有一个线程在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁。
Java 提供了读写接口 java.util.concurrent.locks.ReadWriteLock
,也提供了具体的实现 ReentrantReadWriteLock。
共享锁和独占锁
Java 并发包提供的加锁模式分为独占锁和共享锁。
独占锁
独占锁模式下,每次只能有一个线程持有锁,ReentrantLock 就是以独占方式实现的互斥锁。 独占锁是一种悲观保守的加锁策略,他避免了读/读冲突,如果某个只读线程获取锁,则其它读线程只能等待, 这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
共享锁
共享锁则允许多个线程同时获取锁,并发访问共享资源,如:ReadWriteLock。 共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等待线程的锁获取模式。
Java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。
重量级锁(Mutex Lock)
Synchronized 是通过对象内部的一个叫监视器锁(monitor)来实现的。但是监视器本质又是依赖于底层的操作系统的 Mutex Lock 来实现的。 而操作系统实现线程之间的切换需要从用户态转换到和心态,这个成本非常高,状态之间的转换需要相对较长的时间,这就是为什么 Synchronized 效率低的原因。 因此,这种依赖于操作系统 Mutex Lock 所实现的锁通常称之为“重量级锁”
。JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。 JDK 1.6 之后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了轻量级锁
和偏向锁
。
轻量级锁
轻量级
是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下, 减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况, 就会导致轻量级锁膨胀为重量级锁
。
偏向锁
Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销, 看起来让这个线程得到了偏护
。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令
(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。 上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提供性能
。
分段锁
分段锁并非一种实际的锁,而是一种思想,ConcurrentHashMap 是学习分段锁的最好实践。
锁优化
减少锁持有时间:只用在有线程安全要求的程序上加锁;
减小锁粒度:将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。 最最典型的减小锁粒度的案例就是 ConcurrentHashMap。
锁分离:
最常见的锁分离就是读写锁 ReadWriteLock
,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。读写分离思想可以 延申,只要操作互不影响,锁就可以分离。比如 LinkedBlockingQueue 从头部取数据,从尾部存数据。锁粗化:通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡是都有一个度,
如果对同一个锁不停的进行请求、 同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化
。锁消除:锁消除是在编译器级别的事情。在即时编译时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。