关于AQS学习时的一些问题


前言

小菜鸡学习AQS的时候的一些疑问。


一、1.Locksupport的park和unpark实现原理是什么?

是不是unpark一次,就可以一直park,都不会被阻塞了?

public static void main(String[] args) throws Exception
{
    Thread thread = Thread.currentThread();
    
    LockSupport.unpark(thread);
    
    System.out.println("a");
    LockSupport.park();
    System.out.println("b");
    LockSupport.park();
    System.out.println("c");
}

这段代码打印出a和b,不会打印c,我的理解是unpark给与该线程许可,但是park的时候会把许可消耗掉,第二次park的时候因为没有许可而陷入阻塞。就是说park是不可重入的。

那他可不可以响应中断呢?

public static void t2() throws Exception
{
    Thread t = new Thread(new Runnable()
    {
        private int count = 0;
 
        @Override
        public void run()
        {
            long start = System.currentTimeMillis();
            long end = 0;
 
            while ((end - start) <= 1000)
            {
                count++;
                end = System.currentTimeMillis();
            }
 
            System.out.println("after 1 second.count=" + count);
 
	    //等待或许许可
            LockSupport.park();
            System.out.println("thread over." + Thread.currentThread().isInterrupted());
 
        }
    });
 
    t.start();
 
    Thread.sleep(2000);
 
    // 中断线程
    t.interrupt();
 
    
    System.out.println("main over");
}

最终线程会打印出thread over.true。这说明线程如果因为调用park而阻塞的话,能够响应中断请求(中断状态被设置成true),但是不会抛出InterruptedException。
原文链接:https://blog.csdn.net/aitangyong/article/details/38373137

  • 那跟wait和notify比较,park和unpark用来唤醒线程有什么优点呢?

在Java5里是用wait/notify/notifyAll来同步的。wait/notify机制有个很蛋疼的地方是,比如线程B要用notify通知线程A,那么线程B要确保线程A已经在wait调用上等待了,否则线程A可能永远都在等待。这样就很麻烦了。但是使用park和unpark了之后,我们可以解决上面的问题。就是一个线程它在启动之后,可以先被别的线程unPark(),然后这个线程在进行park()。

LockSupport类的底层实现

class Parker : public os::PlatformParker {
private:
  volatile int _counter ;
  ...
public:
  void park(bool isAbsolute, jlong time);
  void unpark();
  ...
}
class PlatformParker : public CHeapObj<mtInternal> {
  protected:
    pthread_mutex_t _mutex [1] ;
    pthread_cond_t  _cond  [1] ;
    ...
}

LockSupport就是通过控制变量_counter来对线程阻塞唤醒进行控制的。原理有点类似于信号量机制。

当调用park()方法时,会将_counter置为0,同时判断前值,等于1说明前面被unpark过,则直接退出,否则将使该线程阻塞。

当调用unpark()方法时,会将_counter置为1,同时判断前值,等于0会进行线程唤醒,否则直接退出。

当先调用两次unpark()之后,那么_counter置还是1,然后第一次调用park(),将_counter置为0,同时前值等于1,所以直接退出了,但是在第二次park()的时候,_count值是0,所以此时直接被阻塞了。

原文链接:https://blog.csdn.net/weixin_43689480/article/details/93912894

还有一篇关于unsafe.park底层原理讲得比较透彻的 (java菜鸡看不懂c)
https://blog.csdn.net/weixin_39687783/article/details/85058686

二、AQS那个资源信号state是存在哪里?

确实是学的时候有的疑问,就记录一下,但凡看过一点源码也不会问这种问题。哈哈

我们都知道,像这种并发锁,一般都是会通过一个信号量来判断当前资源获取的状态。都知道基于JVM的synchronized锁是依赖于锁对象的对象头,对象头里存放着对象哈希值啊,GC分代年龄,持有锁线程ID,偏向线程ID,类元指针等等一些东西。那JUC锁的这个信号量是不是也是放在对象头里面?

看一下AbstractQueuedSynchronizer类的定义

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {    
    // 版本号
    private static final long serialVersionUID = 7373984972572414691L;    
    // 头节点
    private transient volatile Node head;    
    // 尾结点
    private transient volatile Node tail;    
    // 状态
    private volatile int state;    
    // 自旋时间
    static final long spinForTimeoutThreshold = 1000L;
    
    // Unsafe类实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // state内存偏移地址
    private static final long stateOffset;
    // head内存偏移地址
    private static final long headOffset;
    // state内存偏移地址
    private static final long tailOffset;
    // tail内存偏移地址
    private static final long waitStatusOffset;
    // next内存偏移地址
    private static final long nextOffset;
    // 静态初始化块
    static {
        try {
            stateOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
            headOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
            tailOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
            waitStatusOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("waitStatus"));
            nextOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("next"));

        } catch (Exception ex) { throw new Error(ex); }
    }
}

可以看到那个state变量。其实想reentrantLock使用的资源信号量就是用是这个state,由于reentrantLock是可重入锁,于是在锁住的时候,state+1,解锁的时候state-1. 只有当state=0时,其他线程才可以通过CAS操作去尝试获得锁。

三、同步队列和条件队列有什么关联?

当时有点疑惑,AQS数据结构定义的是CLH队列,虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

也就是说把线程加入到这条双向队列中,阻塞啊,唤醒啊都在这条队列上操作不就行了,这条就是sync queue,那为什么又要定义Condition queue呢?

在这里插入图片描述

就拿生产者消费者例子来说

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

public class Depot {
    private int size;
    private int capacity;
    private Lock lock;
    private Condition fullCondition;
    private Condition emptyCondition;
    
    public Depot(int capacity) {
        this.capacity = capacity;    
        lock = new ReentrantLock();
        fullCondition = lock.newCondition();
        emptyCondition = lock.newCondition();
    }
    
    public void produce(int no) {
        lock.lock();
        int left = no;
        try {
            while (left > 0) {
                while (size >= capacity)  {
                    System.out.println(Thread.currentThread() + " before await");
                    fullCondition.await();
                    System.out.println(Thread.currentThread() + " after await");
                }
                int inc = (left + size) > capacity ? (capacity - size) : left;
                left -= inc;
                size += inc;
                System.out.println("produce = " + inc + ", size = " + size);
                emptyCondition.signal();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    
    public void consume(int no) {
        lock.lock();
        int left = no;
        try {            
            while (left > 0) {
                while (size <= 0) {
                    System.out.println(Thread.currentThread() + " before await");
                    emptyCondition.await();
                    System.out.println(Thread.currentThread() + " after await");
                }
                int dec = (size - left) > 0 ? left : size;
                left -= dec;
                size -= dec;
                System.out.println("consume = " + dec + ", size = " + size);
                fullCondition.signal();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

生产者因为库存满了无法继续生产,这个时候需要fullCondition.await()一下,把这个生产者线程放入到生产者线程专门等待的一个条件队列里面,再调用emptyCondition.signal(),唤醒一个消费者线程去消费。等到消费者线程把库存消费消耗一些之后,再调用fullCondition.signal()唤醒一个生产者线程来生产。

使用两个条件队列分别来存储生产者线程和消费者线程。使得开发者写代码的时候不是更加轻松了?并且对于唤醒和阻塞哪一类线程也很清晰,用的还是同一个lock锁。等线程被唤醒了,在条件队列的第一个节点就会被移到同步队列的尾部,等着前面的线程运行完再轮到自己运行。

四、ReentrantLock是如何实现公平锁和非公平锁的?

我们都知道ReentrantLock可以声明成公平锁也可以声明成非公平锁,那具体的应该如何设计跟实现呢?

其实并不难,看过AQS的都知道ReentrantLock也是基于AQS来实现的。那么在AQS的同步队列中,线程被放到同步队列中是要按照顺序排队等待前一个线程节点释放资源后再将后面的一个线程节点唤醒的。也就是说,你只要排在了同步队列里面了,就应该按照顺序来获取资源,就是公平的。那非公平就应该在还没加入同步节点的时候来做文章。

看一下非公平锁和公平锁的源码

// 非公平锁
static final class NonfairSync extends Sync {
    // 版本号
    private static final long serialVersionUID = 7316153563782823691L;

    // 获得锁
    final void lock() {
        if (compareAndSetState(0, 1)) // 比较并设置状态成功,状态0表示锁没有被占用
            // 把当前线程设置独占了锁
            setExclusiveOwnerThread(Thread.currentThread());
        else // 锁已经被占用,或者set失败
            // 以独占模式获取对象,忽略中断
            acquire(1); 
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

// 公平锁
static final class FairSync extends Sync {
    // 版本序列化
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        // 以独占模式获取对象,忽略中断
        acquire(1);
    }

    /**
        * Fair version of tryAcquire.  Don't grant access unless
        * recursive call or no waiters or is first.
        */
    // 尝试公平获取锁
    protected final boolean tryAcquire(int acquires) {
        // 获取当前线程
        final Thread current = Thread.currentThread();
        // 获取状态
        int c = getState();
        if (c == 0) { // 状态为0
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) { // 不存在已经等待更久的线程并且比较并且设置状态成功
                // 设置当前线程独占
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) { // 状态不为0,即资源已经被线程占据
            // 下一个状态
            int nextc = c + acquires;
            if (nextc < 0) // 超过了int的表示范围
                throw new Error("Maximum lock count exceeded");
            // 设置状态
            setState(nextc);
            return true;
        }
        return false;
    }
}

可以看到公平锁和非公平锁的源码实现在lock()方法中的区别在于,非公平锁在执行acquire()方法之前,非公平锁会先进行一次CAS操作,在把线程封装成节点放进同步对列之前直接进行一次CAS操作,尝试持有锁,如果CAS成功就setExclusiveOwnerThread(),把当前持有锁的线程ID更改成本线程ID。

这个acquire()方法呢,就会查看当前锁是否有别的线程持有,同步队列中是否有线程在排队,把线程放进同步队列,自旋尝试,更改前一个节点的state,最后再挂起。总之挺复杂的,可以具体去看看。

五、synchronize锁不响应中断,JUC锁是如何响应中断的?如何设计的?

(累了,以后再写吧…)


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值