Java高并发核心编程—JUC显示锁原理

news2024/11/19 22:42:26

注:本笔记是阅读《Java高并发核心编程卷2》整理的笔记!

显示锁

使用Java内置锁时,不需要通过Java代码显式地对同步对象的监视器进行抢占和释放,这些工作由JVM底层完成,而且任何一个Java对象都能作为一个内置锁使用,所以, Java的对象锁使用起来非常方便。但是, Java内置锁的功能相对单一,不具备一些比较高级的锁功能,比如:

  • 限时抢锁:在抢锁时设置超时时长,如果超时还未获得锁就放弃。
  • 可中断抢锁:在抢锁时,外部线程给抢锁线程发出一个中断信号,就能唤起等待锁的线程,并终止抢占过程。
  • 多个等待队列:生产者和消费者共用一把锁,该锁上维持两个等待队列。

除了以上功能问题之外, Java对象锁还存在性能问题。在竞争稍微激烈的情况下, Java对象锁会膨胀为重量级锁(基于操作系统的Mutex Lock实现),而重量级锁的线程阻塞和唤醒操作需要进程在内核态和用户态之间来回切换,导致其性能非常低。所以,迫切需要提供一种新的锁来提升争用激烈场景下锁的性能。 Java显式锁就是为了解决这些Java对象锁的功能问题、性能问题而生的。 Lock接口位于java.util.concurrent.locks包中,是JUC显式锁的一个抽象类。

在这里插入图片描述

可重入锁 ReentrantLock

ReentrantLock是JUC包提供的显式锁的一个基础实现类, ReentrantLock类实现了Lock接口,它拥有与synchronized相同的并发性和内存语义,但是拥有了限时抢占、可中断抢占等一些高级锁特性。此外, ReentrantLock基于内置的抽象队列同步器(Abstract Queued Synchronized, AQS)实现,在争用激烈场景下,能表现出表内置锁更佳的性能。 ReentrantLock是一个可重入的独占(或互斥)锁 。

使用 lock()方法抢锁的模板代码

使用lock()方法进行阻塞式的锁抢占,其模板代码如下:

//创建所对象, ReentrantLock 为Lock的某个实现类
Lock lock = new ReentrantLock();
lock.lock(); //step1:抢占锁
try {
	//step2:抢锁成功,执行临界区代码
} finally {
	lock.unlock(); //step3:释放锁
}

以上抢锁模板代码有以下几个需要注意的要点:

  • 释放锁操作lock.unlock()必须在try-catch结构的finally块中执行,避免锁因为异常得不到释放。
  • 抢占锁操作lock.lock()必须在try语句块之外,而不是放在try块之内。lock()方法没有声明抛出异常,所以可以不包含到try块中。lock()方法并不是一定能够抢占锁成功,如果没有抢占成功,当然也就不需要释放锁 。
  • 在抢占锁操作lock.lock()和try语句之间不要插入任何代码,避免抛出异常而无法执行释放锁操作lock.unlock(),导致锁无法被释放。

调用 tryLock()方法非阻塞抢锁的模板代码

lock()是阻塞式抢占,在没有抢到锁的情况下, 当前线程会阻塞。如果不希望线程阻塞,可以使用tryLock()方法抢占锁。 tryLock()是非阻塞抢占,在没有抢到锁的情况下,当前线程会立即返回,不会被阻塞。

//创建所对象, ReentrantLock为Lock的某个实现类
Lock lock = new ReentrantLock();
if (lock.tryLock()) { //step1:尝试抢占锁
    try {
        //step2:抢锁成功,执行临界区代码
    } finally {
    	lock.unlock(); //step3:释放锁
    }
}	
else{
	//step4:抢锁失败,执行后备动作
}

这种处理方式在实际开发中使用不多,但是其重载版本tryLock(long time, TimeUnit unit)方法在限时阻塞抢锁的场景中非常有用。

调用 tryLock(long time, TimeUnit unit)方法抢锁的模板代码

tryLock(long time, TimeUnit unit)方法用于限时抢锁,该方法在抢锁时会进行一段时间的阻塞等待,其time参数代表最大的阻塞时长,其unit参数为时长的单位(如秒)。

//创建所对象, SomeLock为Lock的某个实现类,如ReentrantLock
Lock lock = new SomeLock();
//抢锁时阻塞一段时间,如1秒
if (lock.tryLock(1, TimeUnit.SECONDS)) { //step1:限时阻塞抢占
    try {
    	//step2:抢锁成功,执行临界区代码
    } finally {
    	lock.unlock(); //step3:释放锁
    }
}
else{
	//限时抢锁失败,执行后备动作
}

总结

  • lock()方法用于阻塞抢锁,抢不到锁时线程会一直阻塞。
  • tryLock()方法用于尝试抢锁,该方法有返回值,如果成功则返回true,如果失败(即锁已被其他线程获取)则返回false。此方法无论如何都会立即返回。
  • tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过这个方法在抢不到锁时会阻塞一段时间。如果在阻塞期间获取到锁立即返回true,超时则返回false。

基于显式锁进行“等待-通知”方式的线程间通信

“等待-通知”方式的线程间通信机制,具体来说是指一个线程A调用了同步对象的wait()方法进入等待状态,而另一个线程B调用了同步对象的notify()或者notifyAll()方法去唤醒等待线程;当线程A收到线程B的唤醒通知后,就可以重新开始执行。 与Object对象的wait()、 notify()两类方法类似,基于Lock显式锁JUC也为大家提供了一个用于线程间进行“等待-通知”方式通信的接口—java.util.concurrent.locks.Condition。

public interface Condition
{
    //方法1:等待。此方法在功能上与 Object.wait()语义等效
    //使当前线程加入 await() 等待队列中,并释放当前锁
    //当其他线程调用signal()时,等待队列中的某个线程会被唤醒,重新去抢锁
    void await() throws InterruptedException;
    //方法2:限时等待。此方法与await()语义等效
    //不同点在于,在指定时间time等待超时后,如果没有被唤醒,线程将中止等待
    //线程等待超时返回false,其他情况返回true
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    //方法3:通知。此方法在功能上与Object.notify()语义等效
    // 唤醒一个在await()等待队列中的线程
    void signal();
    //方法4: 通知全部。唤醒await()等待队列中所有的线程
    //此方法与object.notifyAll()语义上等效
    void signalAll();
}

Condition的“等待-通知”方法和Object的“等待-通知”方法的语义等效关系为:

  • Condition类的await()方法和Object类的wait()方法等效。
  • Condition类的signal()方法和Object类的notify()方法等效。
  • Condition类的signalAll()方法和Object类的notifyAll()方法等效。
// 创建一个显式锁
static Lock lock = new ReentrantLock();
//获取一个显式锁绑定的Condition对象
static private Condition condition = lock.newCondition();

//========在第一个线程中,执行下面代码========
lock.lock(); // ①抢锁
try{
    Print.tcfo("我是等待方");
    condition.await(); // ②开始等待,并且释放锁
    Print.tco("收到通知,等待方继续执行");
} catch (InterruptedException e){
	e.printStackTrace();
} finally{
	lock.unlock(); //释放锁
}

//========在第二个线程中,执行下面代码========
lock.lock(); //③抢锁
try{
    Print.tcfo("我是通知方");
    condition.signal(); // ④发送通知,记得立马释放锁,否则被通知的的线程拿不到锁
    Print.tco("发出通知了,但是线程还没有立马释放锁");
} finally{
	lock.unlock(); //⑤释放锁之后,等待线程才能获得所
}

使用ReentrantLock(重入锁)作为显式锁的实现类,然后通过该显式锁去获取一个Condition实例。在调用await()方法前,等待线程必须获得显式锁(如语句①), await()方法会让当前线程加入到Condition对象等待队列中。在调用signal()方法前,通知线程也必须获得相应显式锁(如语句③)。在语句④调用signal()方法后, JUC会从Condition对象等待队列中唤醒一个线程。当等待线程被唤醒后,将会重新尝试获得与Condition对象绑定的显式锁,一旦抢占成功将继续执行。

由于Lock有公平锁和非公平锁之分,而Condition是与Lock绑定的,所以就有与Lock一样的公平特性:如果是公平锁,等待线程为按照FIFO(先进先出)顺序从Condition对象的等待队列中唤醒;如果是非公平锁,那么后续的唤醒次序就不保证FIFO顺序了。

LockSupport

LockSupport是JUC提供的一线程阻塞与唤醒的工具类,该工具类可以让线程在任意位置阻塞和唤醒,其所有的方法都是静态方法,常用三个方法:

// 无限期阻塞当前线程
public static void park();
// 唤醒某个被阻塞的线程
public static void unpark(Thread thread);
// 阻塞当前线程,有超时时间的限制
public static void parkNanos(long nanos);

LockSupport.park()和 Thread.sleep()的区别

  • Thread.sleep()没法从外部唤醒,只能自己醒过来;而被LockSupport.park()方法阻塞的线程可以通过调用LockSupport.unpark()方法去唤醒。
  • 当一个线程被park()阻塞时,调用Thread.interrupt()方法会设置了线程的中断标志,被阻塞线程都会响应线程的中断信号,唤醒线程的执行。 而Thread.sleep(),wait,join方法的阻塞别打断时会抛出InterruptedException异常,并将打断标记置为false。
  • 共同的就是都不会释放所持有的锁。

LockSupport.park()与 Object.wait()的区别

  • Object.wait()方法需要在synchronized块中执行,而LockSupport.park()可以在任意地方执行。
  • 当被阻塞线程中断时, Object.wait()方法抛出了中断异常,调用者需要捕获;当被阻塞线程中断时, LockSupport.park()不会抛出异常,调用时不需要处理中断异常。

显示锁的分类

可重入锁与不可重入锁

可重入锁也被称为递归锁,指的是一个线程可以多次抢占同一个锁。例如,线程A在进入外层函数抢占了一个Lock显式锁之后,当线程A继续进入内层函数时,如果遇到有抢占同一个Lock显式锁的代码,线程A依然可以抢到该Lock显式锁。 JUC的ReentrantLock类是可重入锁的一个标准实现类。

悲观锁和乐观锁

悲观锁就是悲观思想,每次去入临界区操作数据的时候都认为别的线程会修改,所以线程每次在读写数据时都会上锁,锁住同步资源,这样其他线程需要读写这个数据时就会阻塞,一直等到拿到锁。总体来说,悲观锁适用于写多读少的场景,遇到高并发写的可能性高。 Java的Synchronized重量级锁是一种悲观锁。

乐观锁是一种乐观思想,每次去拿数据的时候都认为别的线程不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样就更新),如果失败就要重复读-比较-写的操作。总体来说,乐观锁适用于读多写少的场景,遇到高并发写的可能性低。 Java中的乐观锁基本都是通过CAS自旋操作实现的。 CAS是一种更新原子操作,比较当前值跟传入值是否一样,是则更新,不是则失败。在争用激烈的场景下, CAS自旋会出现大量的空自旋,会导致乐观锁性能大大降低。Java的Synchronized轻量级锁是一种乐观锁。ReentrantLock 都是乐观锁,JUC的显式锁都是基于AQS实现的,而AQS通过对队列的使用很大程度上减少了锁的争用,极大地减少了空的CAS自旋。

悲观锁存在的问题:

  • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
  • 一个线程持有锁后,会导致其他所有抢占此锁的线程挂起。
CAS实现乐观锁

CAS操作可以非常清晰地分为两个步骤:

  • 检测内存位置V的值是否为开始读取的V作为预期原值A。
  • 如果是,说明没人修改V,将位置V更新为新值B;否则不要更改该位置,进入自旋。

非重入CAS自旋锁的实现原理 :

public class SpinLock implements Lock {
    /**
     * 当前锁的拥有者
     * 使用Thread 作为同步状态
     */
    private AtomicReference<Thread> owner = new AtomicReference<>();

    /**
     * 抢占锁
     */
    @Override
    public void lock() {
        Thread t = Thread.currentThread();
        //自旋,如果owner不为null则自旋,为null说明没有占用
        while (!owner.compareAndSet(null, t)) { //为空则设为t
            // DO nothing
            Thread.yield();//让出当前剩余的CPU时间片
        }
    }

    //释放锁
    @Override
    public void unlock() {
        Thread t = Thread.currentThread();
        //只有拥有者才能释放锁
        if (t == owner.get()) {
            // 设置拥有者为空,这里不需要 compareAndSet操作
            // 因为已经通过owner做过线程检查
            owner.set(null); //设为null
        }
    }
	// 省略其他代码
}

以上是不支持重入的,即当一个线程第一次已经获取到了该锁,在锁没有被释放之前,如果又一次重新获取该锁owner不为空。 为了实现可重入锁,这里引入一个计数器,用来记录一个线程获取锁的次数。一个简单的可重入的自旋锁的代码大致如下:

public class ReentrantSpinLock implements Lock {
    /**
     * 当前锁的拥有者
     * 使用拥有者Thread作为同步状态,而不是使用一个简单的整数作为同步状态
     */
    private AtomicReference<Thread> owner = new AtomicReference<>();
    /**
     * 记录一个线程重复获取锁的次数
     * 此变量为同一个线程在操作,没有必要加上volatile保障可见性和有序性
     */
    private int count = 0;

    /**
     * 抢占锁
     */
    @Override
    public void lock() {
        Thread t = Thread.currentThread();
        // 如果是重入,增加重入次数后返回
        if (t == owner.get()) {
            ++count;
            return;
        }
        //自旋
        while (owner.compareAndSet(null, t)) {
            // DO nothing
            Thread.yield(); //让出当前剩余的CPU时间片
        }
    }

    /**
     * 释放锁
     */
    @Override
    public void unlock() {
        Thread t = Thread.currentThread();
        //只有拥有者才能释放锁
        if (t == owner.get()) {
            if (count > 0) {
                // 如果重入的次数大于0,减少重入次数后返回
                --count;
            } else {
                // 设置拥有者为空
                //这里不需要compareAndSet,因为已经通过owner做过线程检查
                owner.set(null);
            }
        }
    }
// 省略其他代码
}

自旋锁的特点: 线程获取锁的时候,如果锁被其他线程持有,当前线程将循环等待,直到获取到锁。线程抢锁期间状态不会改变,一直是运行状态(RUNNABLE),在操作系统层面线程处于用户态。自旋锁的问题: 在争用激烈的场景下,如果某个线程持有锁的时间太长,就会导致其他空自旋的线程耗尽CPU资源。另外,如果大量的线程进行空自旋,还可能导致硬件层面的“总线风暴”。总线风暴当然与CPU的架构和设计有关,并不是所有的CPU都会产生总线风暴。JUC基于CAS实现的轻量级锁如何避免总线风暴呢?答案是:使用队列对抢锁线性进行排队,最大程度上减少了CAS操作数量。

CLH自旋锁

CLH锁其实就是一种是基于队列(具体为单向链表)排队的自旋锁,抢锁线程在队列尾部加入一个节点,然后仅在前驱节点上做普通自旋,它不断轮询前一个节点状态,如果发现前一个节点释放锁,当前节点抢锁成功。 由于CLH锁只有在节点入队时进行一下CAS的操作,在节点在加入队列之后,抢锁线程不需要进行CAS自旋,只需普通自旋即可。因此,在争用激烈的场景下, CLH锁能大大减少的CAS操作的数量,以避免CPU的总线风暴。

在这里插入图片描述

请注意:CAS自旋使用到了原子操作与禁止指令重排机制,而这些操作会要求缓存一致性。如果缓存一致性流量过大会导致总线风暴。而普通自旋仅仅是判断前面节点是否是Empty状态—未锁。

  • CLHLock的尾指针tail总是指向最后一个线程的节点。
  • CLHLock队列中的抢锁线程一直进行普通自旋,循环判断前一线程的locked状态,如果是true,那么说明前一线程处于自旋等待状态或正在执行临界区代码,所以自己需要自旋等待。

释放过程:

线程B执行抢到锁并且完成临界区代码的执行后,开始unlock(释放)操作,设置其nodeB的前驱引用为null,锁状态locked为false,具体如图5-6所示。线程B释放锁之后, nodeA对象已经没有任何的强引用,可以被GC回收了。
在这里插入图片描述

公平锁和非公平锁

公平锁是指不同的线程抢占锁的机会是公平的、平等的,从抢占时间上来说,先对锁进行抢占的线程一定被先满足,抢锁成功的次序体现为FIFO(先进先出)顺序。简单来说,公平锁就是保障了各个线程获取锁都是按照顺序来的,先到的线程先获取锁。非公平锁是指不同的线程抢占锁的机会是非公平的、不平等的,从抢占时间上来说,先对锁进行抢占的线程不一定被先满足,抢锁成功的次序不会体现为FIFO(先进先出)顺序。默认情况下, ReentrantLock实例是非公平锁,但是,如果在实例构造时传入了参数true,所得到的锁就是公平锁。非公平锁的优点在于吞吐量比公平锁大,其缺点是有可能会导致线程优先级反转或者线程饥饿现象。不公平锁的含义是阻塞队列内公平,队列外非公平。

基本使用

构造方法:ReentrantLock lock = new ReentrantLock(true)

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantLock 默认是不公平的:

public ReentrantLock() {
    sync = new NonfairSync();
}

说明:公平锁一般没有必要,会降低并发度

可中断锁与不可中断锁

什么是可中断锁? 如果某一线程A正占有锁在执行临界区代码,另一线程B正在阻塞式抢占锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己的阻塞等待,这种就是可中断锁。什么是不可中断锁? 一旦这个锁被其他线程占有,如果自己还想抢占,自己只能选择等待或者阻塞,直到别的线程释放这个锁,如果别的线程永远不释放锁,那么自己只能永远等下去,并且没有办法终止等待或阻塞。

简单来说,在抢锁过程中能通过某些方法去终止抢占过程,这就是可中断锁,否则就是不可中断锁。 Java的synchronized内置锁就是一个不可中断锁,而JUC的显式锁(如ReentrantLock)是一个可中断锁。

lockInterruptibly()

public void lockInterruptibly():获得可打断的锁,thread.interrupt();可用来打断。如果抢占过程收到由Thread.interrupt()方法发出的线程中断信号, lockInterruptibly()方法会抛出InterruptedException。

  • 如果没有竞争此方法就会获取 lock 对象锁
  • 如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 打断

注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待状态中的线程中断

public static void main(String[] args) throws InterruptedException {    
    ReentrantLock lock = new ReentrantLock();    
    Thread t1 = new Thread(() -> {        
        try {            
            System.out.println("尝试获取锁");            
            lock.lockInterruptibly(); // 拿不到锁,在此等待,可被中断
        } catch (InterruptedException e) {            
            System.out.println("没有获取到锁,被打断,直接返回");            
            return;        
        }        
        try {            
            System.out.println("获取到锁");        
        } finally {            
            lock.unlock();        
        }    
    }, "t1");    
    lock.lock();   //锁被拿了 
    t1.start();    
    Thread.sleep(2000);    
    System.out.println("主线程进行打断锁");    
    t1.interrupt();
}
tryLock()

tryLock(long timeout, TimeUnit unit)

阻塞式“限时抢占”(在timeout时间内)锁抢占过程中会处理Thread.interrupt()中断信号,如果线程被中断,就会终止抢占并抛出InterruptedException异常。timeout之外自动抢锁终止。

独占锁和共享锁

独占锁指的是每次只有一个线程能持有的锁。独占锁是一种悲观保守的加锁策略。JUC的ReentrantLock类是一个标准的独占锁实现类。共享锁允许多个线程同时获取锁,容许线程并发进入临界区。与独占锁不同,共享锁是一种乐观锁,允许多个执行读操作的线程同时访问共享资源。

JUC的ReentrantReadWriteLock(读写锁)类是一个共享锁实现类。使用该读写锁时,读操作可以有很多线程一起读,但是写操作只能有一个线程去写,而且在写入的时候,别的线程也不能进行读的操作。因为多个读操作并没有线程安全问题,所以在读的地方使用读锁,在写的地方使用写锁,可以提高程序执行效率。

JUC中的共享锁包括Semaphore(信号量)、 ReadLock(读写锁)中的读锁、 CountDownLatch倒数闩。

共享锁 Semaphore ()

synchronized 可以起到锁的作用,但某个时间段内,只能有一个线程允许执行

Semaphore(信号量)用来限制能同时访问共享资源的线程上限,非重入锁

构造方法:

  • public Semaphore(int permits):permits 表示许可线程的数量(state)
  • public Semaphore(int permits, boolean fair):fair 表示公平性,如果设为 true,下次执行的线程会是等待最久的线程

常用API:

  • public void acquire():表示获取许可
  • public void release():表示释放许可,acquire() 和 release() 方法之间的代码为同步代码
public static void main(String[] args) {
    // 1.创建Semaphore对象
    Semaphore semaphore = new Semaphore(3);

    // 2. 10个线程同时运行
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            try {
                // 3. 获取许可
                semaphore.acquire();
                sout(Thread.currentThread().getName() + " running...");
                Thread.sleep(1000);
                sout(Thread.currentThread().getName() + " end...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 4. 释放许可
                semaphore.release();
            }
        }).start();
    }
}

Semaphore 的 permits(state)为 3,这时 5 个线程来获取资源。假设其中 Thread-1,Thread-2,Thread-4 CAS 竞争成功,permits 变为 0,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列park 阻塞

倒数栓 CountDown

CountDownLatch:计数器,用来进行线程同步协作,等待所有线程完成。CountDownLatch可以指定一个计数值,在并发环境下由线程进行减1操作,当计数值变为0之后,被await方法阻塞的线程将会唤醒。通过CountDownLatch可以实现线程间的计数同步。

构造器:

  • public CountDownLatch(int count):初始化唤醒需要的 down 几步

常用API:

  • public void await() :让当前线程等待,必须 down 完初始化的数字才可以被唤醒,否则进入无限等待
  • public void countDown():计数器进行减 1(down 1)

应用:同步等待多个 Rest 远程调用结束

// LOL 10人进入游戏倒计时
public static void main(String[] args) throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(10);
    ExecutorService service = Executors.newFixedThreadPool(10);
    String[] all = new String[10];
    Random random = new Random();

    for (int j = 0; j < 10; j++) {
        int finalJ = j;//常量
        service.submit(() -> {
            for (int i = 0; i <= 100; i++) {
                Thread.sleep(random.nextInt(100));	//随机休眠
                all[finalJ] = i + "%";
                System.out.print("\r" + Arrays.toString(all));	// \r代表覆盖
            }
            latch.countDown();
        });
    }
    latch.await();
    System.out.println("\n游戏开始");
    service.shutdown();
}
/*
[100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%]
游戏开始
CyclicBarrier

CyclicBarrier字面意思是“可重复使用的栅栏”,CyclicBarrier 相比 CountDownLatch 来说,非常像但是要简单很多,其源码没有什么高深的地方,它是 ReentrantLock 和 Condition 的组合使用。

看如下示意图,CyclicBarrier 和 CountDownLatch 是不是很像,只是 CyclicBarrier 可以有不止一个栅栏,因为它的栅栏(Barrier)可以重复使用(Cyclic)。

在这里插入图片描述

读写锁

读写锁的内部包含了两把锁:一把是为读(操作)锁,是一种共享锁;另一把写(操作)锁,是一种独占锁。在没有写锁的时候,读锁可以被多个线程同时持有。写锁是排他性的:如果写锁被一个线程持有,其他的线程不能再持有写锁,抢占写锁会阻塞;进一步来说,如果写锁被一个线程持有,其他的线程不能再持有读锁,抢占读锁也会阻塞。

JUC包中的读写锁接口为ReadWriteLock,主要有两个方法,具体如下:

public interface ReadWriteLock {
    /**
    * 返回读锁
    */
    Lock readLock();
    /**
    * 返回写锁
    */
    Lock writeLock();
}

通过ReadWriteLock接口能获取其内部的两把锁:一把ReadLock,负责读操作;另一把是WriteLock,负责写操作。 JUC中ReadWriteLock接口实现类为ReentrantReadWriteLock。 其读锁是可以多线程共享的共享锁,而其写锁是排他锁,在被占时候不允许其他线程再抢占操作。

public ReentrantReadWriteLock.ReadLock readLock()`:返回读锁
public ReentrantReadWriteLock.WriteLock writeLock()`:返回写锁
public void lock()`:加锁
public void unlock()`:解锁
public boolean tryLock()`:尝试获取锁
public class ReadWriteLockTest{
    //创建一个Map,代表共享数据
    final static Map<String, String> MAP = new HashMap<String, String>();
    //创建一个读写锁
    final static ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
    //获取读锁
    final static Lock READ_LOCK = LOCK.readLock();
    //获取写锁
    final static Lock WRITE_LOCK = LOCK.writeLock();
    //对共享数据的写操作
    public static Object put(String key, String value){
        WRITE_LOCK.lock(); //抢写锁
        try{
            Print.tco(DateUtil.getNowTime()+" 抢占了WRITE_LOCK,开始执行write操作");
            Thread.sleep(1000);
            String put = MAP.put(key, value); //写入共享数据
            return put;
        } catch (Exception e){
            e.printStackTrace();
        } finally{
            WRITE_LOCK.unlock(); //释放写锁
        }
        return null;
    }
    //对共享数据的读操作
    public static Object get(String key){
        READ_LOCK.lock(); //抢占读锁
        try{
            Print.tco(DateUtil.getNowTime()+" 抢占了READ_LOCK,开始执行read操作");
            Thread.sleep(1000);
            String value = MAP.get(key); //读取共享数据
            return value;
        } catch (InterruptedException e){
            e.printStackTrace();
        } finally{
            READ_LOCK.unlock(); //释放读锁
        }
        return null;
    }
    //入口方法
    public static void main(String[] args){
        //创建Runnable异步可执行目标实例
        Runnable writeTarget = () -> put("key", "value");
        Runnable readTarget = () -> get("key");
        //创建4个读线程
        for (int i = 0; i < 4; i++){
            new Thread(readTarget, "读线程" + i).start();
        }
        //创建2个写线程,并启动
        for (int i = 0; i < 2; i++){
            new Thread(writeTarget, "写线程" + i).start();
        }
    }
}

锁升级是指读锁升级为写锁,锁降级指的是写锁降级为读锁。在ReentrantReadWriteLock读写锁中,只支持写锁降级为读锁,而不支持读锁升级为写锁。 ReentrantReadWriteLock不支持读锁的升级,主要是避免死锁,例如两个线程A和B都占了读锁并且都需要升级成写锁, A升级要求B释放读锁, B升级要求A释放读锁,二者就会由于互相等待形成死锁。 与ReentrantLock相比, ReentrantReadWriteLock更适合于读多写少的场景,可以提高并发读的效率;而ReentrantLock更适合于读写比例相差不大或写比读多的场景。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/568971.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Apache Kafka - ConsumerInterceptor 实战 (1)

文章目录 概述使用场景实战配置文件配置类自定义ConSumerInterceptor使用 概述 ConsumerInterceptor是Kafka中的一个重要组件&#xff0c;它允许开发人员在Kafka消费者端拦截和修改消息的处理过程。ConsumerInterceptor可以用于实现各种功能&#xff0c;从消息监控到数据转换和…

【Python开发】FastAPI 01:hello world

FastAPI 是一个轻量级的后端框架&#xff0c;对于定制化不高或者功能简单的业务完全可以拿他作为后台。 FastAPI 一个比较重要的特性就是异步&#xff0c;简单来说就是相比 django 和 flask 快&#xff0c;FastAPI 和 flask 的语法类似。本篇文章介绍 FastAPI 运用的简单实例&a…

从索引结点出发探索软、硬链接

索引结点的初步认识 对于整个计算机系统的资源管理&#xff0c;我们可以认为&#xff0c;OS先将这些资源的数据信息&#xff0c;给描述起来构成一个部分&#xff0c;然后再将它们组织起来&#xff0c;就能够实现由OS集中管理。举一个最经典的例子&#xff0c;进程的引入是为了…

Cos上传(腾讯云):图片存储方案

Cos上传(腾讯云) 01.图片存储方案介绍 目标 了解主流的图片存储方案 两种常见方案 方案一&#xff1a;存到自己公司购买的服务器上 优点&#xff1a;好控制 缺点&#xff1a;成本高由于图片都存放到自己的服务器上&#xff0c;占据空间很大 方案二&#xff1a;存到三方…

总结SpringBoot常用读取配置文件的3种方法

文章目录 1、使用 Value 读取配置文件2、 使用 ConfigurationProperties 读取配置文件3、读取配置文件中的List 1、使用 Value 读取配置文件 注&#xff1a;这种方法适用于对象的参数比较少的情况 使用方法&#xff1a; 在类上添加configuration注解&#xff0c;将这个配置对…

Hotbit交易平台停运,百万用户待清退,币圈危机再度蔓延

“币圈”的危机似乎还没有走到尽头。5月22日&#xff0c;加密货币交易平台Hotbit发文宣布&#xff0c;决定从世界标准时间当日4:00停止所有CEX&#xff08;中心化交易所&#xff09;操作&#xff0c;希望所有用户在6月21日4:00之前提取剩余资产。据悉&#xff0c;该平台在其任期…

微前端乾坤

1. 乾坤 简介 qiankun 是一个基于 single-spa 的微前端实现库&#xff0c;旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统 官网&#xff1a;https://qiankun.umijs.org/zh/guide 2.使用 背景&#xff1a; vue2.0 , vue-cli 5.0 主应用&#xff1a; 安装乾坤…

基于上下文折扣的多模态医学图像分割证据融合

文章目录 Evidence Fusion with Contextual Discounting for Multi-modality Medical Image Segmentation摘要本文方法Evidential SegmentationMulti-modality Evidence FusionDiscounted Dice Loss 实验结果 Evidence Fusion with Contextual Discounting for Multi-modality …

利用PaddleOCR识别增值税发票平台验证码(开箱即用)

前言:全国增值税发票查验平台验证码没什么好说的,根据指定的颜色识别验证码中的文字,图片如下 下面直接讲解利用paddleocr识别的思路,为什么使用paddleocr,因为paddle中集成了较好的ocr文字识别模型,开箱即用即可,废话不多说,剑指主题,识别思路步骤如下 步骤如下 1、…

BI技巧丨度量值的动态格式字符串

2023年4月版本新增了度量值的动态格式字符串功能。从字面上看可能小伙伴们不是很理解这个功能的用途&#xff0c;这里白茶给大家解释一下。 通俗一点来说&#xff0c;就是可以在数值中加入文本&#xff0c;将其转化为字符串&#xff0c;而不改变其原有的数据类型。 看到这里&…

Java调用第三方库JNA(C/C++)

GitHub - java-native-access/jna: Java Native Access 源代码 在Java 中使用C语言库的传统做法是使用JNI编程。但是现在有更好的替代方案&#xff0c;即JNA(Java Native Access)&#xff1b;JNA是一个开源的Java框架,是SUN公司推出的调用本地库方法的技术&#xff0c;是建立在…

传染病学模型 | Matlab实现SEIRS传染病学模型 (SEIRS Epidemic Model)

文章目录 效果一览基本描述模型介绍程序设计参考资料效果一览 基本描述 传染病学模型 | Matlab实现SEIRS传染病学模型 (SEIRS Epidemic Model) 模型介绍 SEIRS是一种基于计算机模拟的传染病学模型,用于研究人群中传染病的传播和控制。与其他传染病学模型不同,SEIRS模型考虑了…

第二章.­ Learning to Answer Yes­_No

第二章. Learning to Answer Yes_No 2.1 Perceptron Hypothesis Set 1.机器学习流程图&#xff1a; 在机器学习的整个流程中&#xff0c;模型的选择(Hypothesis Set)是非常重要的&#xff0c;它决定了机器学习的最终效果。 2.常用的机器学习模型——感知机&#xff08;Percep…

L2-001 紧急救援(dijkstra算法练习)

作为一个城市的应急救援队伍的负责人&#xff0c;你有一张特殊的全国地图。在地图上显示有多个分散的城市和一些连接城市的快速道路。每个城市的救援队数量和每一条连接两个城市的快速道路长度都标在地图上。当其他城市有紧急求助电话给你的时候&#xff0c;你的任务是带领你的…

Android 12系统源码_窗口管理(二)WindowManager对窗口的管理过程

前言 上一篇我们具体分析了窗口管理者WindowManagerService的启动流程&#xff0c;对于WindowManagerService有了一个初步的认识。在此基础上&#xff0c;我本打算应该进一步分析WindowManagerService是如何管理系统中的各种窗口的&#xff0c;然而由于Android系统的架构设计&…

如何搭建远程服务器-(cpolar)

文章目录 前言一、安装注册下载安装包认证开通指定端口监听开机自启动设置 二、使用步骤电脑端远程手机端远程 三、卸载软件安装说明&#xff1a; 总结 前言 之前已经有写到一篇文章《如何用树莓派搭建远程服务器 (zerotier)》&#xff0c;对此已经使用了很长一段时间。 优点…

MySQL 事务(w字)

目录 事务 首先我们来看一个简单的问题 什么是事务 为什么会出现事务 事务的版本支持 事务提交方式 事务常见操作方式 设置隔离级别 事物操作 事物结论 事务隔离级别 理解隔离性 隔离级别 查看与设置隔离性 注意可重复读【Repeatable Read】的可能问题&#xff…

AI数字人盛行,如何选择合适的AI数字人制作平台?

2023万象大会已然开启了直播&#xff0c;当AI照进生活、照亮你我&#xff0c;为我们的想象力插上翅膀&#xff0c;世界变得更加便捷、更加智能。可以说近年来&#xff0c;AI帮助人们解决了各种问题&#xff0c;在提高生产效率、改善生活质量等方面做出来很大的贡献&#xff0c;…

LeetCode: 二叉树的直径(java)

二叉树的直径 leetcode 543题。原题链接题目描述解题代码二叉树专题 leetcode 543题。原题链接 543题&#xff1a;二叉树的直径 题目描述 给你一棵二叉树的根节点&#xff0c;返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也…

WICC · 出海嘉年华|嘉宾就位、话题揭晓,峰会 派对报名倒计时

双厨狂喜&#xff01;移步【融云全球互联网通信云】回复“地图”免费领 6 月 2 日即将在广州举办的“WICC 社交泛娱乐出海嘉年华”&#xff0c;将是一场集 WICC 通信行业大会高端峰会规格、前沿技术内容和社交泛娱乐出海务实场景落地、垂直圈子社交于一体的大型盛会。 大咖嘉…