JUC并发工具类--锁(Lock类)的使用

本文详细介绍了Java并发编程中Lock接口的使用,包括ReentrantLock的独占锁、公平锁与非公平锁、可重入性、Condition实现生产者消费者模式。此外,还探讨了读写锁ReentrantReadWriteLock的特性与使用,以及锁降级的概念。最后,文章提到了StampLock的三种访问模式及其在缓存中的应用,强调了其在读多写少场景下的优势。
摘要由CSDN通过智能技术生成

Lock:定义了锁功能的规范

API

Lock类提供的API

API功能描述
void lock()获取锁,调用该方法当前线程会获取锁,当锁获得后,该方法返回
void lockInterruptibly() throws InterruptedException可中断的获取锁,和lock()方法不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程
boolean tryLock()尝试非阻塞的获取锁,调用该方法后立即返回。如果能够获取到返回true,否则返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException超时获取锁,当前线程在以下三种情况下会被返回:
1:当前线程在超时时间内获取了锁
2:当前线程在超时时间内被中断
3:超时时间结束,返回false
void unlock()释放锁
Condition newCondition()获取等待通知组件,该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将释放锁

基础语法

//加锁  阻塞 
lock.lock(); 
try {  
    ...
} finally { 
    // 解锁 
    lock.unlock();  
}


//尝试加锁   非阻塞
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        ...
    } finally {
        lock.unlock();
    }
}

注意事项

  1. 加锁次数和释放锁次数一定要保持一致,否则会导致线程阻塞或程序异常;
  2. 加锁操作一定要放在 try 代码之前,这样可以避免未加锁成功又释放锁的异常;
  3. 释放锁一定要放在 finally 中,否则会导致线程阻塞。

ReentrantLock

ReentrantLock是一种可重入的独占锁,它允许同一个线程多次获取同一个锁而不会被阻塞。

它的功能类似于synchronized是一种互斥锁,可以保证线程安全。相对于 synchronized, ReentrantLock具备如下特点:

  1. 可中断
  2. 可以设置超时时间
  3. 可以设置为公平锁
  4. 支持多个条件变量
  5. 与 synchronized 一样,都支持可重入

它的主要应用场景是在多线程环境下对共享资源进行独占式访问,以保证数据的一致性和安全性。

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

示例

独占锁:模拟抢票场景

import java.util.concurrent.locks.ReentrantLock;

/**
 * 模拟抢票场景
 */
public class ReentrantLockDemo {

    /**
     * 默认非公平
     */
    private final ReentrantLock lock = new ReentrantLock();

    /**
     * 总票数
     */
    private static int tickets = 8;

    public void buyTicket() {
        // 获取锁
        lock.lock();
        try {
            if (tickets > 0) {
                // 还有票    读
                try {
                    // 休眠10ms
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //写
                System.out.println(Thread.currentThread().getName() + "购买了第" + tickets-- + "张票");
                
            } else {
                System.out.println("票已经卖完了," + Thread.currentThread().getName() + "抢票失败");
            }

        } finally {
            // 释放锁
            lock.unlock();
        }
    }


    public static void main(String[] args) {
        ReentrantLockDemo ticketSystem = new ReentrantLockDemo();
        for (int i = 1; i <= 10; i++) {
            Thread thread = new Thread(() -> {
                ticketSystem.buyTicket(); // 抢票

            }, "线程" + i);
            // 启动线程
            thread.start();
        }


        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("剩余票数:" + tickets);
    }
}

模拟抢票场景执行结果

公平锁和非公平锁

ReentrantLock支持公平锁和非公平锁两种模式:

  • 公平锁:线程在获取锁时,按照等待的先后顺序获取锁。
  • 非公平锁:线程在获取锁时,不按照等待的先后顺序获取锁,而是随机获取锁。ReentrantLock默认是非公平锁
    /**
     * 数默认false,不公平锁
     */
    private final ReentrantLock lock = new ReentrantLock();
    /**
     * 公平锁
     */
    private final ReentrantLock lock = new ReentrantLock(Boolean.TRUE);

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。在实际开发中,可重入锁常常应用于递归操作、调用同一个类中的其他方法、锁嵌套等场景中。

import java.util.concurrent.locks.ReentrantLock;

/**
 * 模拟抢票场景
 */
public class ReentrantLockDemo {

    /**
     * 数默认false,不公平锁
     */
    private final ReentrantLock lock = new ReentrantLock();

    /**
     * 总票数
     */
    private static int tickets = 8;

    public void buyTicket() {
        // 获取锁
        lock.lock();
        try {
            if (tickets > 0) {
                // 还有票    读
                try {
                    // 休眠10ms
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //写
                System.out.println(Thread.currentThread().getName() + "购买了第" + tickets-- + "张票");
                //可重入
                buyTicket();
            } else {
                System.out.println("票已经卖完了," + Thread.currentThread().getName() + "抢票失败");
            }

        } finally {
            // 释放锁
            lock.unlock();
        }
    }


    public static void main(String[] args) {
        ReentrantLockDemo ticketSystem = new ReentrantLockDemo();
        for (int i = 1; i <= 10; i++) {
            Thread thread = new Thread(() -> {
                ticketSystem.buyTicket(); // 抢票

            }, "线程" + i);
            // 启动线程
            thread.start();
        }


        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("剩余票数:" + tickets);
    }
}

可重入锁抢票场景执行结果

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo2 {

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter(); // 创建计数器对象

        // 测试递归调用
        counter.recursiveCall(10);
    }


}

class Counter {
    private final ReentrantLock lock = new ReentrantLock(); // 创建 ReentrantLock 对象
    private volatile int count = 0; // 计数器


    public void recursiveCall(int num) {
        lock.lock(); // 获取锁
        try {
            if (num == 0) {
                return;
            }
            System.out.println("执行递归,num = " + num);
            recursiveCall(num - 1);
        } finally {
            lock.unlock(); // 释放锁
        }
    }
}

递归调用示例代码执行结果

结合Condition实现生产者消费者模式

java.util.concurrent类库中提供Condition类来实现线程之间的协调。调用Condition.await() 方法使线程等待,其他线程调用Condition.signal() 或 Condition.signalAll() 方法唤醒等待的线程。

注意:调用Condition的await()和signal()方法,都必须在lock保护之内。

案例:基于ReentrantLock和Condition实现一个简单队列

import java.util.Random;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 基于ReentrantLock和Condition实现一个简单队列
 */
public class ReentrantLockDemo3 {

    public static void main(String[] args) {
        // 创建队列
        Queue queue = new Queue(5);
        //启动生产者线程
        new Thread(new Producer(queue)).start();
        //启动消费者线程
        new Thread(new Customer(queue)).start();

    }
}

/**
 * 队列封装类
 */
class Queue {
    private Object[] items;
    int size = 0;
    int takeIndex;
    int putIndex;
    private ReentrantLock lock = new ReentrantLock();
    //消费者线程阻塞唤醒条件,队列为空阻塞,生产者生产完唤醒
    public Condition notEmpty = lock.newCondition();
    //生产者线程阻塞唤醒条件,队列满了阻塞,消费者消费完唤醒
    public Condition notFull = lock.newCondition();

    public Queue(int capacity) {
        this.items = new Object[capacity];
    }


    public void put(Object value) throws Exception {
        //加锁
        lock.lock();
        try {
            while (size == items.length)
                // 队列满了让生产者等待
                notFull.await();

            items[putIndex] = value;
            if (++putIndex == items.length)
                putIndex = 0;
            size++;
            // 生产完唤醒消费者
            notEmpty.signal();

        } finally {
            System.out.println("producer生产:" + value);
            //解锁
            lock.unlock();
        }
    }

    public Object take() throws Exception {
        lock.lock();
        try {
            // 队列空了就让消费者等待
            while (size == 0)
                notEmpty.await();

            Object value = items[takeIndex];
            items[takeIndex] = null;
            if (++takeIndex == items.length)
                takeIndex = 0;
            size--;
            //消费完唤醒生产者生产
            notFull.signal();
            return value;
        } finally {
            lock.unlock();
        }
    }
}

/**
 * 生产者
 */
class Producer implements Runnable {

    private Queue queue;

    public Producer(Queue queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            // 隔1秒轮询生产一次
            while (true) {
                Thread.sleep(1000);
                queue.put(new Random().nextInt(1000));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

/**
 * 消费者
 */
class Customer implements Runnable {

    private Queue queue;

    public Customer(Queue queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            // 隔2秒轮询消费一次
            while (true) {
                Thread.sleep(2000);
                System.out.println("consumer消费:" + queue.take());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

示例代码执行结果

应用场景总结

  • 解决多线程竞争资源的问题。
    • 例如:多个线程通过修改一个数据,可以保证每次只会有一个线程去操作
  • 实现多线程任务的顺序执行
    • 例如:在一个线程执行完某个任务后,再让另一个线程执行任务。
  • 实现多线程等待/通知机制
    • 例如:在某个线程执行完某个任务后,通知其他线程继续执行任务。

ReentrantReadWriteLock

什么是读写锁?

一把锁分为读与写两部分,读锁允许多个线程同时获得,因为读操作本身是线程安全的。而写锁是互斥锁,不允许多个线程同时获得写锁。并且读与写操作也是互斥的。读写锁适合多读少写的业务场景。

ReentrantReadWriteLock介绍

读写锁ReentrantReadWriteLock内部,维护了一对相关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁,描述如下:

  • 线程进入读锁的前提条件:

    • 没有其他线程的写锁
    • 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
  • 线程进入写锁的前提条件:

    • 没有其他线程的读锁
    • 没有其他线程的写锁

读写锁的三个重要的特性:

  • 公平选择性
    • 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
  • 可重入
    • 读锁和写锁都支持线程重入。以读写线程为例:读线程获取读锁后,能够再次获取读锁。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。
  • 锁降级
    • 遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读锁。

读写锁的使用

ReadWriteLock接口

读写锁的顶级接口。两个方法,分别获得读锁和写锁 Lock 对象。

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

ReentrantReadWriteLock类结构

ReentrantReadWriteLock是可重入的读写锁实现类。在它内部,维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 Writer 线程,读锁可以由多个 Reader 线程同时持有。也就是说,写锁是独占的,读锁是共享的。

    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    /** Performs all synchronization mechanics */
    final Sync sync;

    /**
     * Creates a new {@code ReentrantReadWriteLock} with
     * default (nonfair) ordering properties.
     */
    public ReentrantReadWriteLock() {
        this(false);
    }

    /**
     * Creates a new {@code ReentrantReadWriteLock} with
     * the given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

使用示例

//使用读写锁
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();

// 读操作上读锁
public Data get(String key) {
  readLock .lock();
  try { 
      // TODO 业务逻辑
  }finally { 
       readLock .unlock(); 
   }
}

// 写操作上写锁
public Data put(String key, Data value) {
  writeLock.lock();
  try { 
      // TODO 业务逻辑
  }finally { 
       writeLock.unlock(); 
   }
}

注意事项

  • 读锁不支持条件变量
  • 重入时不支持升级:持有读锁的情况下去获取写锁,会导致获取永久等待
  • 重入时支持降级: 持有写锁的情况下可以去获取读锁

应用场景

读多写少

ReentrantReadWriteLock适用于读操作比写操作频繁的场景,因为它允许多个读线程同时访问共享数据,而写操作是独占的。

缓存

ReentrantReadWriteLock可以用于实现缓存,因为它可以有效地处理大量的读操作,同时保护缓存数据的一致性。

示例代码
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Cache {
    static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();

    // 获取一个key对应的value
    public static final Object get(String key) {
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }

    // 设置key对应的value,并返回旧的value
    public static final Object put(String key, Object value) {
        w.lock();
        try {
            return map.put(key, value);
        } finally {
            w.unlock();
        }
    }

    // 清空所有的内容
    public static final void clear() {
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }
    
}

锁降级

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。

锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

锁降级可以帮助我们拿到当前线程修改后的结果而不被其他线程所破坏,防止更新丢失。

锁降级的使用示例

因为数据不常变化,所以多个线程可以并发地进行数据处理,当数据变更后,如果当前线程感知到数据变化,则进行数据的准备工作,同时其他处理线程被阻塞,直到当前线程完成数据的准备工作。

private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock readLock= rwl.readLock();
private final Lock writeLock= rwl.writeLock();
private volatile boolean update = false;

public void processData() {
    readLock.lock();
    if (!update) {
        // 必须先释放读锁
        readLock.unlock();
        // 锁降级从写锁获取到开始
        writeLock.lock();
        try {
            if (!update) {
                // TODO 准备数据的流程(略)  
                update = true;
            }
            readLock.lock();
        } finally {
            writeLock.unlock();
        }
        // 锁降级完成,写锁降级为读锁
    }
    try {
        //TODO  使用数据的流程(略)
    } finally {
        readLock.unlock();
    }
}

锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。

读写锁设计思路

读写锁设计思路

读写状态的设计

设计的精髓:用一个变量如何维护多种状态.

在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。

分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过位运算。假如当前同步状态为S,那么:

  • 写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1.
  • 读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于S+(1<<16)

根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。
读写状态的设计

代码的实现
	/*
	 * Read vs write count extraction constants and functions.
	 * Lock state is logically divided into two unsigned shorts:
	 * The lower one representing the exclusive (writer) lock hold count,
	 * and the upper the shared (reader) hold count.
	 */
	
	static final int SHARED_SHIFT   = 16;
	static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
	static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
	static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
	
	/** Returns the number of shared holds represented in count  */
	//  静态方法,获得持有读状态的锁的数量。
	//  不同于写锁,读锁可以同时被多个线程持有。
	//  而每个线程持有的读锁支持重入的特性,所以需要对每个线程持有的读锁的数量单独计数,这就需要用到 HoldCounter 计数器
	static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
	/** Returns the number of exclusive holds represented in count  */
	// 静态方法,获得持有写状态的锁的次数。
	static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
HoldCounter 计数器

读锁的内在机制其实就是一个共享锁。一次共享锁的操作就相当于对HoldCounter 计数器的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。

	static final class HoldCounter {
	    int count = 0;
	    // Use id, not reference, to avoid garbage retention
	    final long tid = getThreadId(Thread.currentThread());
	}
	
	/**
	 * ThreadLocal subclass. Easiest to explicitly define for sake
	 * of deserialization mechanics.
	 */
	static final class ThreadLocalHoldCounter
	    extends ThreadLocal<HoldCounter> {
	    public HoldCounter initialValue() {
	        return new HoldCounter();
	    }
	}

通过 ThreadLocalHoldCounter 类,HoldCounter 与线程进行绑定。HoldCounter 是绑定线程的一个计数器,而 ThreadLocalHoldCounter 则是线程绑定的 ThreadLocal。

  • HoldCounter是用来记录读锁重入数的对象
  • ThreadLocalHoldCounter是ThreadLocal变量,用来存放不是第一个获取读锁的线程的其他线程的读锁重入数对象

StampLock

ReentrantReadWriteLock有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。

为了进一步提升并发执行效率,Java 8引入了新的读写锁:StampedLock。

StampedLock和ReentrantReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!在原先读写锁的基础上新增了一种叫乐观读(Optimistic Reading)的模式。该模式并不会加锁,所以不会阻塞线程,会有更高的吞吐量和更高的性能。

它的设计初衷是作为一个内部工具类,用于开发其他线程安全的组件,提升系统性能,并且编程模型也比ReentrantReadWriteLock 复杂,所以用不好就很容易出现死锁或者线程安全等莫名其妙的问题。

StampLock的使用

StampLock三种访问模式

  • Writing(独占写锁)

    • writeLock 方法会使线程阻塞等待独占访问,可类比ReentrantReadWriteLock 的写锁模式,同一时刻有且只有一个写线程获取锁资源。
  • Reading(悲观读锁)

    • readLock方法,允许多个线程同时获取悲观读锁,悲观读锁与独占写锁互斥,与乐观读共享。
  • Optimistic Reading(乐观读)

    • 乐观读并没有加锁,也就是不会有 CAS 机制并且没有阻塞线程。仅当当前未处于 Writing 模式 tryOptimisticRead 才会返回非 0 的邮戳(Stamp),如果在获取乐观读之后没有出现写模式线程获取锁,则在方法validate返回 true ,允许多个线程获取乐观读以及读锁,同时允许一个写线程获取写锁。

在使用乐观读的时候一定要按照固定模板编写,否则很容易出 bug,我们总结下乐观读编程模型的模板:

public void optimisticRead() {
    // 1. 非阻塞乐观读模式获取版本信息
    long stamp = lock.tryOptimisticRead();
    // 2. 拷贝共享数据到线程本地栈中
    copyVaraibale2ThreadMemory();
    // 3. 校验乐观读模式读取的数据是否被修改过
    if (!lock.validate(stamp)) {
        // 3.1 校验未通过,上读锁
        stamp = lock.readLock();
        try {
            // 3.2 拷贝共享变量数据到局部变量
            copyVaraibale2ThreadMemory();
        } finally {
            // 释放读锁
            lock.unlockRead(stamp);
        }
    }
    // 3.3 校验通过,使用线程本地栈的数据进行逻辑操作
    useThreadMemoryVarables();
}

在缓存中的应用

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.locks.StampedLock;

/**
 * @author Fox
 */
public class CacheStampedLock {
    /**
     * 共享变量数据
     */
    private final Map<Integer, String> idMap = new HashMap<>();
    private final StampedLock lock = new StampedLock();


    /**
     * 添加数据,独占模式
     */
    public void put(Integer key, String value) {
        long stamp = lock.writeLock();
        try {
            idMap.put(key, value);
        } finally {
            lock.unlockWrite(stamp);
        }
    }

    /**
     * 读取数据,只读方法
     * 使用了乐观读,使得读写可以并发执行
     */
    public String get(Integer key) {
        // 1. 尝试通过乐观读模式读取数据,非阻塞
        long stamp = lock.tryOptimisticRead();
        // 2. 读取数据到当前线程栈
        String currentValue = idMap.get(key);
        // 3. 校验是否被其他线程修改过,true 表示未修改,否则需要加悲观读锁
        if (!lock.validate(stamp)) {
            // 4. 上悲观读锁,并重新读取数据到当前线程局部变量
            stamp = lock.readLock();
            try {
                currentValue = idMap.get(key);
            } finally {
                lock.unlockRead(stamp);
            }
        }
        // 5. 若校验通过,则直接返回数据
        return currentValue;
    }

    /**
     * 如果数据不存在则从数据库读取添加到 map 中,锁升级运用
     * 使用了读锁转换成写锁的编程模型,先查询缓存,当不存在的时候从数据库读取数据并添加到缓存中。
     *
     * @param key
     * @return
     */
    public String getIfNotExist(Integer key) {
        // 获取读锁,也可以直接调用 get 方法使用乐观读
        long stamp = lock.readLock();
        String currentValue = idMap.get(key);
        // 缓存为空则尝试上写锁从数据库读取数据并写入缓存
        try {
            while (Objects.isNull(currentValue)) {
                // 尝试升级写锁
                long wl = lock.tryConvertToWriteLock(stamp);
                // 不为 0 升级写锁成功
                if (wl != 0L) {
                    stamp = wl;
                    //TODO 模拟从数据库读取数据, 写入缓存中
                    currentValue = "query db";
                    idMap.put(key, currentValue);
                    break;
                } else {
                    // 升级失败,释放之前加的读锁并上写锁,通过循环再试
                    lock.unlockRead(stamp);
                    stamp = lock.writeLock();
                }
            }
        } finally {
            // 释放最后加的锁
            lock.unlock(stamp);
        }
        return currentValue;
    }

}

使用场景和注意事项

对于读多写少的高并发场景 StampedLock的性能很好,通过乐观读模式很好的解决了写线程“饥饿”的问题,我们可以使用StampedLock 来代替ReentrantReadWriteLock ,但是需要注意的是 StampedLock 的功能仅仅是 ReadWriteLock 的子集,在使用的时候,还是有几个地方需要注意一下。

  • StampedLock 写锁是不可重入的,如果当前线程已经获取了写锁,再次重复获取的话就会死锁,使用过程中一定要注意。

  • 悲观读、写锁都不支持条件变量 Conditon ,当需要这个特性的时候需要注意。

  • readLock()和 writeLock() 不支持中断 interrupt() 方法,会导致CPU飙升。

  • 需要中断时,可以使用悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。

思考

为何 StampedLock 性比 ReentrantReadWriteLock 好?

关键在于StampedLock 提供的乐观读。ReentrantReadWriteLock 支持多个线程同时获取读锁,但是当多个线程同时读的时候,所有的写线程都是阻塞的。StampedLock 的乐观读允许一个写线程获取写锁,所以不会导致所有写线程阻塞,也就是当读多写少的时候,写线程有机会获取写锁,减少了线程饥饿的问题,吞吐量大大提高。

允许多个乐观读和一个写线程同时进入临界资源操作,那读取的数据可能是错的怎么办?

乐观读不能保证读取到的数据是最新的,所以将数据读取到局部变量的时候需要通过lock.validate(stamp)校验是否被写线程修改过,若是修改过则需要上悲观读锁,再重新读取数据到局部变量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值