第十三章 ReentrantLock、ReentrantReadWriteLock、StampedLock 讲解

13.1 关于锁的面试题

  • 你知道 Java 里面有哪些锁?

  • 你说你用过读写锁,锁饥饿问题是什么?

  • 有没有比读写锁更快的锁?

  • StampedLock 知道吗?(邮戳锁/票据锁)

  • ReentrantReadWriteLock 有锁降级机制,你知道吗?

13.2 本章路线总纲

无锁 -> 独占锁 -> 读写锁 -> 邮戳锁

13.3 简单聊聊 ReentrantReadWriteLock

13.3. 1 是什么?

读写锁说明

一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程

一体两面,读写互斥,读读共享

再说说演变

无锁无序 -> 加锁 -> 读写锁演变复习

读写锁意义和特点

它只允许读读共存,而读写和写写依然是互斥的,大多实际场景是“读/读”线程间并不存在互斥关系

只有“读/写”线程或“写/写”线程间的操作需要互斥的。因此引入 ReentrantReadWriteLock

一个 ReentrantReadWriteLock 同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。也即一个资源可以被多个读操作访问或一个写操作访问,两者不能同时进行

只有在读多写少情景之下,读写锁才具有较高的性能体现

13.3.2 特点

  • 可重入
  • 读写兼顾

代码演示

ReentrantLock 存在的问题,即读读不共享,影响效率

public class ReentrantReadWriteLockDemo {

    /**
     * 运行结果:
     * 0	 正在写入
     * 0	 写入完成
     * 1	 正在写入
     * 1	 写入完成
     * ...
     * 0	 正在读取
     * 0	 读取完成
     * 1	 正在读取
     * 1	 读取完成
     * ...
     */
    public static void main(String[] args) {
        MyResource myResource = new MyResource();
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                myResource.write(finalI + "", finalI + "");
            }, String.valueOf(i)).start();
        }
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                myResource.read(finalI + "");
            }, String.valueOf(i)).start();
        }
    }

}

//资源类,模拟一个简单的缓存
class MyResource {

    Map<String, String> map = new HashMap<>();
    //====ReentrantLock 等价于 ==== synchronized
    Lock lock = new ReentrantLock();

    public void write(String key, String value) {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在写入");
            map.put(key, value);
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t 写入完成");
        } finally {
            lock.unlock();
        }
    }

    public void read(String key) {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在读取");
            String result = map.get(key);
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t 读取完成");
        } finally {
            lock.unlock();
        }

    }

}

使用 ReentrantReadWriteLock,解决读读共享问题

public class ReentrantReadWriteLockDemo {

    /**
     * 运行结果:
     * 0	 正在写入
     * 0	 写入完成
     * 1	 正在写入
     * 1	 写入完成
     * ...
     * 3	 正在读取
     * 4	 正在读取
     * 5	 正在读取
     * 9	 正在读取
     * 5	 读取完成
     * 3	 读取完成
     * 4	 读取完成
     * 9	 读取完成
	 * ...
     */
    public static void main(String[] args) {
        MyResource myResource = new MyResource();
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                myResource.write(finalI + "", finalI + "");
            }, String.valueOf(i)).start();
        }
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                myResource.read(finalI + "");
            }, String.valueOf(i)).start();
        }
    }

}

//资源类,模拟一个简单的缓存
class MyResource {

    Map<String, String> map = new HashMap<>();
    //====ReentrantReadWriteLock 一体两面,读写互斥,读读共享
    ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void write(String key, String value) {
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在写入");
            map.put(key, value);
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t 写入完成");
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public void read(String key) {
        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在读取");
            String result = map.get(key);
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t 读取完成");
        } finally {
            rwLock.readLock().unlock();
        }

    }

}

读没有完成时,其他线程写锁无法获得

public class ReentrantReadWriteLockDemo {

    /**
     * 运行结果:
     * 0	 正在写入
     * 0	 写入完成
     * 2	 正在写入
     * 2	 写入完成
     * ...
     * 0	 正在读取
     * 3	 正在读取
     * ...
     * 3	 读取完成
     * 0	 读取完成
     * ...
     * 新写锁线程:1	 正在写入
     * 新写锁线程:1	 写入完成
     * 新写锁线程:0	 正在写入
     * 新写锁线程:0	 写入完成
     * 新写锁线程:2	 正在写入
     * 新写锁线程:2	 写入完成
     */
    public static void main(String[] args) {
        MyResource myResource = new MyResource();
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                myResource.write(finalI + "", finalI + "");
            }, String.valueOf(i)).start();
        }
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                myResource.read(finalI + "");
            }, String.valueOf(i)).start();
        }
        //暂停 1 秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 3; i++) {
            int finalI = i;
            new Thread(() -> {
                myResource.write(finalI + "", finalI + "");
            }, "新写锁线程:" + String.valueOf(i)).start();
        }
    }

}

//资源类,模拟一个简单的缓存
class MyResource {

    Map<String, String> map = new HashMap<>();
    //====ReentrantReadWriteLock 一体两面,读写互斥,读读共享
    ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void write(String key, String value) {
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在写入");
            map.put(key, value);
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t 写入完成");
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public void read(String key) {
        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t 正在读取");
            String result = map.get(key);
            //演示读锁没有完成之前,写锁无法获得
            try {
                TimeUnit.MILLISECONDS.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t 读取完成");
        } finally {
            rwLock.readLock().unlock();
        }

    }

}

结论:一体两面,读写互斥,读读共享,读没有完成的时候其他线程写锁无法获得

出现写锁饥饿的问题

锁降级

从写锁 -> 读锁,ReentrantReadWriteLock 可以降级

《Java 并发编程的艺术》中关于锁降级的说明

ReentrantReadWriteLock 锁降级:将写入锁降级为读锁(类似 Linux 文件读写权限理解,就像写权限要高于读权限一样),锁的严苛程度变强叫做升级,反之叫做降级

特性 说明
公平性选择 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
重进入 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁
锁降级 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁

写锁的降级,降级成为了读锁

  1. 如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁
  2. 规则惯例,先获取写锁,然后获取读锁,再释放写锁的次序
  3. 如果释放了写锁,那么就完全转换为读锁

锁降级:遵循获取写锁 -> 再获取读锁 -> 再释放写锁的次序,写锁能够降级成为读锁

如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁

image

重入还允许通过获取写入锁,然后读取锁然后释放写锁,从写锁到读取锁。但是,从读锁定升级到写锁是不可能的

锁降级是为了让当前线程感知到数据的变化,目的是保证数据的可见性

读写锁降级演示

正常情况

public class LockDownGradingDemo {

    /**
     * 运行结果:
     * ----读取
     * ----写入
     */
    public static void main(String[] args) {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        //正常 A B 两个线程
        //A
        readLock.lock();
        System.out.println("----读取");
        readLock.unlock();
        //B
        writeLock.lock();
        System.out.println("----写入");
        writeLock.unlock();

    }

}

锁降级,写锁降级为读锁

/**
 * 锁降级:遵循获取写锁->再获取读锁->再释放写锁的次序,写锁能够降级成为读锁
 * 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁
 */
public class LockDownGradingDemo {

    /**
     * 运行结果:
     * ----写入
     * ----读取
     */
    public static void main(String[] args) {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        //本例,only one 同一个线程
        writeLock.lock();
        System.out.println("----写入");

        readLock.lock();
        System.out.println("----读取");

        writeLock.unlock();

        readLock.unlock();

    }

}

读没有完成时候写锁无法获得锁,必须要等着读锁读完后才有机会写

/**
 * 锁降级:遵循获取写锁->再获取读锁->再释放写锁的次序,写锁能够降级成为读锁
 * 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁
 * 读没有完成时候写锁无法获得锁,必须要等着读锁读完后才有机会写
 */
public class LockDownGradingDemo {

    /**
     * 运行结果:
     * ----读取
     * 程序阻塞!
     */
    public static void main(String[] args) {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        /*//正常 A B 两个线程
        //A
        readLock.lock();
        System.out.println("----读取");
        readLock.unlock();
        //B
        writeLock.lock();
        System.out.println("----写入");
        writeLock.unlock();*/

        //本例,only one 同一个线程
        readLock.lock();
        System.out.println("----读取");

        writeLock.lock();
        System.out.println("----写入");

        writeLock.unlock();

        readLock.unlock();

    }

}

结论:如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略

线程获取读锁是不能直接升级为写入锁的

在 ReentrantReadWriteLock 中,当读锁被使用时,如果有线程尝试获取写锁,该线程会被阻塞。所以需要释放所有读锁,才可获取写锁

写锁和读锁是互斥的

写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作

因此,分析读写锁 ReentrantReadWriteLock,会发现它有个潜在的问题:

读锁结束,写锁有望;写锁独占,读写全堵

如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁

即 ReentrantReadWriteLock 读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,也就是写入必须等待,这是一种悲观的读锁,人家还在读着呢,你先别去写,省的数据乱

13.3.3 读写锁之后读写规矩,解释为什么要锁降级

ReentrantWriteReadLock 源码总结

class CacheData {
    
    Object data;
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock;

    void processCachedData() {
        // 第1步: 获取读锁,检查缓存是否有效
        rwl.readLock().lock();
        if (!cacheValid) {
            //  关键: 必须先释放读锁,才能获取写锁
            rwl.readLock().unlock();

            // 第2步: 获取写锁,更新缓存
            rwl.writeLock().lock();   //
            try {
                // 双重检查: 其他线程可能已经更新了缓存
                if (!cacheValid) {
                    data = ...;              // 从数据库加载数据
                    cacheValid = true;       // 标记缓存有效
                }

                // 第3步: 锁降级 - 在释放写锁前获取读锁
                rwl.readLock().lock();
            } finally {
                // 第4步: 释放写锁,但仍持有读锁
                rwl.writeLock().unlock();
            }
        }

        // 第5步: 使用数据(此时持有读锁)
        try {
            use(data);
        } finally {
            // 第6步: 最终释放读锁
            rwl.readLock().unlock();
        }
    }
    
}
  1. 代码中声明了一个 volatile 类型的 cacheValid 变量,保证其可见性

  2. 首先获取读锁,如果 cache 不可用,则释放读锁。获取写锁,在更改数据之前,在检查一次 cacheValid 的值,然后修改数据,将 cacheValid 置为 true,然后在释放写锁前立刻抢夺获取读锁;此时,cache 中数据可用,处理 cache 中数据,最后释放读锁。这个过程就是一个完整的锁降级的过程,目的是保证数据可见性

总结:一句话,同一个线程自己持有写锁时再去拿读锁,其本质相当于重入

如果违背锁降级的步骤

如果当前的线程 C 在修改完 cache 中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另外一个线程 D 获取了写锁并且修改了数据,那么 C 线程无法感知到数据已被修改,则数据出现错误

如果遵循锁降级的步骤

线程 C 在释放写锁之前获取读锁,那么线程 D 在获取写锁时将被阻塞,直到线程 C 完成数据处理过程,释放读锁。这样可以保证返回的数据是这次更新的数据,该机制是专门为了缓存设计的

有没有比读写锁更快的锁?

13.4 邮戳锁 StampedLock

无锁 -> 独占锁 -> 读写锁 -> 邮戳锁

13.4.1 是什么?

StampedLock 是 JDK1.8 中新增的一个读写锁,也是对 JDK1.5 中的读写锁 ReentrantReadWriteLock 的优化

邮戳锁,也叫票据锁

stamp(戳记,long 类型)

代表了锁的状态。当 stamp 返回零的时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的 stamp 值

13.4.2 它是由锁饥饿问题引出

锁饥饿问题

ReentrantReadWriteLock 实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了

假如当前 1000 个线程,999 个读,1 个写,有可能 999 个读线程长时间抢到了锁,那 1 个写线程就悲剧了

因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写

如何缓解锁饥饿问题?

使用“公平”策略可以一定程度上解决这个问题,如:new ReentrantReadWriteLock(true)

但是“公平”策略是以牺牲系统吞吐量为代价的

StampedLock 类的乐观读锁闪亮登场

  • ReentrantReadWriteLock:

    允许多个线程同时读,但是只允许一个线程写,再线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的 synchronized 速度要快很多,原因就是在于 ReentrantReadWriteLock 支持读并发,读读可以共享

  • StampedLock 横空出世:

    ReentrantReadWriteLock 的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。但是,StampedLcock 采用乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验

13.4.3 StampedLock 的特点

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

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

StampLock 是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)

Stamped 有三种访问模式

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

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

  3. Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式

读的过程中也允许获取写锁介入

13.4.4 乐观读模式 Code 演示

传统的读写模式

读的时候写锁不能获取

写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥

/**
 * Stamped = ReentrantReadWriteLock + 读的过程中也允许获取写锁介入
 */
public class StampedLockDemo {

    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write() {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
        try {
            number = number + 13;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程修改完成");
    }

    public void read() {
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "读线程准备读取,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 result = number;
            System.out.println(Thread.currentThread().getName() + "\t" + "读线程读取结果:" + result);
            System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥");
        } finally {
            stampedLock.unlockRead(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "读线程读取完成");
    }

    /**
     * 运行结果:
     * readThread	读线程准备读取,4 秒钟后继续
     * readThread	读线程正在读取中...
     * writeThread	已进入写线程
     * readThread	读线程正在读取中...
     * readThread	读线程正在读取中...
     * readThread	读线程正在读取中...
     * readThread	读线程读取结果:37
     * 写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥
     * readThread	读线程读取完成
     * writeThread	写线程准备修改
     * writeThread	写线程修改完成
     * main	number=50
     */
    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();
        new Thread(() -> {
            resource.read();
        }, "readThread").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "已进入写线程");
            resource.write();
        }, "writeThread").start();

        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "number=" + number);

    }

}

乐观读模式

读的过程中也允许获取写锁介入

/**
 * Stamped = ReentrantReadWriteLock + 读的过程中也允许获取写锁介入
 */
public class StampedLockDemo {

    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write() {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
        try {
            number = number + 13;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程修改完成");
    }

    //悲观读,没有完成的时候写锁无法获得锁
    public void read() {
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "读线程准备读取,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 result = number;
            System.out.println(Thread.currentThread().getName() + "\t" + "读线程读取结果:" + result);
            System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥");
        } finally {
            stampedLock.unlockRead(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "读线程读取完成");
    }

    //乐观读,读的时候也运行获取写锁介入
    public void tryOptimisticRead() {
        long stamp = stampedLock.tryOptimisticRead();
        int result = number;
        //故意间隔 4 秒钟,很乐观认为读取中没有其他线程修改过 number 值,具体靠判断
        System.out.println("4 秒前 stampedLock.validate 方法值(true 无修改,false 有修改)" + "\t" + stampedLock.validate(stamp));
        for (int i = 0; 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 = number;
                System.out.println("重新悲观读后,result:" + result);
            } finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "finally value:" + result);
    }

    /**
     * 运行结果:
     * 4 秒前 stampedLock.validate 方法值(true 无修改,false 有修改)	true
     * readThread	读线程正在读取中...0秒后stampedLock.validate 方法值(true 无修改,false 有修改)	true
     * writeThread	已进入写线程
     * writeThread	写线程准备修改
     * writeThread	写线程修改完成
     * readThread	读线程正在读取中...1秒后stampedLock.validate 方法值(true 无修改,false 有修改)	false
     * readThread	读线程正在读取中...2秒后stampedLock.validate 方法值(true 无修改,false 有修改)	false
     * readThread	读线程正在读取中...3秒后stampedLock.validate 方法值(true 无修改,false 有修改)	false
     * 有人修改过----有写操作
     * 从乐观读 升级为 悲观读
     * 重新悲观读后,result:50
     * readThread	finally value:50
     */
    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();

        new Thread(() -> {
            resource.tryOptimisticRead();
        }, "readThread").start();
        //暂停 2 秒钟线程,读过程可以写介入,演示
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "已进入写线程");
            resource.write();
        }, "writeThread").start();

    }

}

将中间的暂停时间由 2 秒改成 6 秒

/**
 * Stamped = ReentrantReadWriteLock + 读的过程中也允许获取写锁介入
 */
public class StampedLockDemo {

    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write() {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
        try {
            number = number + 13;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程修改完成");
    }

    //悲观读,没有完成的时候写锁无法获得锁
    public void read() {
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "读线程准备读取,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 result = number;
            System.out.println(Thread.currentThread().getName() + "\t" + "读线程读取结果:" + result);
            System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥");
        } finally {
            stampedLock.unlockRead(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "读线程读取完成");
    }

    //乐观读,读的时候也运行获取写锁介入
    public void tryOptimisticRead() {
        long stamp = stampedLock.tryOptimisticRead();
        int result = number;
        //故意间隔 4 秒钟,很乐观认为读取中没有其他线程修改过 number 值,具体靠判断
        System.out.println("4 秒前 stampedLock.validate 方法值(true 无修改,false 有修改)" + "\t" + stampedLock.validate(stamp));
        for (int i = 0; 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 = number;
                System.out.println("重新悲观读后,result:" + result);
            } finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "finally value:" + result);
    }

    /**
     * 运行结果:
     * 4 秒前 stampedLock.validate 方法值(true 无修改,false 有修改)	true
     * readThread	读线程正在读取中...0秒后stampedLock.validate 方法值(true 无修改,false 有修改)	true
     * readThread	读线程正在读取中...1秒后stampedLock.validate 方法值(true 无修改,false 有修改)	true
     * readThread	读线程正在读取中...2秒后stampedLock.validate 方法值(true 无修改,false 有修改)	true
     * readThread	读线程正在读取中...3秒后stampedLock.validate 方法值(true 无修改,false 有修改)	true
     * readThread	finally value:37
     * writeThread	已进入写线程
     * writeThread	写线程准备修改
     * writeThread	写线程修改完成
     */
    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();

        new Thread(() -> {
            resource.tryOptimisticRead();
        }, "readThread").start();
        //暂停 6 秒钟线程
        try {
            TimeUnit.SECONDS.sleep(6);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t" + "已进入写线程");
            resource.write();
        }, "writeThread").start();

    }

}

13.4.5 StampedLock 的缺点

  • StampedLock 不支持重入,没有 Re 开头
  • StampedLock 的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意
  • 使用 StampedLock 一定不要调用中断操作,即不要调用 interrupt() 方法
posted @ 2026-04-27 09:50  清风含薰  阅读(2)  评论(0)    收藏  举报