此时情绪此时天,无事小神仙
好好生活,平平淡淡每一天

编辑

StampedLock

StampedLock提供三种模式的读写锁,分别为写锁、悲观读锁、乐观读锁。
记忆口诀是写写互斥、读写互斥、读读共享。

简介

StampedLock类,在JDK8中加入全路径为java.util.concurrent.locks.StampedLock。
功能与RRW(ReentrantReadWriteLock)功能类似提供三种读写锁,但是又有些许不同。
StampedLock中引入了一个stamp(邮戳)的概念。它代表线程获取到锁的版本,每一把锁都有一个唯一的stamp。是一个long类型的数字。

StampedLock 独占写锁:writeLock

writeLock,是排它锁、不可重入锁、也叫独占锁,相同时间只能有一个线程获取锁,其他线程请求读锁和写锁都会被阻塞,当前没有线程持有读锁或写锁的时候才可以获得获取到该锁。

tryWriteLock,和writeLock类似,唯一的区别就是它非阻塞的特性,当获取不到锁时不会阻塞线程但是会返回一个stamp = 0的标识。
stamp > 0表示成功获取到锁;stamp = 0表示未获取到锁,但不会阻塞线程

想要开锁(释放锁)必须使用对应的钥匙(stamp)。

StampedLock writeLock:简单使用

writeLock与unlockWrite必须成对儿使用,解锁时必须需要传入相对应的stamp才可以释放锁。
每次获得锁之后都会得到一个新stamp值。

public static void StampedWriteLockExample1() {
    //创建StampedLock对象
    StampedLock stampedLock = new StampedLock();
    //获取写锁,并且返回stamp
    long stamp = stampedLock.writeLock();
    System.out.println("get writeLock,stamp=" + stamp);
    //使用完毕,释放锁,但是要传入对应的stamp
    stampedLock.unlockWrite(stamp);
    //再次获取写锁,并获得新的stamp
    stamp = stampedLock.writeLock();
    System.out.println("get writeLock,stamp=" + stamp);
    //释放写锁
    stampedLock.unlockWrite(stamp);
}

运行结果

get writeLock,stamp=384
get writeLock,stamp=640

StampedLock writeLock:非重入锁示例

同一个线程获取锁后,再次尝试获取锁而无法获取,则证明其为非重入锁

public static void StampedWriteLockExample2() {
    StampedLock stampedLock = new StampedLock();
    // 第一次获得写锁
    long stamp = stampedLock.writeLock();
    System.out.println("get writeLock,stamp=" + stamp);
    /**
     * 第一次获得写锁还未释放
     * 来获取写锁,是否能够获取到?
     * 如果是重入锁则可以获取到,如果不是则获取不到
     */
    System.out.println("开始尝试获取锁...");
    stamp = stampedLock.writeLock();
    System.out.println("get writeLock,stamp=" + stamp);
    // 释放锁
    stampedLock.unlockWrite(stamp);
}

运行结果
image

StampedLock tryWriteLock:非阻塞获取锁示例

尝试获取写锁,如果能够获取到则直接加锁,并返回stamp,如果获取不到锁也不会阻塞线程,但返回的stamp为0(与writeLock的重要区别)

  • stamp > 0 表示成功获取到锁
  • stamp = 0 表示未获取到锁,但不会阻塞线程
public static void StampedTryWriteLockExample() {
    StampedLock stampedLock = new StampedLock();
    //第一次尝试获取锁,并得到锁,返回stamp
    long tryLockStamp1 = stampedLock.tryWriteLock();
    System.out.println("get StampedLock.tryWriteLock,tryLockStamp1=" + tryLockStamp1);
    /**
     * 由于第一次未释放,所以第二次获取失败,返回stamp=0
     * 但是程序并不阻塞,继续向下运行
     * 与它的名字一样 tryWriteLock,先尝试获取,能获取到就加锁,获取不到就算了,不阻塞线程。
     */
    long tryLockStamp2 = stampedLock.tryWriteLock();
    System.out.println("can not get StampedLock.tryWriteLock,tryLockStamp2=" + tryLockStamp2);
    //第三次直接使用writeLock获取锁,导致线程阻塞
    long writeLockStamp = stampedLock.writeLock();
    System.out.println("can not get StampedLock.writeLock,writeLockStamp=" + writeLockStamp);
    stampedLock.unlockWrite(tryLockStamp1);
}

运行结果
image

ReentrantLock:重入锁

同一个线程获取锁后,再次尝试获取锁依然可以获取,则证明其为重入锁。

public static void reentrantLockExample() {
    ReentrantLock reentrantLock = new ReentrantLock();
    //获得锁
    reentrantLock.lock();
    System.out.println("get ReentrantLock lock1");
    //未释放锁,再次获得锁,依然可以获得锁
    reentrantLock.lock();
    System.out.println("get ReentrantLock lock2");
    reentrantLock.unlock();
}

运行结果

get ReentrantLock lock1
get ReentrantLock lock2

ReentrantReadWriteLock:重入读写锁

同一个线程获取写锁后,再次尝试获取锁依然可获取锁,则证明其为重入锁

public static void reentrantReadWriteLockExample() {
    ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    //获得锁
    writeLock.lock();
    System.out.println("get ReentrantReadWriteLock.WriteLock lock1");
    //未释放,再次获得锁,依然可以获取
    writeLock.lock();
    System.out.println("get ReentrantReadWriteLock.WriteLock lock2");
    writeLock.unlock();
}

运行结果

get ReentrantReadWriteLock.WriteLock lock1
get ReentrantReadWriteLock.WriteLock lock2

StampedLock 独占写锁:writeLock总结

writeLock、tryWriteLock相同点:相同时间只能有一个线程获取锁
writeLock、tryWriteLock不同点:当获取不到锁时,writeLock会阻塞线程;tryWriteLock不会阻塞线程,但返回一个stamp = 0的标识
tryWriteLock非阻塞的特性可以让开发人员更灵活的玩转代码。
ReentrantLock、ReentrantReadWriteLock是重入锁,从他们的名字就可以看出来这个特性(Reentrant,重入的)

StampedLock 悲观读锁:readLock

悲观读锁是一个共享锁,没有线程占用写锁的情况下,多个线程可以同时获取读锁。如果其他线程已经获得了写锁,则阻塞当前线程。

StampedLock readLock:简单使用

读锁可以多次获取(没有写锁占用的情况下),写锁必须在读锁全部释放之后才能获取写锁。

public static void StampedReadLockExample1() {
    StampedLock stampedLock = new StampedLock();
    //获取读锁,并得到readLockStamp1
    long readLockStamp1 = stampedLock.readLock();
    System.out.println("get readLock1,readLockStamp1=" + readLockStamp1);
    //再次获取读锁,并得到readLockStamp2
    long readLockStamp2 = stampedLock.readLock();
    System.out.println("get readLock2,readLockStamp2=" + readLockStamp2);
    //使用readLockStamp1解锁
    stampedLock.unlockRead(readLockStamp1);
    //使用readLockStamp2解锁
    stampedLock.unlockRead(readLockStamp2);
    //获得写锁,并得到writeLockStamp
    long writeLockStamp = stampedLock.writeLock();
    System.out.println("get writeLock,writeLockStamp=" + writeLockStamp);
}

运行结果

get readLock1,readLockStamp1=257
get readLock2,readLockStamp2=258
get writeLock,writeLockStamp=384

StampedLock readLock:同线程读写互斥示例

只要还有任意的锁没有释放(无论是写锁还是读锁),这时候来尝试获取写锁都会失败,因为读写互斥,写写互斥。写锁本身就是排它锁。

public static void StampedReadLockExample2() {
    StampedLock stampedLock = new StampedLock();
    //获取读锁,成功
    long readLockStamp1 = stampedLock.readLock();
    System.out.println("get readLock1,readLockStamp1=" + readLockStamp1);
    //获取读锁,成功
    long readLockStamp2 = stampedLock.readLock();
    System.out.println("get readLock2,readLockStamp2=" + readLockStamp2);
    //释放readLockStamp2的读锁,成功
    stampedLock.unlockRead(readLockStamp2);
    /**
     * readLockStamp1未释放
     * 获取写锁,失败,被阻塞
     */
    long writeLockStamp = stampedLock.writeLock();
    System.out.println("get writeLock,writeLockStamp=" + writeLockStamp);
}

运行结果
image

StampedLock readLock:不同线程读写互斥示例

在多个线程之间依然存在写写互斥、读写互斥、读读共享的关系。

public static void StampedReadLockExample3() {
    StampedLock stampedLock = new StampedLock();
    //获取读锁
    long stamp12 = stampedLock.readLock();
    System.out.println(Thread.currentThread().getName() + " get read lock1,stamp=" + stamp12);

    CompletableFuture.runAsync(() -> {
        System.out.println(Thread.currentThread().getName() + " run");
        //如果main线程的读锁释放了,才能获得写锁
        long stamp121 = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + " get write lock2,stamp=" + stamp121);
        //释放写锁
        stampedLock.unlock(stamp121);
        System.out.println(Thread.currentThread().getName() + " unlock write lock2,stamp=" + stamp121);
    });

    try {
        // 睡眠3秒,然后再释放读锁
        Thread.sleep(3000);
    } catch (InterruptedException e) {
    }
    System.out.println(Thread.currentThread().getName() + " unlock read lock1,stamp=" + stamp12);
    // 释放读锁
    stampedLock.unlockRead(stamp12);
}

运行结果

main get read lock1,stamp=257
ForkJoinPool.commonPool-worker-1 run
main unlock read lock1,stamp=257
ForkJoinPool.commonPool-worker-1 get write lock2,stamp=384
ForkJoinPool.commonPool-worker-1 unlock write lock2,stamp=384

StampedLock 悲观读锁:readLock总结

悲观锁认为数据是极有可能被修改的,所以在使用数据之前都需要先加锁,锁未释放之前如果有其他线程想要修改数据(加写锁)就必须阻塞它。

StampedLock 乐观读锁:tryOptimisticRead

tryOptimisticRead通过名字来记忆很简单,try代表尝试,说明它是无阻塞的。Optimistic乐观的,Read代表读锁。
乐观锁认为数据不会轻易的被修改,因此在操作数据前并没有加锁(使用CAS方式更新锁的状态),而是采用试探的方式。只要当前没有写锁就可以获得一个非0的stamp,如果已经存在写锁则返回一个为0的stamp。
由于没有使用CAS方法,也没有真正的加锁,所以并发性能要比readLock还要高。但是由于没有使用真正的锁,如果数据中途被修改,就会造成数据不一致问题。
特别适用于读多写少的高并发场景。

StampedLock tryOptimisticRead:乐观读锁的简单使用

乐观读锁的使用要分为两步,第一步是试探,第二步是验证。
tryOptimisticRead与validate一定要紧紧挨着使用,否则在获取和验证之间很可能数据被修改。

public static void StampedOptimisticReadExample1() {
    StampedLock stampedLock = new StampedLock();
    //尝试获取乐观读锁,由于当前没有任何线程,所以获取成功,获得非0的stamp
    long stamp = stampedLock.tryOptimisticRead();
    System.out.println("获取乐观锁,stamp=" + stamp);
    //验证从获取乐观锁,到该运行点为止,锁是否发生过变化
    if (stampedLock.validate(stamp)) {
        System.out.println("验证乐观锁成功,stampedLock.validate(stamp)=" + true);
    } else {
        System.out.println("验证乐观锁失败,stampedLock.validate(stamp)=" + false);
    }
}

运行结果

获取乐观锁,stamp=256
验证乐观锁成功,stampedLock.validate(stamp)=true

StampedLock tryOptimisticRead:乐观读锁同线程不阻塞示例1

获取乐观锁前若是某个线程已经获取了写锁,这时候再尝试获取乐观锁也是可以获取的,只是得到的stamp为0,并且无法通过validate验证。
虽然已经有线程已经获取了读锁,并且获取乐观锁会失败,但是方法并不会阻塞

public static void StampedOptimisticReadExample2() {
    StampedLock stampedLock = new StampedLock();
    long writeStamp = stampedLock.writeLock();
    System.out.println("获取写锁,stamp=" + writeStamp);
    //尝试获取乐观读锁,由于写锁未释放,获得的stamp为0
    long stamp = stampedLock.tryOptimisticRead();
    System.out.println("获取乐观锁,stamp=" + stamp);
    //因为stamp = 0,验证肯定是false
    if (stampedLock.validate(stamp)) {
        System.out.println("验证乐观读锁成功,stampedLock.validate(stamp)=" + true);
    } else {
        System.out.println("验证乐观读锁失败,stampedLock.validate(stamp)=" + false);
    }
    stampedLock.unlockWrite(writeStamp);
    System.out.println("释放写锁,stamp=" + writeStamp);
}

运行结果

获取写锁,stamp=384
获取乐观锁,stamp=0
验证乐观读锁失败,stampedLock.validate(stamp)=false
释放写锁,stamp=384

StampedLock tryOptimisticRead:乐观读锁同线程不阻塞示例2

若是首次获得乐观锁成功,然后获得写锁,这时再验证,则会验证失败
若获取的乐观锁和验证乐观锁期间 锁发生变化 则validate返回false,否则返回true。

public static void StampedOptimisticReadExample3() {
    StampedLock stampedLock = new StampedLock();
    // 尝试获取乐观读锁,由于当前没有任何线程,所以获取成功,获得非0的stamp
    long stamp = stampedLock.tryOptimisticRead();
    System.out.println("获取乐观锁,stamp=" + stamp);
    long writeStamp = stampedLock.writeLock();
    System.out.println("获取写锁,stamp=" + writeStamp);
    // 验证从获取乐观锁,到该运行点为止,锁是否发生过变化
    if (stampedLock.validate(stamp)) {
        System.out.println("验证乐观读锁成功,stampedLock.validate(stamp)=" + true);
    } else {
        System.out.println("验证乐观读锁失败,stampedLock.validate(stamp)=" + false);
    }
    stampedLock.unlockWrite(writeStamp);
    System.out.println("释放写锁成功,stamp=" + writeStamp);
}

运行结果

获取乐观锁,stamp=256
获取写锁,stamp=384
验证乐观读锁失败,stampedLock.validate(stamp)=false
释放写锁成功,stamp=384

StampedLock tryOptimisticRead:乐观读锁不同线程不阻塞示例

如果某个线程已经获取了写锁,这时候再尝试获取乐观锁也是可以获取的,只是得到的stamp为0,无法通过validate验证。
如果因为其他线程增加写锁,则会导致stamp发生变化,从而validate失败。这种情况下需要重新获取乐观读锁。

public static void StampedOptimisticReadExample4() {
    StampedLock stampedLock = new StampedLock();
    //尝试获取乐观读锁,由于当前没有任何线程,所以获取成功,获得非0的stamp
    long stamp = stampedLock.tryOptimisticRead();
    System.out.println(Thread.currentThread().getName() + " tryOptimisticRead stamp=" + stamp);
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " run");
            //获取写锁成功,因为乐观读锁本质上并不加锁。
            long writeLockStamp = stampedLock.writeLock();
            System.out.println(Thread.currentThread().getName() + " get writeLock1,stamp=" + writeLockStamp);
            //释放写锁
            stampedLock.unlockWrite(writeLockStamp);
            System.out.println(Thread.currentThread().getName() + " unlock writeLock1,stamp=" + writeLockStamp);
        }
    }).start();
    try {
        //睡眠3秒钟,让线程Thread-0先执行起来。
        Thread.sleep(3000);
    } catch (InterruptedException e) {
    }
    //验证stamp是否发生过变化,此处由于写锁导致数据已经发生变化,所以stamp验证为false
    boolean validate = stampedLock.validate(stamp);
    System.out.println(Thread.currentThread().getName() + " tryOptimisticRead validate=" + validate);
    //此时写锁已经释放,再次尝试获取乐观读锁
    stamp = stampedLock.tryOptimisticRead();
    //验证stamp没有发生变化,返回true
    validate = stampedLock.validate(stamp);
    System.out.println(Thread.currentThread().getName() + " tryOptimisticRead validate=" + validate);
}

运行结果

main tryOptimisticRead stamp=256
Thread-0 run
Thread-0 get writeLock1,stamp=384
Thread-0 unlock writeLock1,stamp=384
main tryOptimisticRead validate=false
main tryOptimisticRead validate=true

StampedLock 乐观读锁:tryOptimisticRead总结

乐观锁本质上并未加锁,而是提供了获取和检测的方法,由程序人员来控制该做些什么。
虽然性能大大提升,但是也增加了开发人员的复杂度,如果不是特别高的并发场景,对性能不要求极致,可以不考虑使用。

posted @ 2022-06-08 11:44  踏步  阅读(1130)  评论(2编辑  收藏  举报