基础 | 并发编程 - [锁]

@

§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 悲观锁、乐观锁

  • 悲观锁
    认为任务总是会被抢占,所以会对操作的数据加锁
    synchronizedReenterLock 都是悲观锁
  • 乐观锁
    认为任务很少会被抢占,所以只在最后确认数据是否在操作中被篡改
    通常实现原理 版本号机制 VersionCAS
posted @ 2025-05-20 14:13  问仙长何方蓬莱  阅读(27)  评论(0)    收藏  举报