学习笔记:Java并发包中锁原理

LockSupport工具类

java.util.concurrent.locks.LockSupport是个工具类,主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础。
在这里插入图片描述
LockSupport类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport类的方法的线程是不持有许可证的。
LockSupport是使用Unsafe类实现的。

几个主要函数:

void park()方法
/**
Disables the current thread for thread scheduling purposes unless the permit is available.
*/
public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

在这里插入图片描述
在其他线程调用unpark(Thread thread)并且将当前线程作为参数时,调用park()方法而被阻塞的线程会返回。
如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者线程被虚假唤醒,则阻塞线程也会返回。
所以,调用park方法时最好也使用循环条件判断方式。

void unpark(Thread thread)方法

当一个线程调用unpark时:
1、参数thread线程没有持有与LockSupport类关联的许可证,则让thread线程持有
2、如果thread之前因调用park而被挂起,则该线程会被唤醒
3、如果thread之前没有调用park,则调用unpark后,再调用park,其会立即返回

parkNanos(long nanos)方法
public static void parkNanos(long nanos) {
        if (nanos > 0)
            UNSAFE.park(false, nanos);
    }

如果没有拿到许可证,则调用线程会被挂起nanos时间后修改为自动返回。
park方法还支持带有blocker参数的方法park(Object blocker)

 public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

当线程在没有持有许可证的情况下调用park(Object blocker),这个blocker对象会被记录到该线程内部。
使用诊断工具可以观察线程被阻塞的原因,诊断工具时通过调用getBlocker(Thread t) 方法来获取blocker对象的。所以jdk推荐使用带有blocker参数的park方法,并且blocker被设置为this,这样当在打印线程堆栈排查问题时就能知道是哪个类被阻塞了。

LockSupport.park(this);

抽象同步队列AQS概述

AQS——锁的底层支持

AQS(AbstractQueuedSynchronizer),是实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的。
在这里插入图片描述

在这里插入图片描述

1、AQS是一个FIFO的双向队列。内部通过节点 head和tail记录队首和队尾元素,队列元素的类型为Node。

static final class Node {
  //标记该线程是获取共享资源时被阻塞挂起后放入AQS队列的      
 static final Node SHARED = new Node();
  //标记该线程是获取独占资源时被阻塞挂起后放入AQS队列的      
 static final Node EXCLUSIVE = null;

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

 // 记录当前线程的等待状态:SIGNAL、CANCELLED、CONDITION、PROPAGATE       
volatile int waitStatus;

volatile Node prev;
volatile Node next;
volatile Thread thread;
  ........      
}

2、在AQS中维持了一个单一的状态信息state

/**
     * The synchronization state.
     */
    private volatile int state;

    protected final int getState() {
        return state;
    }
    protected final void setState(int newState) {
        state = newState;
    }

对于ReentrantLock实现来说,state可以用来表示当前线程获取锁的可重入次数;
对于读写锁ReentrantReadWriteLock来说,sate的高16位表示读状态,即获取该读锁的次数,低16位表示获取该写锁的线程的可重入次数;
对于semaphore来说,state用来表示当前可用信号的个数;
对于CountDownlatch来说,state用来表示计数器当前的值

3、AQS有个内部类ConditionObject,用来结合锁实现线程同步。

  • ConditionObject可以直接访问AQS对象内部的变量,如state状态值和AQS队列。
  • ConditionObject是条件变量,每个条件变量对应一个条件队列(单向链表队列),其用来存放调用条件变量的await方法后被阻塞的线程。
 /** First node of condition queue. */
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;

4、对于AQS来说,线程同步的关键是对状态值state进行操作
根据state是否属于一个线程,操作state的方式分为独占方式和共享方式。
独占方式:

acquire(int arg)
acquireInterruptibly(int arg)
boolean release(int arg)

共享方式:

void acquireShared(int arg)
void acquireSharedInterruptibly(int arg)
boolean releaseShared(int arg)

使用独占方式获取的资源是与具体线程绑定的,即如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作state获取资源时就会发现当前该资源不是自己持有的,获取失败后被阻塞。

如独占锁ReentrantLock的实现,当一个线程获取了ReentrantLock的锁后,在AQS内部会首先使用CAS操作把state状态值从0变为1,然后设置当前锁的持有者位当前线程,当该线程再次获取锁时发现他就是锁的持有者,则会把状态值从1变为2,也就是设置可重入次数。而当另一个线程获取锁时会被放入AQS阻塞队列后挂起

共享方式的资源与具体线程是不相关的,当多个线程去请求资源时通过CAS方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次获取资源时如果当前资源还能满足它的需要,则当前线程只需要使用CAS方式进行获取即可。

比如Semaphore信号量,当一个线程通过acquire方法获取信号量时,会首先看当前信号量个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋CAS获取信号量

class Semaphore
public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

在独占方式下,获取与释放资源的流程如下
获取资源:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

1、使用tryAcquire方法尝试获取资源
2、具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为Node.EXCLUSIVE的Node节点后插入到AQS阻塞队列的尾部,并调用LockSupport.park(this);方法挂起自己

释放资源:

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

1、尝试使用tryRelease(arg)释放资源
2、调用LockSupport.unpark(thread);激活AQS队列里面被阻塞的一个线程
3、被激活的线程使用tryAcquire尝试,看当前状态变量state的值能否满足自己的需要,满足则该线程被激活,否则还是被放入AQS队列并被挂起
AQS的入队操作:
1、当一个线程获取锁失败后该线程会被转换为Node节点,然后就会使用
enq(final Node node)方法将该节点插入到AQS的阻塞队列。

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

在这里插入图片描述

AQS——条件变量的支持

正如notify和wait方法,是配合synchronized内置锁实现线程间同步的基础设施一样,条件变量的signalawait方法也是用来配合锁(使用AQS实现的锁)实现线程间同步的基础设施。不同之处是synchronized只能与一个共享变量的notify和wait方法实现同步,而AQS的一个锁可以对应多个条件变量。

当线程调用条件变量的await()方法时(必须先调用锁的lock()方法获取锁):
1、在内部会构造一个类型为Node.CONDITION的node节点
2、将该节点插入到条件队列的末尾
3、该线程释放锁并被阻塞挂起

当另一个线程调用条件变量的signal方法时(必须先调用锁的lock()方法获取锁):
1、在内部会把条件队列里面队头的一个线程节点从队列里面移除
2、将该线程节点放入AQS的阻塞队列里面
3、激活这个线程

注意:1、AQS只提供了ConditionObject的实现,并没有提供newCondition函数,该函数用来new一个ConditionObject对象。需要由AQS的子类来提供newCondition函数。
2、当多个线程同时调用lock.lock()方法获取锁时,只有一个线程获取到了锁,其他线程会被转换为Node节点插入到lock锁对应的AQS阻塞队列里面,并做自旋CAS尝试获取锁。
在这里插入图片描述
一个锁对应一个AQS阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。

独占锁ReentrantLock的原理

独占锁ReentrantLock的原理

读写锁ReentrantReadWriteLock的原理

读写锁ReentrantReadWriteLock的原理

JDK8中新增的StampedLock锁探究

  • StampedLock 是并发包里面 JDK8 版本新增的 一个锁
  • 该锁提供了三种模式的读写控制,当调用获取锁的系列函数时,会返回一个 long 型的变量,我们称之为戳记 (stamp), 这个戳记代表了锁的状态 。
  • 其中 try 系列获取锁的函数,当获取锁失败后会返回为 0 的 stamp值
  • 当调用释放锁和转换锁的方法时需要传入获取锁时返回的stamp值。

StampedLock提供3种读写模式的锁:
写锁writeLock:排它锁/独占锁
悲观读锁readLock:共享锁
乐观读锁tryOptimisticRead
这三种锁在一定条件下可以进行相互转换。
StampedLock的读写锁都是不可重入锁。当多个线程同时尝试获取读锁和写锁时,谁先获取锁没有一定的规则,是随机的。并且该锁不是直接实现LockReadWriteLock接口,而是在内部自己维护了一个双向阻塞队列。
在这里插入图片描述

《Java并发编程之美》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值