Java多线程并发
前言
本章主要介绍Java多线程的锁。
九、Java锁
9.1. 乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发的可能性低,同时读不会上锁,只有在修改数据的时候才会对比上一次的版本,然后加锁操作。
Java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值和传入值是否一样,一样则更新,否则失败。
9.2. 悲观锁
悲观锁是一种悲观思想,即任务写多,遇到并发可能性高,每次读数据都会上锁,这样别的线程想读写时就会block阻塞直到拿到锁。
Java的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获得锁,获取不到,才会转为悲观锁,如ReentrantLock。
9.3. 自旋锁
自旋锁的原理是,如果持有锁的线程能在很短时间内释放资源,那么那些等待锁的线程不需要做内核态到用户态的转换进入阻塞挂起状态,它们只需要等一等,等待持有锁的线程执行完释放资源即可立刻获得锁,这样就避免用户线程和内核的切换的消耗。
- 优点
自旋锁尽可能减少线程的阻塞,这对应锁竞争不激烈的,且占用锁时间非常短的代码块来说,性能大幅度提升。 - 缺点
如果对于锁竞争激烈,或者持有锁时间非常长的代码块,性能则会造成CPU的浪费。因为自旋锁一直在占用CPU做无用功,且长时间占用CPU,消耗CPU资源。
自旋锁的开启
JDK1.6中-XX:+UseSpinning开启;
-XX:PreBlockSpin=10为自旋次数;
JDK1.7后,去掉此参数,有jvm控制。
9.4. Synchronized同步锁
synchronized它可以把任意一个非NULL的对象当作锁。它属于独占式的悲观锁,同时属于可重入锁。
synchronized作用范围
- 作用于方法时,锁住的是对象的实例(this);
- 当作用于静态方法时,锁住的是Class实例。
- synchronized作用于一个实例时,锁住的是所有以该对象为锁的代码块。
synchronized核心组件
- Wait Set:那些调用wait方法被阻塞的线程被放置在这里;
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
- Entry List:Contention List中那些有资格成为资源的线程被移动到Entry List中;
- OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为Deck;
- Owner:当前已获取到所有资源的线程被称为Owner;
- !Owner:当前释放锁的线程。
synchronized实现
-
JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContenttionList会被大量的并发线程进行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是非公平锁。Synchrozined在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自选锁获取锁的线程还可能直接抢占OnDeck线程的锁资源。
-
每个对象都有个monitor对象,加锁就是在竞争monitor对象,代码块加锁是在前后分别加上monitorenter和monitorexit指令来实现的,方法加锁是通过一个标志位来判断。
-
synchronized是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。
-
Java1.6,synchronized进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后的Java7、Java8中,均对该关键字的实现机理做优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
-
锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀。
-
JDK1.6默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBlasedLocking来禁用偏向锁。
9.5. ReentrantLock
ReentrantLock继承接口Lock并实现了接口中定义的方法,它是一种可重入锁,除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
Lock接口的主要方法
方法名 | 描述 |
---|---|
void lock() | 获取锁。如果获取到锁,则返回,否则阻塞等待,直到获取到锁 。 |
boolean tryLock() | 尝试获取锁。如果获取到锁,则返回true,否则返回false。 |
void unLock() | 释放锁。如果当前线程持有锁,则释放,若当前线程未持有锁,则抛出异常。 |
tryLock(long timeout, TimeUnit unit) | 在给定时间内,尝试获取锁,超时则返回false。 |
Condition newCondition() | 获取条件对象。该组件和当前的锁绑定,只有获得该锁的线程,才能调用该组件await()方法,而调用后,当前线程将释放锁。 |
geiHoldCount() | 获取当前线程持有锁的次数。即调用lock的次数。 |
getQueueLength() | 获取等待获取该锁的线程估计数。 |
getWaitQueueLength(Condition condition) | 获取等待获取该锁给定条件的线程估计数。 |
hasWaiters(Condition condition) | 判断是否有线程等待该锁的给定条件。 |
hasQueuedThread(Thread thread) | 查询给定线程是否等待获取此锁。 |
hasQueuedThreads() | 是否有线程等待此锁。 |
isFair() | 该锁是否为公平锁。 |
isHeldByCurrentThread() | 当前线程是否保存锁锁定。 |
isLock() | 此锁是否有任意线程占用。 |
lockInterruptibly() | 如果当前线程未被中断,获取锁。 |
ReentrantLock和Synchronized的区别
ReentrantLock | synchronized |
---|---|
通过lock()和unlock()进行加锁与解锁操作,通常unlock()需要在finally控制块中执行 | 通过synchronized关键字进行加锁,由JVM自动加锁解锁 |
可中断、有公平锁、多个锁条件 | 非公平锁、只有一个锁条件 |
9.6. Semaphore信号量
Semaphore是一种计数的信号量。它可以设定一个阈值,多个线程竞争许可信号,执行完后归还,超过阈值后,线程申请许可信号将会阻塞。
Semaphore和ReentrantLock的区别
Semaphore | ReentrantLock |
---|---|
通过acquire()和release()来获得和释放临界资源 | 通过lock()和unLock()来加锁、释放锁 |
acquire()可响应中断 | lockInterruptibly()可响应中断 |
可轮询的锁请求与定时锁的功能 | 同理 |
可实现公平锁 | 同理 |
9.7. AtomicInteger
AtomicInteger是一个提供了原子操作的Integer的类,常见的还有AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference等,它们的实现原理相同,区别在于运行对象类型的不同。还可以通过AtomicReference将一个对象的所有操作转化为原子操作。
9.8. 可重入锁(递归锁)
可重入锁也叫递归锁,指的是同一线程,外层函数获取到锁后,进入内层函数,也有获取该锁的代码,直接获取到,不受到影响。
9.9. 公平锁和非公平锁
-
公平锁
加锁前判断是否有排队等待的线程,优先排队的等待线程,先来先得。 -
非公平锁
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
- 非公平锁性能比公平锁高5~10倍,因为公平锁需要在多核的情况下维护一个队列
- Java中的synchronized是非公平锁,ReentrantLock默认的lock()方法采用的是非公平锁。
9.10. ReadWriteLock读写锁
为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制。读写锁分为读锁和写锁,多个读锁不互斥,写锁跟任何锁都相斥。
-
读锁
如果代码只读数据,可以很多人同时读,但不能同时写,那就上读锁。 -
写锁
如果代码修改数据,只能同时有一个人在写,且不能同时读,就上写锁。
9.11. 共享锁和独占锁
java并发包提供的加锁模式分为共享锁和独占锁。
-
独占锁
独占锁模式下,每次只有一个线程持有锁,它是一种悲观保守的加锁策略。 -
共享锁
共享锁则允许多个线程同时获得锁,并发访问资源,它是一种乐观锁,放宽了加锁策略,允许多个执行读的线程同时访问共享资源。
9.12. 重量级锁
synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Metex Lock来实现。而操作系统实现线程之间的切换这就需要用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是synchronized效率低的原因。
因此,这种依赖于Mutex Lock所实现的锁称之为“重量级锁”。
9.13. 轻量级锁
锁的四种状态:无锁状态、偏向锁、轻量级锁和重量级锁。
“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来替代重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就是导致轻量级锁膨胀为重量级锁。
9.14. 偏向锁
偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入开销(CAS),看起来让这个线程得到偏护。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令。
轻量级锁是为了在线程交替执行同步快时提高性能,而偏向锁则是在只有一个线程执行同步快时进一步提高性能。
9.15. 分段锁
分段锁并发是一种实际上的锁,它只是一种思想。ConcurrentHashMap就是最好的实践例子。
9.16. 锁优化
- 减少锁持有时间
只用在有线程安全的程序上加锁 - 减小锁粒度
将大对象拆分为小对象,大大增加并行度,降低锁竞争。这样偏向锁、轻量级锁成功率才会提高。 - 锁分离
根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。读写分离的思想就是只有操作互不影响,锁就可以分离。 - 锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡是都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。 - 锁消除
锁消除是在编译器级别的事情。在即时编译期间,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。
总结
以上就是对Java锁的整体概念介绍和学习笔记。