JUC之Lock及核心AQS

文章介绍了Java并发编程中的Lock机制,包括LockSupport的park和unpark函数在阻塞和激活线程中的作用,StampedLock作为无障碍锁的特性,以及AQS(AbstractQueuedSynchronizer)如何实现线程同步,特别是ReentrantLock和ReentrantReadWriteLock的使用及其公平性和非公平性。

目录

一. 前言

二. 实现类

2.1. LockSupport

2.2. StampedLock无障碍锁

三. AQS(AbstractQueuedSynchronizer)

3.1. ReentrantLock

3.2. ReentrantReadWriteLock


一. 前言

        通过《J.U.C家族》这篇文章的介绍,我们可以知道Lock是J.U.C中很重要的一部分,它非常灵活,可以根据具体需求选择不同的锁类型,并提供了更多的扩展功能,但需要注意加锁和解锁的位置,以免出现死锁等问题。

 

Synchronized 和 Lock 都是 Java 中用于实现线程同步的机制。它们之间的区别如下:
1. Synchronized 是 Java 的关键字,而 Lock 是一个接口;
2. Synchronized 在执行完相应的同步代码后会自动释放锁,而 Lock 则需要手动释放锁;
3. Synchronized 无法进行尝试获取锁的操作,而 Lock 可以通过 tryLock() 方法来尝试获取锁;
4. Synchronized 只能实现非公平锁,而 Lock 可以实现公平锁或非公平锁。

二. 实现类

2.1. LockSupport

LockSupport用来创建锁和其他同步类的基本线程阻塞原语。在分析LockSupport函数之前,先引入sun.misc.Unsafe类中的park和unpark函数,因为LockSupport的核心函数都是基于Unsafe类中定义的park和unpark函数,下面给出两个函数的定义:

public native void park(boolean isAbsolute, long time);
public native void unpark(Thread thread);

核心函数:
park:阻塞线程
unpark:释放线程的许可,即激活调用park后阻塞的线程。这个函数不是安全的,调用这个函数时要确保线程依旧存活。
parkNanos:此函数表示在许可可用前禁用当前线程,并最多等待指定的等待时间。
parkUntil:此函数表示在指定的时限前禁用当前线程,除非许可可用。

2.2. StampedLock无障碍锁

StampedLock 是 JDK1.8 中新增的一个读写锁,没有 Re 开头,是不可重入锁。支持三种模式:写锁、悲观读锁、乐观读tryOptimisticRead。

读/写锁可以保证并发访问下的数据写入安全与读取性能,但是在读线程非常多的情况下,有可能造成写线程的长时间阻塞,从而减少写线程的调度次数。为此JUC中针对读/写锁提出了改进方案,提供了无障碍锁(StampedLock),使用这种锁的特点在于:若干个读线程彼此之间不会互相影响,但是依然可以保证多个写线程的独占操作。

三. AQS(AbstractQueuedSynchronizer)

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

AbstractQueuedSynchronizer类底层的数据结构是使用CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。其中Sync queue,即同步队列,是双向链表,包括head结点和tail结点,head结点主要用作后续的调度。而Condition queue不是必须的,其是一个单向链表,只有当使用Condition时,才会存在此单向链表。并且可能会有多个Condition queue。

AQS核心方法:

isHeldExclusively(); // 该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int); // 独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int); // 独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int); // 共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int); // 共享方式。尝试释放资源,成功则返回true,失败则返回false。

名词解释:
可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。
独占(只有一个线程能访问执行,又根据是否按队列的顺序分为公平锁和非公平锁,如ReentrantLock)。
共享(多个线程可同时访问执行,如Semaphore、CountDownLatch、 CyclicBarrier )。ReentrantReadWriteLock可以看成是组合式,允许多个线程同时对某一资源进行读。

3.1. ReentrantLock

ReentrantLock是一种可重入的独占锁,它允许同一个线程多次获取同一个锁而不会被阻塞。它的功能类似于synchronized是一种互斥锁,可以保证线程安全。

ReentrantLock支持公平锁和非公平锁两种模式:
公平锁:线程在获取锁时,按照等待的先后顺序获取锁。
非公平锁:线程在获取锁时,不按照等待的先后顺序获取锁,而是随机获取锁。ReentrantLock默认是非公平锁。

代码示例:

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);
    }
}

3.2. ReentrantReadWriteLock

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

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

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

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Cache {
    private static Map<String, Object> map = new HashMap<String, Object>();
    private static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private static Lock r = rwl.readLock();
    private 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();
        }
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流华追梦

你的鼓励将是我创作最大的动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值