在程序世界中,Java 锁的最终目的都是为了保证线程安全,避免在并发场景下的资源竞争问题。但考虑适用场合和性能问题,就出现了各种 Java 锁
独占锁和共享锁
从字面意思也能大概理解这两种锁的作用,独占锁是指该锁一次只能被一个线程所持有(独占),共享锁是指该锁可以被多个线程所持有(共享)
像 ReentrantLock
和 Synchronized
都是独占锁,而对于 ReentrantReadWriteLock
,其读锁是共享,写锁是独占,即读的时候能多个线程同时读,写的时候只能一个线程写
为什么会有读锁和写锁?
原来我们使用 ReentrantLock
创建锁的时候,是独占锁,一次只能一个线程访问。但是读取数据并不会造成数据不一致的问题,因此可以多个服务器多个线程同时读共享资源里面的内容,这对读取操作比较多的业务可以大大提升并发性能。但为了保证数据一致性,如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写。
因此 ReentrantReadWriteLock
应满足:读和读能共存,读和写不能共存,写和写不能共存
现在我们用读写锁实现一个简单的读写缓存的操作:
class MyCacheP{
// 缓存中的东西,需保持可见性,用volatile修饰
private volatile Map<String, Object> map = new HashMap<>();
// 创建一个读写锁
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 定义写入缓存操作
public void put(String key, Object value){
// 创建一个写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (Exception e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写入完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
// 定义读取缓存操作
public void get(String key){
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在读取");
try {
// 模拟网络拥堵,延迟0.3秒
TimeUnit.MILLISECONDS.sleep(300);
} catch (Exception e) {
e.printStackTrace();
}
Object value = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读取完成,对应值为:" + value);
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
}
有关加锁和没加锁的区别可以去我的 gitee 仓库查看完整代码
公平锁和非公平锁
公平锁: 是指多个线程按照申请锁的顺序来获取锁,先来后到,先来先服务,类似于队列,是公平的
非公平锁:是指多个线程获取锁的顺序,并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。在高并发环境下,有可能造成优先级翻转,或者饥饿的线程(也就是某个线程一直得不到锁)
JUC 包下 ReentrantLock
锁对象的创建可以指定参数 fair 类型来创建公平锁或者非公平锁,默认为 false,是非公平锁,设置为 true 则为公平锁,ReentrantLock
默认为非公平锁的原因是非公平锁比公平锁的吞吐量大,并发性能高。
synchronized
是一种非公平锁
可重入锁和递归锁
可重入锁就是递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取到该锁,同一线程外层方法获取锁之后,进入内层方法会自动获取该锁。也就是说:线程可以进入任何一个它已经拥有的锁所同步的代码块ReentrantLock
和 synchronized
就是一个典型的可重入锁,可重入锁的最大作用就是避免死锁
自旋锁
自旋锁:spinlock
,是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
之前说的比较并交换(CAS),底层使用的就是自旋,自旋就是多次尝试,多次访问,不会阻塞的状态就是自旋
手写自旋锁
下面通过模仿 AtomicInteger
中 getAndAddInt()
方法的实现来实现自已自旋锁
getAndAddInt()
方法的源码如下,从中可以看出它的实现原理也是自选,不断尝试直到成功为止
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;
}
自旋锁实现Demo:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class SpinLockDemo {
// 原子引用线程,保证可见性
AtomicReference<Thread> atomicReference = new AtomicReference<>();
// 上锁
public void myLock(){
// 获取当前进来的线程
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "\t come in...");
// 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否则自旋
while(!atomicReference.compareAndSet(null, thread)){
}
}
// 解锁
public void myUnLock(){
// 获取当前进来线程
Thread thread = Thread.currentThread();
// 变为null
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "\t invokes myUnLock()...");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
// 开始占用锁
spinLockDemo.myLock();
try {
TimeUnit.MILLISECONDS.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
// 开始释放锁
spinLockDemo.myUnLock();
}, "t1").start();
// 让main线程暂停1秒,使得t1线程先执行
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
new Thread(() -> {
// 开始占有锁
spinLockDemo.myLock();
// 释放锁
spinLockDemo.myUnLock();
}, "t2").start();
}
}
乐观锁和悲观锁
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
这具体可以看 JavaGuide 里的文章 乐观锁和悲观锁详解
最后分享一个面试题目:一道题决定去留:为什么synchronized无法禁止指令重排,却能保证有序性?
参考
- 尚硅谷 Java 大厂面试题第 2 季
- JavaGuide(Java面试+学习指南)