多线程源码学习-StampedLock(邮戳锁)

649 阅读2分钟
  • 读写锁定义

一个资源可以被多个读线程访问,或者被一个写线程访问,但不能同时存在读线线程。(读读共享,读写互斥)

  • 锁降级

遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。《java并发编程的艺术》

可以理解为,写的权力>读的权力,写锁可以降级为读锁,但是读锁无法变成写锁。

image.png

锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性(写后立刻读,感知数据变化)。在ReentrantReadWriteLock中,当读锁被使用时,有线程尝试获得写锁将会被阻塞,直到读锁被释放,才会获得写锁。

public class LockDownGradingDemo {
    public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

        writeLock.lock();
        System.out.println("Hello");

        readLock.lock();
        System.out.println("World");
        
        writeLock.unlock();
        readLock.unlock();
    }
}

image.png

public class LockDownGradingDemo {
    public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        
        readLock.lock();
        System.out.println("World");
        
        writeLock.lock();
        System.out.println("Hello");

        readLock.unlock();
        writeLock.unlock();
    }
}

image.png

  • StampedLock

JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化。 StampedLock可以读的过程中,可以写。(可能出现数据不一致现象)

  • StampedLock的特点

1、所有获得锁的方法,都返回一个邮戳(stamp),stamp为零表示获取失败,其余都表示获取成功。

2、所有释放锁的方法,都需要一个邮戳(stamp),这个stamp必须是和成功获取锁时得到的stamp一致。

3、StampedLock是不可重入的(线程持有写锁,再去获得写锁就会造成死锁)。

  • StampedLock三种访问模式

1、Reading(读模式):功能和ReentrantReadWriteLock的读锁类似

2、Writing(写模式):功能和ReentrantReadWriteLock的写锁类似

3、Optimistic reading(乐观读模式):无锁机制,乐观认为读取时没人修改,假如被修改才进行锁升级。

public class StampedLockDemo {
    static int num=1;
    static StampedLock stampedLock=new StampedLock();

    public void read(){
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName()+" "+"读线程准备");

        //暂停4秒钟线程
        for (int i = 0; i <4 ; i++) {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t 正在读取中......");
        }

        try{
            int res=num;
            System.out.println(Thread.currentThread().getName()+" "+" 获得成员变量值res:" + res);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            stampedLock.unlock(stamp);
        }
        System.out.println(Thread.currentThread().getName()+" "+"读线程结束"+" "+"stamp: "+stamp);
    }

    public void write(){
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName()+" "+"写线程准备修改");
        try {
            num=num+11;
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            stampedLock.unlock(stamp);
        }
        System.out.println(Thread.currentThread().getName()+" "+"写线程结束"+" "+"stamp: "+stamp);
    }

    public void optimistic(){
        long stamp = stampedLock.tryOptimisticRead();
        //先把数据取得一次
        int result = num;
        //间隔4秒钟,我们很乐观的认为没有其他线程修改过number值。
        System.out.println("4秒前stampedLock.validate值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
        for (int i = 1; i <=4 ; i++) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"\t 正在读取中......"+i+
                    "秒后stampedLock.validate值(true无修改,false有修改)"+"\t"
                    +stampedLock.validate(stamp));
        }
        if(!stampedLock.validate(stamp)) {
            System.out.println("有人动过--------存在写操作!");
            //有人动过了,需要从乐观读切换到普通读的模式。
            stamp = stampedLock.readLock();
            try {
                System.out.println("从乐观读 升级为 悲观读并重新获取数据");
                //重新获取数据
                result = num;
                System.out.println("重新悲观读锁通过获取到的成员变量值result:" + result);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName()+"\t finally value: "+result+" "+"stamp: "+stamp);
    }

    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();
        
        //乐观读,失败,重新转为悲观读,重读数据一次
        new Thread(() -> {
            //乐观读
            resource.optimistic();
        },"readThread").start();

        //2秒钟乐观读取resource.tryOptimisticRead()失败
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }


        new Thread(() -> {
            resource.write();
        },"writeThread").start();
    }
}

image.png

  • StampedLock的缺点

1、StampedLock不支持可重入

2、悲观读和写锁不支持条件变量(Condition),即signal或者await之类的条件。

3、StampedLock不能在调用中中断,即不要调用interrput()方法。