Java中的锁--分类辨析

在这里插入图片描述

Lock接口

  • 为什么有synchronized了还要用Lock?

    1. 效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程
    2. 不够灵活(读写锁更灵活):加锁和释放的时机单一、每个锁仅有单一的条件(某个对象)
    3. 无法知道是否成功获取到锁
  • 主要方法:

    • lock()

      • lock()就是最普通的获取锁。如果锁被其他线程获取,则进行等待
      • Lock不会像synchronized一样在异常时自动释放锁
      • 最佳实践应该是在finally中释放锁,以保证发生异常时锁一定被释放
      • lock()方法不能被中断,这会带来隐患:一旦陷入死锁,lock()就会陷入永久等待
    • tryLock()

      • tryLock()用来尝试获取锁,如果当前锁没有被其他线程占有,则获取成功,返回true,否则返回false,代表获取锁失败
      • 该方法会立即返回,拿不到锁就不会等待
      • 所以可以用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;
              r2.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.SECONDS)){
              
              
                              try {
              
              
                                  System.out.println("线程1获取到了锁1");
                                  Thread.sleep(new Random().nextInt(1000));
                                  if (lock2.tryLock(800, TimeUnit.SECONDS)){
              
              
                                      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();
                      }
                  }
              }
          }
      }
      
    • lockInterruptibly

      • 相当于tryLock(long time, TimeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断
      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();
              }
              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) {
              
              
                  e.printStackTrace();
                  System.out.println(Thread.currentThread().getName() + "获得锁的期间被中断了");
              }
          }
      }
      ----------------------
          Thread-0尝试获取锁
          Thread-1尝试获取锁
          Thread-1获取到了锁
          Thread-1睡眠期间被中断了
          Thread-1释放了锁
          Thread-0获取到了锁
          Thread-0释放了锁
      

乐观锁、悲观锁

  • 操作过程

    乐观锁 悲观锁
    a. 线程1和线程2直接获取资源并各自计算 a. 两个线程都抢锁
    b. 线程1先计算完并判断资源是否被修改 b. 线程1抢到,线程2等待
    c. 线程1发现没人在计算期间修改资源,于是把自己的计算结果写到资源里 c. 线程1释放锁,线程2拿到
    d. 线程2计算完并判断资源是否已被修改 d. 都释放锁
    e. 线程2发现在计算期间有人修改了资源,于是报错或者重试
  • 互斥同步锁的劣势

    • 阻塞和唤醒带来的劣势
    • 永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的那些线程将永远得不到执行
    • 优先级反转:优先级低的线程持有了互斥同步锁,一直不释放,那么优先级高的线程将得不到执行
  • 乐观锁即非互斥同步锁,悲观锁即互斥同步锁

    乐观锁典型例子 悲观锁典型例子
    原子类 synchronized
    并发容器 Lock
    Git
  • 悲观锁使用场景:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:

    1. 临界区有IO操作
    2. 临界区代码复杂或者循环量大
    3. 临界区竞争非常激烈
  • 乐观锁使用场景:适合并发写入少,大部分是读取的场景,不加锁能使读取性能大幅提高

可重入锁、非可重入锁

  • 可重入锁:支持重新进入的锁,它表示该锁能支持一个线程对资源的重复加锁
//递归处理,每次处理前都要加锁
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();
                System.out.println(lock.getHoldCount());
            }
        } finally {
    
    
            lock.unlock();
        }
    }

    public static void main(String[] args) {
    
    
        accessResource();
    }
}
-----------------------------------
已经对资源进行了处理
1
已经对资源进行了处理
2
已经对资源进行了处理
3
已经对资源进行了处理
4
已经对资源进行了处理
4
3
2
1

公平锁、非公平锁

  • 公平指的是按照线程请求的顺序来分配锁;非公平是不完全按照请求的顺序,在一定的情况下,可以插队

  • 下面例子是一个演示公平锁和非公平锁情况下打印的例子,10个线程完成打印任务,每个线程都需要打印两次,公平锁下10个线程依次开始到达等待队列。线程0打印完一次后,本应开始下一次打印,但是其他线程在等待,由公平锁的原则,线程0就会排到等待队列的队尾,等待下一轮。

      public class FairLock {
          
          
          public static void main(String[] args) {
          
          
              PrintQueue printQueue = new PrintQueue();
              Thread[] thread = new Thread[10];
              for (int i = 0; i < 10; i++) {
          
          
                  thread[i] = new Thread(new Job(printQueue));
              }
      
              for (int i = 0; i < 10; i++) {
          
          
                  thread[i].start();
                  try {
          
          
                      Thread.sleep(100);
                  } catch (InterruptedException e) {
          
          
                      e.printStackTrace();
                  }
              }
          }
      
      }
      
      class Job implements Runnable{
          
          
      
          PrintQueue printQueue;
      
          public Job(PrintQueue printQueue) {
          
          
              this.printQueue = printQueue;
          }
      
          @Override
          public void run() {
          
          
              System.out.println(Thread.currentThread().getName() + "开始打印");
              printQueue.printJob(new Object());
              System.out.println(Thread.currentThread().getName() + "打印完毕");
          }
      }
      
      class PrintQueue{
          
          
          /**
           * true:公平   false:不公平
           */
          private Lock queueLock = new ReentrantLock(true);
      
          public void printJob(Object document) {
          
          
              queueLock.lock();
              try {
          
          
                  int duration = new Random().nextInt(10) + 1;
                  System.out.println(Thread.currentThread().getName() + "正在打印");
                  Thread.sleep(duration * 1000);
              } catch (InterruptedException e) {
          
          
                  e.printStackTrace();
              } finally {
          
          
                  queueLock.unlock();
              }
      
              queueLock.lock();
              try {
          
          
                  int duration = new Random().nextInt(10) + 1;
                  System.out.println(Thread.currentThread().getName() + "正在打印");
                  Thread.sleep(duration * 1000);
              } catch (InterruptedException e) {
          
          
                  e.printStackTrace();
              } finally {
          
          
                  queueLock.unlock();
              }
          }
      }
      
    

    在这里插入图片描述

  • 非公平锁的情况下,线程可以出现插队现象
    在这里插入图片描述

共享锁、排它锁

  • 排它锁:又称独占锁、独享锁
  • 共享锁:又称读锁,获得共享锁之后,可以查看但无法修改和删除数据
  • 共享锁和排它锁的典型是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是独占锁
  • 多个读操作可以同时进行,并没有线程安全问题
  • 读写锁的规则:
    1. 多个线程只申请读锁,都可以申请到
    2. 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
    3. 如果有一个线程已经占用了写锁,则此时其他线程如果要申请写锁或者读锁,则申请的线程会一直等待释放写锁
    4. 要么多读,要么一写
public class CinemaReadWrite {
    
    

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
    
    
        readLock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write() {
    
    
        writeLock.lock();
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
    
    
        new Thread(()->read(),"Thread1").start();
        new Thread(()->read(),"Thread2").start();
        new Thread(()->write(),"Thread3").start();
        new Thread(()->write(),"Thread4").start();
    }
}

在这里插入图片描述

读写锁

  • 读锁插队策略

    • 公平锁:不允许插队
    • 非公平锁:
      • 写锁可以随时插队
      • 读锁仅在等待队列头结点不是想获取写锁的线程的时候可以插队
  • 特殊情况:读锁可以在等待队列头结点是读锁的情况下插队。原因是如果读锁正在运行,等待队列头结点也是读锁,那就多个读锁可以同时运行,一般情况下头结点都会是写锁,这时不允许插队。但是也有特殊情况,会出现头结点是读锁。

    下面例子中5个线程写读读写读依次运行,但是1000个读锁子线程会一直尝试插队,当Thread1运行完write之后,线程2运行read,此时等待队列头结点是线程3,同样是读线程,但这时子线程依然能插队

    public class NonfairBargeDemo {
          
          
    
        private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
                false);
    
        private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    
        private static void read() {
          
          
            System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
            readLock.lock();
            try {
          
          
                System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
                try {
          
          
                    Thread.sleep(20);
                } catch (InterruptedException e) {
          
          
                    e.printStackTrace();
                }
            } finally {
          
          
                System.out.println(Thread.currentThread().getName() + "释放读锁");
                readLock.unlock();
            }
        }
    
        private static void write() {
          
          
            System.out.println(Thread.currentThread().getName() + "开始尝试获取写锁");
            writeLock.lock();
            try {
          
          
                System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
                try {
          
          
                    Thread.sleep(40);
                } catch (InterruptedException e) {
          
          
                    e.printStackTrace();
                }
            } finally {
          
          
                System.out.println(Thread.currentThread().getName() + "释放写锁");
                writeLock.unlock();
            }
        }
    
        public static void main(String[] args) {
          
          
            new Thread(()->write(),"Thread1").start();
            new Thread(()->read(),"Thread2").start();
            new Thread(()->read(),"Thread3").start();
            new Thread(()->write(),"Thread4").start();
            new Thread(()->read(),"Thread5").start();
            new Thread(new Runnable() {
          
          
                @Override
                public void run() {
          
          
                    Thread thread[] = new Thread[1000];
                    for (int i = 0; i < 1000; i++) {
          
          
                        thread[i] = new Thread(() -> read(), "子线程创建的Thread" + i);
                    }
                    for (int i = 0; i < 1000; i++) {
          
          
                        thread[i].start();
                    }
                }
            }).start();
        }
    }
    

    在这里插入图片描述

  • 锁降级:不释放当前线程拥有的写锁,直接获取到读锁,随后释放写锁的过程

    private static void writeDowngrading() {
          
          
            writeLock.lock();
            try {
          
          
                System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
                Thread.sleep(1000);
                readLock.lock();
                System.out.println("在不释放写锁的情况下,直接获取读锁,成功降级");
            } catch (InterruptedException e) {
          
          
                e.printStackTrace();
            } finally {
          
          
                readLock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放写锁");
                writeLock.unlock();
            }
    }
    

自旋锁、阻塞锁

  • 自旋锁:即请求锁的线程不放弃CPU的执行时间,进行自旋,如果自旋完成后锁定同步资源的线程已经释放了锁,那么当前线程可以不必阻塞而是直接获取同步资源,从而避免了线程切换的开销
  • 阻塞锁:如果遇到没有拿到锁的情况,会直接把线程阻塞,直到被唤醒
//简单实现自旋锁
public class SpinLock {
    
    
    private AtomicReference<Thread> sign = new AtomicReference<>();

    public void lock(){
    
    
        Thread current = Thread.currentThread();
        while (!sign.compareAndSet(null, current)) {
    
    
            System.out.println("自旋获取失败,再次尝试");
        }
    }

    public void unlock(){
    
    
        Thread current = Thread.currentThread();
        sign.compareAndSet(current, null);
    }

    public static void main(String[] args) {
    
    
        SpinLock spinLock = new SpinLock();
        Runnable runnable = new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
                spinLock.lock();
                System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
                try {
    
    
                    Thread.sleep(300);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } finally {
    
    
                    spinLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放了自旋锁");
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_44863537/article/details/113157327
今日推荐