Java多线程之StampedLock

ProjectDaedalus

共 1139字,需浏览 3分钟

 ·

2021-12-13 15:39

这里就JUC包中的StampedLock做相关介绍

abstract.jpeg

概述

在读多写少的场景下,非常适合使用ReentrantReadWriteLock读写锁。但其也存在一定的弊端,其有可能导致写线程饥饿。为此JDK 8中提供了StampedLock类,其是一个非公平的读写锁。其与ReentrantReadWriteLock相比,不仅提供了传统意义上的悲观读锁和写锁,最大的区别是其还为读操作提供了乐观锁的方法——即所谓的乐观读锁。当然其也有弊端,无论是悲观读锁还是写锁,均不支持条件变量Condition;然后从类名也可以看到其是不可重入锁。需要注意的是,虽然一个线程可以多次获取悲观读锁,但究其原因是因为悲观读锁是共享锁。实际实践中,可以直接通过writeLock、readLock等阻塞式 或 tryWriteLock、tryReadLock等非阻塞式的方式获取锁,也可通过ReadLockView读锁视图、WriteLockView写锁视图、ReadWriteLockView读写锁视图来进行相应锁的操作

基本实践

读锁、写锁

这里就基本的悲观读锁、写锁的使用进行实践,示例如下所示

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class StampedLockTest1 {

    private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");

    private static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    private static StampedLock stampedLock = new StampedLock();

    private static Integer count;

    /**
     * 测试: 读锁为共享锁
     */

    @Test
    public void test1() {
        System.out.println("\n---------------------- Test 1 ----------------------");

        count = 100;
        for(int i=1; i<5; i++) {
            Runnable runnable = new ReadTask("Task"+i);
            threadPool.execute( runnable );
        }

        // 主线程等待所有任务执行完毕
        try{ Thread.sleep( 10*1000 ); } catch (Exception e) {}
    }

    /**
     * 测试: 写锁为独占锁
     */

    @Test
    public void test2() {
        System.out.println("\n---------------------- Test 2 ----------------------");

        count = 200;
        for(int i=1; i<5; i++) {
            Runnable runnable = new WriteTask("Task"+i);
            threadPool.execute( runnable );
        }

        // 主线程等待所有任务执行完毕
        try{ Thread.sleep( 10*1000 ); } catch (Exception e) {}
    }

    /**
     * 测试: 读写互斥
     */

    @Test
    public void test3() {
        System.out.println("\n---------------------- Test 3 ----------------------");

        count = 300;
        for(int i=1; i<9; i++) {
            Runnable task = null;
            Boolean isReadTask = RandomUtils.nextBoolean();
            if( isReadTask ) {
                task = new ReadTask2("读任务 #"+i);
            } else {
                task = new WriteTask2("写任务 #"+i);
            }
            threadPool.execute( task );
        }
        // 主线程等待所有任务执行完毕
        try{ Thread.sleep( 20*1000 ); } catch (Exception e) {}
    }

    /**
     * 打印信息
     * @param msg
     */

    public static void info(String msg) {
        String time = formatter.format(LocalTime.now());
        String log = "["+time+"] " + msg;
        System.out.println(log);
    }

    @AllArgsConstructor
    private static class ReadTask implements Runnable {

        private String taskName;

        @Override
        public void run() {
            Integer localData = null;
            long stamp = stampedLock.readLock();
            try{
                info(taskName + ": 成功获取读锁, stamp: " + stamp);
                localData = count;
            } catch (Exception e) {
                info(taskName+"Happen Exception");
            } finally {
                info(taskName + ": 释放读锁, stamp: "+stamp+", localData: "+localData);
                stampedLock.unlockRead(stamp);
            }
        }
    }

    @AllArgsConstructor
    private static class WriteTask implements Runnable {

        private String taskName;

        @Override
        public void run() {
            long stamp = stampedLock.writeLock();
            try {
                info(taskName + ": 成功获取写锁, stamp: " + stamp);
                count++;
            } catch (Exception e) {
                info(taskName+"Happen Exception");
            } finally {
                info(taskName + ": 释放写锁, stamp: "+stamp+", count: " + count);
                stampedLock.unlockWrite(stamp);
            }
        }
    }

    @AllArgsConstructor
    private static class ReadTask2 implements Runnable {

        private String taskName;

        @Override
        public void run() {
            Integer localData = null;
            Lock readLock = stampedLock.asReadLock();
            readLock.lock();
            try{
                info(taskName + ": 成功获取读锁");
                localData = count;
            } catch (Exception e) {
                info(taskName+"Happen Exception");
            } finally {
                info(taskName + ": 释放读锁, localData: "+localData+"\n");
                readLock.unlock();
            }
        }
    }

    @AllArgsConstructor
    private static class WriteTask2 implements Runnable {

        private String taskName;

        @Override
        public void run() {
            Lock writeLock = stampedLock.asWriteLock();
            writeLock.lock();
            try {
                info(taskName + ": 成功获取写锁");
                count++;
            } catch (Exception e) {
                info(taskName+"Happen Exception");
            } finally {
                info(taskName + ": 释放写锁, count: " + count +"\n");
                writeLock.unlock();
            }
        }
    }
}

测试结果如下所示,符合预期

figure 1.jpeg

可以看到StampedLock获取锁、释放锁都需要相应的stamp值。为此也可以通过相应的视图类进行操作,如上述代码的test3所示。其相应测试结果所示。可以看到悲观读锁是一个共享锁,而写锁则是一个互斥锁

figure 2.jpeg

乐观读锁

可通过tryOptimisticRead获取一个stamp,即所谓的乐观读锁。然后在完成读操作后,通过validate方法对stamp进行检查。由于读过程通常是非原子性的,故需要判断是否存在其他线程在此期间获取到了写锁,对数据进行了修改。造成当前线程读取的数据状态不一致(部分为修改前的,部分为修改后的)。如果在当前线程进行读的过程中发生了修改更新,则检查结果为false。这时再获取悲观读锁进行重读。事实上,由于乐观读锁并没有锁。故其一方面不会阻塞写线程获取写锁,也不需要在结束后释放该锁。示例代码如下所示

public class StampedLockTest2 {

    private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");

    private static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    private static StampedLock stampedLock = new StampedLock();

    private static Integer count;

    /**
     * 测试: 乐观读锁
     */

    @Test
    public void test1() {
        count = 500;

        threadPool.execute( new ReadTask("读任务 #1",0) );
        threadPool.execute( new ReadTask("读任务 #2",0) );
        threadPool.execute( new ReadTask("读任务 #33",2*1000) );
        threadPool.execute( new ReadTask("读任务 #44",2*1000) );

        try{ Thread.sleep( 1000 ); } catch (Exception e) {}
        threadPool.execute( new WriteTask("写任务 #55") );

        // 主线程等待所有任务执行完毕
        try{ Thread.sleep( 20*1000 ); } catch (Exception e) {}
    }

    /**
     * 打印信息
     * @param msg
     */

    public static void info(String msg) {
        String time = formatter.format(LocalTime.now());
        String log = "["+time+"] " + msg;
        System.out.println(log);
    }

    @AllArgsConstructor
    private static class ReadTask implements Runnable {

        private String taskName;

        private Integer sleepTime;

        @Override
        public void run() {
            long stamp = stampedLock.tryOptimisticRead();
            info(taskName + ": 成功获取乐观读锁, stamp: "+stamp);
            // 读取数据
            Integer localData = count;
            // 模拟业务耗时
            try{ Thread.sleep(sleepTime); } catch (Exception e) {}
            info(taskName + ":localData: "+localData);

            // 检查在获取乐观锁后, 是否被写锁获得过
            if( !stampedLock.validate(stamp) ) {
                info(taskName+": 数据被其他线程修改需重读");
                stamp = stampedLock.readLock();
                info(taskName + ": 成功获取读锁, stamp: " + stamp);
                try{
                    // 模拟业务耗时
                    try{ Thread.sleep(500); } catch (Exception e) {}
                } catch (Exception e) {
                    info(taskName+"Happen Exception");
                } finally {
                    info(taskName + ": 释放读锁, stamp: "+stamp+", count: " + count+"\n");
                    stampedLock.unlockRead(stamp);
                }
            }
        }
    }

    @AllArgsConstructor
    private static class WriteTask implements Runnable {

        private String taskName;

        @Override
        public void run() {
            long stamp = stampedLock.writeLock();
            try {
                info(taskName + ": 成功获取写锁, stamp: " + stamp);
                count++;
            } catch (Exception e) {
                info(taskName+"Happen Exception");
            } finally {
                info(taskName + ": 释放写锁, stamp: "+stamp+", count: " + count +"\n");
                stampedLock.unlockWrite(stamp);
            }
        }
    }
}

测试结果如下所示

figure 3.jpeg

参考文献

  1. Java并发编程之美 翟陆续、薛宾田著
浏览 25
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报