1、lock的简介地位及作用
lock不是用来替代syncronized的,而是syncronized不合适获取不足以满足需求时,提供高级的功能。
1.1、为什么syncronized不够用
(1) 效率低,锁释放的情况少(1 syncronized代码执行完毕 2 发生异常jvm释放锁),试图获取锁不能够设置超时,不能够中断一个正在试图获取锁的线程
(2)不够灵活,加锁和释放锁的时机单一
(3)无法知道是否成功获取到锁
1.2、lock主要方法介绍
注:lock锁不会想syncronized方法一样在异常时自动释放,必须在finally中释放锁
lock()
获取锁,如果锁被其他线程获取,则进行等待
lock方法不能被中断,一旦陷入死锁,将陷入永久等待tryLock()
尝试获取锁,成功为true失败为false
- tryLock(long time, TimeUnit unit)
尝试获取锁,若不能立即拿到锁,阻塞等待指定时间,指定时间内成功为true失败为false
- lockInterruptibly()
相当于tryLock(long time, TimeUnit unit)把超时时间设置为无限。在等待锁的过程中线程可以被中断
等锁期间被中断
public class LockInterruptibly implements Runnable{
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
LockInterruptibly runnable = new LockInterruptibly();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
Thread.sleep(2000);
thread2.interrupt();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始执行任务");
try {
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + "获取锁");
Thread.sleep(5000);
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName() + "sleep被中断");
}finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放锁");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "获取锁lockInterruptibly被中断");
}
}
}
Thread-0开始执行任务
Thread-0获取锁
Thread-1开始执行任务
Thread-1获取锁lockInterruptibly被中断
Thread-0释放锁
1.3、 可见性保证
拥有happen-before能力,下一个线程加锁后可以看到前一个线程解锁前发生的所有操作
2、锁的分类
2.1 、乐观锁和悲观锁
悲观锁:互斥同步锁
乐观锁:非互斥同步锁
2.1.1、 为什么会诞生非互斥同步锁
互斥同步锁的劣势
**
阻塞和唤醒带来的性能劣势
永久阻塞:如果持有锁的线程永久阻塞,那么等待该锁的线程也会造成无限循环死锁等活跃性问题
优先级反转:优先级低的线程持有优先级高线程需要的锁,优先级高的线程反而最后执行
2.1.2、乐观锁和悲观锁的定义
悲观锁: 共享资源会发生线程安全问题,需要对共享资源上锁,让其它线程无法访问,从而保证结果的正确性
乐观锁:认为对共享资源操作时不会有其他线程干扰,所以不会锁住资源。在更新的时候,对比在修改期间数据是否被其他人修改过,如果没被修改,则正常修改数据。若数据和开始拿到的不一致,则证明数据被修改,采取放弃,报错,重试等策略。
乐观锁实现一般都是通过CAS算法实现的
案例:
悲观锁:synchronized 关键字和 Lock 接口
乐观锁:原子类
乐观锁的典型案例就是原子类,例如 AtomicInteger 在更新数据时,就使用了乐观锁的思想,多个线程可以同时操作同一个原子变量。
大喜大悲:数据库
数据库中同时拥有悲观锁和乐观锁的思想。例如,我们如果在 MySQL 选择 select for update 语句,那就是悲观锁,在提交之前不允许第三方来修改该数据,这当然会造成一定的性能损耗,在高并发的情况下是不可取的。
相反,我们可以利用一个版本 version 字段在数据库中实现乐观锁。
2.1.3、乐观锁和悲观锁的适合场景
悲观锁适合用于并发写入多、临界区代码复杂、竞争激烈等场景,这种场景下悲观锁可以避免大量的无用的反复尝试等消耗。
乐观锁适用于大部分是读取,少部分是修改的场景,也适合虽然读写都很多,但是并发并不激烈的场景。在这些场景下,乐观锁不加锁的特点能让性能大幅提高。
2.2、 可重入锁
2.2.1、 简介
可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁
可重入锁实例:
- synchronized
- ReentrantLock
2.2.2、可重入锁好处
- 避免死锁
- 提高封装性
2.2.3、 示例
public class RecursionLock {
private static ReentrantLock lock = new ReentrantLock();
private static void accessResource(){
lock.lock();
try {
System.out.println("线程" + Thread.currentThread().getName() + " 第" + lock.getHoldCount() + "次递归处理");
if (lock.getHoldCount() < 5){
accessResource();
}
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
accessResource();
}
}
输出
线程main 第1次递归处理
线程main 第2次递归处理
线程main 第3次递归处理
线程main 第4次递归处理
线程main 第5次递归处理
2.2.4、源码分析
ReentrantLock源码
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //判断是否当前线程占有锁,是则status + 1 ,返回true
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
2.3、公平锁和非公平锁
2.3.1、 简介
公平是指安全线程请求的顺序来分配锁,非公平是指不完全按照请求的顺序,在一定情况下,可以插队
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
- 缺点:可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
2.3.2、 示例
非公平锁时会优先选择请求锁并且处于唤醒状态的线程,而公平锁会严格按照锁的请求顺序
/**
* @Author: zhangjx
* @Date: 2020/10/14 21:43
* @Description: 演示公平锁和不公平锁
*/
public class FairLock {
static class PrintQueue{
// true为公平锁
// private static Lock queueLock = new ReentrantLock(true);
//非公平锁
private static Lock queueLock = new ReentrantLock();
public static void printTask(){
queueLock.lock();
try {
int time = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "第一次执行任务 需要耗时" + time + "秒");
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
queueLock.unlock();
}
queueLock.lock();
try {
int time = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "第二次执行任务 需要耗时" + time + "秒");
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
queueLock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
IntStream.range(0,10 ).forEach(e -> executorService.execute(() -> PrintQueue.printTask()));
Thread.sleep(10000);
executorService.shutdownNow();
}
}
公平锁输出
pool-1-thread-1第一次执行任务 需要耗时10秒
pool-1-thread-6第一次执行任务 需要耗时10秒
pool-1-thread-8第一次执行任务 需要耗时10秒
pool-1-thread-3第一次执行任务 需要耗时7秒
pool-1-thread-7第一次执行任务 需要耗时10秒
pool-1-thread-4第一次执行任务 需要耗时5秒
pool-1-thread-5第一次执行任务 需要耗时4秒
pool-1-thread-10第一次执行任务 需要耗时3秒
pool-1-thread-2第一次执行任务 需要耗时4秒
pool-1-thread-9第一次执行任务 需要耗时3秒
pool-1-thread-1第二次执行任务 需要耗时1秒
pool-1-thread-6第二次执行任务 需要耗时2秒
pool-1-thread-8第二次执行任务 需要耗时1秒
pool-1-thread-3第二次执行任务 需要耗时5秒
pool-1-thread-7第二次执行任务 需要耗时4秒
pool-1-thread-4第二次执行任务 需要耗时10秒
pool-1-thread-5第二次执行任务 需要耗时3秒
pool-1-thread-10第二次执行任务 需要耗时5秒
pool-1-thread-2第二次执行任务 需要耗时6秒
pool-1-thread-9第二次执行任务 需要耗时2秒
非公平锁输出
pool-1-thread-6第一次执行任务 需要耗时7秒
pool-1-thread-6第二次执行任务 需要耗时1秒
pool-1-thread-3第一次执行任务 需要耗时10秒
pool-1-thread-3第二次执行任务 需要耗时4秒
pool-1-thread-9第一次执行任务 需要耗时6秒
pool-1-thread-9第二次执行任务 需要耗时9秒
pool-1-thread-8第一次执行任务 需要耗时1秒
pool-1-thread-8第二次执行任务 需要耗时5秒
pool-1-thread-10第一次执行任务 需要耗时10秒
pool-1-thread-10第二次执行任务 需要耗时8秒
pool-1-thread-7第一次执行任务 需要耗时8秒
pool-1-thread-7第二次执行任务 需要耗时1秒
pool-1-thread-2第一次执行任务 需要耗时3秒
pool-1-thread-2第二次执行任务 需要耗时3秒
pool-1-thread-5第一次执行任务 需要耗时7秒
pool-1-thread-5第二次执行任务 需要耗时2秒
pool-1-thread-4第一次执行任务 需要耗时8秒
pool-1-thread-4第二次执行任务 需要耗时3秒
pool-1-thread-1第一次执行任务 需要耗时8秒
pool-1-thread-1第二次执行任务 需要耗时3秒
2.3.3、源码分析
公平锁
hasQueuedPredecessors()判断是否有线程排队,没有线程排队就获取锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
非公平锁
没有判断是否有线程排队
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
2.4、 共享锁和排它锁
2.4.1、简介
排它锁 : 又称独占锁,独享锁
共享锁:又称读锁,获取共享锁后可以查看但是无法修改删除数据
读写锁的规则:
(1) 多个线程只申请读锁,都可以申请到
(2)如果有一个线程已经占用了读锁,若此时有一个线程申请写锁,则申请写锁的线程会一直等待释放读锁
(3)如果有一个线程已经占用了写锁,若其他线程申请读锁或写锁,则申请线程需要等待释放锁
总结:要么一个或者多个线程同时拥有读锁,要么一个线程有写锁,两者不会同时出现。
2.4.2、示例
public class ReadWriteLock {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取读锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
System.out.println(Thread.currentThread().getName() + " 释放读锁");
readLock.unlock();
}
}
private static void write(){
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取写锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
System.out.println(Thread.currentThread().getName() + " 释放写锁");
writeLock.unlock();
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(6);
IntStream.range(0, 3).forEach(e -> executorService.execute(() -> ReadWriteLock.read()));
IntStream.range(0, 3).forEach(e -> executorService.execute(() -> ReadWriteLock.write()));
}
}
输出
pool-1-thread-1 获取读锁
pool-1-thread-3 获取读锁
pool-1-thread-2 获取读锁
pool-1-thread-3 释放读锁
pool-1-thread-1 释放读锁
pool-1-thread-2 释放读锁
pool-1-thread-4 获取写锁
pool-1-thread-4 释放写锁
pool-1-thread-5 获取写锁
pool-1-thread-5 释放写锁
pool-1-thread-6 获取写锁
pool-1-thread-6 释放写锁
2.4.3、读写锁插队策略
场景
线程2和线程4正在同时读取,线程3想要写入,在等待队列等待写锁,线程5不在等待队列,现在要进行读操作
- 公平锁:不允许插队
- 非公平锁
写锁可以随时插队,读锁仅在等待队列头节点不是想获取写锁线程的时候可以插队(有获取写锁的线程排在队列头节点是,读锁如果插队,虽然效率会提高,但是可能会导致获取写锁的线程永远饥饿)
源码分析
非公平锁,获取写锁的线程直接可以插队,获取读锁的只有在排在队列头节点的不是获取写锁的线程时可以插队
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
公平锁,只要等待队列中有等待的线程就需要排队
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
2.4.5、 锁的升降级
2.4.5.1、 结论
支持锁的降级,不支持锁的升级
为什么不支持升级锁?
容易造成死锁
2.4.5.2、示例
public class UpdateLock {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取读锁");
try {
Thread.sleep(1000);
System.out.println("尝试升级成写锁");
writeLock.lock();
System.out.println("成功升级成写锁");
try {
}finally {
writeLock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
System.out.println(Thread.currentThread().getName() + " 释放读锁");
readLock.unlock();
}
}
private static void write(){
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取写锁");
try {
Thread.sleep(1000);
System.out.println("尝试降级成读锁");
readLock.lock();
System.out.println("成功降级成读锁");
try {
}finally {
readLock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
System.out.println(Thread.currentThread().getName() + " 释放写锁");
writeLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(6);
IntStream.range(0, 1).forEach(e -> executorService.execute(() -> UpdateLock.write()));
Thread.sleep(2000);
IntStream.range(0, 1).forEach(e -> executorService.execute(() -> UpdateLock.read()));
}
}
输出
pool-1-thread-1 获取写锁
尝试降级成读锁
成功降级成读锁
pool-1-thread-1 释放写锁
pool-1-thread-2 获取读锁
尝试升级成写锁
2.4.6、共享锁和排他锁总结
- ReentrantReadWriteLock 实现了ReadWriteLock接口,最主要的有两个方法,readLock()和writeLock()来获取读锁和写锁
- 锁的申请和释放策略
(1) 多个线程只申请读锁,都可以申请到
(2)如果有一个线程已经占用了读锁,若此时有一个线程申请写锁,则申请写锁的线程会一直等待释放读锁
(3)如果有一个线程已经占用了写锁,若其他线程申请读锁或写锁,则申请线程需要等待释放锁
- 插队策略:为了防止饥饿,读锁不能插队
- 升降级策略:只能降级,不能升级
- 使用场景:ReentrantReadWriteLock 适合于读多写少的情况,合理应用可以提高并发效率
2.5、自旋锁和阻塞锁
2.5.1、 简介
线程在等待锁时,为了避免线程切换状态消耗资源(阻塞或者唤醒一个java线程时需要操作系统切换cpu来完成,这种状态的转换需要耗费处理器的时间),自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。达到一定次数后如果还未获取到锁,则可以阻塞线程,等待cpu唤醒。
缺点:
如果锁被占用的时间很长那,那么自旋的线程只会白白浪费资源
2.5.2、原理和源码分析
在 Java 1.5 版本及以上的并发包中,也就是 java.util.concurrent 的包中,里面的原子类基本都是自旋锁的实现。
do-while 循环就是一个自旋操作,如果在修改过程中遇到了其他线程竞争导致没修改成功的情况,就会 while 循环里进行死循环,直到修改成功为止
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
2.5.3、 自旋锁示例
public class SpinLock {
private AtomicReference<Thread> sign = new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while(!sign.compareAndSet(null, current)){
System.out.println("自旋尝试获取锁");
}
}
public void unlock(){
Thread current = Thread.currentThread();
sign.compareAndSet(current, null);
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "尝试获取自旋锁");
spinLock.lock();
System.out.println(Thread.currentThread().getName() + "成功获取自旋锁");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放自旋锁");
}
}
};
new Thread(runnable).start();
new Thread(runnable).start();
}
}
输出
Thread-1尝试获取自旋锁
Thread-1成功获取自旋锁
Thread-0尝试获取自旋锁
Thread-1释放自旋锁
Thread-0成功获取自旋锁
Thread-0释放自旋锁
2.5.3、自旋锁的适用场景
自旋锁一般用于多核的服务器,在并发度不是很高的情况下,比阻塞锁的效率高
自旋锁适用于临界区比较短小的情况,如果临界区很大,持有锁的时间很长,那么不适合使用自旋锁
2.6、 可中断锁
2.6.1、 简介
可中断锁即锁在线程持有期间能够响应中断
syncronized是不可中断锁,而lock是可中断锁,因为tryLock和lockInterruptbly都能够响应中断。
2.6.2、示例
等锁期间被中断
public class LockInterruptibly implements Runnable{
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
LockInterruptibly runnable = new LockInterruptibly();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
Thread.sleep(2000);
thread2.interrupt();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始执行任务");
try {
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + "获取锁");
Thread.sleep(5000);
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName() + "sleep被中断");
}finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + "释放锁");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "获取锁lockInterruptibly被中断");
}
}
}
2.7、java虚拟机对锁的优化
自适应的自旋锁
自旋的时间会根据最近自旋尝试的成功率、失败率,以及当前锁的拥有者的状态等多种因素来共同决定。如自旋尝试达到一定次数之后,就转为阻塞锁;最近自旋获取某一把锁失败了,那么可能会省略掉自旋的过程等
锁消除
如果虚拟机能够确定对象只会在一个线程中使用,保证线程安全,编译器便会做出优化,把对应的 锁给消除,省去加锁和解锁的操作,以便增加整体的效率。
锁粗化
连续多次加锁释放同一把锁,那么会把同步区域扩大,几个 锁代码块合并为一个较大的同步块,这样就无须频繁申请与释放锁了,减少了性能开销
如何优化锁和提高并发性能
- 1、 缩小同步代码块
- 2、尽量不要锁住方法
- 3、减少请求锁的次数
- 4、避免人为制造热点
- 5、避免锁嵌套
- 6、选择合适的锁类型和合适的工具类