【问题标题】:How to implement such use case by Java Concurrency Lock如何通过 Java 并发锁实现这样的用例
【发布时间】:2017-11-08 07:38:35
【问题描述】:

在下面的测试用例中,update() 方法在类外部调用,该类每小时仅由一个线程运行。但是有多个线程同时调用getKey1()getKey2()

所以对于这个用例:

最重要的是getKey1()getKey2()几乎是同时调用的,一定要确定

  1. 如果 flag 为真,更新 key1 和 key2 并返回新密钥
  2. 如果没有,获取旧的key1和key2

我们不希望这种情况存在:更新key1并获取旧key2,但是即使我们已经更新了旧key1和key2,也可以获取少量请求。

public class Test {

    private Lock lock = new ReentrantLock();

    private AtomicBoolean flag1 = new AtomicBoolean(false);
    private AtomicBoolean flag2 = new AtomicBoolean(false);

    private volatile String key1;
    private volatile String key2;

    public Test(String key1, String key2) {
        this.key1 = key1;
        this.key2 = key2;
    }

    public void update() {
        lock.lock();
        try {
            flag1.compareAndSet(false, true);
            flag2.compareAndSet(false, true);
        } finally {
            lock.unlock();
        }
    }

    public String getKey1() {
        // TODO if the lock is holding... block over here
        if (flag1.get()) {
            // doing something for key 1
            key1 = getFromFile();
            flag1.set(false);
        }
        return key1;
    }

    public String getKey2() {
        // TODO if the lock is holding... block over here
        if (flag2.get()) {
            // doing something for key 1
            key2 = getFromFile();
            flag2.set(false);
        }
        return key1;
    }
}

我的想法是:

  1. update() 运行时,阻止getKey1()getKey2(),等待获取更新密钥
  2. update() 未运行时,getKey1()getKey2() 应该都可以,直接返回。
  3. 当调用 getKey1() 或 getKey2() 时,我认为我们不需要阻塞 update() 方法。

有人对实现有任何想法吗?

【问题讨论】:

  • 在 getKey1() 和 getKey2() 中都使用 lock.tryLock()
  • 你确定ReentrantLock是你需要的吗?你考虑过ReadWriteLock

标签: java multithreading concurrency atomic volatile


【解决方案1】:

如前所述,java.util.concurrent.locks.ReentrantReadWriteLock 最适合你。

把它放在你的例子中:

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test {

    private ReadWriteLock lock = new ReentrantReadWriteLock();

    private AtomicBoolean flag1 = new AtomicBoolean(false);
    private AtomicBoolean flag2 = new AtomicBoolean(false);

    private volatile String key1;
    private volatile String key2;

    public Test(String key1, String key2) {
        this.key1 = key1;
        this.key2 = key2;
    }

    public void update() {
        Lock writeLock = lock.writeLock();
        try {
            flag1.compareAndSet(false, true);
            flag2.compareAndSet(false, true);
        } finally {
            writeLock.unlock();
        }
    }

    public String getKey1() {
        Lock readLock = lock.readLock();
        try {
            if (flag1.get()) {
                // doing something for key 1
                key1 = getFromFile();
                flag1.set(false);
            }
            return key1;
        } finally {
            readLock.unlock();
        }
    }

    public String getKey2() {
        Lock readLock = lock.readLock();
        try {
            if (flag2.get()) {
                // doing something for key 1
                key2 = getFromFile();
                flag2.set(false);
            }
            return key1;
        } finally {
            readLock.unlock();
        }
    }
}

来自ReadWriteLock 接口的文档:

读锁可能被多个读线程同时持有, 只要没有作家。写锁是独占的。

【讨论】:

  • getKey1()getKey2() 通常同时被调用,但是如果更新发生在getKey1()getKey2() 之间,它会读取旧的key1 但最新的key2?有没有更好的解决方案来避免这种情况(getKey1() 和 getKey2() 是从接口覆盖的,所以我们不能将两者结合成一个方法。
  • flag1 和 flag2 仍然是 AtomicBoolean 吗? key1 和 key2 仍然是不稳定的,因为它已经在锁内。其实flag1和flag2可以合并成一个字段吗?
  • 经过大量互联网搜索后,我找到了this answer。创建key1key2 的包装器,并使用AtomicReference 自动更新它们。
【解决方案2】:

以下 ReadWriteLock 示例的更新,我们必须在开始读/写操作之前获取锁。由于我无权编辑示例,因此发布新答案。

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test {

    private ReadWriteLock lock = new ReentrantReadWriteLock();

    private AtomicBoolean flag1 = new AtomicBoolean(false);
    private AtomicBoolean flag2 = new AtomicBoolean(false);

    private volatile String key1;
    private volatile String key2;

    public Test(String key1, String key2) {
        this.key1 = key1;
        this.key2 = key2;
    }

    public void update() {
        Lock writeLock = lock.writeLock();
        try {
            writeLock.lock(); 
            flag1.compareAndSet(false, true);
            flag2.compareAndSet(false, true);
        } finally {
            writeLock.unlock();
        }
    }

    public String getKey1() {
        Lock readLock = lock.readLock();
        try {
            readLock.lock(); 
            if (flag1.get()) {
                // doing something for key 1
                key1 = getFromFile();
                flag1.set(false);
            }
            return key1;
        } finally {
            readLock.unlock();
        }
    }

    public String getKey2() {
        Lock readLock = lock.readLock();
        try {
            readLock.lock();
            if (flag2.get()) {
                // doing something for key 1
                key2 = getFromFile();
                flag2.set(false);
            }
            return key1;
        } finally {
            readLock.unlock();
        }
    }
}

【讨论】:

  • getKey1() 和 getKey2() 通常同时被调用,但是如果更新发生在 getKey1() 和 getKey2() 之间,它会读取旧的 key1 而读取最新的 key2 怎么办?有没有更好的解决方案来避免这种情况(getKey1() 和 getKey2() 是从接口覆盖的,所以我们不能将两者结合成一个方法。
【解决方案3】:

这当然是ReadWriteLock 应该提供帮助的情况。 许多并发读取器可以在很小的争用下运行,并且写入器很少获得写入锁和更新。

但是提出的关键问题是线程不能一次读取一个旧密钥和一个新密钥。

这是一个经典问题。最简单的解决方案是在更新线程中更新这两个键。

ReentrantReadWriteLock rwlock=new ReentrantReadWriteLock(true);
//If in doubt require a fair lock to avoid live-locking writes...

//...

public void update() {
    String new1=getFromFile();
    String new2=getFromFile();
    //That's the heavy lifting. Only now acquire the lock...
    Lock wlock=rwlock.writeLock();
    wlock.lock();
    try {
        key1=new1;
        key2=new2;
    } finally {
        wlock.unlock();
    }
}

在读者中:

public String[] getKeys() {
    String k1;
    String k2;
    Lock rlock=rwlock.readLock();
    rlock.lock();
    try {
        k1=key1;
        k2=key2;
    } finally {
        rlock.unlock();
    }    
    String[] r={k1,k2};
    return r;
 }

一个读写锁有两个相连的锁。多个读者可以获取读锁,但写锁不包括读者和其他写者。这是一个常见的场景,并且已经建立了解决方案。

在获取锁之前从文件中获取可能很重要,因为 I/O 通常相对较慢,您应该尽可能短地持有锁。

从同一获取锁中返回两个密钥也很重要。没有其他简单的方法可以阻止在调用getKey1()getKey2() 之间发生更新。

由于锁的内存屏障保证,您不再需要使用 volatile 关键字(请参阅文档)。

您实际上并不需要重入锁,但这是为读写锁提供的开箱即用的锁。

在这种情况下,称为顺序锁 (Seqlock) 的东西可能会有所帮助,但这将涉及更多编码,并且可能在此处过度设计。

参考资料:

https://en.wikipedia.org/wiki/Seqlock

https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReadWriteLock.html

【讨论】:

  • 我面临的问题是这个类是从一个接口实现的。所以getKey1()getKey2() 是我无法将它们组合成一种方法的覆盖方法。更新方法是find,我会在类外更新文件然后做writeLock
  • @Jie n 是哪种情况,基于呈现的界面无法保证update()不能在getKey1()getKey2()之间运行。所以无论你做什么,你都会冒着先读旧密钥然后读新密钥的风险。您可能会找到某种方式来暂停对 getKey1()getKey2() 的链接调用之间的写入,但问题中没有足够的代码来查看它在哪里。可能会有黑客攻击(在getKey1() 早期锁定并在getKey2() 后期解锁)但这在很大程度上取决于调用接口的代码。
【解决方案4】:

使用 tryLock()

public String getKey1() {
  if (lock.tryLock())
  {
    // Got the lock
    try
    {
        // Process record
    }
    finally
    {
        // Make sure to unlock so that we don't cause a deadlock
        lock.unlock();
    }
 }
}

如果您不希望 getKey1() 或 getKey2() 执行锁定。您可以使用以下方法,

static boolean isLocked=false;
public void update() {
        lock.lock();
        isLocked = true;
        try {
            flag1.compareAndSet(false, true);
            flag2.compareAndSet(false, true);
        } finally {
            lock.unlock();
            isLocked = false; 
        }
    }

    public String getKey1() {
        while(isLocked); #wait till the lock release.
        if (flag1.get()) {
            // doing something for key 1
            key1 = getFromFile();
            flag1.set(false);
        }
        return key1;
    }

参考:how-do-determine-if-an-object-is-locked-synchronized-so-not-to-block-in-java

【讨论】:

  • 您应该(至少)将isLocked 声明为volatile,因为不能保证阅读器线程会看到其值发生变化。对其他 volatiles 的访问存在隐式内存障碍,但它们在 isLocked 更改之后或之前,因此无济于事。那就是说我不明白这能达到什么目的。如果在while(isLocked); 之后获取锁,getKey1() 的主体仍然可以在持有锁时执行。
  • 感谢 Persixty3。它应该是易变的。
  • 我仍然看不到该代码实现了什么。在该方法期间可以随时获取锁。
  • 锁可以实现。但是在执行更新操作时,getKey 函数会等到锁被释放。
  • 但是可以在方法中获取。该代码并不能确保在读取期间未持有锁。它不能确保同步。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-15
  • 1970-01-01
  • 2023-04-05
  • 2014-01-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多