【问题标题】:Locking a method until another is done锁定一个方法直到另一个方法完成
【发布时间】:2016-10-14 19:52:29
【问题描述】:

我必须调用一个需要验证令牌的外部 API。调用 API 的应用程序将被线程化。此外,只允许 5 个并发连接。我将使用固定线程池进行连接,但在弄清楚如何处理过期/无效令牌时遇到了问题。我想要做的是,当一个线程遇到过期令牌时,阻止其他线程获取令牌,直到它被刷新。我正在考虑使用 ReentrantLock 来执行此操作,但我不确定我的实现是否正确。

public static void main(String[] args){

    for(int i = 0; i < 100; i++){
        new Thread(new LockTest()).start();
    }
}

public void testLock(String message) throws InterruptedException{ 
    try{
        getToken(message);

        /*
         * Use token here
         */
        Thread.sleep(1000);

        Random r = new Random();
        int num = r.nextInt((25-0) + 1);

        if(num == 1){ //testing only - exception thrown randomly.
            throw new Exception("Token Expired!");
        }

        System.out.println("Message: " +  message);

    }catch(Exception e){
        System.err.println(e.getMessage());
        awaitTokenRefresh = true;
        refreshToken(); 
    }
}

private void refreshToken() throws InterruptedException {
    lock.lock();
    try{
        System.out.println("Refreshing token...");
        Thread.sleep(2000l);
        System.out.println("Refreshed!");
        awaitTokenRefresh = false;
        awaitRefresh.signalAll();
    }
    finally{
        lock.unlock();
    }   
}

//test use case for salesforce token
private void getToken(String message) throws InterruptedException {
    lock.lock();
    try{
        while(awaitTokenRefresh){
            System.out.println(message + " waiting for token refresh...");
            awaitRefresh.await();
        }
    }
    finally{
        lock.unlock();
    }
}

public void run(){
    try {
        System.out.println("Starting thread...");
        testLock(Thread.currentThread().getName());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

只是为了测试,我已经进行了一些睡眠来模拟正在完成的工作。我不知道的主要事情是,当线程 A 在 getToken() 内部解锁时,线程 B 进入,但我们还不知道令牌是否无效。所以 B 实际上可能得​​到了 A 必须找到的错误令牌。有没有好的方法来处理这个?还是使用锁的想法完全错误?

【问题讨论】:

    标签: java multithreading locking


    【解决方案1】:

    我注意到的第一件事是您的代码没有正确同步。 testLock() 中的异常处理程序会修改共享变量 awaitTokenRefresh,此时写入相对于读取 getToken() 中的值的其他线程没有排序。

    我不知道的主要事情是,当线程 A 在 getToken() 内部解锁时,线程 B 进入,但我们还不知道令牌是否无效。所以 B 实际上可能得​​到了 A 必须找到的错误令牌。有没有好的方法来处理这个?还是使用锁的想法完全错误?

    我猜你真正想要避免的是当当前令牌变得无效时不必要的令牌刷新。恰好一个线程应该刷新它;其他人应该只是等待刷新,然后继续他们的业务。您的方法的问题是线程没有好的方法来确定它们是否是第一个检测到过期的,因此应该负责刷新。这确实是有道理的,因为在多线程应用程序中并不总是明确定义哪个线程首先执行任何事情的概念。

    您是否使用锁与同步是一个影响相对较小的实现细节。关键是你必须有一些共享状态来告诉线程他们建议刷新的令牌实际上是否仍然是最新的。我可能会像这样实现它:

    public class MyClass {
        private Object token = null;
        private final Object tokenMonitor = new Object();
    
        // ...
    
        private Object getToken() {
            synchronized (tokenMonitor) {
                if (token == null) {
                    return refreshToken(null);
                } else {
                    return token;
                }
            }
        }
    
        private Object refreshToken(Object oldToken) {
            synchronized (tokenMonitor) {
                if (token == oldToken) {  // test reference equality
                    token = methodToPerformARefreshAndGenerateANewToken();
                }
                return token;
            }
        }
    
        // ...
    }
    

    当它尝试刷新令牌时的想法是,每个线程指定它尝试刷新的哪个令牌。只有当它实际上是当前令牌时才会执行刷新,并且无论哪种方式,都会返回当前令牌。

    您可以使用ReentrantLock 代替我的tokenMonitor,使用锁定和解锁而不是同步块,但是当范围被很好地包含时,我更喜欢简单的同步,就像在这种情况下一样。除此之外,它更安全——当你离开一个同步块时,你离开它;不可能不释放相关监视器。锁对象不能这样说。

    【讨论】:

    • 无论如何,我更喜欢同步语句而不是锁。它们看起来更干净、更简单。我试试看。
    【解决方案2】:

    这实际上看起来是一个可以通过版本控制解决的问题:

    public class LockTest {
    
        private int currentVersion = -1;
        private Object token = null;
    
        private synchronized int refreshToken(int requestorVersion) {
            if (requestorVersion == currentVersion) {
                try {
                    //do the actual refresh
                    Thread.sleep(1000);
                    token = new Object();
                    currentVersion++;
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                }
            }
    
            return currentVersion;
        }
    
        public VersionedToken takeToken() {
            if (currentVersion == -1) {
                refreshToken(-1);
            }
    
            return new VersionedToken(currentVersion);
        }
    
        public class VersionedToken {
    
            private int version;
    
            public VersionedToken(int version) {
                this.version = version;
            }
    
            private void refresh() {
                version = refreshToken(version);
            }
    
            private Object getToken() {
                return token;
            }
    
        }
    
        public static void main(String[] args) {
            LockTest t = new LockTest();
            for (int i = 0; i < 5; i++) {
                new Thread(() -> {
                    VersionedToken vtoken = t.takeToken();
                    Object token = vtoken.getToken();
    
                    try {
                    //do something with the token 
                    }catch (Exception ex) {
                        //if token went bad - just refresh it and continue to work with it afterwords
                        vtoken.refresh();
                        token = vtoken.getToken();
                    }
                }).start();
            }
        }
    
    }
    

    【讨论】:

    • 这可行。唯一的问题是外部 API 中没有用于测试令牌是否良好的端点。我只需要使用它并等待 HTTP 错误。
    • @jordaniac89,在考虑到您在最后评论中提供的详细信息后,请查看我的上一次编辑,我不再认为 RW 锁适合.. 版本控制看起来更合适。跨度>
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-11
    • 2021-12-24
    • 2021-02-19
    • 2011-10-11
    • 2020-11-21
    相关资源
    最近更新 更多