常见锁的介绍

1、Lock锁

1.1简介、作用

  • 是一种工具,用于控制对共享资源的访问。
  • Lock和synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有很大的差别。
  • Lock并不是用来代替synchronized的,而是当synchronized不合适或者是不满足情况的时候。提供更高级的使用方法.
  • 通常情况下,Lock只允许一个线程来访问共享资源.不过在一些特殊的实现类中允许并发访问,比如ReadWriteLock里面的ReadLock.

1.2、为什么需要Lock?

  • synchronized在很多场景下不够用
  • 效率低:锁的释放情况少、试图获得锁的时候不能设定超时、不能中断一个正在试图获得锁的线程。
  • 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),很多情况下是不够的。
  • 无法知道是否成功获取到锁。

1.3Lock锁的常用方法

  • Lock中声明了四个方法来获取锁

    1. lock()

      1. 是最普通的获取锁,如果锁已经被其它线程获取,则进行等待。
      2. 它不会像synchronized(jvm会自动释放)一样在异常的时候自动释放锁,所以需要在finally中释放锁。
      3. lock()方法不能被中断,一旦中断会带来很大的隐患:一旦陷入死锁,lock()就会陷入永远的等待。
    2. tryLock()

      1. 用来尝试获取锁,如果当前锁没有被其它线程占用,则获取成功,返回true,否则为false。
    3. tryLock(long time,TimeUnit unit)

      1. 和上一个功能相类似,超时就放弃

      2. 代码演示

      3. /**
         * 描述:     TODO 用tryLock来避免死锁
         */
        public class TryLockDeadlock implements Runnable {
        
        
            int flag = 1;
            static Lock lock1 = new ReentrantLock();
            static Lock lock2 = new ReentrantLock();
        
            public static void main(String[] args) {
                TryLockDeadlock r1 = new TryLockDeadlock();
                TryLockDeadlock r2 = new TryLockDeadlock();
                r1.flag = 1;
                r1.flag = 0;
                new Thread(r1).start();
                new Thread(r2).start();
        
            }
        
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (flag == 1) {
                        try {
                            if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                                try {
                                    System.out.println("线程1获取到了锁1");
                                    Thread.sleep(new Random().nextInt(1000));
                                    if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                                        try {
                                            System.out.println("线程1获取到了锁2");
                                            System.out.println("线程1成功获取到了两把锁");
                                            break;
                                        } finally {
                                            lock2.unlock();
                                        }
                                    } else {
                                        System.out.println("线程1获取锁2失败,已重试");
                                    }
                                } finally {
                                    lock1.unlock();
                                    Thread.sleep(new Random().nextInt(1000));
                                }
                            } else {
                                System.out.println("线程1获取锁1失败,已重试");
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
        
                    if (flag == 0) {
                        try {
                            if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                                try {
                                    System.out.println("线程2获取到了锁2");
                                    Thread.sleep(new Random().nextInt(1000));
                                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                                        try {
                                            System.out.println("线程2获取到了锁1");
                                            System.out.println("线程2成功获取到了两把锁");
                                            break;
                                        } finally {
                                            lock1.unlock();
                                        }
                                    } else {
                                        System.out.println("线程2获取锁1失败,已重试");
                                    }
                                } finally {
                                    lock2.unlock();
                                    Thread.sleep(new Random().nextInt(1000));
                                }
                            } else {
                                System.out.println("线程2获取锁2失败,已重试");
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        
        

        线程1获取到了锁1
        线程2获取到了锁2
        线程2获取锁1失败,已重试
        线程1获取到了锁2
        线程1成功获取到了两把锁
        线程2获取到了锁2
        线程2获取到了锁1
        线程2成功获取到了两把锁

    4. lockInterruptibly()

      1. 相当于tryLock()方法的时间设置为无限。在等待锁的过程中,线程可以被中断(因为时间无限长)。

      2. 代码演示

      3. /**
         * 描述:     TODO 在获取锁的过程中被打断
         */
        public class LockInterruptibly implements Runnable {
        
            private Lock lock = new ReentrantLock();
        public static void main(String[] args) {
            LockInterruptibly lockInterruptibly = new LockInterruptibly();
            Thread thread0 = new Thread(lockInterruptibly);
            Thread thread1 = new Thread(lockInterruptibly);
            thread0.start();
            thread1.start();
        
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //模拟线程1被打断
            thread1.interrupt();
        }
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "尝试获取锁");
                try {
                    lock.lockInterruptibly();
                    try {
                        System.out.println(Thread.currentThread().getName() + "获取到了锁");
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        System.out.println(Thread.currentThread().getName() + "睡眠期间被中断了");
                    } finally {
                        lock.unlock();
                        System.out.println(Thread.currentThread().getName() + "释放了锁");
                    }
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName() + "获得锁期间被中断了");
                }
            }
        }
        
        

1.4可见性保证

  • Lock的加解锁和synchronized有同样的内存语义。也就是说下一个线程加锁后可以看到前一个线程解锁前发生的所有操作。

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

2、锁的分类

在这里插入图片描述

2.1 乐观锁和悲观锁

ReenTrantLock即是互斥锁,也是可重入锁。

​ 互斥同步锁又称为悲观锁,非互斥同步又称为乐观锁

2.1.1 为什么会诞生非互斥同步锁(乐观锁)?

  • 互斥同步锁的劣势。
    1. 阻塞唤醒带来的性能劣势。
    2. 永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无线循环、死锁等活跃性问题。那么等待该线程释放锁的那几个悲催的线程,永远也无法执行。
    3. 优先级反转:如果阻塞的优先级高,而持有锁的优先级比较低,就会造成反转,阻塞的那个线程的优先级降低。

2.1.2 什么是乐观锁和悲观锁

  • 从性格上分析

    • 乐观的人认为出错的概率总是很小的,真的遇到问题的时候再做出修改。
    • 悲观的人认为出错是常态,做事需要考虑到万无一失。
  • 是否锁住资源的角度分类

    • 悲观锁:如果不锁住当前资源,别人就会来争抢,就会造成数据结果错误。所以每次悲观锁为了确保结果的正确性,再每次获取并修改数据的时候,把该数据锁住,别人无法进行修改。

      Java中常见的悲观锁就是synchronized 和Lock的相关类。

      在这里插入图片描述

在这里插入图片描述

  • 乐观锁:认为自己在处理操作的时候不会有其它的线程来干扰,所以不会锁住对象。

    ​ 在更新的时候,去对比在我修改数据的期间数据是否被别人修改过。如果没修改过,那就说明只有自己在操作,正常修改数据。如果被修改过,就选择放弃、报错、重试等策略。

    乐观锁的实现一般都是利用CAS算法来实现的。典型例子是原子类、并发容器。GIT代码管理也是(被修改过就提示提交失败)。

2.1.3典型的悲观锁和乐观锁的实现

  • 悲观锁

    
    /**
     * 描述:    TODO Lock不会像synchronized一样,异常的时候自动释放锁,所以最佳实践是,finally中释放锁,以便保证发生异常的时候锁一定被释放
     */
    public class MustUnlock {
    
        //最为典型的锁实现
        private static Lock lock = new ReentrantLock();
    
        public static void main(String[] args) {
            lock.lock();
            try{
                //获取本锁保护的资源
                System.out.println(Thread.currentThread().getName()+"开始执行任务");
            }finally {
                lock.unlock();
            }
        }
    }
    
    
  • 乐观锁

    /**
     * 描述:     TODO  简单的乐观锁
     */
    public class PessimismOptimismLock {
    
        int a;
    
        public static void main(String[] args) {
            //原子整型
            AtomicInteger atomicInteger = new AtomicInteger();
            atomicInteger.incrementAndGet();
        }
    
        public synchronized void testMethod() {
            a++;
        }
    }
    
    

2.1.4 开销对比

  • 悲观锁:原始开销高于乐观锁,但是一劳永逸(消耗的资源是固定的),临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响。

    ​ 适合并发写入多的情况,适用于邻接持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗(无法获取锁就会自行等待)。典型情况:

    ​ 1、临界区有IO操作

    ​ 2、临界区代码复杂或者循环量大

    ​ 3、临界区竞争激烈(并发量高)

  • 相反,虽然乐观锁一开始的开销小,但是如果自旋时间很长或者不停的尝试,那么消耗的资源也会越来越多

    ​ 适合并发写入少(想什么时候获取锁就什么时候获取),大部分是读取的场景。不加锁能让读取性能大幅提高。

2.2 可重入锁和非可重入锁

  • 以ReentrantLock为使用案例。

  • 电影院预定座位案例,(任意两个线程无法同时订票)

    
    /**
     * 模拟预定座位
     * @author tyeerth
     * @date 2020/9/15 - 15:16
     */
    public class CinemaBookedSeat {
        private static ReentrantLock lock = new ReentrantLock();
        public static void bookSeat(){
            //获取锁
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName()+"正在获取座位");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"获取座位完成");
            }finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) {
            new Thread(() -> bookSeat()).start();
            new Thread(() -> bookSeat()).start();
            new Thread(() -> bookSeat()).start();
        }
    }
    
    

2.2.1 什么是可重入?

  • ​ 指同一个线程可多次获取同一把锁

  • 好处:可以避免死锁

    说明:如果同一把锁锁住了两个方法,一个线程访问了其中一个方法获取到了锁。如果是可重入锁,可以在不释放当前锁的情况下去访问第二个方法,如果还想获取第二把锁就会发生死锁。如果是非可重入锁,就必须先释放当前锁,到第二个方法的时候再获取锁。

​ 代码演示1,可看到重入几次,再次获取锁的时候不需要释放。


/**
 * 描述:     TODO  演示可重入锁
 */
public class GetHoldCount {
    private  static ReentrantLock lock =  new ReentrantLock();

    public static void main(String[] args) {
        System.out.println(lock.getHoldCount());
        lock.lock();
        //拿到锁就加一
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
    }
}

运行结果

0
1
2
3
2
1
0

代码演示2


/**
 * 描述:     TODO 演示资源在上锁的情况下,对其递归处理4次
 */
public class RecursionDemo {

    private static ReentrantLock lock = new ReentrantLock();

    private static void accessResource() {
        lock.lock();
        try {
            System.out.println("已经对资源进行了处理");
            if (lock.getHoldCount()<5) {
                System.out.println("获取锁"+lock.getHoldCount()+"次");
                accessResource();
            }
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        accessResource();
    }
}

已经对资源进行了处理
获取锁1次
已经对资源进行了处理
获取锁2次
已经对资源进行了处理
获取锁3次
已经对资源进行了处理
获取锁4次
已经对资源进行了处理

2.2.2 可重入锁的源码分析

  • ​ 可重入锁ReentranLock以及非可重入锁ThreadPoolExecutor的Worker类。
  • 在这里插入图片描述

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_45372719/article/details/108601969