一、锁的分类
自旋锁
当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。乐观锁
假定没有冲突,在修改数据时如果发现和之前获取的不一致,则读取最新数据,重试修改。悲观锁
假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。独享锁(写锁)
给资源加上写锁,线程可以修改资源。其它线程不能再加锁(只能写)共享锁(读锁)
给资源加上读锁后只能读不能改,其它线程也只能加读锁,不能加写锁(可供多个线程获取)重入锁
同一个线程多次获取同一把锁,不会出现死锁公平锁与非公平锁
争抢锁的顺序,如果按先来后到为公平锁,反之为不公平锁。
二、同步关键字 synchronized 的特性
- 可以用于实例方法,静态方法,隐式指定锁对象
- 用于代码块时,显示指定锁对象
- 锁的作用域:对象锁,类锁,分布式锁
- 可重入锁,独享锁,悲观锁,非公平锁
- 锁消除,锁粗化
下面针对锁消除和锁粗化两个特性重点说明一下:
锁消除
删除不必要的加锁操作。在连续进行加锁和解锁的情况下,触发JIT 编译将锁忽略的情况叫锁消除。举例:
// 举例
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("A");
stringBuffer.append("B");
stringBuffer.append("C");
stringBuffer.append("D");
stringBuffer.append("E");
System.out.println(stringBuffer.toString());
}
// 查看StringBuffer append 方法中使用了synchronized
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
这里由于append()
中使用了 synchronized
,因此在 main()
中 每一次append
不会都进行加锁。
锁粗化
将小粒度的锁合并为大粒度的锁称为锁虚化:
public void test(){
int i =0;
synchronized (this){
i++;
}
synchronized (this){
i--;
}
System.out.println(i);
}
下面是粗化后的样子(其实就可以理解为锁合并):
public void test1(){
int i =0;
synchronized (this){
i++;
i--;
}
System.out.println(i);
}
三、synchronized(this)
的原理
我们都知道,Java对象存储在堆(Heap)内存中,那么一个对象包含:对象头、对象体和对齐字节。
对象头
对象头中的Mark Word 主要用来表示对象的线程锁的状态,还可以用来配合GC,存放该对象的hashCode
对象头部的 Mark Word
每个状态如下:
Unlocked 未锁定
Light-weight locked 轻量级锁
Heavy-weight locked 重量级锁
Marked for GC
Biased/biasable 偏向锁
偏向锁
在JDK6
以后,默认开启了偏向锁这个优化,通过JVM 参数 -XX:-UseBiasedLocking
来禁用偏向锁。若偏向锁开启,只有一个线程抢锁,可获取到偏向锁(因此称为偏向,理解为偏向于某一个线程)。在Mark Word 中的体现:
有无开启偏向锁是由状态0、1 决定的,有无锁定则查看有无 thread id
轻量级锁
在未锁定的状态下,通过CAS 来抢锁,抢到的是轻量级锁。抢锁过程如下:
两个线程同时进行CAS 操作,然后只有线程1操作成功,修改 Mark word
:
重量级锁
轻量级锁中的自旋有一定的次数限制,超过了这个次数限制,轻量级锁就会升级为重量级锁。
依据上图中的描述,现在T2 要执行抢锁,执行步骤:
- T1 线程调用
obj.wait()
,T1 就进入了waitSet
进行等待,此时owner
=null
- T2 线程抢锁成,
owner=T2,
执行obj.notify()
将T1唤醒 - T1 线程继续进入
entryList
中排队等待 - T2 执行完代码块,就执行
exitMonitor
,将owner
赋值为null
- T1 线程抢锁…
锁的升级过程
参考资料:
四、面试题之 ReentrantLock
和synchronized
的区别?
1)可重入性
从上面已经可以知道 synchronized
关键字是可重入的,而 ReentrantLock
顾名思义也是可以重入的。
2)锁的实现不同
synchronized :
依赖于JVM 实现,实现方式难以看到。ReentrantLock :
JDK 实现,有直接的源码可供阅读
3)性能不同
synchronized : 在优化以前,由于自旋产生CPU 性能大耗,当JDK 6 引入偏向锁,轻量级锁后,两者的性能差别不大。可以说两者都采用了CAS 技术,视图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
4)功能不同
使用的便利性: synchronized
优于 ReentrantLock
,因为 ReentrantLock
需要手动声明加锁和解锁。
锁的细粒度和灵活度: ReentrantLock
优于 synchronized
ReentrantLock
独有的能力:
1) ReentrantLock
区分是公平锁还是非公平锁,而synchronized
只能是非公平锁。
2)ReentrantLock 提供了Condition 类,用来实现分组唤醒需要唤醒的线程,而 synchronized
要么随机唤醒一个线程,要么唤醒全部线程。
3) ReentrantLock
提供了 lockInterruptibly()
中断等待锁的线程。而 synchronized
是不能中断的。
五、synchronized 与Lock 比较
synchronized
优点:
- 使用简单,语义清晰
- 由JVM 提供,提供了多种优化方案(锁粗化,锁消除,偏向锁,轻量级锁)
- 锁的释放由JVM 完成,不用人工干预,也降低了死锁的可能
缺点:
无法实现一些锁的高级功能:公平锁,中断锁,超时锁,读写锁,共享锁。
Lock
优点
- 所有synchronized 的缺点
- 可以实现更多的功能
缺点
需要手动释放锁,如使用不当可能造成死锁