二. 锁
2.1 乐观锁和悲观锁
2.1.1乐观锁使用细节
特点:读多写少。遇到并发写的可能性低
使用:更新的时候会判断在此期间别人有没有更新这个数据,采取在写时加上版本号,然后加锁,如果失败,则要重复读-比较-写的操作。乐观锁基本跟上都是通过CAS操作实现的,CAS是一种更新的原子操作
2.1.2 悲观锁使用细节
特点:写多,默认遇到的并发可能性高
使用:每次独写数据都会上锁,这样别人香独写这个数据就会block直到拿到锁,java中的悲观锁就是synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到,才会转化成悲观锁,如RetreenLock。
2.2 自旋锁
介绍:如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,他们只需要等(自旋),等待持有锁的线程释放锁,这样必满了用户线程和内核线程的切换消耗。
优点:锁竞争不激烈时,性能大幅度提升。自旋的消耗小于线程阻塞挂起再唤醒的消耗
缺点:自旋锁占着CPU的资源不释放,做无用功,如果长时间自旋,会造成cpu的浪费
2.2.1 自旋锁时间阈值
JVM对于自旋周期的选择,jdk1.5这个限度一定是写死的,在1.6引入了适应性自旋锁,自旋时间不固定,由前一次在同一个锁上的自旋时间以及锁的持有者状态来决定。基本上认为一个线程上下文切换的时间是一次最佳时间。
优化:
1.如果平均负载CPUs则一直自旋
2.如果由超过
2.2.2自旋锁的开启
JDK1.6 中-XX:+UseSpinning 开启;
-XX:PreBlockSpin=10 为自旋次数;
JDK1.7 后,去掉此参数,由 jvm 控制;
2.2.3自旋锁的实现:
public class TestLock {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(100);
CountDownLatch countDownLatch = new CountDownLatch(100);
SimpleSpinningLock simpleSpinningLock = new SimpleSpinningLock();
for (int i = 0 ; i < 100 ; i++){
executorService.execute(new Runnable() {
@Override
public void run() {
simpleSpinningLock.lock();
++count;
simpleSpinningLock.unLock();
countDownLatch.countDown();
}
});
}
countDownLatch.await();
System.out.println(count);
}
}
2.3 Synchronized
介绍:它可以把任意一个非NULL对象当作锁。他属于独占式的悲观锁,同时属于可重入锁
特点:原子性,可见性,有序性,可重入性
作用范围:
1.作用在对象时,锁住的对象的实例(this)
2.作用静态方法时,锁住的是Class实例
3.作用对象实例时,锁住的时所有以该对象为锁的代码块
synchronized的实现原理:
synchronized时jvm的一种互斥同步访问方式,底层是基于每个对象的监视器来实现的,被synchronized修饰的代码,在被编译后在被编译代码的前后加上了一组字节指令。
在代码开始前加入了monitorenter,在代码后面加上了monitoreexit。字节码指令配合完成了修饰代码的互斥访问。
- 当执行monitorenter时,若对象未被锁定时,或者当前线程已经拥有了此对象的monitor锁,则锁计数器+1,该线程获得对象锁
- 当执行monitorexit时,锁计数器-1,当计数器为0时,锁被释放,其他线程阻塞的线程可以请求获取该monitor锁
2.4 ReentrantLock
介绍:继承接口Lock并实现了接口中定义的方法,是一种可重入锁。除了能完成synchronized所能完成的工作外,还提供了诸如可响应中断锁,可轮询锁请求,定时锁等避免多线程死锁的方法
特点:
响应中断:一个线程一直获取不到锁,Reentrantlock会给一个中断的回应
限时等待:选择传入时间参数,表示的等待指定的时间,无参表示立即返回申请锁的响应
主要方法:
- tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false.
- isFair():该锁是否公平锁
- hasQueuedThreads():是否有线程等待此锁
reentrantlock的实现原理
是一种自旋锁,通过循环调用CAS操作来实现加锁。
Reentrantlock锁与synchronied锁的比较
1.Reentrantlock是通过lock锁加锁解锁实现的,synchronized是由jvm自动解锁的,Reentrantlock加锁解锁数量必须是相同的
2.Reentrantlock相比synchronized的优势是公平锁,多个锁,可中断。
3.synchronized是jvm层面上的,而Reentrantlock是jdk层面上的。
2.5 ReadWriteLock
介绍:在读的时候采用读锁,写的时候采用写锁,灵活控制
特点:写写互斥,读写互斥,读读共享
1.写操作的时候,当一个线程在写的时候,别的线程无法插入,当这个线程写完成的时候,别的线程才能进行写操作
2.读的时候别的线程可以进行共享读,但是写的时候别的线程也是无法进行读的
3.ReentReadWriteLock也是可重入锁,构造方法可以传入boolean,表示公平与非公平锁
2.6 共享锁和独占锁
独占锁:每次只有一个线程能持有锁,ReentrantLock就是以独占方式实现的互斥锁,悲观保守的加锁策略
共享锁:允许多个线程同时获得锁,并发访问共享资源,如ReadWriteLock,是乐观锁
2.7 锁升级:
无锁 ----> 偏向锁 ----> 轻量级锁 ----->重量级锁
2.7.1偏向锁:
当线程1访问代码块并获取锁对象时,会在java对象头和战阵中记录偏向的锁的threadID,因为偏向锁不会主动释放锁
再次获取锁时,需要比较当前线程的threadID和对象头中的threadID是否一致,如果一致,无需使用CAS加锁解锁
如果不一致:先判断是否存活,
如果不存活,锁对象重置为无锁,线程2置为偏向锁
如果存活,查找线程1的栈帧信息,如果需要继续持有这个锁对象,那么暂停当前线程1,升级为轻量级锁
2.7.2 轻量级锁:
自旋的次数是有限制的,如果次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋的等待,这时又来一个线程3过来一起竞争,那么这个时候轻量级锁就会膨胀为重量级锁。
2.7.3 重量级锁:
重量级锁把拥有锁的线程都阻塞,防止CPU空转
2.7.4 几种锁的比较:
2.8 常见的锁名词
2.8.1 锁分离
介绍:常见的锁分离就是独写锁ReadWriteLock,根据功能分成读锁和写锁,既保证了线程安全,又提高了性能
读写分离思想可以延申,只要操作互不影响,锁就可以分离,比如:LinkedBlockingQueue从头部取出,尾部放
2.8.2 锁粗化
介绍:在遇到一连串地对同以锁不断进行请求和释放地操作时,把所有地锁操作整合成A锁地一次请求,从而减少对锁地请求同步次数,这个次数叫做所得粗化
2.8.3 锁消除
介绍:是在编译器级别的事如果发现不可共享的对象,可以消除这些对象的锁操作,多数是因为程序员编码不规范引起的
有时候写代码完全不需要加锁,但却执行了加锁操作