JVM1.6时对synchronized进行优化,引入了:自旋锁、自适应自旋锁、锁粗化、锁消除、偏向锁、轻量级锁
自旋锁:
等待锁时,不放弃CPU的执行权限,进行忙循环,尝试获取锁,默认是10次,可以通过-XX:PreBlockSpin参数进行配置
自适应自旋锁:
在自旋锁的基础上,动态调节自旋次数。自旋的次数由前一次在同一个锁上的自旋次数和锁的拥有者的状态来决定。如果前面线程成功获取锁并且正常运行,那么本次获取锁的可能性很大,所以自旋的次数相对多一些;如果前面线程很少成功获取锁,那么本次获取锁的概率也很小,就可能不执行自旋了。
锁粗化:
如果在一段代码中同一线程反复获取、释放同一个对象的锁,将会生产不必要的性能开销,所以需要把获锁的范围扩大,对同一个对象的锁操作只进行一次,在头部获取锁,在尾部释放锁。
锁消除:
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。
偏向锁、轻量级锁、重量级锁:
早Java对象头的Mark word中,synchronized锁一共具有四种状态:无锁、偏向锁、轻量级锁、重量级锁。
偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
偏向锁
目的:大多数情况下锁不仅不存在多线程竞争,而且总是由同一个线程多次获取,所以引入偏向锁让线程获得锁的代价更低。
偏向锁认为环境中不存在竞争情况,锁只被一个线程持有,一旦有不同的线程获取或竞争锁对象,偏向锁就升级为轻量级锁。
偏向锁在无多线程竞争的情况下可以减少不必须要的轻量级锁执行路径。
轻量级锁
目的:在大多数情况下同步块并不会出现竞争情况,大部分情况是不同线程交替持有锁,所以引入轻量级锁可以减少重量级锁对线程的阻塞带来的开销。
轻量级锁认为环境中线程几乎没有对锁对象的竞争,即使有竞争也只需要稍微等待(自旋)下就可以获取锁,但是自旋次数有限制,如果超过该次数,则会升级为重量级锁。
重量级锁
监视器锁Monito
1、锁优化
1.1 synchronized优化-减少临界区
减少临界区,就可以减少锁被持有的时间,从而降低锁被征用的概率,达到减少锁开销的目的
实际代码操作如下:
优化器代码:
public synchronized void doSomething() {
step1();
syncStep2();
step3();
}
说明: 代码中我们其实只需要同步syncStep2,其他step1、step3不需要同步,那么其实我们可以仅锁定syncStep2
优化后代码:
public void doSomething() {
step1();
synchronized (this) {
syncStep2();
}
step3();
}
说明: synchronized使用在成员方法上,锁定对象是当前对象(this),锁定临界区是整个方法,所有等待锁都在方法外进行排队。
修改后:锁定对象还是当前对象(this),但是锁定临界区仅在syncStep2。
为什么这样写,锁的性能就提高了呢?
答: 假设 step1 执行耗时10ms, syncStep2执行耗时 10ms,step3执行耗时10ms。
如果锁定范围是整个方法,那么一个锁执行的时间是30ms,也就是说下一个锁需要等待30ms才能拿到锁。修改后,一个锁执行的时间是10ms,也就是说下一个锁需要等待10ms就能拿到锁,从而提高了锁性能。
1.1 synchronized优化-减小锁的颗粒度
在使用synchronized锁定资源时,我们常用的锁定是,锁定Class>、或者锁定this、或者锁定一个我们创建的全局Object,这些锁的粒度都很大,锁定Class>和全局Object都是全局的,意思就是整个虚拟机执行到锁定代码所有线程都要排队。如果this是单例的,那么也是整个虚拟机全部线程都要排队。
那怎么是否需要减少锁的颗粒度呢?
如果资源冲突不是全局的时候,就需要考虑减少锁的颗粒度。
举两个例子:
用户发短信操作,需求是限制了发送短信频率为一分钟只能发送一次。那么我们在加锁时候锁定粒度是针对一个手机号的。
用户下单操作,需求是限制用户下单频率,以及业务上防止订单重复提交。那么我们的加锁粒度是针对一个用户的。
下面以发短信操作为例,进行代码演示。
修改前代码:
package com.liyong.reactor.test;
import org.springframework.stereotype.Service;
@Service
public class MessageService {
public synchronized boolean sendMsg(String mobile, String msg) {
doValidate();
doSend();
doUpdateStatus();
return true;
}
private void doUpdateStatus() {
}
private void doSend() {
}
private void doValidate() {
}
}
说明:因为校验、发送、更新发送状态是一个原子性操作,所以这里我们没法通过所有锁的临界区来进行锁优化。
优化后:
package com.liyong.reactor.test;
import org.springframework.stereotype.Service;
@Service
public class MessageService {
public boolean sendMsg(String mobile, String msg) {
mobile = mobile.intern();
synchronized (mobile) {
doValidate();
doSend();
doUpdateStatus();
return true;
}
}
private void doUpdateStatus() {
}
private void doSend() {
}
private void doValidate() {
}
}
说明:mobile.intern()这样的一步操作很关键,通过这个方法,我们获取到的字符串将是虚拟机常量区的string对象,对整个虚拟机来说,字面量相同的string是全局唯一的。
通过对mobile字符串的加锁,我们将全局的锁粒度,变成仅针对手机号的锁粒度。
集群部署时的加锁怎么解决?
通过分布式锁来解决,当前博客解决的问题都是针对单个虚拟机的锁优化。
1.3 可重入锁-ReentrantLock
可重入:同一个线程可以重复获取一把锁。比如: A->B->C A/B/C3个方法都必须获取同一个可重入锁是被允许的
代码实例:基于ReentrantLock和Condition实现一个连接池的demo,通过condition可以实现线程间交互
package com.liyong.reactor.test;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConnectionPool {
private final ReentrantLock lock = new ReentrantLock();
private final Condition getCondition = lock.newCondition();
private final LinkedList
1.4、读写锁-ReentrantReadWriteLock
读写锁: 即锁分读锁,写锁。
那什么时候会阻塞呢?
A已经获取了读锁,B申请获取写锁,那么B会阻塞,即 读-写 阻塞
A已经获取了写锁,B申请获取读锁,那么B会阻塞,即 写-读 阻塞
A已经获取了写锁,B申请获取写锁,那么B会阻塞,即 写-写 阻塞
那什么时候不阻塞呢?
A已经获取了读锁,B申请获取读锁,那么B不会阻塞,即 读-读 不阻塞
代码示例:已实现一个计数器为例,使用读写锁保证线程安全:
package com.liyong.reactor.test;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Counter {
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock readLock = readWriteLock.readLock();
private final Lock writeLock = readWriteLock.writeLock();
private volatile int count = 0;
public void inc(int index) {
writeLock.lock();
try {
count++;
} finally {
writeLock.unlock();
}
}
public int get() {
readLock.lock();
try {
return count;
} finally {
readLock.unlock();
}
}
}