锁 GO!
- 乐观锁
- 悲观锁
- 自旋锁
- 原理
- 自旋锁优缺点
- 优点
- 缺点
- 自旋锁时间阈值(1.6 引入了适应性自旋锁)
- 自旋锁的开启
- Synchronized 同步锁
- Synchronized作用范围
- Synchronized核心组件
- Synchronized实现
- JDK1.6后的优化
- ReentrantLock
- Lock接口主要方法
- tryLock 和 lock 和 lockInterruptibly
- ReentrantLock 与 synchronized
- 非公平锁
- 公平锁
- 可重入锁(递归锁)
- 读写锁
- 独占锁共享锁
- 独占锁
- 共享锁
- 锁状态
- 重量级锁(Mutex Lock)
- 轻量级锁
- 偏向锁
- 锁升级
- 分段锁
- 同步锁与死锁
- 同步锁
- 死锁
- 锁优化思路
- 减少锁持有时间
- 减小锁粒度
- 锁分离
- 锁粗化
- Condition 类和 Object 类锁方法
乐观锁
- 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新时会判断此期间数据是否被更新
- 采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作
- java 中的乐观锁基本通过== CAS 操作实现的,CAS 是一种更新的原子操作==,比较当前值跟传入值是否一样,一样则更新,否则失败
悲观锁
- 悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁
- Java 中的悲观锁就是Synchronized,AQS 框架下的锁则是先尝试 cas 乐观锁去获取锁,获取不到,
才会转换为悲观锁,如 RetreenLock
自旋锁
原理
- 自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需自旋,等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
- 线程自旋需消耗 cup 的,如果一直获取不到锁,则线程长时间占用CPU自旋,需要设定一个自旋等待最大事件在最大等待时间内仍未获得锁就会停止自旋进入阻塞状态。
自旋锁优缺点
优点
- 自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗(这些操作会导致线程发生两次上下文切换)
缺点
- 锁竞争激烈或者持有锁的线程需要长时间占用锁执行同步块,不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cup 的线程又不能获取到 cpu,造成 cpu 的浪费
自旋锁时间阈值(1.6 引入了适应性自旋锁)
- 自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理
- 自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能
- JVM 对于自旋周期的选择,jdk1.5 这个限度是一定的写死的
- 在 1.6 引入了适应性自旋锁,自旋的时间不固定,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间
自旋锁的开启
- JDK1.6 中-XX:+UseSpinning 开;XX:PreBlockSpin=10 为自旋次数
- JDK1.7 后,去掉此参数,由 jvm 控制
Synchronized 同步锁
- 关键字,用于解决多个线程间访问资源同步性问题,保证其修饰的方法或代码块任意时刻只能有一个线程访问
- synchronized 它可以把任非 NULL 的对象当作锁。他属于独占式悲观锁,同时属于可重入锁
Synchronized作用范围
-
作用实例方法时,锁住的是对象的实例(this)
-
作用静态方法时,锁住的是该类,该 Class所有实例,又因为 Class 的相关数据存储在永久带 PermGen(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程
- 线程A调用一个实例对象非静态Synchronized方法,允许线程B调用该实例对象所属类的静态s方法而不会发生互斥,前者锁的是当前实例对象,后者锁的是当前类
-
作用于同步代码块 锁住的当前对象,进入同步代码块前需要获得对象的锁
Synchronized核心组件
Synchronized实现
- Synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多
- Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理了优化。引入了偏向锁和轻量级锁,都是在对象头中有标记位,不需要经过操作系统加锁
JDK1.6后的优化
- synchronized是根据JVM实现的,该关键字的优化也是在JVM层面实现
而未直接暴露 - JDK1.6后对锁做了大量优化如偏向锁,轻量锁,自旋锁,自适应锁等等
- 锁主要有四种状态:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态,他们会随着锁竞争的激烈而逐渐升级且这种升级不可降,利用该策略提高获得锁和释放锁的效率
ReentrantLock
- ReentantLock 继承接口Lock并实现了接口中定义的方法,他是一种可重入锁
- 除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法
Lock接口主要方法
- void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁
- lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行.
- boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false.
- tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程阻塞挂起,当前线程仍然继续往下执行代码.
- void unlock() 解锁
- isLock():此锁是否有任意线程占用
tryLock 和 lock 和 lockInterruptibly
- tryLock 能获得锁就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnitunit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false
- lock 能获得锁就返回 true,不能的话一直等待获得锁
- lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两线程,但此时中断这两个线程,
lock 不会抛出异常,而 lockInterruptibly 会抛出异常
ReentrantLock 与 synchronized
- 两者均为可重入锁
- Synchronized依赖JVM而Reentrantlock依赖于APi(lock(),trylock()配合try/finally语句块来实现)
- ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作, synchronized 会被 JVM 自动解锁
- ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作
- ReentrantLock 相比 synchronized 的优势是可中断、公平锁、可选择通知,多个锁,这种情况下需ReentrantLock。
非公平锁
- JVM 随机就近原则分配锁的机制则称为不公平锁,非公平锁实际执行的效率要远超公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。
- 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护等待队列
-Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁(构造时提供了公平);
公平锁
- 公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁
- 加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
可重入锁(递归锁)
- 可重入锁(递归锁),指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响
- 在 JAVA 环境下 ReentrantLock 和 synchronized 都是 可重入锁
读写锁
- 为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制
- 如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率
- 读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥由 jvm 控制的,程序员只需要上好相应的锁
- 要求代码只读数据,可以很多人同时读,但不能同时写,可上读锁
- 代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁
- Java 中 读 写 锁 有 个 接 口 java.util.concurrent.locks.ReadWriteLock , 也 有 具 体 的 实 现
ReentrantReadWriteLock
独占锁共享锁
java 并发包提供的加锁模式分为独占锁和共享锁
独占锁
- 独占锁模式下,每次只能有一个线程能持有锁
- 独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性
- ReentrantLock 就是以独占方式实现的互斥锁。
共享锁
- 共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock
- 共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源
- java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行
锁状态
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁
重量级锁(Mutex Lock)
- 这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁”
- Synchronized 是通过对象内部的做监视器锁(monitor)实现。监视器锁是依赖于底层的操作系统的 Mutex Lock 来实现,而操作系统实现线程之间的切换这就需要从用
户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized 效率低的原因
轻量级锁
- 轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的
- 轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗
- 轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
偏向锁
- 引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换
ThreadID 的时候依赖一次 CAS 原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗) - 轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能
锁升级
== 随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁==(但是锁的升级是单向的, 也就是说只能从低到高升级,不会出现锁的降级)。
分段锁
分段锁也并非一种实际的锁,而是一种思想 ConcurrentHashMap 是学习分段锁的最好实践
同步锁与死锁
同步锁
- 当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程
- 在同一时间内只允许一个线程访问共享数据。 Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。
死锁
就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
锁优化思路
减少锁持有时间
只用在有线程安全要求的程序上加锁
减小锁粒度
- 将大对象(这个对象可能会被很多线程访问),拆成小对象,增加并行度,降低锁竞争
- 降低了锁的竞争,偏向锁,轻量级锁成功率才会提高,最最典型的减小锁粒度的案例就ConcurrentHashMap
锁分离
- 最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能,
- 读写分离思想可以延伸,只要操作互不影响,锁就可以分离,比如LinkedBlockingQueue 从头部取出,从尾部放数据
锁粗化
- 通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短(使用完公共资源后,应该立即释放锁)
- 如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化所以可以锁粗化使得占有锁的时间加长
Condition 类和 Object 类锁方法
- Condition 类的 awiat 方法和 Object 类的 wait 方法等效
- Condition 类的 signal 方法和 Object 类的 notify 方法等效
- Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
- ReentrantLock 类可以唤醒指定条件线程,而 object 的唤醒是随机的