§1 总览
锁类型 | 描述 | 优点 | 缺点 | 举例 |
---|---|---|---|---|
公平锁 | 线程按申请锁的顺序获取锁 | 有序 | 吞吐量较低 | new ReentrantLock(true) |
非公平锁 | 线程可能不按申请锁的顺序获取锁,可能后申请锁的先获取锁 | 吞吐量相对大 | 高并发时可能导致优先级反转或饥饿 | new ReentrantLock() |
可重入锁(递归锁) | 线程可以任意进入它已经持有的锁所包围的代码块中 | 避免重复调用时的死锁 | ReentrantLock / synchronized | |
自旋锁 | 加锁失败时,线程不进入阻塞而是通过循环等待锁 | 减少线程上下文切换的消耗 | 长时间循环消耗 CPU | CAS 原子类 |
独占锁 / 排他锁 / 互斥锁 | 锁被一个线程独享,加了独占锁的资源不能加其他锁 | synchronized | ||
共享锁 | 锁可以又多个线程共享,加了共享锁的资源可以继续加共享锁 | 并发程度高 | ReentrantReadWriteLock | |
读写锁 | 一种共享锁,管理一个共享读锁和一个独占写锁 | ReentrantReadWriteLock | ||
乐观锁 | 认为读多写少,通常不会并发抢锁 | 快 | CAS、数据版本 | |
悲观锁 | 认为写多读少,通常并发抢锁 | 安全 | 会锁住资源,被锁的资源会阻塞其他需要此资源的线程 | synchronized |
无锁 | 对象上的监视器处于无并发状态 | synchronized 阶段 0 | ||
偏向锁 | 同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价 | 低并发时减少 CAS 开销 | synchronized 阶段 1 | |
轻量级锁 | 同步代码被多个线程访问,优先依赖自旋尝试获取锁 | 低并发时效率高 | 高并发时除线程切换还加上 CAS 开销 | synchronized 阶段 2 |
重量级锁 | 自旋到一定程度锁膨胀时切换 | 效率低 | synchronized 阶段 3 | |
统一锁 | 被锁的资源是资源全体 | synchronized | ||
分段锁 | 被锁的资源是资源的一部分 | 效率高,同一组资源分几段就能容纳最大几段并发 | 不能无限分段 | ConcurrentHashMap |
§2 公平锁、非公平锁
// 默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁
线程按申请锁的顺序获取锁
公平锁会维护一个等待队列,
若当前线程是队列中第一个,则获取锁
否则,加入等待队列队尾,按先进先出规则排队
非公平锁
线程可能不按申请锁的顺序获取锁,可能后申请锁的先获取锁
线程申请非公平锁时会直接跳到申请队列开头,如果没有申请成功,再按照类似公平锁排队
synchronized 因为有抢锁,所以也是非公平锁
优点
吞吐量比公平锁大
节省了频繁切换线程上下文的浪费
缺点
高并发时可能导致优先级反转或饥饿
- 优先级反转:可能后申请锁的先获取锁
- 饥饿:可能有的线程长时间或一直获取不到锁
§3 可重入锁(递归锁)
线程的外层函数获取锁后,内层[递归]函数可自动获取锁
线程可以任意进入它已经持有的锁所包围的代码块中
示例
public class LOC {
Lock lock = new ReentrantLock();
public synchronized void m1() throws Exception{
System.out.println(Thread.currentThread().getName()+" m1");
// 同一个线程,已持有锁,访问另一个需要锁的同步方法,自动获取线程上的锁
m2();
}
public synchronized void m2() throws Exception{
System.out.println(Thread.currentThread().getName()+" m2");
}
public void m3() throws Exception{
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" m3");
// 同一个线程,已持有锁,访问另一个需要锁的方法,自动获取线程上的锁
m4();
}finally {
lock.unlock();
}
}
public void m4() throws Exception{
lock.lock();
// 可重入锁,锁两层约等于递归一次,还是可以获取持有的锁
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" m4");
}finally {
//只要解锁与加锁匹配就没问题
lock.unlock();
lock.unlock();//若注释此行,不报错,但解锁时阻塞
}
}
public static void main(String[] args) {
LOC loc = new LOC();
new Thread(()->{
try {
new LOC().m1();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
new Thread(()->{
try {
new LOC().m3();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
§4 自旋锁
线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态
public class SpinLock {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock(){
Thread t = Thread.currentThread();
while(!atomicReference.compareAndSet(null,t)){
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.getName()+" waiting");
}
System.out.println(t.getName()+" locked");
}
public void unlock(){
Thread t = Thread.currentThread();
// 解锁时不用自旋,如果不是说明已经解了
// 但必须使用 compareAndSet 防止解了其他线程的锁
atomicReference.compareAndSet(t,null);
System.out.println(t.getName()+" unlocked");
}
public void run (){
lock();
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+ " run");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
unlock();
}
}
public static void main(String[] args) {
SpinLock demo = new SpinLock();
new Thread(demo::run,"A").start();
new Thread(demo::run,"B").start();
}
}
自旋锁 与 非自旋锁
非自旋锁 在加锁失败时会进入 阻塞(Block)状态,再次被唤醒时需要从 阻塞(Block)状态 切换为 运行(Runnable)状态,状态切换时涉及线程上下文的切换,性能较差。
自旋锁 在加锁失败时会进入 自旋状态,自旋其实就是就是一个不停尝试获取锁的循环,此时线程始终是 运行(Runnable)状态 的,当真的获取到锁时,也不会涉及到线程状态或上下文的切换,性能相对 非自旋锁 高很多
缺点
- 在加锁失败时依然占用 CPU,因此若一直加锁不成功会导致 CPU 效率变低
- 在递归逻辑中使用自旋锁必然导致死锁
外层逻辑获取锁后,内层逻辑在此尝试获取锁,此时内层一直尝试,但外层没有执行完所以也没释放,因此死锁(疑惑,这是在内层另开线程获取锁了吗,否则同一个线程里内层循环天然持有锁)
适用场景
因为 自旋锁 会一直占有 CPU,因此 自旋锁 适用于很快可能获取锁的场景,即持有锁的线程可以快速处理完成并释放锁的场景,比如 CAS 操作
§5 独占锁、共享锁
独占锁
锁被一个线程独享
共享锁
锁可以又多个线程共享
读写锁
读写锁中管理多个锁,一个共享读锁和一个独占写锁
public class ReadWriteLockDemo {
private static Map<String,String> map = new HashMap<>();
private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void put(String key,String value){
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" put "+key);
TimeUnit.MILLISECONDS.sleep(100);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+" put done "+key);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.writeLock().unlock();
}
}
public static void get(String key){
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" get "+key);
map.get(key);
TimeUnit.MILLISECONDS.sleep(1000);
System.out.println(Thread.currentThread().getName()+" get done "+key);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.readLock().unlock();
}
}
public static void main(String[] args) {
for(int i=0;i<3;i++){
int finalI = i;
new Thread(()->{
put(String.valueOf(finalI),UUID.randomUUID().toString().substring(0,8));
},String.valueOf(i)).start();
}
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<3;i++){
int finalI = i;
new Thread(()->{
get(String.valueOf(finalI));
},String.valueOf(i)).start();
}
}
}
读时无所谓,写时必须每个线程独占整个锁
§6 悲观锁、乐观锁
- 悲观锁
认为任务总是会被抢占,所以会对操作的数据加锁
synchronized
、ReenterLock
都是悲观锁 - 乐观锁
认为任务很少会被抢占,所以只在最后确认数据是否在操作中被篡改
通常实现原理版本号机制 Version
和CAS