目录
5. synchronized 优化(偏向锁、轻量级锁及重量级锁)
1. 定义及作用
定义:synchronized是Java中的一个关键字
作用:synchronized关键字能解决多个线程访问共享资源的同步性问题,能保证被他修饰的方法或代码块在任一时刻只有一个线程执行。
2. 使用方式
synchronized 用于修饰代码块,类的实例方法和静态方法
2.1 常见的三种使用方式如下:
- 修饰代码块,即同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
- 修饰普通方法,即同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
- 修饰静态方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象
2.2 锁的使用规则
1.锁对象的设置:
a. 修饰代码块时,需要一个 reference 对象 作为锁的对象
b. 修饰实例方法时,默认锁的对象是=当前对象
c. 修饰类方法(静态方法) 时,默认的锁对象是当前类的class对象
2. 根据锁对象的不同,一把锁同时最多只能被一个线程持有
a. 若当前线程持有目标锁,其他线程仍然可以调用目标类种没有被synchronized修饰的方法
3. 当对象获取多个锁时,必须以相反顺序释放
2.3 锁的类型
由于synchronized会修饰代码块,类的实例方法和静态方法,所以有不同锁的类型
3. 特点
保证原子性、可见性和有序性
- 可重入性:对同一线程获得锁后,在调用其他需要同样锁的代码时可直接调用
- 不可中断性:通俗解释:一旦这个锁被别人获得了,如果我想获得,只能等待或者阻塞,直到别的线程释放这个锁,如果别人永远不释放锁,我只能永远等下去;很执着。
- 重量级:底层时通过监视器对象(monitor)完成的,监视器锁本质是依赖于底层操作系统的互斥锁(Mutex Lock)实现,而操作系统实现线程需要从用户态转换为内核态。因此synchronized效率低,是重量级锁
4.缺点及使用synchronized的注意事项
4.1 缺陷
效率低:锁的释放情况少,试图获的锁时不能设定超时、不能中断一个正在试图获得锁的线程
不够灵活:加锁和释放的时机单一,每个锁仅有单一的条件(某个对象)
无法知道是否成功获取到锁
4.2 注意事项
a .锁的对象不能为空
b. 作用域不宜过大
c. 避免死锁(一组相互竞争资源的线程因为互相等待,导致“永久”阻塞的现象)
死锁发生的条件:
- 互斥:共享资源 X 和 Y 只能被一个线程占用;
- 占有且等待:线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
- 不可抢占:其他线程不能强行抢占线程 T1 占有的资源;
- 循环等待:线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待
破坏死锁发生的条件:
- 占用且等待:一次性申请所有的资源,这样就不存在等待了。
- 不可抢占,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。(java.util.concurrent提供了
Lock
解决这个问题)- 循环等待,靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化申请后就不存在循环了。
5. synchronized 优化(偏向锁、轻量级锁及重量级锁)
为了减少获得锁和释放锁所带来的性能消耗,在JDK 1.6里引入了4种锁的状态:无锁、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。
这三种锁中偏向锁和轻量级锁是乐观锁,重量级锁是针对synchronized的,都是指锁的状态。JDK1.6通过引入锁升级(偏向锁->轻量级锁->重量级锁)机制来高效实现synchronized。
乐观锁与悲观锁
独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
5.1 自旋锁
首先,内核态与用户态的切换上不容易优化。但通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)。
如果锁的粒度小,那么锁的持有时间比较短(尽管具体的持有时间无法得知,但可以认为,通常有一部分锁能满足上述性质)。那么,对于竞争这些锁的而言,因为锁阻塞造成线程切换的时间与锁持有的时间相当,减少线程阻塞造成的线程切换,能得到较大的性能提升。具体如下:
- 当前线程竞争锁失败时,打算阻塞自己
- 不直接阻塞自己,而是自旋(空等待,比如一个空的有限for循环)一会
- 在自旋的同时重新竞争锁
- 如果自旋结束前获得了锁,那么锁获取成功;否则,自旋结束后阻塞自己
5.2 偏向锁
在没有实际竞争的情况下,还能够针对部分场景继续优化。如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。
“偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。
偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。
5.3 轻量级锁
自旋锁的目标是降低线程切换的成本。如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。
顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。
Mark Word是对象头的一部分;每个线程都拥有自己的线程栈(虚拟机栈),记录线程和函数调用的基本信息。二者属于JVM的基础内容,此处不做介绍。
当然,由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁。
5.4 锁分配和膨胀过程