关于Java中的锁,画图简单了解一下
1、乐观锁和悲观锁
1.1 释义
- 悲观锁
- 见名知意,悲观锁就像悲观的人,总是想事情会往坏的方向发展,获得的东西或机会常常会牢牢抓在手中。
- 悲观锁就是这样,给某个资源加悲观锁,当线程每次去获取资源时,会加设其他线程参与竞争,所以每次操作前都会上锁,其他线程只能阻塞
- 乐观锁
- 乐观锁在操作数据时不会上锁,在更新的时候会判断一下在此期间是否有其他线程去更新这个数据。
1.2 看图
1.3 关于CAS算法的说明
-
CAS算法:Compare And Swap,比较与交换
-
说明:
- 如果线程的期望值跟物理内存的真实值一样,就更新值到物理内存当中,并返回true
- 如果线程的期望值跟物理内存的真实值不一样,返回false,那么本次修改失败,那么此时需要重新获得主物理内存的新值
-
看图:
- 主物理内存有一个共享变量值为5,有两个线程A和B,都有自己的工作内存,并且有变量的拷贝(快照5)
- A线程现在把值改为10,然后写回主物理内存并通知其它线程可见(加volatile)
- 这个过程中,A的期望值为5,跟主物理内存的值5进行对比,如果相同,说明没有其它线程改变,则将主物理内存的值改为10,并返回true
- 这时,B线程也将自己工作内存的值改为了20,当写回主物理内存时,发现自己的期望值(5),与现在的主物理内存(10)不一样了,就会写入失败,返回false,此时需要重新获得主物理内存的新值
1.4 使用场景
- 乐观锁适用于写操作比较少的场景。省去获得释放锁的开销
- 悲观锁适用于写多读少的场景。写操作频繁,加锁免去频繁重试
2、独占锁和共享锁(也叫写锁和读锁)
2.1 独占锁(排它锁)
-
锁每次次只能被一个线程所持有。如果一个线程对数据加上排他锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。
-
图示
2.2 共享锁(共享的是读操作、施加共享锁的操作)
-
锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据。
-
图示
3、互斥锁和读写锁
3.1、互斥锁
- 独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性
3.2、读写锁
-
是共享锁的一种具体实现。读写锁管理一组锁,一个是只读的锁,一个是写锁。
- 读锁可以在没有写锁的时候被多个线程同时持有
- 写锁是独占的。写锁的优先级要高于读锁,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
-
读写锁比互斥锁并发程度更高,每次只有一个写线程,但是同时可以有多个线程并发读。
-
图示
4、公平锁和非公平锁
4.1、公平锁
-
多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来后到
-
图示
-
/** * 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁 */ Lock lock = new ReentrantLock(true);
4.2、非公平锁
-
多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁
-
在高并发环境下,有可能造成优先级翻转,或者饥饿的状态(某个线程一直得不到锁)。
-
图示
-
/** * 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁 */ Lock lock = new ReentrantLock(false);
5、可重入锁(一定程度避免死锁)
-
又称之为递归锁,是指同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁。
-
图示
-
ReentrantLock和Synchronized都是是一个可重入锁。
-
public synchronized void A() throws Exception{ B(); } public synchronized void B() throws Exception{ } //A调用B,如果一个线程调用A已经获取了锁再去调用B,就不需要再次获取锁了,这就是可重入锁的特性。 //如果不是可重入锁的话,B可能不会被当前线程执行,可能造成死锁。
6、自旋锁
-
线程在没有获得锁时不是被直接挂起,而是执行一个忙循环,这个忙循环就是自旋
-
自旋锁是为了减少线程被挂起的几率,因为挂起和唤醒也都耗资源
-
如果锁被另一个线程长时间占用,即使自旋了之后当前线程还是会被挂起,就会变成浪费系统资源的操作。因此自旋锁是不适应锁占用时间长的并发情况的。
-
图示
-
CAS操作就是在做自旋操作
-
DK1.6又引入了自适应自旋锁
- 自旋时间不再固定
- 如果虚拟机认为自旋有可能成功那就会持续,如果自旋很少成功,可能就直接省略掉自旋过程
7、分段锁
-
一种设计,不是具体的锁
-
将锁的粒度进一步细化,当前操作不需要更新整个数组的时候,只针对数组中的一项进行加锁操作
-
图示
8、锁升级(随多线程竞争而升级,不能降级)
8.1 无锁
- 其实就是上面讲的乐观锁
8.2 偏向锁
-
偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁。
-
偏向锁的实现是通过控制对象Mark Word的标志位来实现的,如果当前是可偏向状态,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入。
8.3 轻量级锁
- 当线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁
- 轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋的方式等待上一个线程释放锁。
8.4 重量级锁
- 线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时,升级为重量级锁
- 其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态
- 其实synchronized 关键字内部实现原理就是锁升级的过程
9、锁优化
9.1 锁粗化
-
将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求
-
举例
private static final Object LOCK = new Object(); for(int i = 0;i < 10; i++) { synchronized(LOCK){ ... } } //锁粗化之后 synchronized(LOCK){ for(int i = 0;i < 100; i++) { ... } }
9.2 锁消除
- 虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除
可见JVM基础回顾
10、通过问题简单总结
- 线程是否要锁住同步资源?
- 悲观锁——锁住
- 乐观锁——不锁
- 锁住同步资源失败,线程阻塞吗?
- 阻塞
- 不阻塞——自旋锁、适应性自旋锁
- 同一个线程竞争同步资源的过程有什么区别?
- 无锁——不锁住资源,多个线程只能有一个操作资源成功,其他线程会重试
- 偏向锁——同一个线程操作同步资源时将自动获取
- 轻量级锁——多个线程竞争,没有获得资源的线程自旋等待
- 重量级锁——自旋过长,阻塞等待唤醒
- 多个线程竞争资源是否排队?
- 公平锁——排队
- 非公平锁——不好说
- 一个线程中的多个流程能够获取同一把锁?
- 可重入锁——能
- 非可重入锁——不能
- 多个线程能否共享一把锁?
- 共享锁——能
- 排它锁——不能