【问题标题】:Java- Allow one thread to update a value, others to wait and skip critical sectionJava-允许一个线程更新一个值,其他线程等待并跳过临界区
【发布时间】:2020-01-29 13:06:09
【问题描述】:

您好,我有一种情况,我必须只允许一个线程说更新变量。

有一个触发器,它可能调用多个线程来更新这个变量,但是更新应该只由第一个线程发生一次,以到达临界区为准。

理想情况下,流程应如下所示:

线程-1;调用 Thread-2 和 Thread-3 来更新由锁或互斥锁保护的临界区中的变量

使用此保护的关键部分只允许一个线程进入,线程 2 和线程 3 就在外面等待。

一旦这个变量被 Thread-1 更新; Thread-2 和 Thread-3 继续进行其他工作,而不会对变量造成影响。

我想出了以下实现,但我无法让其他线程等待并跳过更新:

public class Main {


    private static ReentrantLock lock = new ReentrantLock();
    private int counter = 0;

    public static void main(String[] args) {
        Main m = new Main();

        new Thread(m::doSomeOperation).start();

        new Thread(m::doSomeOperation).start();

        new Thread(m::doSomeOperation).start();


    }


    private void doSomeOperation() {

    try {
        System.out.println("Thread about to acquire lock: " + Thread.currentThread().getName());
        if (lock.tryLock()) {
            System.out.println("Lock held by " + Thread.currentThread().getName() + " " + lock.isHeldByCurrentThread());
            counter++;
            // Thread.sleep(3000);
            System.out.println("Counter value: " + counter + " worked by thread " + Thread.currentThread().getName());
        }

    } catch (Exception ex) {
        ex.printStackTrace();
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
            System.out.println("Unlocked: " + Thread.currentThread().getName());
        }
    }


}
}

最后,计数器值为 3,我希望计数器值为 1,我希望其他线程等到第一个线程更新计数器。想法赞赏。我更喜欢使用锁/互斥锁而不是等待和通知的解决方案。

输出:

Thread about to acquire lock: Thread-0
Lock held by Thread-0 true
Thread about to acquire lock: Thread-1
Counter value: 1 worked by thread Thread-0
Unlocked: Thread-0
Thread about to acquire lock: Thread-2
Lock held by Thread-2 true
Counter value: 2 worked by thread Thread-2
Unlocked: Thread-2

Process finished with exit code 0

注意 我的用例不同——更新计数器的例子是为了简单起见。实际上我正在更新 doSomeOperation 方法中的会话令牌。

【问题讨论】:

  • if (lock.tryLock()) { counter++; }.
  • 如果我使用 if(lock.tryLock) Thread about to acquire lock: Thread-0 Lock held by Thread-0 true Thread about to acquire lock: Thread-1 Counter value: 1 worked by thread Thread-0 Unlocked: Thread-1 Unlocked: Thread-0 Thread about to acquire lock: Thread-2 Lock held by Thread-2 true Counter value: 2 worked by thread Thread-2 Unlocked: Thread-2 我有以下输出
  • 计数器似乎更新了两次 - 仍然。
  • 你想要一个变量来检查是否有东西已经进入了临界区。
  • 我不能依赖一个变量,这个操作可能需要稍后再做一次,在这种情况下这个变量将不允许由一组新的线程进行更新。否则我将不得不承担多次创建此类的开销。

标签: java multithreading locking mutex semaphore


【解决方案1】:

Java 并发工具似乎没有提供完全符合您(和我)需要的东西。但这里有一个相对简单的方法。

public class Sample {

    private String token;

    public String getToken(boolean forceNew) {
        boolean expired = false; // would be replace with actual check
        if (expired) {
            return getNewAccessToken();
        } else {
            return token;
        }
    }

    private String getNewAccessToken() {
        String currentToken = token; // Get current token
        synchronized (currentToken) { // Semaphore on the current token
            if (currentToken != token) // Now check if it changed after obtaining the lock
                return token; // If so, it has just been renewed and we don't need another exchange
            // Otherwise do the renew
            Response response = null;
            // Token fetch/refresh code here...
            // response = result of some call
            processResponse(response);
            return token;
        }
    }

    private void processResponse(Response response) {
        token = "test"; // in reality get the value from the response
    }

}

假设线程 A 和 B 同时到达getNewAccessToken()。以下情况是可能的:

  • 线程 A 将 token 字段分配给局部变量 currentToken。线程 B 在其调用中执行相同的操作。现在线程 A 进入同步块。线程 B 必须等待。线程 A 更新令牌并退出同步块。线程 B 现在可以进入,但 token 字段不再是与 currentToken 变量相同的对象,所以它只是返回。
  • 线程A将token字段赋值给局部变量currentToken并进入同步块。直到现在,线程 B 才将 token 字段分配给局部变量 currentToken。线程 B 必须等到 A 退出同步块才能继续。这里有两种可能:
    • 线程 A 已经完成更新并在线程 B 将 token 分配给 currentToken 之前调用了 processResponse。然后线程 B 将进行不必要的刷新,因为 currentToken == token。事实上,它不必等待同步,因为 A 和 B 被锁定在不同的对象上(A 在旧令牌上,B 在新令牌上)。
    • 线程 A 仍在更新令牌的过程中。线程 B 不会进行不必要的刷新,因为 currentToken != token 一旦进入同步块。
  • 任何其他顺序很容易被证明是上述顺序的镜像。

这种情况并非 100% 防水,因为仍然存在对并发线程进行不必要的更新的情况。但这不会发生在耗时最长的过程中,即通过一些网络调用实际更新令牌。在那之后,我们碰巧在线程 A 完成网络调用和在线程 B 进入时更新字段之间的狭窄窗口的场景之间没有任何区别,线程 B 在 A 完全完成后才进入。

如果您想对此进行改进以避免不必要的更新,您还可以查看响应中的一些过期信息,或者保留上次续订的时间戳并在太短的时间内阻止新的更新。

【讨论】:

    【解决方案2】:

    问题之所以出现,是因为最初一个线程增加计数器并释放锁,而您的程序运行速度如此之快,以至于一旦第一个线程释放锁,另一个线程进入该方法并且它看到锁已释放,因此它获取锁并进一步增加计数器。在这种情况下,您可以使用countDownLatch

    这里有一个获得锁的线程将闩锁计数减一并使其为零,之后没有一个线程将能够处理,因为 latch.getCount()==1 条件将失败。

    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Test2 {
    
    
        private static ReentrantLock lock = new ReentrantLock();
        private static CountDownLatch latch= new CountDownLatch(1);
        private int counter = 0;
    
        public static void main(String[] args) {
            Test2 m = new Test2();
    
            new Thread(m::doSomeOperation).start();
    
            new Thread(m::doSomeOperation).start();
    
            new Thread(m::doSomeOperation).start();
    
    
        }
    
    
        private void doSomeOperation() {
    
        try {
            System.out.println("Thread about to acquire lock: " + Thread.currentThread().getName());
            if (lock.tryLock() && latch.getCount()==1) {
                System.out.println("Lock held by " + Thread.currentThread().getName() + " " + lock.isHeldByCurrentThread());
                counter++;
                latch.countDown();
                 Thread.sleep(3000);
                System.out.println("Counter value: " + counter + " worked by thread " + Thread.currentThread().getName());
                }
            System.out.println("Exiting" + Thread.currentThread().getName());
    
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
                System.out.println("Unlocked: " + Thread.currentThread().getName());
            }
        }
    
    
    }
    }
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Test2 {
    
    
        private static ReentrantLock lock = new ReentrantLock();
        private static CountDownLatch latch= new CountDownLatch(1);
        private int counter = 0;
    
        public static void main(String[] args) {
            Test2 m = new Test2();
    
            new Thread(m::doSomeOperation).start();
    
            new Thread(m::doSomeOperation).start();
    
            new Thread(m::doSomeOperation).start();
    
    
        }
    
    
        private void doSomeOperation() {
    
        try {
            System.out.println("Thread about to acquire lock: " + Thread.currentThread().getName());
            if (lock.tryLock() && latch.getCount()==1) {
                System.out.println("Lock held by " + Thread.currentThread().getName() + " " + lock.isHeldByCurrentThread());
                counter++;
                latch.countDown();
                 Thread.sleep(3000);
                System.out.println("Counter value: " + counter + " worked by thread " + Thread.currentThread().getName());
                }
            System.out.println("Exiting" + Thread.currentThread().getName());
    
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
                System.out.println("Unlocked: " + Thread.currentThread().getName());
            }
        }
    
    
    }
    }
    

    【讨论】:

    • 这看起来很理想,但是我可以在这里看到一个问题 - 假设我想在 x 时间后再次执行此操作或当触发器再次触发时,在这种情况下闩锁不会允许任何线程进入。
    • Hmm.. 在这种情况下,是否可以在每次触发发生时初始化锁存计数?
    • 不,事件是由网络调用随机触发的。
    • 对于每个触发的事件,您都在创建一个新线程?任何特定的最小或最大间隔?
    • 是的,没错。对于每个事件 - 而是为每个网络调用创建一个线程。有时这些调用可能是并行的,有时它们可​​能一个接一个。
    【解决方案3】:

    您可以在这里使用AtomicInteger,而不必担心正式锁:

    public class Worker {
        private static AtomicInteger counter = new AtomicInteger(0);
    
        private void doSomeOperation() {
            counter.incrementAndGet();
            System.out.println("Counter value: " + counter + " worked by thread " + Thread.currentThread().getName());
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Worker w = new Worker();
            new Thread(w::doSomeOperation).start();
            new Thread(w::doSomeOperation).start();
            new Thread(w::doSomeOperation).start();
        }
    }
    

    【讨论】:

    • 我的用例不同——更新计数器的例子是为了简单起见。实际上,我正在更新 doSomeOperation 方法中的会话令牌。话虽如此,上面的输出是:Counter value: 0 worked by thread Thread-0 Counter value: 0 worked by thread Thread-1 Counter value: 0 worked by thread Thread-2
    • @User3 对不起......我原来在那里的东西可以编译和运行,但不能反映你的问题。尝试制作AtomicInteger static,以模拟单个共享资源。
    • 没有区别,方法是从 main() 调用的,没有创建类的新实例。使用静态,输出仍然相同。在这里查看:ideone.com/mcvC2k
    • @User3 您在混合counterxcounter 时在代码末尾打错字。
    • 哦,我的错..!这是新的输出:Counter value: 1 worked by thread Thread-0 Counter value: 2 worked by thread Thread-1 Counter value: 3 worked by thread Thread-2
    【解决方案4】:

    如果您知道每轮开始的确切线程数: 你可以这样做:

     private int threadCounter;
     private int threadCount = 3;
        private void doSomeOperation() {
    
            try {
                System.out.println("Thread about to acquire lock: " + Thread.currentThread().getName());
                if (lock.tryLock()) {
                    System.out.println("Lock held by " + Thread.currentThread().getName() + " " + lock.isHeldByCurrentThread());
                    if (threadCounter++ % threadCount == 0) {
                        counter++;
                        // Thread.sleep(3000);
                        System.out.println("Counter value: " + counter + " worked by thread " + Thread.currentThread().getName());
                    }
                }
    
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                    System.out.println("Unlocked: " + Thread.currentThread().getName());
                }
            }
    
        }
    

    它只允许在每轮的第一个线程上增加计数器。

    【讨论】:

    • 线程由一个事件触发,这个事件可以触发任意数量的线程。可以是顺序的,也可以是并行的,一开始我无法知道线程的数量,
    【解决方案5】:

    看起来您尝试构建的是生产者消费者问题的经典示例。 你会在网上找到数以千计的解决方案来解决这个问题。 下面列出了其中一些。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-11-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-10-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多