美文网首页
Java常用锁

Java常用锁

作者: 蓝调_4f2b | 来源:发表于2023-06-05 08:58 被阅读0次

    1. 并发竞争概述

    竟态条件:多线程在临界区执行,由于代码执行序列不可预知而导致无法预测结果
    解决方式:
    (1)阻塞式:sync, Lock(ReentrantLock)
    (2)非阻塞式:Cas方式(自旋式)

    2. Synchronized

    2.1 使用方法

    (1)实例方法加锁:锁住实例对象

    public class MyClass {
        private int count;
    
        public synchronized void increment() {
            count++;
        }
    }
    

    在实例化对象上进行锁的判断
    (2)静态方法加锁:锁住类对象

    public class MyClass {
        private static int count;
    
        public static synchronized void increment() {
            count++;
        }
    }
    

    在静态类MyClass上进行锁的判断
    (3)加锁在代码块上:

    public class MyClass {
        private int count;
        private final Object lock = new Object();
        public void increment() {
            synchronized (lock) {
                count++;
            }
        }
        public int getCount() {
            synchronized (lock) {
                return count;
            }
        }
    }
    

    我们创建了一个名为 lock 的对象,并在每个方法中使用 synchronized 关键字来获取这个对象的锁。当一个线程进入 synchronized 代码块时,它会获取 lock 对象的锁,然后执行代码块中的代码。其他线程如果想要进入这个代码块,必须等待当前线程释放锁之后才能获取锁并执行代码块中的代码。

    2.2 synchronized锁竞争

    sync工作流程.png

    2.3 synchronized加锁原理

    (1)早期实现:sync为JVM内置锁,(JVM层面)基于Monitor(监视器)机制实现,(Linux层面)依赖底层操作系统的Mutux(互斥量)实现,由于进行了内核态及用户态的切换,性能较低
    (2)优化后:通过锁升级实现加锁过程:偏向锁,自旋锁,轻量级,重量级
    (3)monitor(监视器)原理:
    同步方法由Au_SYNCHRONIZED标志实现, 同步代码块由monitorenter与monitorexit实现;
    是管理共享变量以及对共享变量操作的过程,让它们支持并发;
    synchronized中wait(), notify(), notifyAll()等方法有monitor实现;
    补充:
    java线程等待方法:sleep, wait, join, unpark

    名称 作用 特点
    sleep 让当前线程暂停执行一段时间,再继续执行 Thread 类的静态方法线程不会释放锁,其他线程无法访问被锁定的资源
    wait 让当前线程暂停执行,直到其他线程调用该对象的 notify() 或 notifyAll() 方法唤醒它 在调用 wait() 方法时,线程会释放锁,其他线程可以访问被锁定的资源。wait() 方法必须在 synchronized 块中调用,否则会抛出 IllegalMonitorStateException 异常
    join 当前线程会暂停执行,直到另一个线程执行完毕 Thread 类的实例方法,可以让当前线程等待另一个线程执行完毕。在调用 join() 方法时,当前线程会暂停执行,直到另一个线程执行完毕。
    LockSupport.park() LockSupport 类的静态方法,可以让当前线程暂停执行,直到其他线程调用该线程的 unpark() 方法唤醒它 LockSupport.park() 方法可以用于线程的阻塞和唤醒,例如等待某个条件的满足后再继续执行

    注:sleep() 方法和 wait() 方法可以在任何地方调用,而 join() 方法和 LockSupport.park() 方法必须在线程内部调用。sleep() 方法和 wait() 方法可以让线程等待一段时间后继续执行,而 join() 方法和 LockSupport.park() 方法可以让线程等待其他线程的执行或唤醒。

    (4)sync加锁解锁流程图:

    (5)对象内存布局


    对象内存布局.png

    2.4 synchronized锁升级

    2.4.1 偏向锁:

    (1)流程:程序启动时,将对象的标记设置为偏向锁,表示该对象目前没有竞争,可以被当前线程独占;
    当其他线程访问该对象时,会检查该对象的标记,如果是偏向锁,则会判断当前线程是否是偏向锁的拥有者,如果是,则直接获取锁,否则会升级为轻量级锁或重量级锁;
    优点:减少锁竞争
    (2)撤销条件:
    obj.wait() 偏向锁撤销,升级为重量级锁
    notify(), notifyAll() 升级为轻量级锁

    2.4.2 锁升级过程:

    (1)在偏向锁的基础上,有线程P2加入竞争,会检查该对象的标记,如果当前线程不是偏向锁的拥有者,则会升级为轻量级锁,轻量级锁(又称自旋锁)的功能是让P2线程不断自旋(while + cas)
    注:若线程P1此时sleep,无竞争,则仍为偏向锁
    (2)线程自旋达到一定次数失败,进行线程阻塞(切换到内核态),此时为重量级锁;
    重量级锁是一种基于操作系统的锁,它的作用是在获取锁时,将当前线程挂起,等待锁的拥有者释放锁。重量级锁的效率比较低,因为它需要进行线程的上下文切换和内核态和用户态之间的切换。

    3. ReentrantLock原理

    3.1 操作系统中的并发处理--管程

    管程概念.png

    3.2 JVM层面对管程的实现

    synchronized:
    objectMonitor & cxq(cas owner)-> 等效于同步等待队列
    waitset -> 等效于条件等待队列

    3.3 java中管程实现

    (1)抽象层面:
    同步等待队列实现:CAS机制, volatile int state,等待队列
    条件等待队列实现:conditional await, signal(), signalAll(), 出队入队条件
    (2)工具层面:
    抽象队列同步器AQS

    3.4 AQS原理

    java.util.concurrent包中年基础能力组件,常用于实现依赖状态同步器

    3.4.1 特性

    (1)阻塞等待队列:由条件队列,同步队列共同实现
    (2)共享/独占锁:共享锁:semaphore(信号量), CountDownLatch
    独占锁:ReentrantLock
    (3)公平/非公平锁
    (4)是否可重入:state表示状态
    (5)是否允许中断:设置中断标志位

    3.4.2 两种等待队列

    (1)同步等待队列:维护获取资源(锁)失败后的线程
    (2)条件等待队列:
    调用await()时释放锁,线程加入条件队列
    signal()时唤醒条件队列中的线程,加入同步等待队列中,等待获取


    等待队列.png

    3.4.3 ReentrantLock应用

    解决库存超卖问题

    public string reduceStock() {
      int stock = Integer.parseInt(StringRedisTemplate.OPsForValue().get("stock"));
      if (stock > 0) {
        stock--;
        StringRedisTemplate.opsForValue().set("stock", stock);
      }
      return "end";
    }
    

    在多线程下会造成stock超卖

    3.4.4 小总结

    解决并发问题
    (1)非阻塞式:cas + 自旋锁
    (2)阻塞式:synchronized, ReentrantLock
    公平锁/非公平锁区别:
    (1)非公平锁效率更高(对CPU的使用效率高);不用经历从等待队列唤醒线程的步骤,有新的任务尝试获取锁资源即可成功
    (2)公平锁保证线程不会长时间陷于饥饿状态

    3.4.5 条件队列的使用场景

    boolean volatile hashcig;
    public static Conditional cond = lock.newCondition();
    public static cigratte() {
      lock.lock();
      try {
        while (!hashcig) {
          try {
            log("no cigurate, wait");
            cond.await();
          } catch(Exception e) e.print();
        } 
       log("begin work);
      finally {
          lock.unlock();
        }
      }
    }
    
    唤醒逻辑
    new Thread(() -> {
      lock.lock();
      try {
        hashcig = true;
        cond.signal("上烟了");
      } ...
    }
    

    3.4.6 流程图

    4. AQS框架扩展

    4.1 semaphore信号量

    (1)作用:实现互斥锁,通过同时只能有一个线程能获取信息量;用于实现限流功能;
    (2)工作原理:设置窗口值,当未达到该窗口值时,工作线程正常工作;当窗口值为0时,不支持新的线程执行任务,新的线程阻塞,进入阻塞队列;当上一批线程释放资源后,到等待队列中唤醒等待线程,重新执行以上流程;
    注:即根据窗口值一个批次一个批次执行多线程任务,上个批次执行时窗口值为0,则后面的线程阻塞;


    信号量流程.png

    (3)举例

    semphore windows = new semphore(3); // 设置窗口值
    for (;;) {
      new Thread(new Runnable() {
          public void run() {
            try {
            windows.acquire();  // 占用窗口, windows - 1
            Thread.sleep();
          } finally {
            window.release();  // 释放
          }
        }
      }
    }
    

    4.2 CountDownLatch

    (1)作用:是一个同步协作类,允许一个或多个线程等待,直到其他线程完成操作集;使用给定的count初始化,await后阻塞直到当前计数值;由于countDown方法调用达到0,count为0后所有等待线程均释放;
    注:将所有线程阻塞,直到countDown减到0值,调用unpark唤醒阻塞线程
    (2)实现原理:每次countDownLatch都执行release(1)减1,当减到0时调用unpark唤醒阻塞线程;即:state != 0,线程阻塞;state = 0,线程继续执行;
    (3)使用场景图:


    countDown.png

    4.3 CyclicBarrier 循环屏障

    4.3.1 使多个线程在cyclicBarrier上阻塞,直到满足某条件放开
    cyclicbarrier.png
    4.3.2 使用实例:
    CyclicBarrier cyc = new CyclicBarrier(3);
    for (int i = 0; i < 5; i++) {
      new Thread(() -> {
        try {
          System.out.println(Thread.current().getName());
          cyc.await();  // 未达到三个线程,则进行线程阻塞;总计达到三个线程时,继续向下执行
         System.out.println(Thread.current().getName());
         Thread.sleep(5000);
         System.out.println(Thread.current().getName());
        } catch (...) { }
      }
    } 
    
    4.3.3 使用场景:

    实现人满发车场景,线程池用于线程复用

    AutoicInteger con = new AutoicInteger();
    ThreadPoolExecutor thread = new ThreadPoolExecutor(5, 5, 1000, Timeunit.Seconds, new ArrayBlockingQueue<>(100), (r)->new Thread(r, con.addAndGet(1)), new ThreadPoolExecutor.Abortpolicy());
    CyclicBarrier cyc = new CyclicBarrier(5, () -> System.out.print("start"));
    for (int i = 0; i < 10; i++) {
      ThreadPoolExecutor.submit(new Runner(cyc));
    }
    
    4.3.4 实现流程:

    (1)首先加独占锁, 锁住int state
    (2)进入条件队列,阻塞线程,并释放锁;由于finally块中的释放锁lock.unlock();需重新获取锁
    (3)由于仅有同步队列逻辑实现中有唤醒线程,并重新获取锁的实现,这里进行对进入同步队列的复用
    (4)唤醒同步队列绑定的线程节点

    4.3.5 两种重要的队列:

    (1)同步等待队列:主要用于维护获取锁失败时入队的线程
    (2)条件等待队列:调用await()方法时会释放锁,线程会加入条件队列;
    调用signal()唤醒时将条件队列中线程节点移动到同步队列中,等待再次获取锁

    4.4 ReentrantReadWriteLock

    针对读多写少场景,该工具特性为:读读可并发;写写、写读互斥;提高读写场景并发量

    4.4.1 总结,实现一把锁有哪些设计?

    (1)cas + 自旋尝试
    (2)管程方式:synchronized
    (3)AQS方式:Cas + 同步队列
    (4)实现tryAcquired()方法 -> ReentrantLock等

    4.4.2 如何设计并实现一把读写分离锁?

    答:采用高地位打标设计
    高16位不为0:有读锁,每多一位表示多一个线程持有读锁,最高为65535个
    低16位不为0:有写锁,没多一位表示多一次重入

    相关文章

      网友评论

          本文标题:Java常用锁

          本文链接:https://www.haomeiwen.com/subject/gymiedtx.html