• 基础 | 并发编程 - [锁]


    §1 总览

    锁类型描述优点缺点举例
    公平锁线程按申请锁的顺序获取锁有序吞吐量较低new ReentrantLock(true)
    非公平锁线程可能不按申请锁的顺序获取锁,可能后申请锁的先获取锁吞吐量相对大高并发时可能导致优先级反转或饥饿new ReentrantLock()
    可重入锁(递归锁)线程可以任意进入它已经持有的锁所包围的代码块中避免重复调用时的死锁ReentrantLock / synchronized
    自旋锁加锁失败时,线程不进入阻塞而是通过循环等待锁减少线程上下文切换的消耗长时间循环消耗 CPUCAS 原子类
    独占锁 / 排他锁 / 互斥锁锁被一个线程独享,加了独占锁的资源不能加其他锁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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    公平锁
    线程按申请锁的顺序获取锁

    公平锁会维护一个等待队列,
    若当前线程是队列中第一个,则获取锁
    否则,加入等待队列队尾,按先进先出规则排队

    非公平锁
    线程可能不按申请锁的顺序获取锁,可能后申请锁的先获取锁

    线程申请非公平锁时会直接跳到申请队列开头,如果没有申请成功,再按照类似公平锁排队

    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();
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    在这里插入图片描述

    §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();
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    自旋锁 与 非自旋锁
    非自旋锁 在加锁失败时会进入 阻塞(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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    读时无所谓,写时必须每个线程独占整个锁
    在这里插入图片描述

    §6 悲观锁、乐观锁

    • 悲观锁
      认为任务总是会被抢占,所以会对操作的数据加锁
      synchronizedReenterLock 都是悲观锁
    • 乐观锁
      认为任务很少会被抢占,所以只在最后确认数据是否在操作中被篡改
      通常实现原理 版本号机制 VersionCAS
  • 相关阅读:
    基于python的django框架选题推荐生鲜超市供应平台
    C#/VB.NET 将XML转为PDF
    vue3源码分析——实现element属性更新,child更新
    进程和线程ID
    如何建立云存储应急演练体系及进行场景设计
    深入解析Java正则表达式:定义、原理和实例
    IDEA创建Mybatis项目
    nginx+uwsgi+django部署(前后端不分离)
    PyTorch入门学习(十二):神经网络-搭建小实战和Sequential的使用
    ssm手机销售网站
  • 原文地址:https://blog.csdn.net/ZEUS00456/article/details/126517866