Java juc系列4 ——locks包(二)

Java JUC系列目录链接

续言

上篇文章中说完了AQS和锁,还剩下几个点这里再做介绍。

Condition

Condition据说是jdk1.6引入的(不重要了),用于替代Object的一种锁等待和通知机制。

Condition和Object

其实就是替代了原来的

object.wait();
object.notify();
object.notifyAll();

更新为

condition.await();
condition.signal()
condition.signalAll()

Condition实现类源码分析

直接上Condition源码分析,这里同样采用较为简单的ReentrantLock类中的ConditionObject类做分析。

直接上源码:

 public final void await() throws InterruptedException {
    if (Thread.interrupted())
         throw new InterruptedException();
         // 1.调用addConditionWaiter将当前线程加入到condition的链表中
     Node node = addConditionWaiter();
     // 2.调用fullyRelease释放当前线程占有的所有锁
     int savedState = fullyRelease(node);
     int interruptMode = 0;
     // 确保当前线程加入到condition的等到队列中并阻塞该线程
     while (!isOnSyncQueue(node)) {
         LockSupport.park(this);
         if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
             break;
     }
     // acquireQueued尝试抢占锁,抢占成功切线程没有被中断则修改当前线程状态为REINTERRUPT
     if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
         interruptMode = REINTERRUPT;
     if (node.nextWaiter != null) // clean up if cancelled
         unlinkCancelledWaiters();
     if (interruptMode != 0)
         reportInterruptAfterWait(interruptMode);
 }
 
 private Node addConditionWaiter() {
   Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    // 移除condition等待链表中node状态不为Node.condition(-2)的节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 将当前线程用Node.CONDITION(-2)类型封装成Node对象并加入到condition的链表中
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

final int fullyRelease(Node node) {
   boolean failed = true;
    try {
        int savedState = getState();
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

这个是condition在AQS中的实现类ConditionObject的await方法。那么condition.await()是如何响应中断的呢?
我们注意到,在await方法中有个interruptMode变量,该变量的三种状态:ROW_IE(在抢占锁前被中断),REINTERRUPT(抢占到锁),0(还没有被中断)。该方法中使用while(其实是一种自旋操作)循环,当线程没有进入同步等待队列时,等待线程状态,当线程状态为0,即没有抢占到锁且没有被中断时会一直等待,直到抢占到锁或被中断。继续往下走,acquireQueued(node, savedState) && interruptMode != THROW_IE当线程成功抢占到锁切没有被中断时,更改线程的中断状态为REINTERRUPT(抢占到锁)。最后通过reportInterruptAfterWait()方法来响应线程的中断状态。

下面再看一下condition.signal(),直接上源码。

 public final void signal() {
 // 如果当前没有持有condition,直接抛出异常
   if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    // 通知唤醒等待队列中的第一个node节点
    if (first != null)
        doSignal(first);
}

 private void doSignal(Node first) {
  do {
       if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}
// 唤醒线程加入到AQS等待队列并准备抢占锁
final boolean transferForSignal(Node node) {
   /*
      * If cannot change waitStatus, the node has been cancelled.
      */
     if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
         return false;

     /*
      * Splice onto queue and try to set waitStatus of predecessor to
      * indicate that thread is (probably) waiting. If cancelled or
      * attempt to set waitStatus fails, wake up to resync (in which
      * case the waitStatus can be transiently and harmlessly wrong).
      */
     Node p = enq(node);
     int ws = p.waitStatus;
     if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
         LockSupport.unpark(node.thread);
     return true;
 }

这样我们就知道了,调用signal()方法时其实就是将condition内部链表中的头结点进行抢占锁的操作,这就跟AQS中的抢占锁一样了。

StampedLock

接下来我们说一下StampedLock这个锁。还记得我之前说的ReadWriteLock这个锁吗,这个锁将读和写分离开,一定程度上降低了锁粒度,提高了性能。但是其本身还是一种独占锁,如果有多个读操作并发存在的情况下,可能会导致写操作线程饥饿。这时候我们就引入了StampedLock。StampedLock是一种乐观锁(写操作之间还是会升级为悲观锁)。
StampedLock允许读线程采用无锁机制来进行读取(即在读期间允许写线程抢占写锁)。

简单例子

下面是一个简单的使用例子:

public class Test {
    private static final StampedLock lock = new StampedLock();
    private static StringBuilder data = new StringBuilder("");

    public static void main(String[] args) {
        Thread readThread1 = new Thread(new ReadRunnable("1", data, lock));
        Thread readThread2 = new Thread(new ReadRunnable("2", data, lock));

        Thread writeThread1 = new Thread(new WriteRunnable("1", data, lock));
        Thread writeThread2 = new Thread(new WriteRunnable("2", data, lock));
        writeThread1.start();
        readThread1.start();
        try {
            Thread.sleep(2000);
            readThread2.start();
            writeThread2.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

public class ReadRunnable implements Runnable {

    private final String name;
    private final StringBuilder data;
    private final StampedLock lock;

    ReadRunnable(String name, StringBuilder data, StampedLock lock) {
        this.name = name;
        this.data = data;
        this.lock = lock;
    }

    @Override
    public void run() {
        long stamp = lock.tryOptimisticRead();
        StringBuilder tempData = new StringBuilder(data);
        boolean validate = lock.validate(stamp);
        if (!validate) {
            stamp = lock.readLock();
            System.out.println("读线程" + name + "****采用了悲观锁占有锁");
            try {
                System.out.println("读线程" + name + "****开始操作");
                Thread.sleep(500);
                System.out.println("读线程" + name + "****data:" + data);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println("读线程" + name + "****结束操作");
                lock.unlockRead(stamp);
            }
        } else {
            System.out.println("读线程" + name + "****采用了乐观锁占有锁" + stamp);
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("读线程" + name + "****data:" + tempData);
        }
    }
}

public class WriteRunnable implements Runnable {

    private final String name;
    private StringBuilder data;
    private final StampedLock lock;

    WriteRunnable(String name, StringBuilder data, StampedLock lock) {
        this.name = name;
        this.data = data;
        this.lock = lock;
    }

    @Override
    public void run() {
        long stamp = lock.writeLock();
        System.out.println("写线程" + name + "****开始操作");
        try {
            Thread.sleep(1000);
            data.append(name);
            System.out.println("data修改为" + data);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("写线程" + name + "****结束操作");
            lock.unlockWrite(stamp);
        }
    }
}

下面是运行结果:

写线程1****开始操作
tempData
data修改为1
写线程1****结束操作
读线程1****采用了悲观锁占有锁
读线程1****开始操作
读线程1****data:1
读线程1****结束操作
tempData1
读线程2****采用了乐观锁占有锁512
写线程2****开始操作
data修改为12
写线程2****结束操作
读线程2****data:1

Process finished with exit code 0

我们可以很清楚的看到,在读线程中可以不抢占读锁而读取数据。
读线程的步骤

  • 使用lock.tryOptimisticRead()来尝试获取一个乐观读锁(其实不算是锁);
  • 使用lock.validate()来验证返回的时间戳,用来判断是否成功获取到乐观读锁(有点像乐观锁的时间戳机制,使用stamp来判断读期间数据是否发生改变)
  • 若申请乐观锁成功则可以直接读取数据,如果失败,使用lock.readLock()来获取一个真正意义上的锁(读锁),其实就是ReadWriteLock的读锁。
  • 释放占有的锁(如果获取到的是乐观读锁则不用释放,因为他根本不算是锁)

废话不多说,直接分析源码

StampedLock原理

stampedLock是在ReadWriteLock上的优化,其引入了乐观读锁的概念。

  • 我们知道,传统读写锁在存在读线程占有锁时是不允许写线程抢占锁的,而读锁本身又是共享锁,即被强占写锁的锁允许其他读线程进入,如果读线程过多,就会表现出锁总是全局读锁,而写线程出现饥饿。

因此,这里的StampedLock引入了乐观读的概念,在读线程预抢占锁时,我们可以先尝试抢占乐观读锁,此时会返回一个时间戳,接着读取数据,在读取完成后验证时间戳是否发生改变,如果时间戳没有发生改变,则证明在此期间读取的数据是没有被更改的,逻辑完成。如果验证发现时间戳发生了改变,则证明数据期间被修改了,这条读的数据是无效的。

下面我们直接上源码:
sta
stampedLock的属性,看名字就能猜出来:

  • readLocked:全局读锁
  • readLockCount:全局读锁数量(有这个就说明多个线程可以同时抢占读锁)
  • writeLocked:全局写锁

接着看一下主要的一些类参数:

/** Wait nodes */
    static final class WNode {
        volatile WNode prev;
        volatile WNode next;
        volatile WNode cowait;    // list of linked readers
        volatile Thread thread;   // non-null while possibly parked
        volatile int status;      // 0, WAITING, or CANCELLED
        final int mode;           // RMODE or WMODE
        WNode(int m, WNode p) { mode = m; prev = p; }
    }

    /** Head of CLH queue */
    private transient volatile WNode whead;
    /** Tail (last) of CLH queue */
    private transient volatile WNode wtail;

看过前两篇文章的不说你们也都明白了(卧槽这也太简单了,每种锁内部都是个等待队列啊!)
值得一提的是:

  • node代表的不是一个线程,而是想要抢占某种所的某个线程,即链表中可能存在两个node的thread是同一个线程。

下面是其中比较重要的一些常量。

// 一堆常量
// 读线程的个数占有低7位
private static final int LG_READERS = 7;
// 读线程个数每次增加的单位
private static final long RUNIT = 1L;
// 写线程个数所在的位置
private static final long WBIT  = 1L << LG_READERS;  // 128 = 1000 0000
// 读线程个数所在的位置
private static final long RBITS = WBIT - 1L;  // 127 = 111 1111
// 最大读线程个数
private static final long RFULL = RBITS - 1L;  // 126 = 111 1110
// 读线程个数和写线程个数的掩码
private static final long ABITS = RBITS | WBIT;  // 255 = 1111 1111
// 读线程个数的反数,高25位全部为1
private static final long SBITS = ~RBITS;  // -128 = 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000 0000

// state的初始值
private static final long ORIGIN = WBIT << 1;  // 256 = 1 0000 0000

关于十进制二进制转换这里引用了他人博客中的一些内容1

直接看最重要的方法:

  • readLock()
     public long readLock() {
            long s = state, next;  // bypass acquireRead on common uncontended case
            return ((whead == wtail && (s & ABITS) < RFULL &&
                 U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
                next : acquireRead(false, 0L));
        }
    
    看到这个return很多人肯定是直接就不想看了,别慌,刚看源码的时候因为不是你自己写的代码很有种陌生感,再加上天生的对“源码”这个词的恐惧,自然就开始懵了。
    我们来一步步分析:
    我们知道,acquireRead()方法是准备抢占锁,说明首次获得readLock()并没有成功,因此就是说((whead == wtail && (s & ABITS) < RFULL &&U.compareAndSwapLong(this, STATE, s, next = s + RUNIT))这么长一段的值必须是真才允许进入读锁。下面我们分段来分析:
    • whead == wtail:表示当前等待的头结点和尾结点相同,即链表长度为1
    • (s & ABITS) < RFULL:这™什么鬼位运算都扯来了??其实这个RFULL代表的是最大读锁占有数量(1111110),就是说当前读锁占有数量最大不得超过126个,超过了就算你是读锁也得给我出去排队!这个ABITS二进制为1111 1111,其与state位运算结果其实就是state的后8位。这时候疑问就来了?为什么是8位,第8位必须是0才满足条件呀,按道理不是应该7位吗?别急继续往下看。
    • U.compareAndSwapLong(this, STATE, s, next = s + RUNIT):这是个native的CAS方法,用于更新state的值使其增加RUNIT,这时候问题又来了。。这个RUNIT是个什么东西,为什么要增加RUNIT?这时候我的内心也是懵逼的,但别急,继续往下看。

只有满足了这三个条件,才允许线程进入读锁,否则进入acquireRead()执行抢锁逻辑(我这里就不讲了。。就是个内部链表,你们想想就知道了)。

  • 接着我们看写锁writeLock()

     public long writeLock() {
            long s, next;  // bypass acquireWrite in fully unlocked case only
            return ((((s = state) & ABITS) == 0L &&
                     U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
                    next : acquireWrite(false, 0L));
        }
    

    跟readLock()有点类似,下面我们也是来一步步看。
    不就是(((s = state) & ABITS) == 0L && U.compareAndSwapLong(this, STATE, s, next = s + WBIT))这玩意儿为真嘛。

    • ((s = state) & ABITS) == 0L这东西为0,要求就是state的后8为全都为0。这时候我们结合上面的读锁想一下,读锁是后8位小于126,写锁是后8位为0,如果说state的后7位代表着最大读锁占有数量,那么第8位不就是写锁位嘛,要知道写锁是不可以多个线程同时占有的。这时候我们就有了个大胆的猜测:statue后7位代表读锁占有数量,第8位代表写锁占有状态,继续往下看验证我们的推理。
    • U.compareAndSwapLong(this, STATE, s, next = s + WBIT)又是CAS更新,这次更新state使其增加了WBIT。等等,WBIT二进制为1000 0000,它的第8位刚好为1,增加一个WBIT不就刚好是写锁状态的改变嘛。回头再看刚刚的读锁,RUNIT二进制为111 1111,刚好是7位,增加一个RUNIT刚好进一位后释放一个读锁嘛。这时候我们需要再来一个大胆的假设:我们的state的初始值是1 0000 0000,当首次使用readLock()时,成功抢占读锁,state+111 1111即1 0000 0000+1111111=1 0111 1111。这时候代表读锁抢占数量为1,如果再来一个线程抢占读锁,即state+111 1111=1 1000 0000,代表读锁抢占数量为2,这恰恰验证了我们刚刚的推测,从1 0111 1111逐渐变化到1 0111 1110,这不刚好代表了写锁的占有数量嘛。
  • 接下来是重头戏:tryOptimisticRead

    public long tryOptimisticRead() {
            long s;
            return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
        }
    

    stampedLock允许占有写锁的状态下读,我们看看他是如何实现的。

    • (s = state) & WBIT) == 0L:state按位与1000 0000为0,就是说state后8位不全为0,这不是。。除了初始化的时候不满足,其他时候都满足嘛。
      如果满足了条件,此时就会返回state按位与SBITS,即代表读的后7位变成0,代表写的第8位改变状态。至于为什么,我们结合validate()方法看。
  • validate

    public boolean validate(long stamp) {
            U.loadFence();// 这个与本节关系不大,就不讲了
            return (stamp & SBITS) == (state & SBITS);
        }
    

    这玩意儿通过判断时间戳来验证数据是否被修改过,在获取乐观读锁时,我们得到了state按位与SBITS,现在我们需要比较 ( 旧state & SBITS & SBITS ) 与 ( 新state & SBITS ) 的值是否相等。二进制运算。。我这么懒你们就自己看看吧。

至于锁的释放也差不多。。这里就不讲了。。哇。二进制运算是真的难受。

LockSupport

这个类我估计你们没什么人用过,我也没用过,不过既然是juc系列,它也在这个包里面,索性简单说一下把。
与其说这是一种锁,不如说他是直接对线程的管理。park()用来阻塞挂起一个线程,而unpark()用来唤醒线程。这个类其实在上面我们讲锁的时候,那些底层的线程阻塞和唤醒都是通过LockSupport来实现的。

  • 这个类知道就行了,估计也不会用(比起直接管线程,那么多锁的类都封装完了他们不香嘛)。

  1. 别问,问就是懒,实在不想算了。。。跳转原博客 ↩︎

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值