美文网首页
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