四、LOCK
1、锁的4种状态
【1】锁的优化
JDK6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”
从而使得锁有了4种状态,并随着锁竞争的情况而升级。锁可以升级但不能降级,锁的升级被称为“锁膨胀”
【2】偏向锁状态
大多数情况下,锁不仅不存在多线程竞争,而且还总是由同一个线程多次获得
为了提升这种情况的性能,从而引入偏向锁
在对象头和栈帧中的锁记录里存储锁偏向的线程ID,当一个线程进入和退出同步块时,不需要进行CAS操作来加锁和解锁,直接判断对象头的Mark Word里是否存有指向当前线程的偏向锁即可
如果存在表示线程已经获取了锁,如果不存在则锁膨胀成轻量级锁
偏向锁在JDK6和JDK7中是默认开启的,但是需要在程序启动几秒钟后才会被激活
如果需要关闭这个延迟,使用JVM参数
-XX:BiasedLockingStartupDelay=0
如果关闭偏向锁,使用JVM参数
-XX:-UseBiasedLocking
【3】轻量级锁状态
加锁:JVM在当前线程的栈帧中创建用于存储锁标记的空间,并将对象头中的Displaced Mark Word复制到当中。如果成功表示当前线程获取锁成功,如果失败表示存在锁竞争,当前线程会尝试使用自旋来获取锁
解锁:使用原子的CAS操作,将Displaced Mark Word替换回对象头。如果成功表示没有锁竞争发生,如果失败表示有锁竞争发生,会导致锁膨胀成重量级锁
【4】对比
锁类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外等待, 和无锁状态执行方法相比存在纳秒级(1纳秒 = 1000 * 1000毫秒)的差别 |
如果存在锁竞争, 会带来额外的撤销锁的消耗 |
只有一个线程访问同步块 |
轻量级锁 | 竞争的线程不会阻塞, 提高了程序的响应速度 |
如果线程一直获取不到锁, 使用自旋会消耗CPU |
追求响应速度, 同步块执行速度非常快 |
重量级锁 | 线程竞争不会使用自旋,不会消耗CPU | 竞争的线程会阻塞, 会降低响应速度 |
追求吞吐量,同步块执行时间较长 |
2、公平锁和非公平锁
公平锁:每个线程在获取锁时,会先检查该锁维护的等待队列,如果队列为空,或者当前线程是队列当中的第一个(head),则该线程获取锁。以后会按照FIFO的原则从等待队列中取到自己(排队等待获取锁)
非公平锁:每个线程都可以尝试获取锁
synchronized是非公平锁
java.util.concurrent.locks.ReentrantLock,通过无参构造函数创建的是非公平锁。通过有参构造函数,且传入true,则该锁是一个公平锁
3、可重入锁
可重入锁又称之为递归锁,指的是已经获取锁的线程,当执行同步方法内的同步方法时,无需再次获取锁,即可直接执行方法
synchronized 和 java.util.concurrent.locks.ReentrantLock 都是可重入锁
4、自旋锁
线程通过自旋的方式,尝试获取锁,以减少线程上下文切换的开销
class CasLock {
private final AtomicReference<Thread> reference = new AtomicReference<Thread>();
public void lock() {
for (;;) {
if (tryAcquire()) {
break;
}
}
}
public void unlock() {
for (;;) {
if (tryRelease()) {
break;
}
}
}
private boolean tryAcquire() {
Thread thread = Thread.currentThread();
return reference.compareAndSet(null, thread);
}
private boolean tryRelease() {
Thread thread = Thread.currentThread();
return reference.compareAndSet(thread, null);
}
}
5、共享锁和排他锁
共享锁:多个线程都可以获取该锁
排他锁:锁只能由一个线程所独享
java.util.concurrent.locks.ReentrantReadWriteLock的读锁是线程共享的,而写锁是线程独享的
synchronized 和 java.util.concurrent.locks.ReentrantLock 都是排他锁
6、读写锁
java.util.concurrent.locks.ReentrantReadWriteLock
读写锁,读锁和写锁共同存在,读锁共享,写锁独享
7、线程兼容和线程对立
线程兼容:要操作的对象本身是线程不安全的,但是可以在操作对象时,通过各种线程安全的手段,来保证在多线程的环境下,操作对象是线程安全的
线程对立:要操作的对象本身是线程安全的,但是由于在多线程的环境下操作对象时,多个线程的并发请求不是线程安全的,从而导致操作对象本身变成了线程不安全的
Java中线程对立的例子:Thread类的废弃方法(@Deprecated) suspend
和 resume
可能导致死锁