当前位置: 首页 > 科技观察

说说StampedLock的使用和主要实现思路

时间:2023-03-22 00:05:13 科技观察

前言在多线程开发中,为了控制线程同步,使用最多的关键字是synchronized和reentrantlocks。在JDK8中,引入了一种新武器StampedLock。这是什么?英文单词Stamp的意思是邮戳。那么这里用什么意思呢?傻瓜,请看下面的细分。面对临界区的资源管理问题,一般有两套思路:第一是采用悲观策略。悲观者认为每次访问临界区的共享变量,总会有人跟我发生冲突。因此,每次对于每次访问我都必须锁定整个对象并在访问后解锁它。相反,乐观主义者认为,虽然临界区中的共享变量会发生冲突,但冲突应该是小概率事件。在大多数情况下,它不应该发生。因此,我可以先参观一下。如果我使用的数据没有冲突,那么我的操作就成功了;如果我用完之后发现有人冲突,那么我要么再试一次,要么转为悲观策略。从这里不难看出,可重入锁和synchronized是典型的悲观策略。聪明的你一定猜到了,StampedLock是一个提供乐观锁的工具,所以它是可重入锁的重要补充。StampedLock的基本使用在StampedLock的文档中提供了一个很好的例子,让我们可以快速的了解StampedLock的使用。下面我来看看这个例子,它的描述写在评论里。这里再解释一下validate()方法的含义。函数签名如下所示:publicbooleanvalidate(longstamp)它接受的参数是上次锁定操作返回的邮票。如果锁在调用validate()之前没有申请过写锁,那么它返回true,这也意味着被锁保护的共享数据没有被修改,所以前面的读操作肯定能保证数据的完整性和一致性。反之,如果锁在validate()之前有成功的写锁申请,说明之前的数据读写操作发生冲突,程序需要重试,或者升级为悲观锁。与可重入锁相比,从上面的例子不难看出。在编程复杂度上,StampedLock其实比可重入锁要复杂很多,代码也没有以前那么简洁了。那么,为什么我们仍然使用它呢?最本质的原因是为了提高性能!一般来说,这种乐观锁的性能要比普通的可重入锁快数倍,而且随着线程数的不断增加,性能上的差距会越来越大。总之,StampedLock的性能在大量并发场景下碾压可重入锁和读写锁。但毕竟世界上没有完美的东西,StampedLock也不是万能的。它的缺点如下:编码比较麻烦。如果使用乐观阅读,那么冲突的场景必须由应用程序自己处理。它不可重入。在一个线程中调用两次,您的世界就干净了。....它不支持等待/通知机制。如果以上3点对你来说都不是问题,那么相信StampedLock应该是你的首选。内部数据结构为了帮助大家更好的理解StampedLock,这里简单介绍一下它的内部实现和数据结构。在StampedLock中,有一个队列存放等待锁的线程。队列是一个链表,链表中的元素是一个叫做WNode的对象:当队列中有多个线程在等待时,整个队列可能是这样的:除了这个等待队列,StampedLock中还有一个特别重要的字段是longstate,是一个64位整数,StampedLock用的很巧妙。state的初始值为:privatestaticfinalintLG_READERS=7;privatestaticfinallongWBIT=1L<apply->release”时,CAS操作可以检查数据变化,从而判断发生了写操作。作为乐观锁,可以准确的判断出已经发生了冲突,剩下的就交给应用程序来解决冲突了。所以这里记录释放锁的次数,以准确监控线程冲突。状态剩余字节的7位用于记录读取锁的线程数。由于只有7位,所以只能记录其中的126位。看下面代码中的RFULL,就是满载的线程数。.超过了怎么办,多出的部分记录在readerOverflow字段中。privatestaticfinallongWBIT=1L<{//阻塞在悲观读锁lock.readLock();});t2.start();//保证t2阻塞在读锁Thread.sleep(100);//中断线程t2会导致线程t2的CPU飙升t2。interrupt();t2.join();}}上面代码中,t2被中断后,t2的CPU占用率会达到100%。此时t2被阻塞在readLock()函数上。也就是说,被中断后,StampedLock的读锁可能会占用CPU。这是什么原因?机制的小傻瓜肯定已经想到了,这是StampedLock自旋过多造成的!是的,你的猜测是正确的。具体原因如下:如果没有中断,阻塞在readLock()上的线程会在自旋数次后进入park()等待,一旦进入park()等待,就不会占用CPU。但是park()函数有一个特点,就是一旦线程被中断,park()会立即返回,而且返回不算,也不会抛给你任何异常,就尴尬了。本来想在锁准备好的时候unpark()这个线程,现在锁不好了,直接打断了,park()也返回了,但是毕竟锁不好,就去再次旋转起来。转来转去,又转向了park()函数,但是悲催的是,线程的中断标志已经打开,park()无法阻塞,于是下一次自旋又开始了,没完没了的自旋无法停止,因此CPU已满。解决这个问题,本质上需要在StampedLock里面。当park()返回时,需要判断中断标志,做出正确的处理。例如退出,抛出异常,或者清除中断位都可以解决问题。.但遗憾的是,至少在JDK8中,并没有这样的处理。所以就有了上面中断readLock()后CPU占满的问题。请小心。写在最后今天比较仔细的介绍了StampedLock的使用和主要实现思路。StampedLock是对可重入锁和读写锁的重要补充。它提供了一种乐观锁策略,是一种独特的锁实现。当然,在编程难度上,StampedLock会比可重入锁和读写锁麻烦一点,但是会带来双倍的性能提升。这里给大家提一些小建议。如果申请线程数量可控,不要太多,竞争不是太激烈,那么我们可以直接使用简单的synchronized、可重入锁、读写锁;如果应用线程数很多,竞争激烈,对性能比较敏感,那我们还是要下功夫使用比较复杂的StampedLock来提高程序的吞吐量。使用StampedLock时需要特别注意两点:第一,StampedLock是不可重入的。不要用一个线程与自己死锁。其次,StampedLock没有等待/通知机制。如果一定要用到这个功能,只能绕过!我是敖丙,知道的越多,不知道的越多,下次见。