提高锁性能
- 减少锁持有时间
- 减小锁粒度
- 用读写分离所代替独占锁
- 锁分离
- 锁粗化:把多个锁的操作整合成对锁的一次请求,减少对锁的请求的次数。
锁优化
对象的内存布局
锁优化
锁偏向:对加锁的优化:如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁事,无需再做任何同步操作。所以几乎没有锁竞争场合。
-XX:+UseBiastedLocking 开启偏向锁。
但是当在竞争激烈的场合,即每次都是不同的线程请求相同的锁,这时候偏向锁就会失败。
初始化过程:如果锁对象被线程第一次获取,虚拟机会将对象头中的标志位置为“01”,并使用CAS将这个锁的线程ID记录在Mark Word中。如果CAS操作成功,持有偏向锁的线程每次进入锁相关的同步块时可以不用进行任何同步操作。
轻量级锁:偏向锁失败,线程不会立即挂起,而是使用轻量级锁进行优化。轻量级锁可以减少获得锁和释放锁带来的性能消耗。
加锁过程:在执行同步块之前,会在当前线程线程栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。
使用CAS操作将对象的Mark Word更新为指向锁记录的指针。如果更新成功,将Mark Word的锁标志位置位“00”,该对象处于轻量级锁状态;如果更新失败,表示有其他锁竞争,当前线程尝试使用自旋来获取锁
锁自旋:当前线程无法获得锁,且假设在不久的将来线程可以获得这把锁,因此,虚拟机会让当前线程做几个空循环,在经过若干次循环后,可以得到锁,进入临界区;如果还是不能获得锁,线程将咋操作系统层面真正挂起。
锁消除:去除不可能存在共享资源竞争的锁。
补充
重量级锁:也就是互斥锁、悲观锁、阻塞同步
重量级锁依赖对象内部的monitor锁来实现,而monitor又依赖操作系统的MutexLock(互斥锁)来实现,, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能,其开销很大。
锁膨胀过程
锁膨胀过程:
人手一支笔 ThreadLocal
是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只能指定线程可以得到存储数据。通过get和set方法就可以得到当前线程对应的值。
目的:解决多线程中相同变量的访问冲突。
与synchronized区别
Synchronized是通过线程等待,牺牲时间来解决访问冲突
ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
无锁
乐观锁 VS 悲观锁
【悲观锁】:认为冲突一定会发生,每次去拿数据的时候都会认为数据会被别的线程修改,所以会在每次获取数据的时候上锁,别的线程想要获取数据就会被阻塞,直到悲观锁被释放。
【乐观锁】:认为冲突不会发生,所以每次获取数据都不会上锁,只有在想要更新数据的时候会检查这个数据在获取到更新这段时间内有没有被别的数据修改过,如果修改过,重新读取数据,再次尝试更新;
CAS (Compare And Swap) 比较交换
CAS是以一种无锁策略。是一种乐观策略。
实现原理:CAS( V,E,N),CAS维护三个值,其中V表示将要被更新的值,E表示预期值,N表示新值。当且仅当V值等于E值,才会将V的值设置为N。如果V值与E值不相等,说明有其他线程更新了这个值,所以当前线程什么都不做。
多个线程同时使用CAS操作一个变量时,只会有一个线程胜出并成功更新,其余失败线程不会被挂起,仅仅只会被告知失败,并且允许再次尝试,也允许失败的线程放弃操作。
ABA问题
线程1初次读取数据的时候值为A,此时,线程2进来将数据修改为B,之后又来了线程3将数据修改回了A,则对于线程来说,就会认为这个数据一直没有被修改过。
解决:加一个时间戳;或者版本号
AQS(AbstractQueuedSynchronizer) 抽象队列同步器
AQS维护了一个 volatile int state(代表共享资源) 和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)
AQS有两种资源定义方式:
· Exclusive 独占,只有一个线程执行 ReentrantLock
· Share 共享,多个线程可同时执行 Semaphore/CountDownLatch
实现源码解析
- 使用state表示同步状态,且可以通过CAS对该同步状态进行原子操作实现对其值的修改。
getState();
//返回同步状态的当前值
protected final int getState(){
return state;
}
setState();
//设置同步状态值
protected final void setState(int newState){
state = newState;
}
compareAndSetState()
//如果当前同步状态值等于expert(期望值),原子的(CAS)将同步状态值设置为给定值update
protected final boolean compareAndSetState(int expert, int update){
return unsafe.compareAndSwapInt(this,stateOffset,except,update);
}
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可.
- 因为AQS是模板设计模式,自定义同步器实现需要实现以下几种方法:
isHeldExclusively()
//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)
//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)
//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)
//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)
//共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
- 独占锁例子(ReentrantLock)
a. state初始化为0,表示未锁定状态。
b. A线程lock()时,会调用tryAcquire()独占该锁并将state+1。
c. 此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。
d. 释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。
注:但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。 - 共享例子(CountDownLatch)
a. 任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。
b. 这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。
c. 等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回。