• 【并发编程】锁机制


    1.锁的分类
    • 自旋锁:线程状态及上下文切换消耗系统资源,当访问共享资源的时间短,频繁上下文切换不值得。jvm实现,使线程在没有获得锁的时候,不被挂起,转而执行空循环,循环几次之后,如果还没能获得锁,则被挂起。

    • 阻塞锁:阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或者时间)时,才可以进入线程的准备就绪状态,转为就绪状态的所有线程,通过竞争,进入运行状态。

    • 重入锁:支持线程再次进入的锁,就像我们有房间的钥匙,可以多次进入房间类似。

    • 读写锁:两把锁,读锁跟写锁,写写互斥、读写互斥、读读共享。

    • 互斥锁:当线程抢占到资源,其他线程进不来。

    • 悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

    • 乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。

    • 公平锁:所有线程老老实实排队,对大家而言都很公平。

    • 非公平锁:一部分线程排着队,但是新来的线程可能插队。

    • 偏向锁:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

    • 独占锁:独占锁模式下,每次只能有一个线程能持有锁。

    • 共享锁:允许多个线程同时获取锁,并发访问共享资源。

    2.深入理解Lock接口

    (1)lock于synchronized的区别

    • synchronized是java内置的关键字,在jvm层面,Lock是个java类。
    • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁。
    • synchronized会自动释放锁(线程在执行代码块的过程中发生异常会自动释放锁),Lock需要在finally中手动释放锁(unlock方法),否则会造成死锁。
    • 用synchronized关键字的两个线程1和线程2,如果当线程1获取锁,线程2等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不会一直等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。
    • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可公平(两者皆可)
    • Lock锁适合大量同步的代码的同步问题,而synchronized锁适合少量代码同步问题。

    (2)Lock继承体系图

    在这里插入图片描述

    (3)Lock常用的API

    • lock()、unlock()加锁、解锁
    public class LockTest {
    
        private Lock lock = new ReentrantLock();
    
        /**
         * 当前线程释放锁后,其他线程才可以获取到锁
         */
        public void lock(){
            lock.lock();
            try {
                System.out.println("线程"+Thread.currentThread().getName()+":获取到锁资源");
                Thread.sleep(2000L);
            }catch (Exception e){
                System.out.println("线程"+Thread.currentThread().getName()+":释放锁发生异常");
            }finally {
                lock.unlock();
                System.out.println("线程"+Thread.currentThread().getName()+":释放锁完毕");
            }
        }
    
        public static void main(String[] args) {
    
            LockTest lockTest = new LockTest();
    
            new Thread(()->{
                lockTest.lock();
            }).start();
    
            new Thread(()->{
                lockTest.lock();
            }).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

    在这里插入图片描述

    • 尝试获取锁tryLock(),表示用来尝试获取锁,如果获取成功返回true,失败返回false。
    public class LockTest {
    
        private Lock lock = new ReentrantLock();
    
        /**
         * 线程尝试获取线程锁,如果有其他线程占用,就返回false,无法拿到锁
         */
        public void lock(){
            if(lock.tryLock()){
                try {
                    System.out.println("线程"+Thread.currentThread().getName()+":获取到锁资源");
                    Thread.sleep(2000L);
                }catch (Exception e){
                    System.out.println("线程"+Thread.currentThread().getName()+":释放锁发生异常");
                }finally {
                    lock.unlock();
                    System.out.println("线程"+Thread.currentThread().getName()+":释放锁完毕");
                }
            }else{
                System.out.println("线程"+Thread.currentThread().getName()+":尝试获取锁失败,其他线程持有锁");
            }
        }
    
        public static void main(String[] args) {
    
            LockTest lockTest = new LockTest();
    
            new Thread(()->{
                lockTest.lock();
            }).start();
    
            new Thread(()->{
                lockTest.lock();
            }).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

    在这里插入图片描述

    • tryLock(3000,TimeUtil.MILLISECONDS)尝试获取锁,获取不到就等待3s,如果3s后还是获取不到就返回false
    public class LockTest {
    
        private Lock lock = new ReentrantLock();
    
        /**
         * 线程尝试获取线程锁,如果有其他线程占用,就返回false,无法拿到锁
         */
        public void lock() throws InterruptedException {
            if(lock.tryLock(3000,  TimeUnit.MILLISECONDS)){
                try {
                    System.out.println("线程"+Thread.currentThread().getName()+":获取到锁资源");
                    Thread.sleep(5000L);
                }catch (Exception e){
                    System.out.println("线程"+Thread.currentThread().getName()+":释放锁发生异常");
                }finally {
                    lock.unlock();
                    System.out.println("线程"+Thread.currentThread().getName()+":释放锁完毕");
                }
            }else{
                System.out.println("线程"+Thread.currentThread().getName()+":尝试获取锁失败,其他线程持有锁");
            }
        }
    
        public static void main(String[] args) {
    
            LockTest lockTest = new LockTest();
    
            new Thread(()->{
                try {
                    lockTest.lock();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
    
            new Thread(()->{
                try {
                    lockTest.lock();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).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

    在这里插入图片描述

    3.自定义实现可重入锁

    (1)可重入锁简介

    • 可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他线程是不可以的。
    • synchronized和ReetrantLock都是可重入锁。
    • 可重入锁的意义就在于防止死锁。
    • 实现原理是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的,线程请求一个未被占用的锁时,JVM将记录锁的占有者,并且将请求计数器置为1。
    • 如果同一个线程再次请求这个锁,计数器将递增。
    • 每次占用线程退出同步块,计数器将递减,直到计数器为0,锁被释放。
    • 关于父类和子类的锁的重入:子类覆盖了父类的synchonized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将会产生死锁。

    (2)伪代码案例

    class A
    public synchronized methodA(){
    	methodB();
    }
    
    public synchronized methodB(){
    }
    
    #当线程调用A类的对象methodA同步方法,如果其他线程没有获取A类的对象锁,那么当前线程就获得点钱A类对象的锁,然后执行methodB同步方法,当前线程能够在次获取A类对象的锁,其他线程是不可以的,这就是可重入锁。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    (3)自定义锁发生死锁问题

    • 自定义锁继承ReentrantLock类重写lock和unlock方法
    public class MyLock extends ReentrantLock {
    
        //定义锁的标志位
        private boolean isHoldLock = false;
    
        /**
         * 同一时刻,能且仅能有一个线程获取到锁
         * 其他线程只能等待该线程释放锁之后才能获取到锁
         */
        @Override
        public synchronized void lock() {
            //判断是否有当前线程持有锁,如果有就进入等待
            if (isHoldLock) {
                try {
                    wait();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            //将锁的标志置成true,表示当前线程持有锁
            isHoldLock = true;
        }
    
        @Override
        public synchronized void unlock() {
            //唤醒等待的线程
            notify();
            //释放锁资源
            isHoldLock = false;
        }
    }
    
    • 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
    • 定义ReentryDemo创建两个方法A、B其中在A方法中调用B方法
    public class ReentryDemo {
    
        private Lock lock = new MyLock();
    
        public void methodA(){
            lock.lock();
            System.out.println("进入方法A");
            methodB();
            lock.unlock();
        }
    
        public void methodB(){
            lock.lock();
            System.out.println("进入方法B");
            lock.unlock();
        }
    
        public static void main(String[] args) {
            ReentryDemo reentryDemo = new ReentryDemo();
            reentryDemo.methodA();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 上面这段代码看似没有问题,但其实已经造成了死锁。
    • 运行结果:

    在这里插入图片描述

    • jconsole调出java管控台,查看当前运行的线程,发现并没有出现死锁的现象。

    在这里插入图片描述

    • 回到代码分析原因

    在这里插入图片描述

    • 解决办法:将自定义的锁改成可重入锁,当同一线程再次访问的时候,允许再次获取锁。

    (4)自定义可重入锁

    • 自定义锁继承ReentrantLock类重写lock和unlock方法
    public class MyLock extends ReentrantLock {
    
        private boolean isHoldLock = false;
    
        //当前线程实例
        private Thread holdLockThread = null;
    
        //重入的次数
        private int reentryCount = 0;
    
        /**
         * 同一时刻,能且仅能有一个线程获取到锁
         * 其他线程只能等待该线程释放锁之后才能获取到锁
         */
        @Override
        public synchronized void lock() {
            //判断当前线程是否持有锁,并且是不是同一个线程重复进入
            if(isHoldLock && Thread.currentThread() != holdLockThread){
                try{
                    wait();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            holdLockThread = Thread.currentThread();
            isHoldLock = true;
            //记录加锁的次数
            reentryCount++;
        }
    
        @Override
        public synchronized void unlock() {
            //判断当前线程是否是持有锁的线程,是,重入次数减1,不是就不做处理
            if(Thread.currentThread() == holdLockThread){
                reentryCount--;
                if(reentryCount == 0){
                    notify();
                    isHoldLock = false;
                }
            }
        }
    }
    
    
    • 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
    • 测试代码和上面相同,此处省略

    • 执行结果:

    在这里插入图片描述

    • 分析原因:

    在这里插入图片描述
    在这里插入图片描述

    (5) 一个线程执行同步代码时,再次重入该锁过程中,如果抛出异常,会释放锁吗?

    • 注意:如果有涉及锁的操作的业务方法的时候,都要加上try-catch-finally,在finally中释放锁资源。
    • 锁不释放的案例

    在这里插入图片描述

    public class ReentryDemo {
    
        private Lock lock = new MyLock();
    
        private int num = 0;
    
        public void methodA() throws InterruptedException {
    
            lock.lock();
            System.out.println("进入方法A");
            methodB();
            Thread.sleep(3000L);
            if (num == 0) {
                num++;
                int a = 1 / 0;
            }
            lock.unlock();
        }
    
        public void methodB() {
            lock.lock();
            System.out.println("进入方法B");
            lock.unlock();
        }
    
        public static void main(String[] args) {
            ReentryDemo reentryDemo = new ReentryDemo();
            new Thread(() -> {
                try {
                    reentryDemo.methodA();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
    
            new Thread(() -> {
                try {
                    reentryDemo.methodA();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).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

    在这里插入图片描述

    • 修改代码优化
    public class ReentryDemo {
    
        private Lock lock = new MyLock();
    
        private int num = 0;
    
        public void methodA() {
            try {
                lock.lock();
                System.out.println("进入方法A");
                methodB();
                Thread.sleep(3000L);
                if (num == 0) {
                    num++;
                    int a = 1 / 0;
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void methodB() {
            try {
                lock.lock();
                System.out.println("进入方法B");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) {
            ReentryDemo reentryDemo = new ReentryDemo();
            new Thread(() -> {
                reentryDemo.methodA();
            }).start();
    
            new Thread(() -> {
                reentryDemo.methodA();
            }).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

    在这里插入图片描述

  • 相关阅读:
    能带你起飞的【数据结构】成王第十二篇:堆2
    Leetcode1021. 删除最外层的括号(simple)
    springboot 整合 redis
    Webpack 中 Plugin 的作用是什么?常用 plugin 有哪些?
    Flutter高仿微信-第26篇-新的朋友
    dig 简明教程
    【GEE】6、在 Google 地球引擎中构建各种遥感指数
    tauri+vue开发小巧的跨OS桌面应用-股票体检
    数学分析:数项级数的概念
    【Java进阶】多线程(一)
  • 原文地址:https://blog.csdn.net/weixin_47533244/article/details/127795865