一文搞懂Java并发AQS的共享锁模式
什么是AQS
AQS全称为AbstractQueuedSynchronizer(抽象队列式同步器),是Java并发包中的一种基础组件,用于实现锁和同步器工具类。在Java中,锁和同步器的实现往往都依赖于AQS。
AQS实现了一个双向队列,队列里面的元素是“线程节点”,每一个线程节点都可以对应一个线程。线程节点可以用来保存等待线程相关的状态(如是否被中断、是否已经获取到锁等),并且可以被高效的挂起/恢复。AQS中定义了一些方法,可以让我们在实现锁和同步器时方便的使用和扩展这个队列。
什么是共享锁
在并发编程中,共享锁是一种允许多个线程同时读取同一个资源的锁。在一个线程持有共享锁时,其他线程也可以获得该锁,从而多个线程同时访问同一个资源。
共享锁和排它锁是相对的,排它锁只允许一个线程访问资源,其他线程需要等待当前线程释放锁之后才能访问。
为什么需要共享锁
共享锁可以提高系统吞吐量,因为多个线程可以同时读取同一份资源,避免单线程瓶颈。
同时,共享锁的使用也有助于避免数据不一致问题。在并发编程中,如果多个线程同时写入同一份资源,可能导致数据不一致,而共享锁仅允许多个线程同时读取同一个资源,避免了这个问题,是一种非常重要的多线程编程技术。
AQS中的共享锁
AQS中实现了独占锁和共享锁两种锁,其中共享锁有两种模式:共享锁和读锁。
共享锁
共享锁是一种允许多个线程同时访问同一个资源的锁。共享锁可以使用AQS中的tryAcquireShared方法尝试获取锁,使用tryReleaseShared方法释放锁。这些方法在实现上非常类似于独占锁中的tryAcquire和tryRelease方法。不过,共享锁允许多个线程同时获取锁,因此在实现时需要考虑多个线程并发访问时的线程安全问题。
AQS中共享锁的实现依赖于一个int类型的state变量。state变量存储了当前锁被持有的次数,其中低16位表示共享锁的数量,高16位表示独占锁的次数。当一个线程尝试获取共享锁时,它会调用AQS中的tryAcquireShared方法,这个方法会判断state变量中低16位的值是否为0。如果是0,那么表示当前没有线程占用共享锁,当前线程可以获取到锁;否则,当前线程需要被加入到等待队列中,等待其他线程释放锁之后再次尝试获取。当线程成功获取共享锁时,state变量的低16位的值会增加1。
当一个线程释放共享锁时,它会调用AQS中的tryReleaseShared方法。这个方法会把当前线程持有的共享锁数量减1,并且唤醒等待队列中的一个线程。如果当前线程持有的共享锁数量减为0,那么表示当前线程不再持有锁,tryReleaseShared方法返回true。
读锁
读锁和共享锁类似,也是允许多个线程同时访问同一个资源的锁。不过,读锁只能用于读操作,不能用于写操作。和共享锁一样,读锁也可以使用AQS中的tryAcquireShared方法尝试获取锁,使用tryReleaseShared方法释放锁。和共享锁不同的是,读锁的获取和释放并不会影响state变量,也就是说,读锁获取和释放的实现不使用state变量,而是使用类似于共享锁的等待队列来实现。
两个示例
示例1:并发读写文件共享锁
本示例中假设有一个文件,多个线程需要并发读写这个文件。因为读操作是安全的,所以可以使用共享锁实现多线程的读操作;而写操作是需要互斥的,因此需要使用独占锁实现多线程的写操作。
示例实现中定义了ReadWriteLock类,通过实现AQS的共享锁和独占锁,封装了对文件的并发读写操作。
public class ReadWriteLock {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Map<String, byte[]> files = new ConcurrentHashMap<>();
public byte[] read(String fileName) throws InterruptedException {
lock.readLock().lockInterruptibly();
try {
return files.get(fileName);
} finally {
lock.readLock().unlock();
}
}
public void write(String fileName, byte[] data) throws InterruptedException {
lock.writeLock().lockInterruptibly();
try {
files.put(fileName, data);
} finally {
lock.writeLock().unlock();
}
}
}
在这个示例中,通过ReentrantReadWriteLock类实例化读写锁ReentrantReadWriteLock,并调用lock的readLock和writeLock方法获取读锁和写锁。读写操作分别对应两个公共方法read和write,这些方法使用了AQS中的共享锁和独占锁实现。
示例2:乐观读取并发HashSet
本示例中假设有多个线程需要并发读写一个HashSet。因为读操作是安全的,所以可以使用共享锁实现多线程的读操作;而写操作比较耗时,需要减小独占锁的获取和释放等开销,因此可以使用乐观读取(optimistic reads)来实现多线程的写操作。
示例实现中定义了ConcurrentHashSet类,通过继承AbstractSet类和使用AQS的共享锁和条件变量,实现对HashSet的并发读写操作。
public class ConcurrentHashSet<E> extends AbstractSet<E> {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Set<E> set = new HashSet<E>();
@Override
public Iterator<E> iterator() {
return set.iterator();
}
@Override
public int size() {
lock.readLock().lock();
try {
return set.size();
} finally {
lock.readLock().unlock();
}
}
@Override
public boolean add(E e) {
long stamp = lock.writeLock();
try {
return set.add(e);
} finally {
lock.unlockWrite(stamp);
}
}
@Override
public boolean remove(Object o) {
long stamp = lock.writeLock();
try {
return set.remove(o);
} finally {
lock.unlockWrite(stamp);
}
}
}
在这个示例中,通过ReentrantReadWriteLock类实例化读写锁ReentrantReadWriteLock,并调用lock的readLock和writeLock方法获取读锁和写锁。通过使用tryOptimisticRead方法不用加锁实现读操作,使用AQS中的tryConvertToWriteLock方法转化读锁为独占写锁实现写操作。
总结
AQS是Java并发包中一种非常实用的同步机制,通过实现队列,给我们提供了很好的锁和同步器实现基础。在AQS中,共享锁是一种非常重要的同步机制,可以方便的实现多个线程同时读取同一个资源的需求。在使用共享锁时,需要注意线程安全问题,保证锁的正确性。在实际项目中,需要根据实际需求综合考虑锁和同步器的实现方式,使用AQS进行实现。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:一文搞懂Java并发AQS的共享锁模式 - Python技术站