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